diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt index a1e9da41b4b32..06920a97ddc88 100644 --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -108,6 +108,7 @@ add_clang_library(clangDaemon STATIC SemanticHighlighting.cpp SemanticSelection.cpp SourceCode.cpp + SymbolDocumentation.cpp SystemIncludeExtractor.cpp TidyProvider.cpp TUScheduler.cpp diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.cpp b/clang-tools-extra/clangd/CodeCompletionStrings.cpp index 9b4442b0bb76f..d6579640cb0fb 100644 --- a/clang-tools-extra/clangd/CodeCompletionStrings.cpp +++ b/clang-tools-extra/clangd/CodeCompletionStrings.cpp @@ -7,13 +7,18 @@ //===----------------------------------------------------------------------===// #include "CodeCompletionStrings.h" +#include "Config.h" +#include "SymbolDocumentation.h" #include "clang-c/Index.h" #include "clang/AST/ASTContext.h" +#include "clang/AST/Comment.h" +#include "clang/AST/Decl.h" #include "clang/AST/RawCommentList.h" #include "clang/Basic/SourceManager.h" #include "clang/Sema/CodeCompleteConsumer.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" #include #include @@ -100,16 +105,51 @@ std::string getDeclComment(const ASTContext &Ctx, const NamedDecl &Decl) { // the comments for namespaces. return ""; } - const RawComment *RC = getCompletionComment(Ctx, &Decl); - if (!RC) - return ""; - // Sanity check that the comment does not come from the PCH. We choose to not - // write them into PCH, because they are racy and slow to load. - assert(!Ctx.getSourceManager().isLoadedSourceLocation(RC->getBeginLoc())); - std::string Doc = - RC->getFormattedText(Ctx.getSourceManager(), Ctx.getDiagnostics()); - if (!looksLikeDocComment(Doc)) - return ""; + + const RawComment *RC = nullptr; + const Config &Cfg = Config::current(); + + std::string Doc; + + if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen && + isa(Decl)) { + // Parameters are documented in their declaration context (function or + // template function). + const NamedDecl *ND = dyn_cast(Decl.getDeclContext()); + if (!ND) + return ""; + + RC = getCompletionComment(Ctx, ND); + if (!RC) + return ""; + + // Sanity check that the comment does not come from the PCH. We choose to + // not write them into PCH, because they are racy and slow to load. + assert(!Ctx.getSourceManager().isLoadedSourceLocation(RC->getBeginLoc())); + + comments::FullComment *FC = RC->parse(Ctx, /*PP=*/nullptr, ND); + if (!FC) + return ""; + + SymbolDocCommentVisitor V(FC, Ctx.getLangOpts().CommentOpts); + std::string RawDoc; + llvm::raw_string_ostream OS(RawDoc); + + V.parameterDocToString(dyn_cast(&Decl)->getName(), OS); + + Doc = StringRef(RawDoc).trim().str(); + } else { + RC = getCompletionComment(Ctx, &Decl); + if (!RC) + return ""; + // Sanity check that the comment does not come from the PCH. We choose to + // not write them into PCH, because they are racy and slow to load. + assert(!Ctx.getSourceManager().isLoadedSourceLocation(RC->getBeginLoc())); + Doc = RC->getFormattedText(Ctx.getSourceManager(), Ctx.getDiagnostics()); + if (!looksLikeDocComment(Doc)) + return ""; + } + // Clang requires source to be UTF-8, but doesn't enforce this in comments. if (!llvm::json::isUTF8(Doc)) Doc = llvm::json::fixUTF8(Doc); diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index 1e0718d673260..0afa90285db52 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -18,6 +18,7 @@ #include "Protocol.h" #include "Selection.h" #include "SourceCode.h" +#include "SymbolDocumentation.h" #include "clang-include-cleaner/Analysis.h" #include "clang-include-cleaner/IncludeSpeller.h" #include "clang-include-cleaner/Types.h" @@ -41,6 +42,7 @@ #include "clang/AST/Type.h" #include "clang/Basic/CharInfo.h" #include "clang/Basic/LLVM.h" +#include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/Specifiers.h" @@ -627,6 +629,9 @@ HoverInfo getHoverContents(const NamedDecl *D, const PrintingPolicy &PP, HI.Name = printName(Ctx, *D); const auto *CommentD = getDeclForComment(D); HI.Documentation = getDeclComment(Ctx, *CommentD); + // save the language options to be able to create the comment::CommandTraits + // to parse the documentation + HI.CommentOpts = D->getASTContext().getLangOpts().CommentOpts; enhanceFromIndex(HI, *CommentD, Index); if (HI.Documentation.empty()) HI.Documentation = synthesizeDocumentation(D); @@ -1388,9 +1393,100 @@ static std::string formatOffset(uint64_t OffsetInBits) { return Offset; } -markup::Document HoverInfo::present() const { - markup::Document Output; +void HoverInfo::calleeArgInfoToMarkupParagraph(markup::Paragraph &P) const { + assert(CallPassType); + std::string Buffer; + llvm::raw_string_ostream OS(Buffer); + OS << "Passed "; + if (CallPassType->PassBy != HoverInfo::PassType::Value) { + OS << "by "; + if (CallPassType->PassBy == HoverInfo::PassType::ConstRef) + OS << "const "; + OS << "reference "; + } + if (CalleeArgInfo->Name) + OS << "as " << CalleeArgInfo->Name; + else if (CallPassType->PassBy == HoverInfo::PassType::Value) + OS << "by value"; + if (CallPassType->Converted && CalleeArgInfo->Type) + OS << " (converted to " << CalleeArgInfo->Type->Type << ")"; + P.appendText(OS.str()); +} + +void HoverInfo::usedSymbolNamesToMarkup(markup::Document &Output) const { + markup::Paragraph &P = Output.addParagraph(); + P.appendText("provides "); + + const std::vector::size_type SymbolNamesLimit = 5; + auto Front = llvm::ArrayRef(UsedSymbolNames).take_front(SymbolNamesLimit); + + llvm::interleave( + Front, [&](llvm::StringRef Sym) { P.appendCode(Sym); }, + [&] { P.appendText(", "); }); + if (UsedSymbolNames.size() > Front.size()) { + P.appendText(" and "); + P.appendText(std::to_string(UsedSymbolNames.size() - Front.size())); + P.appendText(" more"); + } +} + +void HoverInfo::providerToMarkupParagraph(markup::Document &Output) const { + markup::Paragraph &DI = Output.addParagraph(); + DI.appendText("provided by"); + DI.appendSpace(); + DI.appendCode(Provider); +} + +void HoverInfo::definitionScopeToMarkup(markup::Document &Output) const { + std::string Buffer; + + // Append scope comment, dropping trailing "::". + // Note that we don't print anything for global namespace, to not annoy + // non-c++ projects or projects that are not making use of namespaces. + if (!LocalScope.empty()) { + // Container name, e.g. class, method, function. + // We might want to propagate some info about container type to print + // function foo, class X, method X::bar, etc. + Buffer += "// In " + llvm::StringRef(LocalScope).rtrim(':').str() + '\n'; + } else if (NamespaceScope && !NamespaceScope->empty()) { + Buffer += "// In namespace " + + llvm::StringRef(*NamespaceScope).rtrim(':').str() + '\n'; + } + + if (!AccessSpecifier.empty()) { + Buffer += AccessSpecifier + ": "; + } + Buffer += Definition; + + Output.addCodeBlock(Buffer, DefinitionLanguage); +} + +void HoverInfo::valueToMarkupParagraph(markup::Paragraph &P) const { + P.appendText("Value = "); + P.appendCode(*Value); +} + +void HoverInfo::offsetToMarkupParagraph(markup::Paragraph &P) const { + P.appendText("Offset: " + formatOffset(*Offset)); +} + +void HoverInfo::sizeToMarkupParagraph(markup::Paragraph &P) const { + P.appendText("Size: " + formatSize(*Size)); + if (Padding && *Padding != 0) { + P.appendText(llvm::formatv(" (+{0} padding)", formatSize(*Padding)).str()); + } + if (Align) + P.appendText(", alignment " + formatSize(*Align)); +} + +markup::Document HoverInfo::presentDoxygen() const { + // NOTE: this function is currently almost identical to presentDefault(). + // This is to have a minimal change when introducing the doxygen parser. + // This function will be changed when rearranging the output for doxygen + // parsed documentation. + + markup::Document Output; // Header contains a text of the form: // variable `var` // @@ -1407,14 +1503,99 @@ markup::Document HoverInfo::present() const { if (Kind != index::SymbolKind::Unknown) Header.appendText(index::getSymbolKindString(Kind)).appendSpace(); assert(!Name.empty() && "hover triggered on a nameless symbol"); + Header.appendCode(Name); if (!Provider.empty()) { - markup::Paragraph &DI = Output.addParagraph(); - DI.appendText("provided by"); - DI.appendSpace(); - DI.appendCode(Provider); + providerToMarkupParagraph(Output); + } + + // Put a linebreak after header to increase readability. + Output.addRuler(); + // Print Types on their own lines to reduce chances of getting line-wrapped by + // editor, as they might be long. + if (ReturnType) { + // For functions we display signature in a list form, e.g.: + // → `x` + // Parameters: + // - `bool param1` + // - `int param2 = 5` + Output.addParagraph().appendText("→ ").appendCode( + llvm::to_string(*ReturnType)); + } + + SymbolDocCommentVisitor SymbolDoc(Documentation, CommentOpts); + + if (Parameters && !Parameters->empty()) { + Output.addParagraph().appendText("Parameters:"); + markup::BulletList &L = Output.addBulletList(); + for (const auto &Param : *Parameters) { + markup::Paragraph &P = L.addItem().addParagraph(); + P.appendCode(llvm::to_string(Param)); + + if (SymbolDoc.isParameterDocumented(llvm::to_string(Param.Name))) { + P.appendText(" -"); + SymbolDoc.parameterDocToMarkup(llvm::to_string(Param.Name), P); + } + } + } + // Don't print Type after Parameters or ReturnType as this will just duplicate + // the information + if (Type && !ReturnType && !Parameters) + Output.addParagraph().appendText("Type: ").appendCode( + llvm::to_string(*Type)); + + if (Value) { + valueToMarkupParagraph(Output.addParagraph()); + } + + if (Offset) + offsetToMarkupParagraph(Output.addParagraph()); + if (Size) { + sizeToMarkupParagraph(Output.addParagraph()); + } + + if (CalleeArgInfo) { + calleeArgInfoToMarkupParagraph(Output.addParagraph()); + } + + SymbolDoc.docToMarkup(Output); + + if (!Definition.empty()) { Output.addRuler(); + definitionScopeToMarkup(Output); + } + + if (!UsedSymbolNames.empty()) { + Output.addRuler(); + usedSymbolNamesToMarkup(Output); + } + + return Output; +} + +markup::Document HoverInfo::presentDefault() const { + markup::Document Output; + // Header contains a text of the form: + // variable `var` + // + // class `X` + // + // function `foo` + // + // expression + // + // Note that we are making use of a level-3 heading because VSCode renders + // level 1 and 2 headers in a huge font, see + // https://github.com/microsoft/vscode/issues/88417 for details. + markup::Paragraph &Header = Output.addHeading(3); + if (Kind != index::SymbolKind::Unknown) + Header.appendText(index::getSymbolKindString(Kind)).appendSpace(); + assert(!Name.empty() && "hover triggered on a nameless symbol"); + Header.appendCode(Name); + + if (!Provider.empty()) { + providerToMarkupParagraph(Output); } // Put a linebreak after header to increase readability. @@ -1445,41 +1626,17 @@ markup::Document HoverInfo::present() const { llvm::to_string(*Type)); if (Value) { - markup::Paragraph &P = Output.addParagraph(); - P.appendText("Value = "); - P.appendCode(*Value); + valueToMarkupParagraph(Output.addParagraph()); } if (Offset) - Output.addParagraph().appendText("Offset: " + formatOffset(*Offset)); + offsetToMarkupParagraph(Output.addParagraph()); if (Size) { - auto &P = Output.addParagraph().appendText("Size: " + formatSize(*Size)); - if (Padding && *Padding != 0) { - P.appendText( - llvm::formatv(" (+{0} padding)", formatSize(*Padding)).str()); - } - if (Align) - P.appendText(", alignment " + formatSize(*Align)); + sizeToMarkupParagraph(Output.addParagraph()); } if (CalleeArgInfo) { - assert(CallPassType); - std::string Buffer; - llvm::raw_string_ostream OS(Buffer); - OS << "Passed "; - if (CallPassType->PassBy != HoverInfo::PassType::Value) { - OS << "by "; - if (CallPassType->PassBy == HoverInfo::PassType::ConstRef) - OS << "const "; - OS << "reference "; - } - if (CalleeArgInfo->Name) - OS << "as " << CalleeArgInfo->Name; - else if (CallPassType->PassBy == HoverInfo::PassType::Value) - OS << "by value"; - if (CallPassType->Converted && CalleeArgInfo->Type) - OS << " (converted to " << CalleeArgInfo->Type->Type << ")"; - Output.addParagraph().appendText(OS.str()); + calleeArgInfoToMarkupParagraph(Output.addParagraph()); } if (!Documentation.empty()) @@ -1487,49 +1644,12 @@ markup::Document HoverInfo::present() const { if (!Definition.empty()) { Output.addRuler(); - std::string Buffer; - - if (!Definition.empty()) { - // Append scope comment, dropping trailing "::". - // Note that we don't print anything for global namespace, to not annoy - // non-c++ projects or projects that are not making use of namespaces. - if (!LocalScope.empty()) { - // Container name, e.g. class, method, function. - // We might want to propagate some info about container type to print - // function foo, class X, method X::bar, etc. - Buffer += - "// In " + llvm::StringRef(LocalScope).rtrim(':').str() + '\n'; - } else if (NamespaceScope && !NamespaceScope->empty()) { - Buffer += "// In namespace " + - llvm::StringRef(*NamespaceScope).rtrim(':').str() + '\n'; - } - - if (!AccessSpecifier.empty()) { - Buffer += AccessSpecifier + ": "; - } - - Buffer += Definition; - } - - Output.addCodeBlock(Buffer, DefinitionLanguage); + definitionScopeToMarkup(Output); } if (!UsedSymbolNames.empty()) { Output.addRuler(); - markup::Paragraph &P = Output.addParagraph(); - P.appendText("provides "); - - const std::vector::size_type SymbolNamesLimit = 5; - auto Front = llvm::ArrayRef(UsedSymbolNames).take_front(SymbolNamesLimit); - - llvm::interleave( - Front, [&](llvm::StringRef Sym) { P.appendCode(Sym); }, - [&] { P.appendText(", "); }); - if (UsedSymbolNames.size() > Front.size()) { - P.appendText(" and "); - P.appendText(std::to_string(UsedSymbolNames.size() - Front.size())); - P.appendText(" more"); - } + usedSymbolNamesToMarkup(Output); } return Output; @@ -1538,21 +1658,19 @@ markup::Document HoverInfo::present() const { std::string HoverInfo::present(MarkupKind Kind) const { if (Kind == MarkupKind::Markdown) { const Config &Cfg = Config::current(); - if ((Cfg.Documentation.CommentFormat == - Config::CommentFormatPolicy::Markdown) || - (Cfg.Documentation.CommentFormat == - Config::CommentFormatPolicy::Doxygen)) - // If the user prefers Markdown, we use the present() method to generate - // the Markdown output. - return present().asMarkdown(); + if (Cfg.Documentation.CommentFormat == + Config::CommentFormatPolicy::Markdown) + return presentDefault().asMarkdown(); + if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen) + return presentDoxygen().asMarkdown(); if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::PlainText) // If the user prefers plain text, we use the present() method to generate // the plain text output. - return present().asEscapedMarkdown(); + return presentDefault().asEscapedMarkdown(); } - return present().asPlainText(); + return presentDefault().asPlainText(); } // If the backtick at `Offset` starts a probable quoted range, return the range diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h index 2f65431bd72de..614180a7b9846 100644 --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -74,6 +74,8 @@ struct HoverInfo { std::optional SymRange; index::SymbolKind Kind = index::SymbolKind::Unknown; std::string Documentation; + // required to create a comments::CommandTraits object without the ASTContext + CommentOptions CommentOpts; /// Source code containing the definition of the symbol. std::string Definition; const char *DefinitionLanguage = "cpp"; @@ -118,10 +120,23 @@ struct HoverInfo { // alphabetical order. std::vector UsedSymbolNames; - /// Produce a user-readable information. - markup::Document present() const; - + /// Produce a user-readable information based on the specified markup kind. std::string present(MarkupKind Kind) const; + +private: + void usedSymbolNamesToMarkup(markup::Document &Output) const; + void providerToMarkupParagraph(markup::Document &Output) const; + void definitionScopeToMarkup(markup::Document &Output) const; + void calleeArgInfoToMarkupParagraph(markup::Paragraph &P) const; + void valueToMarkupParagraph(markup::Paragraph &P) const; + void offsetToMarkupParagraph(markup::Paragraph &P) const; + void sizeToMarkupParagraph(markup::Paragraph &P) const; + + /// Parse and render the hover information as Doxygen documentation. + markup::Document presentDoxygen() const; + + /// Render the hover information as a default documentation. + markup::Document presentDefault() const; }; inline bool operator==(const HoverInfo::PrintedType &LHS, diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp new file mode 100644 index 0000000000000..dea637b9100da --- /dev/null +++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp @@ -0,0 +1,297 @@ +//===--- SymbolDocumentation.cpp ==-------------------------------*- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "SymbolDocumentation.h" + +#include "support/Markup.h" +#include "clang/AST/Comment.h" +#include "clang/AST/CommentCommandTraits.h" +#include "clang/AST/CommentVisitor.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/StringRef.h" + +namespace clang { +namespace clangd { +namespace { + +std::string commandMarkerAsString(comments::CommandMarkerKind CommandMarker) { + switch (CommandMarker) { + case comments::CommandMarkerKind::CMK_At: + return "@"; + case comments::CommandMarkerKind::CMK_Backslash: + return "\\"; + } + llvm_unreachable("Unknown command marker kind"); +} + +void commandToMarkup(markup::Paragraph &Out, StringRef Command, + comments::CommandMarkerKind CommandMarker, + StringRef Args) { + Out.appendBoldText(commandMarkerAsString(CommandMarker) + Command.str()); + if (!Args.empty()) { + Out.appendSpace(); + Out.appendEmphasizedText(Args.str()); + } +} +} // namespace + +class ParagraphToMarkupDocument + : public comments::ConstCommentVisitor { +public: + ParagraphToMarkupDocument(markup::Paragraph &Out, + const comments::CommandTraits &Traits) + : Out(Out), Traits(Traits) {} + + void visitParagraphComment(const comments::ParagraphComment *C) { + if (!C) + return; + + for (const auto *Child = C->child_begin(); Child != C->child_end(); + ++Child) { + visit(*Child); + } + } + + void visitTextComment(const comments::TextComment *C) { + // Always trim leading space after a newline. + StringRef Text = C->getText(); + if (LastChunkEndsWithNewline && C->getText().starts_with(' ')) + Text = Text.drop_front(); + + LastChunkEndsWithNewline = C->hasTrailingNewline(); + Out.appendText(Text.str() + (LastChunkEndsWithNewline ? "\n" : "")); + } + + void visitInlineCommandComment(const comments::InlineCommandComment *C) { + + if (C->getNumArgs() > 0) { + std::string ArgText; + for (unsigned I = 0; I < C->getNumArgs(); ++I) { + if (!ArgText.empty()) + ArgText += " "; + ArgText += C->getArgText(I); + } + + switch (C->getRenderKind()) { + case comments::InlineCommandRenderKind::Monospaced: + Out.appendCode(ArgText); + break; + case comments::InlineCommandRenderKind::Bold: + Out.appendBoldText(ArgText); + break; + case comments::InlineCommandRenderKind::Emphasized: + Out.appendEmphasizedText(ArgText); + break; + default: + commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(), + ArgText); + break; + } + } else { + if (C->getCommandName(Traits) == "n") { + // \n is a special case, it is used to create a new line. + Out.appendText(" \n"); + LastChunkEndsWithNewline = true; + return; + } + + commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(), + ""); + } + } + + void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) { + std::string TagText = "<" + STC->getTagName().str(); + + for (unsigned I = 0; I < STC->getNumAttrs(); ++I) { + const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I); + TagText += " " + Attr.Name.str() + "=\"" + Attr.Value.str() + "\""; + } + + if (STC->isSelfClosing()) + TagText += " /"; + TagText += ">"; + + LastChunkEndsWithNewline = STC->hasTrailingNewline(); + Out.appendText(TagText + (LastChunkEndsWithNewline ? "\n" : "")); + } + + void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) { + LastChunkEndsWithNewline = ETC->hasTrailingNewline(); + Out.appendText("getTagName().str() + ">" + + (LastChunkEndsWithNewline ? "\n" : "")); + } + +private: + markup::Paragraph &Out; + const comments::CommandTraits &Traits; + + /// If true, the next leading space after a new line is trimmed. + bool LastChunkEndsWithNewline = false; +}; + +class ParagraphToString + : public comments::ConstCommentVisitor { +public: + ParagraphToString(llvm::raw_string_ostream &Out, + const comments::CommandTraits &Traits) + : Out(Out), Traits(Traits) {} + + void visitParagraphComment(const comments::ParagraphComment *C) { + if (!C) + return; + + for (const auto *Child = C->child_begin(); Child != C->child_end(); + ++Child) { + visit(*Child); + } + } + + void visitTextComment(const comments::TextComment *C) { Out << C->getText(); } + + void visitInlineCommandComment(const comments::InlineCommandComment *C) { + Out << commandMarkerAsString(C->getCommandMarker()); + Out << C->getCommandName(Traits); + if (C->getNumArgs() > 0) { + for (unsigned I = 0; I < C->getNumArgs(); ++I) + Out << " " << C->getArgText(I); + } + Out << " "; + } + + void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) { + Out << "<" << STC->getTagName().str(); + + for (unsigned I = 0; I < STC->getNumAttrs(); ++I) { + const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I); + Out << " " << Attr.Name.str(); + if (!Attr.Value.str().empty()) + Out << "=\"" << Attr.Value.str() << "\""; + } + + if (STC->isSelfClosing()) + Out << " /"; + Out << ">"; + + Out << (STC->hasTrailingNewline() ? "\n" : ""); + } + + void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) { + Out << "getTagName().str() << ">" + << (ETC->hasTrailingNewline() ? "\n" : ""); + } + +private: + llvm::raw_string_ostream &Out; + const comments::CommandTraits &Traits; +}; + +class BlockCommentToMarkupDocument + : public comments::ConstCommentVisitor { +public: + BlockCommentToMarkupDocument(markup::Document &Out, + const comments::CommandTraits &Traits) + : Out(Out), Traits(Traits) {} + + void visitBlockCommandComment(const comments::BlockCommandComment *B) { + + switch (B->getCommandID()) { + case comments::CommandTraits::KCI_arg: + case comments::CommandTraits::KCI_li: + // \li and \arg are special cases, they are used to create a list item. + // In markdown it is a bullet list. + ParagraphToMarkupDocument(Out.addBulletList().addItem().addParagraph(), + Traits) + .visit(B->getParagraph()); + break; + default: { + // Some commands have arguments, like \throws. + // The arguments are not part of the paragraph. + // We need reconstruct them here. + std::string ArgText; + for (unsigned I = 0; I < B->getNumArgs(); ++I) { + if (!ArgText.empty()) + ArgText += " "; + ArgText += B->getArgText(I); + } + auto &P = Out.addParagraph(); + commandToMarkup(P, B->getCommandName(Traits), B->getCommandMarker(), + ArgText); + if (B->getParagraph() && !B->getParagraph()->isWhitespace()) { + // For commands with arguments, the paragraph starts after the first + // space. Therefore we need to append a space manually in this case. + if (!ArgText.empty()) + P.appendSpace(); + ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph()); + } + } + } + } + + void visitVerbatimBlockComment(const comments::VerbatimBlockComment *VB) { + commandToMarkup(Out.addParagraph(), VB->getCommandName(Traits), + VB->getCommandMarker(), ""); + + std::string VerbatimText; + + for (const auto *LI = VB->child_begin(); LI != VB->child_end(); ++LI) { + if (const auto *Line = cast(*LI)) { + VerbatimText += Line->getText().str() + "\n"; + } + } + + Out.addCodeBlock(VerbatimText, ""); + + commandToMarkup(Out.addParagraph(), VB->getCloseName(), + VB->getCommandMarker(), ""); + } + + void visitVerbatimLineComment(const comments::VerbatimLineComment *VL) { + auto &P = Out.addParagraph(); + commandToMarkup(P, VL->getCommandName(Traits), VL->getCommandMarker(), ""); + P.appendSpace().appendCode(VL->getText().str(), true).appendSpace(); + } + +private: + markup::Document &Out; + const comments::CommandTraits &Traits; + StringRef CommentEscapeMarker; +}; + +void SymbolDocCommentVisitor::parameterDocToMarkup(StringRef ParamName, + markup::Paragraph &Out) { + if (ParamName.empty()) + return; + + if (const auto *P = Parameters.lookup(ParamName)) { + ParagraphToMarkupDocument(Out, Traits).visit(P->getParagraph()); + } +} + +void SymbolDocCommentVisitor::parameterDocToString( + StringRef ParamName, llvm::raw_string_ostream &Out) { + if (ParamName.empty()) + return; + + if (const auto *P = Parameters.lookup(ParamName)) { + ParagraphToString(Out, Traits).visit(P->getParagraph()); + } +} + +void SymbolDocCommentVisitor::docToMarkup(markup::Document &Out) { + for (unsigned I = 0; I < CommentPartIndex; ++I) { + if (const auto *BC = BlockCommands.lookup(I)) { + BlockCommentToMarkupDocument(Out, Traits).visit(BC); + } else if (const auto *P = FreeParagraphs.lookup(I)) { + ParagraphToMarkupDocument(Out.addParagraph(), Traits).visit(P); + } + } +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h new file mode 100644 index 0000000000000..b5120ba04e8f1 --- /dev/null +++ b/clang-tools-extra/clangd/SymbolDocumentation.h @@ -0,0 +1,155 @@ +//===--- SymbolDocumentation.h ==---------------------------------*- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Class to parse doxygen comments into a flat structure for consumption +// in e.g. Hover and Code Completion +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H + +#include "support/Markup.h" +#include "clang/AST/Comment.h" +#include "clang/AST/CommentLexer.h" +#include "clang/AST/CommentParser.h" +#include "clang/AST/CommentSema.h" +#include "clang/AST/CommentVisitor.h" +#include "clang/Basic/SourceManager.h" +#include "llvm/Support/raw_ostream.h" +#include + +namespace clang { +namespace clangd { + +class SymbolDocCommentVisitor + : public comments::ConstCommentVisitor { +public: + SymbolDocCommentVisitor(comments::FullComment *FC, + const CommentOptions &CommentOpts) + : Traits(Allocator, CommentOpts), Allocator() { + if (!FC) + return; + + for (auto *Block : FC->getBlocks()) { + visit(Block); + } + } + + SymbolDocCommentVisitor(llvm::StringRef Documentation, + const CommentOptions &CommentOpts) + : Traits(Allocator, CommentOpts), Allocator() { + + if (Documentation.empty()) + return; + + CommentWithMarkers.reserve(Documentation.size() + + Documentation.count('\n') * 3); + + // The comment lexer expects doxygen markers, so add them back. + // We need to use the /// style doxygen markers because the comment could + // contain the closing the closing tag "*/" of a C Style "/** */" comment + // which would break the parsing if we would just enclose the comment text + // with "/** */". + CommentWithMarkers = "///"; + bool NewLine = true; + for (char C : Documentation) { + if (C == '\n') { + CommentWithMarkers += "\n///"; + NewLine = true; + } else { + if (NewLine && (C == '<')) { + // A comment line starting with '///<' is treated as a doxygen + // comment. Therefore add a space to separate the '<' from the comment + // marker. This allows to parse html tags at the beginning of a line + // and the escape marker prevents adding the artificial space in the + // markup documentation. The extra space will not be rendered, since + // we render it as markdown. + CommentWithMarkers += ' '; + } + CommentWithMarkers += C; + NewLine = false; + } + } + SourceManagerForFile SourceMgrForFile("mock_file.cpp", CommentWithMarkers); + + SourceManager &SourceMgr = SourceMgrForFile.get(); + // The doxygen Sema requires a Diagostics consumer, since it reports + // warnings e.g. when parameters are not documented correctly. These + // warnings are not relevant for us, so we can ignore them. + SourceMgr.getDiagnostics().setClient(new IgnoringDiagConsumer); + + comments::Sema S(Allocator, SourceMgr, SourceMgr.getDiagnostics(), Traits, + /*PP=*/nullptr); + comments::Lexer L(Allocator, SourceMgr.getDiagnostics(), Traits, + SourceMgr.getLocForStartOfFile(SourceMgr.getMainFileID()), + CommentWithMarkers.data(), + CommentWithMarkers.data() + CommentWithMarkers.size()); + comments::Parser P(L, S, Allocator, SourceMgr, SourceMgr.getDiagnostics(), + Traits); + comments::FullComment *FC = P.parseFullComment(); + + if (!FC) + return; + + for (auto *Block : FC->getBlocks()) { + visit(Block); + } + } + + bool isParameterDocumented(StringRef ParamName) const { + return Parameters.contains(ParamName); + } + + void parameterDocToMarkup(StringRef ParamName, markup::Paragraph &Out); + + void parameterDocToString(StringRef ParamName, llvm::raw_string_ostream &Out); + + void docToMarkup(markup::Document &Out); + + void visitBlockCommandComment(const comments::BlockCommandComment *B) { + BlockCommands[CommentPartIndex] = B; + CommentPartIndex++; + } + + void visitParagraphComment(const comments::ParagraphComment *P) { + FreeParagraphs[CommentPartIndex] = P; + CommentPartIndex++; + } + + void visitParamCommandComment(const comments::ParamCommandComment *P) { + Parameters[P->getParamNameAsWritten()] = P; + } + +private: + comments::CommandTraits Traits; + llvm::BumpPtrAllocator Allocator; + std::string CommentWithMarkers; + + /// Index to keep track of the order of the comments. + /// We want to rearange some commands like \\param. + /// This index allows us to keep the order of the other comment parts. + unsigned CommentPartIndex = 0; + + /// Parsed paragaph(s) of the "param" comamnd(s) + llvm::SmallDenseMap + Parameters; + + /// All the block commands. + llvm::SmallDenseMap + BlockCommands; + + /// All "free" text paragraphs. + llvm::SmallDenseMap + FreeParagraphs; +}; + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp index a13083026f26b..89bdc656d440f 100644 --- a/clang-tools-extra/clangd/support/Markup.cpp +++ b/clang-tools-extra/clangd/support/Markup.cpp @@ -363,7 +363,12 @@ class CodeBlock : public Block { void renderMarkdown(llvm::raw_ostream &OS) const override { std::string Marker = getMarkerForCodeBlock(Contents); // No need to pad from previous blocks, as they should end with a new line. - OS << Marker << Language << '\n' << Contents << '\n' << Marker << '\n'; + OS << Marker << Language << '\n' << Contents; + if (!Contents.empty() && Contents.back() != '\n') + OS << '\n'; + // Always end with an empty line to separate code blocks from following + // paragraphs. + OS << Marker << "\n\n"; } void renderPlainText(llvm::raw_ostream &OS) const override { diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt index d425070c7f3b7..9656eeaeb37ce 100644 --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -92,6 +92,7 @@ add_unittest(ClangdUnitTests ClangdTests SourceCodeTests.cpp StdLibTests.cpp SymbolCollectorTests.cpp + SymbolDocumentationTests.cpp SymbolInfoTests.cpp SyncAPI.cpp TUSchedulerTests.cpp diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 12d260db7ea11..3331164ab0024 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -3762,6 +3762,127 @@ provides Foo, Bar, Baz, Foobar, Qux and 1 more)"}}; } } +TEST(Hover, PresentDocumentation) { + struct { + const std::function Builder; + llvm::StringRef ExpectedRender; + } Cases[] = { + {[](HoverInfo &HI) { + HI.Kind = index::SymbolKind::Function; + HI.Documentation = "@brief brief doc\n\n" + "longer doc"; + HI.Definition = "void foo()"; + HI.Name = "foo"; + }, + R"(### function `foo` + +--- +**@brief** brief doc + +longer doc + +--- +```cpp +void foo() +```)"}, + {[](HoverInfo &HI) { + HI.Kind = index::SymbolKind::Function; + HI.Documentation = "@brief brief doc\n\n" + "longer doc"; + HI.Definition = "int foo()"; + HI.ReturnType = "int"; + HI.Name = "foo"; + }, + R"(### function `foo` + +--- +→ `int` + +**@brief** brief doc + +longer doc + +--- +```cpp +int foo() +```)"}, + {[](HoverInfo &HI) { + HI.Kind = index::SymbolKind::Function; + HI.Documentation = "@brief brief doc\n\n" + "longer doc\n@param a this is a param\n@return it " + "returns something"; + HI.Definition = "int foo(int a)"; + HI.ReturnType = "int"; + HI.Name = "foo"; + HI.Parameters.emplace(); + HI.Parameters->emplace_back(); + HI.Parameters->back().Type = "int"; + HI.Parameters->back().Name = "a"; + }, + R"(### function `foo` + +--- +→ `int` + +Parameters: + +- `int a` - this is a param + +**@brief** brief doc + +longer doc + +**@return** it returns something + +--- +```cpp +int foo(int a) +```)"}, + {[](HoverInfo &HI) { + HI.Kind = index::SymbolKind::Function; + HI.Documentation = "@brief brief doc\n\n" + "longer doc\n@param a this is a param\n@param b " + "does not exist\n@return it returns something"; + HI.Definition = "int foo(int a)"; + HI.ReturnType = "int"; + HI.Name = "foo"; + HI.Parameters.emplace(); + HI.Parameters->emplace_back(); + HI.Parameters->back().Type = "int"; + HI.Parameters->back().Name = "a"; + }, + R"(### function `foo` + +--- +→ `int` + +Parameters: + +- `int a` - this is a param + +**@brief** brief doc + +longer doc + +**@return** it returns something + +--- +```cpp +int foo(int a) +```)"}, + }; + + for (const auto &C : Cases) { + HoverInfo HI; + C.Builder(HI); + Config Cfg; + Cfg.Hover.ShowAKA = true; + Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::Doxygen; + WithContextValue WithCfg(Config::Key, std::move(Cfg)); + EXPECT_EQ(HI.present(MarkupKind::Markdown), C.ExpectedRender); + } +} + TEST(Hover, ParseDocumentation) { struct Case { llvm::StringRef Documentation; @@ -4339,6 +4460,149 @@ constexpr u64 pow_with_mod(u64 a, u64 b, u64 p) { EXPECT_TRUE(H->Value); EXPECT_TRUE(H->Type); } + +TEST(Hover, FunctionParameters) { + struct { + const char *const Code; + const std::function ExpectedBuilder; + std::string ExpectedRender; + } Cases[] = { + {R"cpp(/// Function doc + void foo(int [[^a]]); + )cpp", + [](HoverInfo &HI) { + HI.Name = "a"; + HI.Kind = index::SymbolKind::Parameter; + HI.NamespaceScope = ""; + HI.LocalScope = "foo::"; + HI.Type = "int"; + HI.Definition = "int a"; + HI.Documentation = ""; + }, + "### param `a`\n\n---\nType: `int`\n\n---\n```cpp\n// In foo\nint " + "a\n```"}, + {R"cpp(/// Function doc + /// @param a this is doc for a + void foo(int [[^a]]); + )cpp", + [](HoverInfo &HI) { + HI.Name = "a"; + HI.Kind = index::SymbolKind::Parameter; + HI.NamespaceScope = ""; + HI.LocalScope = "foo::"; + HI.Type = "int"; + HI.Definition = "int a"; + HI.Documentation = "this is doc for a"; + }, + "### param `a`\n\n---\nType: `int`\n\nthis is doc for " + "a\n\n---\n```cpp\n// In foo\nint a\n```"}, + {R"cpp(/// Function doc + /// @param b this is doc for b + void foo(int [[^a]], int b); + )cpp", + [](HoverInfo &HI) { + HI.Name = "a"; + HI.Kind = index::SymbolKind::Parameter; + HI.NamespaceScope = ""; + HI.LocalScope = "foo::"; + HI.Type = "int"; + HI.Definition = "int a"; + HI.Documentation = ""; + }, + "### param `a`\n\n---\nType: `int`\n\n---\n```cpp\n// In foo\nint " + "a\n```"}, + {R"cpp(/// Function doc + /// @param b this is doc for \p b + void foo(int a, int [[^b]]); + )cpp", + [](HoverInfo &HI) { + HI.Name = "b"; + HI.Kind = index::SymbolKind::Parameter; + HI.NamespaceScope = ""; + HI.LocalScope = "foo::"; + HI.Type = "int"; + HI.Definition = "int b"; + HI.Documentation = "this is doc for \\p b"; + }, + "### param `b`\n\n---\nType: `int`\n\nthis is doc for " + "`b`\n\n---\n```cpp\n// In foo\nint b\n```"}, + {R"cpp(/// Function doc + /// @param b this is doc for \p b + template + void foo(T a, T [[^b]]); + )cpp", + [](HoverInfo &HI) { + HI.Name = "b"; + HI.Kind = index::SymbolKind::Parameter; + HI.NamespaceScope = ""; + HI.LocalScope = "foo::"; + HI.Type = "T"; + HI.Definition = "T b"; + HI.Documentation = "this is doc for \\p b"; + }, + "### param `b`\n\n---\nType: `T`\n\nthis is doc for " + "`b`\n\n---\n```cpp\n// In foo\nT b\n```"}, + {R"cpp(/// Function doc + /// @param b this is doc for \p b + void foo(int a, int [[^b]]); + )cpp", + [](HoverInfo &HI) { + HI.Name = "b"; + HI.Kind = index::SymbolKind::Parameter; + HI.NamespaceScope = ""; + HI.LocalScope = "foo::"; + HI.Type = "int"; + HI.Definition = "int b"; + HI.Documentation = + "this is doc for \\p b"; + }, + "### param `b`\n\n---\nType: `int`\n\nthis is \\doc\\ " + "\\ \\for\\ " + "`b`\n\n---\n```cpp\n// In foo\nint b\n```"}, + }; + + // Create a tiny index, so tests above can verify documentation is fetched. + Symbol IndexSym = func("indexSymbol"); + IndexSym.Documentation = "comment from index"; + SymbolSlab::Builder Symbols; + Symbols.insert(IndexSym); + auto Index = + MemIndex::build(std::move(Symbols).build(), RefSlab(), RelationSlab()); + + for (const auto &Case : Cases) { + SCOPED_TRACE(Case.Code); + + Annotations T(Case.Code); + TestTU TU = TestTU::withCode(T.code()); + auto AST = TU.build(); + Config Cfg; + Cfg.Hover.ShowAKA = true; + Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::Doxygen; + WithContextValue WithCfg(Config::Key, std::move(Cfg)); + auto H = getHover(AST, T.point(), format::getLLVMStyle(), Index.get()); + ASSERT_TRUE(H); + HoverInfo Expected; + Expected.SymRange = T.range(); + Case.ExpectedBuilder(Expected); + + EXPECT_EQ(H->present(MarkupKind::Markdown), Case.ExpectedRender); + EXPECT_EQ(H->NamespaceScope, Expected.NamespaceScope); + EXPECT_EQ(H->LocalScope, Expected.LocalScope); + EXPECT_EQ(H->Name, Expected.Name); + EXPECT_EQ(H->Kind, Expected.Kind); + EXPECT_EQ(H->Documentation, Expected.Documentation); + EXPECT_EQ(H->Definition, Expected.Definition); + EXPECT_EQ(H->Type, Expected.Type); + EXPECT_EQ(H->ReturnType, Expected.ReturnType); + EXPECT_EQ(H->Parameters, Expected.Parameters); + EXPECT_EQ(H->TemplateParameters, Expected.TemplateParameters); + EXPECT_EQ(H->SymRange, Expected.SymRange); + EXPECT_EQ(H->Value, Expected.Value); + } +} + } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp new file mode 100644 index 0000000000000..69eb13b2142d2 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp @@ -0,0 +1,161 @@ +//===-- SymbolDocumentationTests.cpp --------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "SymbolDocumentation.h" + +#include "support/Markup.h" +#include "clang/Basic/CommentOptions.h" +#include "llvm/ADT/StringRef.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +TEST(SymbolDocumentation, Parse) { + + CommentOptions CommentOpts; + + struct Case { + llvm::StringRef Documentation; + llvm::StringRef ExpectedRenderEscapedMarkdown; + llvm::StringRef ExpectedRenderMarkdown; + llvm::StringRef ExpectedRenderPlainText; + } Cases[] = { + { + "foo bar", + "foo bar", + "foo bar", + "foo bar", + }, + { + "foo\nbar\n", + "foo\nbar", + "foo\nbar", + "foo bar", + }, + { + "foo\n\nbar\n", + "foo\n\nbar", + "foo\n\nbar", + "foo\n\nbar", + }, + { + "foo \\p bar baz", + "foo `bar` baz", + "foo `bar` baz", + "foo bar baz", + }, + { + "foo \\e bar baz", + "foo \\*bar\\* baz", + "foo *bar* baz", + "foo *bar* baz", + }, + { + "foo \\b bar baz", + "foo \\*\\*bar\\*\\* baz", + "foo **bar** baz", + "foo **bar** baz", + }, + { + "foo \\ref bar baz", + "foo \\*\\*\\\\ref\\*\\* \\*bar\\* baz", + "foo **\\ref** *bar* baz", + "foo **\\ref** *bar* baz", + }, + { + "foo @ref bar baz", + "foo \\*\\*@ref\\*\\* \\*bar\\* baz", + "foo **@ref** *bar* baz", + "foo **@ref** *bar* baz", + }, + { + "\\brief this is a \\n\nbrief description", + "\\*\\*\\\\brief\\*\\* this is a \nbrief description", + "**\\brief** this is a \nbrief description", + "**\\brief** this is a\nbrief description", + }, + { + "\\throw exception foo", + "\\*\\*\\\\throw\\*\\* \\*exception\\* foo", + "**\\throw** *exception* foo", + "**\\throw** *exception* foo", + }, + { + "\\brief this is a brief description\n\n\\li item 1\n\\li item " + "2\n\\arg item 3", + "\\*\\*\\\\brief\\*\\* this is a brief description\n\n- item 1\n\n- " + "item " + "2\n\n- " + "item 3", + "**\\brief** this is a brief description\n\n- item 1\n\n- item " + "2\n\n- " + "item 3", + "**\\brief** this is a brief description\n\n- item 1\n\n- item " + "2\n\n- " + "item 3", + }, + { + "\\defgroup mygroup this is a group\nthis is not a group description", + "\\*\\*@defgroup\\*\\* `mygroup this is a group`\n\nthis is not a " + "group " + "description", + "**@defgroup** `mygroup this is a group`\n\nthis is not a group " + "description", + "**@defgroup** `mygroup this is a group`\n\nthis is not a group " + "description", + }, + { + "\\verbatim\nthis is a\nverbatim block containing\nsome verbatim " + "text\n\\endverbatim", + "\\*\\*@verbatim\\*\\*\n\n```\nthis is a\nverbatim block " + "containing\nsome " + "verbatim text\n```\n\n\\*\\*@endverbatim\\*\\*", + "**@verbatim**\n\n```\nthis is a\nverbatim block containing\nsome " + "verbatim text\n```\n\n**@endverbatim**", + "**@verbatim**\n\nthis is a\nverbatim block containing\nsome " + "verbatim text\n\n**@endverbatim**", + }, + { + "@param foo this is a parameter\n@param bar this is another " + "parameter", + "", + "", + "", + }, + { + "@brief brief docs\n\n@param foo this is a parameter\n\nMore " + "description\ndocumentation", + "\\*\\*@brief\\*\\* brief docs\n\nMore description\ndocumentation", + "**@brief** brief docs\n\nMore description\ndocumentation", + "**@brief** brief docs\n\nMore description documentation", + }, + { + "this is a bold text\nnormal text\nthis is an italic " + "text\nthis is a code block", + "\\this is a bold text\\\nnormal text\n\\this is an italic " + "text\\\n\\this is a code block\\", + "\\this is a bold text\\\nnormal text\n\\this is an italic " + "text\\\n\\this is a code block\\", + "this is a bold text normal text this is an italic " + "text this is a code block", + }, + }; + for (const auto &C : Cases) { + markup::Document Doc; + SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts); + + SymbolDoc.docToMarkup(Doc); + + EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText); + EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown); + EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown); + } +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp index 482f230fb86fe..5f91f31557176 100644 --- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp +++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp @@ -463,6 +463,7 @@ TEST(Document, Separators) { ```cpp test ``` + bar)md"; EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); @@ -559,6 +560,7 @@ foo bar baz ``` + ```cpp foo ```)md"; @@ -571,6 +573,12 @@ foo foo)pt"; EXPECT_EQ(D.asPlainText(), ExpectedPlainText); + + Document D2; + D2.addCodeBlock(""); + EXPECT_EQ(D2.asEscapedMarkdown(), "```cpp\n```"); + EXPECT_EQ(D2.asMarkdown(), "```cpp\n```"); + EXPECT_EQ(D2.asPlainText(), ""); } TEST(BulletList, Render) { diff --git a/clang/include/clang/AST/Comment.h b/clang/include/clang/AST/Comment.h index dd9906727293f..5ba95c8291d38 100644 --- a/clang/include/clang/AST/Comment.h +++ b/clang/include/clang/AST/Comment.h @@ -19,6 +19,7 @@ #include "clang/Basic/SourceLocation.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/StringRef.h" +#include "llvm/Support/Compiler.h" namespace clang { class Decl; @@ -119,6 +120,11 @@ class Comment { LLVM_PREFERRED_TYPE(CommandTraits::KnownCommandIDs) unsigned CommandID : CommandInfo::NumCommandIDBits; + + /// Describes the syntax that was used in a documentation command. + /// Contains values from CommandMarkerKind enum. + LLVM_PREFERRED_TYPE(CommandMarkerKind) + unsigned CommandMarker : 1; }; enum { NumInlineCommandCommentBits = NumInlineContentCommentBits + 3 + CommandInfo::NumCommandIDBits }; @@ -347,6 +353,16 @@ class InlineCommandComment : public InlineContentComment { InlineCommandCommentBits.RenderKind = llvm::to_underlying(RK); InlineCommandCommentBits.CommandID = CommandID; } + InlineCommandComment(SourceLocation LocBegin, SourceLocation LocEnd, + unsigned CommandID, InlineCommandRenderKind RK, + CommandMarkerKind CommandMarker, ArrayRef Args) + : InlineContentComment(CommentKind::InlineCommandComment, LocBegin, + LocEnd), + Args(Args) { + InlineCommandCommentBits.RenderKind = llvm::to_underlying(RK); + InlineCommandCommentBits.CommandID = CommandID; + InlineCommandCommentBits.CommandMarker = llvm::to_underlying(CommandMarker); + } static bool classof(const Comment *C) { return C->getCommentKind() == CommentKind::InlineCommandComment; @@ -384,6 +400,11 @@ class InlineCommandComment : public InlineContentComment { SourceRange getArgRange(unsigned Idx) const { return Args[Idx].Range; } + + CommandMarkerKind getCommandMarker() const { + return static_cast( + InlineCommandCommentBits.CommandMarker); + } }; /// Abstract class for opening and closing HTML tags. HTML tags are always diff --git a/clang/include/clang/AST/CommentSema.h b/clang/include/clang/AST/CommentSema.h index 916d7945329c5..3169e2b0d86b9 100644 --- a/clang/include/clang/AST/CommentSema.h +++ b/clang/include/clang/AST/CommentSema.h @@ -131,6 +131,7 @@ class Sema { InlineCommandComment *actOnInlineCommand(SourceLocation CommandLocBegin, SourceLocation CommandLocEnd, unsigned CommandID, + CommandMarkerKind CommandMarker, ArrayRef Args); InlineContentComment *actOnUnknownCommand(SourceLocation LocBegin, diff --git a/clang/lib/AST/CommentParser.cpp b/clang/lib/AST/CommentParser.cpp index e61846d241915..2e5821a8e4436 100644 --- a/clang/lib/AST/CommentParser.cpp +++ b/clang/lib/AST/CommentParser.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "clang/AST/CommentParser.h" +#include "clang/AST/Comment.h" #include "clang/AST/CommentCommandTraits.h" #include "clang/AST/CommentSema.h" #include "clang/Basic/CharInfo.h" @@ -569,6 +570,8 @@ BlockCommandComment *Parser::parseBlockCommand() { InlineCommandComment *Parser::parseInlineCommand() { assert(Tok.is(tok::backslash_command) || Tok.is(tok::at_command)); + CommandMarkerKind CMK = + Tok.is(tok::backslash_command) ? CMK_Backslash : CMK_At; const CommandInfo *Info = Traits.getCommandInfo(Tok.getCommandID()); const Token CommandTok = Tok; @@ -580,7 +583,7 @@ InlineCommandComment *Parser::parseInlineCommand() { InlineCommandComment *IC = S.actOnInlineCommand( CommandTok.getLocation(), CommandTok.getEndLocation(), - CommandTok.getCommandID(), Args); + CommandTok.getCommandID(), CMK, Args); if (Args.size() < Info->NumArgs) { Diag(CommandTok.getEndLocation().getLocWithOffset(1), diff --git a/clang/lib/AST/CommentSema.cpp b/clang/lib/AST/CommentSema.cpp index 88520d7940e34..c02983b03163f 100644 --- a/clang/lib/AST/CommentSema.cpp +++ b/clang/lib/AST/CommentSema.cpp @@ -363,12 +363,13 @@ void Sema::actOnTParamCommandFinish(TParamCommandComment *Command, InlineCommandComment * Sema::actOnInlineCommand(SourceLocation CommandLocBegin, SourceLocation CommandLocEnd, unsigned CommandID, + CommandMarkerKind CommandMarker, ArrayRef Args) { StringRef CommandName = Traits.getCommandInfo(CommandID)->Name; - return new (Allocator) - InlineCommandComment(CommandLocBegin, CommandLocEnd, CommandID, - getInlineCommandRenderKind(CommandName), Args); + return new (Allocator) InlineCommandComment( + CommandLocBegin, CommandLocEnd, CommandID, + getInlineCommandRenderKind(CommandName), CommandMarker, Args); } InlineContentComment *Sema::actOnUnknownCommand(SourceLocation LocBegin, diff --git a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn index b609d4a7462fb..f8c4838ab7ee3 100644 --- a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn +++ b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn @@ -122,6 +122,7 @@ static_library("clangd") { "SemanticHighlighting.cpp", "SemanticSelection.cpp", "SourceCode.cpp", + "SymbolDocumentation.cpp", "SystemIncludeExtractor.cpp", "TUScheduler.cpp", "TidyProvider.cpp",