https://github.com/tcottin updated https://github.com/llvm/llvm-project/pull/140498
>From 8fadd8d51fa3d96c7fb82b9d749ef3f35441ac64 Mon Sep 17 00:00:00 2001 From: Tim Cottin <timcot...@gmx.de> Date: Mon, 19 May 2025 06:26:36 +0000 Subject: [PATCH 1/3] [clangd] Improve Markup Rendering --- clang-tools-extra/clangd/Hover.cpp | 81 +----- clang-tools-extra/clangd/support/Markup.cpp | 252 ++++++++++-------- clang-tools-extra/clangd/support/Markup.h | 32 ++- .../clangd/test/signature-help.test | 4 +- .../clangd/unittests/CodeCompleteTests.cpp | 8 +- .../clangd/unittests/HoverTests.cpp | 75 ++++-- .../clangd/unittests/support/MarkupTests.cpp | 214 +++++++++++---- 7 files changed, 410 insertions(+), 256 deletions(-) diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index 3ab3d89030520..88755733aa67c 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -960,42 +960,6 @@ std::optional<HoverInfo> getHoverContents(const Attr *A, ParsedAST &AST) { return HI; } -bool isParagraphBreak(llvm::StringRef Rest) { - return Rest.ltrim(" \t").starts_with("\n"); -} - -bool punctuationIndicatesLineBreak(llvm::StringRef Line) { - constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt"; - - Line = Line.rtrim(); - return !Line.empty() && Punctuation.contains(Line.back()); -} - -bool isHardLineBreakIndicator(llvm::StringRef Rest) { - // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote, - // '#' headings, '`' code blocks - constexpr llvm::StringLiteral LinebreakIndicators = R"txt(-*@\>#`)txt"; - - Rest = Rest.ltrim(" \t"); - if (Rest.empty()) - return false; - - if (LinebreakIndicators.contains(Rest.front())) - return true; - - if (llvm::isDigit(Rest.front())) { - llvm::StringRef AfterDigit = Rest.drop_while(llvm::isDigit); - if (AfterDigit.starts_with(".") || AfterDigit.starts_with(")")) - return true; - } - return false; -} - -bool isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest) { - // Should we also consider whether Line is short? - return punctuationIndicatesLineBreak(Line) || isHardLineBreakIndicator(Rest); -} - void addLayoutInfo(const NamedDecl &ND, HoverInfo &HI) { if (ND.isInvalidDecl()) return; @@ -1601,51 +1565,32 @@ std::optional<llvm::StringRef> getBacktickQuoteRange(llvm::StringRef Line, return Line.slice(Offset, Next + 1); } -void parseDocumentationLine(llvm::StringRef Line, markup::Paragraph &Out) { +void parseDocumentationParagraph(llvm::StringRef Text, markup::Paragraph &Out) { // Probably this is appendText(Line), but scan for something interesting. - for (unsigned I = 0; I < Line.size(); ++I) { - switch (Line[I]) { + for (unsigned I = 0; I < Text.size(); ++I) { + switch (Text[I]) { case '`': - if (auto Range = getBacktickQuoteRange(Line, I)) { - Out.appendText(Line.substr(0, I)); + if (auto Range = getBacktickQuoteRange(Text, I)) { + Out.appendText(Text.substr(0, I)); Out.appendCode(Range->trim("`"), /*Preserve=*/true); - return parseDocumentationLine(Line.substr(I + Range->size()), Out); + return parseDocumentationParagraph(Text.substr(I + Range->size()), Out); } break; } } - Out.appendText(Line).appendSpace(); + Out.appendText(Text); } void parseDocumentation(llvm::StringRef Input, markup::Document &Output) { - std::vector<llvm::StringRef> ParagraphLines; - auto FlushParagraph = [&] { - if (ParagraphLines.empty()) - return; - auto &P = Output.addParagraph(); - for (llvm::StringRef Line : ParagraphLines) - parseDocumentationLine(Line, P); - ParagraphLines.clear(); - }; + llvm::StringRef Paragraph, Rest; + for (std::tie(Paragraph, Rest) = Input.split("\n\n"); + !(Paragraph.empty() && Rest.empty()); + std::tie(Paragraph, Rest) = Rest.split("\n\n")) { - llvm::StringRef Line, Rest; - for (std::tie(Line, Rest) = Input.split('\n'); - !(Line.empty() && Rest.empty()); - std::tie(Line, Rest) = Rest.split('\n')) { - - // After a linebreak remove spaces to avoid 4 space markdown code blocks. - // FIXME: make FlushParagraph handle this. - Line = Line.ltrim(); - if (!Line.empty()) - ParagraphLines.push_back(Line); - - if (isParagraphBreak(Rest) || isHardLineBreakAfter(Line, Rest)) { - FlushParagraph(); - } + if (!Paragraph.empty()) + parseDocumentationParagraph(Paragraph, Output.addParagraph()); } - FlushParagraph(); } - llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const HoverInfo::PrintedType &T) { OS << T.Type; diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp index 63aff96b02056..b1e6252e473f5 100644 --- a/clang-tools-extra/clangd/support/Markup.cpp +++ b/clang-tools-extra/clangd/support/Markup.cpp @@ -11,7 +11,6 @@ #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" -#include "llvm/Support/Compiler.h" #include "llvm/Support/raw_ostream.h" #include <cstddef> #include <iterator> @@ -56,80 +55,28 @@ bool looksLikeTag(llvm::StringRef Contents) { return true; // Potentially incomplete tag. } -// Tests whether C should be backslash-escaped in markdown. -// The string being escaped is Before + C + After. This is part of a paragraph. -// StartsLine indicates whether `Before` is the start of the line. -// After may not be everything until the end of the line. -// -// It's always safe to escape punctuation, but want minimal escaping. -// The strategy is to escape the first character of anything that might start -// a markdown grammar construct. -bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, - bool StartsLine) { - assert(Before.take_while(llvm::isSpace).empty()); - auto RulerLength = [&]() -> /*Length*/ unsigned { - if (!StartsLine || !Before.empty()) - return false; - llvm::StringRef A = After.rtrim(); - return llvm::all_of(A, [C](char D) { return C == D; }) ? 1 + A.size() : 0; - }; - auto IsBullet = [&]() { - return StartsLine && Before.empty() && - (After.empty() || After.starts_with(" ")); - }; - auto SpaceSurrounds = [&]() { - return (After.empty() || llvm::isSpace(After.front())) && - (Before.empty() || llvm::isSpace(Before.back())); - }; - auto WordSurrounds = [&]() { - return (!After.empty() && llvm::isAlnum(After.front())) && - (!Before.empty() && llvm::isAlnum(Before.back())); - }; - +/// \brief Tests whether \p C should be backslash-escaped in markdown. +/// +/// The MarkupContent LSP specification defines that `markdown` content needs to +/// follow GFM (GitHub Flavored Markdown) rules. And we can assume that markdown +/// is rendered on the client side. This means we do not need to escape any +/// markdown constructs. +/// The only exception is when the client does not support HTML rendering in +/// markdown. In that case, we need to escape HTML tags and HTML entities. +/// +/// **FIXME:** handle the case when the client does support HTML rendering in +/// markdown. For this, the LSP server needs to check the +/// [supportsHtml capability](https://github.com/microsoft/language-server-protocol/issues/1344) +/// of the client. +/// +/// \param C The character to check. +/// \param After The string that follows \p C . This is used to determine if \p C is +/// part of a tag or an entity reference. +/// \returns true if \p C should be escaped, false otherwise. +bool needsLeadingEscape(char C, llvm::StringRef After) { switch (C) { - case '\\': // Escaped character. - return true; - case '`': // Code block or inline code - // Any number of backticks can delimit an inline code block that can end - // anywhere (including on another line). We must escape them all. - return true; - case '~': // Code block - return StartsLine && Before.empty() && After.starts_with("~~"); - case '#': { // ATX heading. - if (!StartsLine || !Before.empty()) - return false; - llvm::StringRef Rest = After.ltrim(C); - return Rest.empty() || Rest.starts_with(" "); - } - case ']': // Link or link reference. - // We escape ] rather than [ here, because it's more constrained: - // ](...) is an in-line link - // ]: is a link reference - // The following are only links if the link reference exists: - // ] by itself is a shortcut link - // ][...] is an out-of-line link - // Because we never emit link references, we don't need to handle these. - return After.starts_with(":") || After.starts_with("("); - case '=': // Setex heading. - return RulerLength() > 0; - case '_': // Horizontal ruler or matched delimiter. - if (RulerLength() >= 3) - return true; - // Not a delimiter if surrounded by space, or inside a word. - // (The rules at word boundaries are subtle). - return !(SpaceSurrounds() || WordSurrounds()); - case '-': // Setex heading, horizontal ruler, or bullet. - if (RulerLength() > 0) - return true; - return IsBullet(); - case '+': // Bullet list. - return IsBullet(); - case '*': // Bullet list, horizontal ruler, or delimiter. - return IsBullet() || RulerLength() >= 3 || !SpaceSurrounds(); case '<': // HTML tag (or autolink, which we choose not to escape) return looksLikeTag(After); - case '>': // Quote marker. Needs escaping at start of line. - return StartsLine && Before.empty(); case '&': { // HTML entity reference auto End = After.find(';'); if (End == llvm::StringRef::npos) @@ -142,10 +89,6 @@ bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, } return llvm::all_of(Content, llvm::isAlpha); } - case '.': // Numbered list indicator. Escape 12. -> 12\. at start of line. - case ')': - return StartsLine && !Before.empty() && - llvm::all_of(Before, llvm::isDigit) && After.starts_with(" "); default: return false; } @@ -156,8 +99,7 @@ bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, std::string renderText(llvm::StringRef Input, bool StartsLine) { std::string R; for (unsigned I = 0; I < Input.size(); ++I) { - if (needsLeadingEscape(Input[I], Input.substr(0, I), Input.substr(I + 1), - StartsLine)) + if (needsLeadingEscape(Input[I], Input.substr(I + 1))) R.push_back('\\'); R.push_back(Input[I]); } @@ -303,11 +245,12 @@ class CodeBlock : public Block { std::string indentLines(llvm::StringRef Input) { assert(!Input.ends_with("\n") && "Input should've been trimmed."); std::string IndentedR; - // We'll add 2 spaces after each new line. + // We'll add 2 spaces after each new line which is not followed by another new line. IndentedR.reserve(Input.size() + Input.count('\n') * 2); - for (char C : Input) { + for (size_t I = 0; I < Input.size(); ++I) { + char C = Input[I]; IndentedR += C; - if (C == '\n') + if (C == '\n' && (((I + 1) < Input.size()) && (Input[I + 1] != '\n'))) IndentedR.append(" "); } return IndentedR; @@ -348,20 +291,24 @@ void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const { if (C.SpaceBefore || NeedsSpace) OS << " "; switch (C.Kind) { - case Chunk::PlainText: + case ChunkKind::PlainText: OS << renderText(C.Contents, !HasChunks); break; - case Chunk::InlineCode: + case ChunkKind::InlineCode: OS << renderInlineBlock(C.Contents); break; + case ChunkKind::Bold: + OS << "**" << renderText(C.Contents, !HasChunks) << "**"; + break; + case ChunkKind::Emphasized: + OS << "*" << renderText(C.Contents, !HasChunks) << "*"; + break; } HasChunks = true; NeedsSpace = C.SpaceAfter; } - // Paragraphs are translated into markdown lines, not markdown paragraphs. - // Therefore it only has a single linebreak afterwards. - // VSCode requires two spaces at the end of line to start a new one. - OS << " \n"; + // A paragraph in markdown is separated by a blank line. + OS << "\n\n"; } std::unique_ptr<Block> Paragraph::clone() const { @@ -370,8 +317,8 @@ std::unique_ptr<Block> Paragraph::clone() const { /// Choose a marker to delimit `Text` from a prioritized list of options. /// This is more readable than escaping for plain-text. -llvm::StringRef chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, - llvm::StringRef Text) { +llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, + llvm::StringRef Text) const { // Prefer a delimiter whose characters don't appear in the text. for (llvm::StringRef S : Options) if (Text.find_first_of(S) == llvm::StringRef::npos) @@ -379,18 +326,94 @@ llvm::StringRef chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, return Options.front(); } +bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line) const{ + constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt"; + + Line = Line.rtrim(); + return !Line.empty() && Punctuation.contains(Line.back()); +} + +bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest) const { + // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote, + // '#' headings, '`' code blocks, two spaces (markdown force newline) + constexpr llvm::StringLiteral LinebreakIndicators = R"txt(-*@\>#`)txt"; + + Rest = Rest.ltrim(" \t"); + if (Rest.empty()) + return false; + + if (LinebreakIndicators.contains(Rest.front())) + return true; + + if (llvm::isDigit(Rest.front())) { + llvm::StringRef AfterDigit = Rest.drop_while(llvm::isDigit); + if (AfterDigit.starts_with(".") || AfterDigit.starts_with(")")) + return true; + } + return false; +} + +bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line, + llvm::StringRef Rest) const { + // In Markdown, 2 spaces before a line break forces a line break. + // Add a line break for plaintext in this case too. + // Should we also consider whether Line is short? + return Line.ends_with(" ") || punctuationIndicatesLineBreak(Line) || + isHardLineBreakIndicator(Rest); +} + void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { bool NeedsSpace = false; + std::string ConcatenatedText; + llvm::raw_string_ostream ConcatenatedOS(ConcatenatedText); + for (auto &C : Chunks) { + + if (C.Kind == ChunkKind::PlainText) { + if (C.SpaceBefore || NeedsSpace) + ConcatenatedOS << ' '; + + ConcatenatedOS << C.Contents; + NeedsSpace = llvm::isSpace(C.Contents.back()) || C.SpaceAfter; + continue; + } + if (C.SpaceBefore || NeedsSpace) - OS << " "; + ConcatenatedOS << ' '; llvm::StringRef Marker = ""; - if (C.Preserve && C.Kind == Chunk::InlineCode) + if (C.Preserve && C.Kind == ChunkKind::InlineCode) Marker = chooseMarker({"`", "'", "\""}, C.Contents); - OS << Marker << C.Contents << Marker; + else if (C.Kind == ChunkKind::Bold) + Marker = "**"; + else if (C.Kind == ChunkKind::Emphasized) + Marker = "*"; + ConcatenatedOS << Marker << C.Contents << Marker; NeedsSpace = C.SpaceAfter; } - OS << '\n'; + + // We go through the contents line by line to handle the newlines + // and required spacing correctly. + llvm::StringRef Line, Rest; + + for (std::tie(Line, Rest) = + llvm::StringRef(ConcatenatedText).trim().split('\n'); + !(Line.empty() && Rest.empty()); + std::tie(Line, Rest) = Rest.split('\n')) { + + Line = Line.ltrim(); + if (Line.empty()) + continue; + + OS << canonicalizeSpaces(Line); + + if (isHardLineBreakAfter(Line, Rest)) + OS << '\n'; + else if (!Rest.empty()) + OS << ' '; + } + + // Paragraphs are separated by a blank line. + OS << "\n\n"; } BulletList::BulletList() = default; @@ -398,12 +421,13 @@ BulletList::~BulletList() = default; void BulletList::renderMarkdown(llvm::raw_ostream &OS) const { for (auto &D : Items) { + std::string M = D.asMarkdown(); // Instead of doing this we might prefer passing Indent to children to get // rid of the copies, if it turns out to be a bottleneck. - OS << "- " << indentLines(D.asMarkdown()) << '\n'; + OS << "- " << indentLines(M) << '\n'; } // We need a new line after list to terminate it in markdown. - OS << '\n'; + OS << "\n\n"; } void BulletList::renderPlainText(llvm::raw_ostream &OS) const { @@ -412,6 +436,7 @@ void BulletList::renderPlainText(llvm::raw_ostream &OS) const { // rid of the copies, if it turns out to be a bottleneck. OS << "- " << indentLines(D.asPlainText()) << '\n'; } + OS << '\n'; } Paragraph &Paragraph::appendSpace() { @@ -420,29 +445,44 @@ Paragraph &Paragraph::appendSpace() { return *this; } -Paragraph &Paragraph::appendText(llvm::StringRef Text) { - std::string Norm = canonicalizeSpaces(Text); - if (Norm.empty()) +Paragraph &Paragraph::appendChunk(llvm::StringRef Contents, ChunkKind K) { + if (Contents.empty()) return *this; Chunks.emplace_back(); Chunk &C = Chunks.back(); - C.Contents = std::move(Norm); - C.Kind = Chunk::PlainText; - C.SpaceBefore = llvm::isSpace(Text.front()); - C.SpaceAfter = llvm::isSpace(Text.back()); + C.Contents = std::move(Contents); + C.Kind = K; return *this; } +Paragraph &Paragraph::appendText(llvm::StringRef Text) { + if (!Chunks.empty() && Chunks.back().Kind == ChunkKind::PlainText) { + Chunks.back().Contents += std::move(Text); + return *this; + } + + return appendChunk(Text, ChunkKind::PlainText); +} + +Paragraph &Paragraph::appendEmphasizedText(llvm::StringRef Text) { + return appendChunk(canonicalizeSpaces(std::move(Text)), + ChunkKind::Emphasized); +} + +Paragraph &Paragraph::appendBoldText(llvm::StringRef Text) { + return appendChunk(canonicalizeSpaces(std::move(Text)), ChunkKind::Bold); +} + Paragraph &Paragraph::appendCode(llvm::StringRef Code, bool Preserve) { bool AdjacentCode = - !Chunks.empty() && Chunks.back().Kind == Chunk::InlineCode; + !Chunks.empty() && Chunks.back().Kind == ChunkKind::InlineCode; std::string Norm = canonicalizeSpaces(std::move(Code)); if (Norm.empty()) return *this; Chunks.emplace_back(); Chunk &C = Chunks.back(); C.Contents = std::move(Norm); - C.Kind = Chunk::InlineCode; + C.Kind = ChunkKind::InlineCode; C.Preserve = Preserve; // Disallow adjacent code spans without spaces, markdown can't render them. C.SpaceBefore = AdjacentCode; @@ -475,7 +515,9 @@ Paragraph &Document::addParagraph() { return *static_cast<Paragraph *>(Children.back().get()); } -void Document::addRuler() { Children.push_back(std::make_unique<Ruler>()); } +void Document::addRuler() { + Children.push_back(std::make_unique<Ruler>()); +} void Document::addCodeBlock(std::string Code, std::string Language) { Children.emplace_back( diff --git a/clang-tools-extra/clangd/support/Markup.h b/clang-tools-extra/clangd/support/Markup.h index 3a869c49a2cbb..a74fade13d115 100644 --- a/clang-tools-extra/clangd/support/Markup.h +++ b/clang-tools-extra/clangd/support/Markup.h @@ -49,6 +49,12 @@ class Paragraph : public Block { /// Append plain text to the end of the string. Paragraph &appendText(llvm::StringRef Text); + /// Append emphasized text, this translates to the * block in markdown. + Paragraph &appendEmphasizedText(llvm::StringRef Text); + + /// Append bold text, this translates to the ** block in markdown. + Paragraph &appendBoldText(llvm::StringRef Text); + /// Append inline code, this translates to the ` block in markdown. /// \p Preserve indicates the code span must be apparent even in plaintext. Paragraph &appendCode(llvm::StringRef Code, bool Preserve = false); @@ -58,11 +64,9 @@ class Paragraph : public Block { Paragraph &appendSpace(); private: + typedef enum { PlainText, InlineCode, Bold, Emphasized } ChunkKind; struct Chunk { - enum { - PlainText, - InlineCode, - } Kind = PlainText; + ChunkKind Kind = PlainText; // Preserve chunk markers in plaintext. bool Preserve = false; std::string Contents; @@ -73,6 +77,19 @@ class Paragraph : public Block { bool SpaceAfter = false; }; std::vector<Chunk> Chunks; + + Paragraph &appendChunk(llvm::StringRef Contents, ChunkKind K); + + llvm::StringRef chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, + llvm::StringRef Text) const; + bool punctuationIndicatesLineBreak(llvm::StringRef Line) const; + bool isHardLineBreakIndicator(llvm::StringRef Rest) const; + bool isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest) const; +}; + +class ListItemParagraph : public Paragraph { +public: + void renderMarkdown(llvm::raw_ostream &OS) const override; }; /// Represents a sequence of one or more documents. Knows how to print them in a @@ -82,6 +99,9 @@ class BulletList : public Block { BulletList(); ~BulletList(); + // A BulletList rendered in markdown is a tight list if it is not a nested + // list and no item contains multiple paragraphs. Otherwise, it is a loose + // list. void renderMarkdown(llvm::raw_ostream &OS) const override; void renderPlainText(llvm::raw_ostream &OS) const override; std::unique_ptr<Block> clone() const override; @@ -118,8 +138,8 @@ class Document { BulletList &addBulletList(); /// Doesn't contain any trailing newlines. - /// We try to make the markdown human-readable, e.g. avoid extra escaping. - /// At least one client (coc.nvim) displays the markdown verbatim! + /// It is expected that the result of this function + /// is rendered as markdown. std::string asMarkdown() const; /// Doesn't contain any trailing newlines. std::string asPlainText() const; diff --git a/clang-tools-extra/clangd/test/signature-help.test b/clang-tools-extra/clangd/test/signature-help.test index a642574571cc3..cc6f3a09cee71 100644 --- a/clang-tools-extra/clangd/test/signature-help.test +++ b/clang-tools-extra/clangd/test/signature-help.test @@ -2,7 +2,7 @@ # Start a session. {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument": {"signatureHelp": {"signatureInformation": {"documentationFormat": ["markdown", "plaintext"]}}}},"trace":"off"}} --- -{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"// comment `markdown` _escape_\nvoid x(int);\nint main(){\nx("}}} +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"// comment `markdown` _noescape_\nvoid x(int);\nint main(){\nx("}}} --- {"jsonrpc":"2.0","id":1,"method":"textDocument/signatureHelp","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":3,"character":2}}} # CHECK: "id": 1, @@ -14,7 +14,7 @@ # CHECK-NEXT: { # CHECK-NEXT: "documentation": { # CHECK-NEXT: "kind": "markdown", -# CHECK-NEXT: "value": "comment `markdown` \\_escape\\_" +# CHECK-NEXT: "value": "comment `markdown` _noescape_" # CHECK-NEXT: }, # CHECK-NEXT: "label": "x(int) -> void", # CHECK-NEXT: "parameters": [ diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp index b12f8275b8a26..db9626bee300e 100644 --- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp @@ -1098,7 +1098,7 @@ TEST(CompletionTest, Documentation) { EXPECT_THAT(Results.Completions, Contains(AllOf( named("foo"), - doc("Annotation: custom_annotation\nNon-doxygen comment.")))); + doc("Annotation: custom_annotation\n\nNon-doxygen comment.")))); EXPECT_THAT( Results.Completions, Contains(AllOf(named("bar"), doc("Doxygen comment.\n\\param int a")))); @@ -2297,7 +2297,7 @@ TEST(CompletionTest, Render) { EXPECT_EQ(R.insertTextFormat, InsertTextFormat::PlainText); EXPECT_EQ(R.filterText, "x"); EXPECT_EQ(R.detail, "int"); - EXPECT_EQ(R.documentation->value, "From \"foo.h\"\nThis is x()"); + EXPECT_EQ(R.documentation->value, "From \"foo.h\"\n\nThis is x()"); EXPECT_THAT(R.additionalTextEdits, IsEmpty()); EXPECT_EQ(R.sortText, sortText(1.0, "x")); EXPECT_FALSE(R.deprecated); @@ -2332,7 +2332,7 @@ TEST(CompletionTest, Render) { C.BundleSize = 2; R = C.render(Opts); EXPECT_EQ(R.detail, "[2 overloads]"); - EXPECT_EQ(R.documentation->value, "From \"foo.h\"\nThis is x()"); + EXPECT_EQ(R.documentation->value, "From \"foo.h\"\n\nThis is x()"); C.Deprecated = true; R = C.render(Opts); @@ -2340,7 +2340,7 @@ TEST(CompletionTest, Render) { Opts.DocumentationFormat = MarkupKind::Markdown; R = C.render(Opts); - EXPECT_EQ(R.documentation->value, "From `\"foo.h\"` \nThis is `x()`"); + EXPECT_EQ(R.documentation->value, "From `\"foo.h\"`\n\nThis is `x()`"); } TEST(CompletionTest, IgnoreRecoveryResults) { diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 69f6df46c87ce..0047eed03d8d9 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -3233,8 +3233,8 @@ TEST(Hover, ParseProviderInfo) { struct Case { HoverInfo HI; llvm::StringRef ExpectedMarkdown; - } Cases[] = {{HIFoo, "### `foo` \nprovided by `\"foo.h\"`"}, - {HIFooBar, "### `foo` \nprovided by `<bar.h>`"}}; + } Cases[] = {{HIFoo, "### `foo`\n\nprovided by `\"foo.h\"`"}, + {HIFooBar, "### `foo`\n\nprovided by `<bar.h>`"}}; for (const auto &Case : Cases) EXPECT_EQ(Case.HI.present().asMarkdown(), Case.ExpectedMarkdown); @@ -3441,6 +3441,7 @@ TEST(Hover, Present) { R"(class foo Size: 10 bytes + documentation template <typename T, typename C = bool> class Foo {})", @@ -3465,8 +3466,8 @@ template <typename T, typename C = bool> class Foo {})", }, "function foo\n" "\n" - "→ ret_type (aka can_ret_type)\n" - "Parameters:\n" + "→ ret_type (aka can_ret_type)\n\n" + "Parameters:\n\n" "- \n" "- type (aka can_type)\n" "- type foo (aka can_type)\n" @@ -3491,8 +3492,11 @@ template <typename T, typename C = bool> class Foo {})", R"(field foo Type: type (aka can_type) + Value = value + Offset: 12 bytes + Size: 4 bytes (+4 bytes padding), alignment 4 bytes // In test::Bar @@ -3514,8 +3518,11 @@ def)", R"(field foo Type: type (aka can_type) + Value = value + Offset: 4 bytes and 3 bits + Size: 25 bits (+4 bits padding), alignment 8 bytes // In test::Bar @@ -3573,6 +3580,7 @@ protected: size_t method())", R"(constructor cls Parameters: + - int a - int b = 5 @@ -3609,7 +3617,9 @@ private: union foo {})", R"(variable foo Type: int + Value = 3 + Passed as arg_a // In test::Bar @@ -3644,7 +3654,9 @@ Passed by value)", R"(variable foo Type: int + Value = 3 + Passed by reference as arg_a // In test::Bar @@ -3667,7 +3679,9 @@ int foo = 3)", R"(variable foo Type: int + Value = 3 + Passed as arg_a (converted to alias_int) // In test::Bar @@ -3705,7 +3719,9 @@ int foo = 3)", R"(variable foo Type: int + Value = 3 + Passed by const reference as arg_a (converted to int) // In test::Bar @@ -3752,57 +3768,67 @@ TEST(Hover, ParseDocumentation) { llvm::StringRef ExpectedRenderPlainText; } Cases[] = {{ " \n foo\nbar", - "foo bar", + "foo\nbar", "foo bar", }, { "foo\nbar \n ", - "foo bar", + "foo\nbar", "foo bar", }, { "foo \nbar", - "foo bar", - "foo bar", + "foo \nbar", + "foo\nbar", }, { "foo \nbar", - "foo bar", - "foo bar", + "foo \nbar", + "foo\nbar", }, { "foo\n\n\nbar", - "foo \nbar", - "foo\nbar", + "foo\n\nbar", + "foo\n\nbar", }, { "foo\n\n\n\tbar", - "foo \nbar", - "foo\nbar", + "foo\n\n\tbar", + "foo\n\nbar", + }, + { + "foo\n\n\n bar", + "foo\n\n bar", + "foo\n\nbar", + }, + { + "foo\n\n\n bar", + "foo\n\n bar", + "foo\n\nbar", }, { "foo\n\n\n bar", - "foo \nbar", - "foo\nbar", + "foo\n\n bar", + "foo\n\nbar", }, { "foo.\nbar", - "foo. \nbar", + "foo.\nbar", "foo.\nbar", }, { "foo. \nbar", - "foo. \nbar", + "foo. \nbar", "foo.\nbar", }, { "foo\n*bar", - "foo \n\\*bar", + "foo\n*bar", "foo\n*bar", }, { "foo\nbar", - "foo bar", + "foo\nbar", "foo bar", }, { @@ -3812,15 +3838,16 @@ TEST(Hover, ParseDocumentation) { }, { "'`' should not occur in `Code`", - "'\\`' should not occur in `Code`", + "'`' should not occur in `Code`", "'`' should not occur in `Code`", }, { "`not\nparsed`", - "\\`not parsed\\`", + "`not parsed`", "`not parsed`", }}; + //Case C = Cases[2]; for (const auto &C : Cases) { markup::Document Output; parseDocumentation(C.Documentation, Output); @@ -3850,10 +3877,10 @@ TEST(Hover, PresentRulers) { HI.Definition = "def"; llvm::StringRef ExpectedMarkdown = // - "### variable `foo` \n" + "### variable `foo`\n" "\n" "---\n" - "Value = `val` \n" + "Value = `val`\n" "\n" "---\n" "```cpp\n" diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp index 2d86c91c7ec08..f1a4211997c9c 100644 --- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp +++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp @@ -33,26 +33,25 @@ MATCHER(escapedNone, "") { TEST(Render, Escaping) { // Check all ASCII punctuation. std::string Punctuation = R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; - std::string EscapedPunc = R"txt(!"#$%&'()\*+,-./:;<=>?@[\\]^\_\`{|}~)txt"; - EXPECT_EQ(escape(Punctuation), EscapedPunc); + EXPECT_EQ(escape(Punctuation), Punctuation); // Inline code - EXPECT_EQ(escape("`foo`"), R"(\`foo\`)"); - EXPECT_EQ(escape("`foo"), R"(\`foo)"); - EXPECT_EQ(escape("foo`"), R"(foo\`)"); - EXPECT_EQ(escape("``foo``"), R"(\`\`foo\`\`)"); + EXPECT_THAT(escape("`foo`"), escapedNone()); + EXPECT_THAT(escape("`foo"), escapedNone()); + EXPECT_THAT(escape("foo`"), escapedNone()); + EXPECT_THAT(escape("``foo``"), escapedNone()); // Code blocks - EXPECT_EQ(escape("```"), R"(\`\`\`)"); // This could also be inline code! - EXPECT_EQ(escape("~~~"), R"(\~~~)"); + EXPECT_THAT(escape("```"), escapedNone()); + EXPECT_THAT(escape("~~~"), escapedNone()); // Rulers and headings - EXPECT_THAT(escape("## Heading"), escaped('#')); + EXPECT_THAT(escape("## Heading"), escapedNone()); EXPECT_THAT(escape("Foo # bar"), escapedNone()); - EXPECT_EQ(escape("---"), R"(\---)"); - EXPECT_EQ(escape("-"), R"(\-)"); - EXPECT_EQ(escape("==="), R"(\===)"); - EXPECT_EQ(escape("="), R"(\=)"); - EXPECT_EQ(escape("***"), R"(\*\*\*)"); // \** could start emphasis! + EXPECT_THAT(escape("---"), escapedNone()); + EXPECT_THAT(escape("-"), escapedNone()); + EXPECT_THAT(escape("==="), escapedNone()); + EXPECT_THAT(escape("="), escapedNone()); + EXPECT_THAT(escape("***"), escapedNone()); // \** could start emphasis! // HTML tags. EXPECT_THAT(escape("<pre"), escaped('<')); @@ -68,24 +67,24 @@ TEST(Render, Escaping) { EXPECT_THAT(escape("Website <http://foo.bar>"), escapedNone()); // Bullet lists. - EXPECT_THAT(escape("- foo"), escaped('-')); - EXPECT_THAT(escape("* foo"), escaped('*')); - EXPECT_THAT(escape("+ foo"), escaped('+')); - EXPECT_THAT(escape("+"), escaped('+')); + EXPECT_THAT(escape("- foo"), escapedNone()); + EXPECT_THAT(escape("* foo"), escapedNone()); + EXPECT_THAT(escape("+ foo"), escapedNone()); + EXPECT_THAT(escape("+"), escapedNone()); EXPECT_THAT(escape("a + foo"), escapedNone()); EXPECT_THAT(escape("a+ foo"), escapedNone()); - EXPECT_THAT(escape("1. foo"), escaped('.')); + EXPECT_THAT(escape("1. foo"), escapedNone()); EXPECT_THAT(escape("a. foo"), escapedNone()); // Emphasis. - EXPECT_EQ(escape("*foo*"), R"(\*foo\*)"); - EXPECT_EQ(escape("**foo**"), R"(\*\*foo\*\*)"); - EXPECT_THAT(escape("*foo"), escaped('*')); + EXPECT_THAT(escape("*foo*"), escapedNone()); + EXPECT_THAT(escape("**foo**"), escapedNone()); + EXPECT_THAT(escape("*foo"), escapedNone()); EXPECT_THAT(escape("foo *"), escapedNone()); EXPECT_THAT(escape("foo * bar"), escapedNone()); EXPECT_THAT(escape("foo_bar"), escapedNone()); - EXPECT_THAT(escape("foo _bar"), escaped('_')); - EXPECT_THAT(escape("foo_ bar"), escaped('_')); + EXPECT_THAT(escape("foo _bar"), escapedNone()); + EXPECT_THAT(escape("foo_ bar"), escapedNone()); EXPECT_THAT(escape("foo _ bar"), escapedNone()); // HTML entities. @@ -97,8 +96,8 @@ TEST(Render, Escaping) { EXPECT_THAT(escape("foo &?; bar"), escapedNone()); // Links. - EXPECT_THAT(escape("[foo](bar)"), escaped(']')); - EXPECT_THAT(escape("[foo]: bar"), escaped(']')); + EXPECT_THAT(escape("[foo](bar)"), escapedNone()); + EXPECT_THAT(escape("[foo]: bar"), escapedNone()); // No need to escape these, as the target never exists. EXPECT_THAT(escape("[foo][]"), escapedNone()); EXPECT_THAT(escape("[foo][bar]"), escapedNone()); @@ -182,14 +181,87 @@ TEST(Paragraph, SeparationOfChunks) { P.appendCode("no").appendCode("space"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space`"); EXPECT_EQ(P.asPlainText(), "after foobar batno space"); + + P.appendText(" text"); + EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space` text"); + EXPECT_EQ(P.asPlainText(), "after foobar batno space text"); + + P.appendSpace().appendCode("code").appendText(".\n newline"); + EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space` text `code`.\n newline"); + EXPECT_EQ(P.asPlainText(), "after foobar batno space text code.\nnewline"); +} + +TEST(Paragraph, SeparationOfChunks2) { + // This test keeps appending contents to a single Paragraph and checks + // expected accumulated contents after each one. + // Purpose is to check for separation between different chunks + // where the spacing is in the appended string rather set by appendSpace. + Paragraph P; + + P.appendText("after "); + EXPECT_EQ(P.asMarkdown(), "after"); + EXPECT_EQ(P.asPlainText(), "after"); + + P.appendText("foobar"); + EXPECT_EQ(P.asMarkdown(), "after foobar"); + EXPECT_EQ(P.asPlainText(), "after foobar"); + + P.appendText(" bat"); + EXPECT_EQ(P.asMarkdown(), "after foobar bat"); + EXPECT_EQ(P.asPlainText(), "after foobar bat"); + + P.appendText("baz"); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz"); + + P.appendText(" faz "); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz"); + + P.appendText(" bar "); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz bar"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz bar"); + + P.appendText("qux"); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz bar qux"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz bar qux"); +} + +TEST(Paragraph, SeparationOfChunks3) { + // This test keeps appending contents to a single Paragraph and checks + // expected accumulated contents after each one. + // Purpose is to check for separation between different chunks + // where the spacing is in the appended string rather set by appendSpace. + Paragraph P; + + P.appendText("after \n"); + EXPECT_EQ(P.asMarkdown(), "after"); + EXPECT_EQ(P.asPlainText(), "after"); + + P.appendText(" foobar\n"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar"); + + P.appendText("- bat\n"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat"); + + P.appendText("- baz"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat\n- baz"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat\n- baz"); + + P.appendText(" faz "); + EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat\n- baz faz"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat\n- baz faz"); } TEST(Paragraph, ExtraSpaces) { - // Make sure spaces inside chunks are dropped. + // Make sure spaces inside chunks are preserved for markdown + // and dropped for plain text. Paragraph P; P.appendText("foo\n \t baz"); P.appendCode(" bar\n"); - EXPECT_EQ(P.asMarkdown(), "foo baz`bar`"); + EXPECT_EQ(P.asMarkdown(), "foo\n \t baz`bar`"); EXPECT_EQ(P.asPlainText(), "foo bazbar"); } @@ -197,7 +269,7 @@ TEST(Paragraph, SpacesCollapsed) { Paragraph P; P.appendText(" foo bar "); P.appendText(" baz "); - EXPECT_EQ(P.asMarkdown(), "foo bar baz"); + EXPECT_EQ(P.asMarkdown(), "foo bar baz"); EXPECT_EQ(P.asPlainText(), "foo bar baz"); } @@ -206,17 +278,48 @@ TEST(Paragraph, NewLines) { Paragraph P; P.appendText(" \n foo\nbar\n "); P.appendCode(" \n foo\nbar \n "); - EXPECT_EQ(P.asMarkdown(), "foo bar `foo bar`"); + EXPECT_EQ(P.asMarkdown(), "foo\nbar\n `foo bar`"); EXPECT_EQ(P.asPlainText(), "foo bar foo bar"); } +TEST(Paragraph, BoldText) { + Paragraph P; + P.appendBoldText(""); + EXPECT_EQ(P.asMarkdown(), ""); + EXPECT_EQ(P.asPlainText(), ""); + + P.appendBoldText(" \n foo\nbar\n "); + EXPECT_EQ(P.asMarkdown(), "**foo bar**"); + EXPECT_EQ(P.asPlainText(), "**foo bar**"); + + P.appendSpace().appendBoldText("foobar"); + EXPECT_EQ(P.asMarkdown(), "**foo bar** **foobar**"); + EXPECT_EQ(P.asPlainText(), "**foo bar** **foobar**"); +} + +TEST(Paragraph, EmphasizedText) { + Paragraph P; + P.appendEmphasizedText(""); + EXPECT_EQ(P.asMarkdown(), ""); + EXPECT_EQ(P.asPlainText(), ""); + + P.appendEmphasizedText(" \n foo\nbar\n "); + EXPECT_EQ(P.asMarkdown(), "*foo bar*"); + EXPECT_EQ(P.asPlainText(), "*foo bar*"); + + P.appendSpace().appendEmphasizedText("foobar"); + EXPECT_EQ(P.asMarkdown(), "*foo bar* *foobar*"); + EXPECT_EQ(P.asPlainText(), "*foo bar* *foobar*"); +} + TEST(Document, Separators) { Document D; D.addParagraph().appendText("foo"); D.addCodeBlock("test"); D.addParagraph().appendText("bar"); - const char ExpectedMarkdown[] = R"md(foo + const char ExpectedMarkdown[] = R"md(foo + ```cpp test ``` @@ -238,7 +341,7 @@ TEST(Document, Ruler) { // Ruler followed by paragraph. D.addParagraph().appendText("bar"); - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\nbar"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nbar"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar"); D = Document(); @@ -246,7 +349,7 @@ TEST(Document, Ruler) { D.addRuler(); D.addCodeBlock("bar"); // Ruler followed by a codeblock. - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\n```cpp\nbar\n```"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\n```cpp\nbar\n```"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar"); // Ruler followed by another ruler @@ -260,7 +363,7 @@ TEST(Document, Ruler) { // Multiple rulers between blocks D.addRuler(); D.addParagraph().appendText("foo"); - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\nfoo"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nfoo"); EXPECT_EQ(D.asPlainText(), "foo\n\nfoo"); } @@ -272,7 +375,7 @@ TEST(Document, Append) { E.addRuler(); E.addParagraph().appendText("bar"); D.append(std::move(E)); - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\nbar"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nbar"); } TEST(Document, Heading) { @@ -280,8 +383,8 @@ TEST(Document, Heading) { D.addHeading(1).appendText("foo"); D.addHeading(2).appendText("bar"); D.addParagraph().appendText("baz"); - EXPECT_EQ(D.asMarkdown(), "# foo \n## bar \nbaz"); - EXPECT_EQ(D.asPlainText(), "foo\nbar\nbaz"); + EXPECT_EQ(D.asMarkdown(), "# foo\n\n## bar\n\nbaz"); + EXPECT_EQ(D.asPlainText(), "foo\n\nbar\n\nbaz"); } TEST(CodeBlock, Render) { @@ -336,7 +439,7 @@ TEST(BulletList, Render) { // Nested list, with a single item. Document &D = L.addItem(); - // First item with foo\nbaz + // First item with 2 paragraphs - foo\n\n baz D.addParagraph().appendText("foo"); D.addParagraph().appendText("baz"); @@ -352,18 +455,26 @@ TEST(BulletList, Render) { DeepDoc.addParagraph().appendText("baz"); StringRef ExpectedMarkdown = R"md(- foo - bar -- foo - baz - - foo - - baz +- foo + + baz + + - foo + + - baz + baz)md"; EXPECT_EQ(L.asMarkdown(), ExpectedMarkdown); StringRef ExpectedPlainText = R"pt(- foo - bar - foo + baz + - foo + - baz + baz)pt"; EXPECT_EQ(L.asPlainText(), ExpectedPlainText); @@ -371,21 +482,30 @@ TEST(BulletList, Render) { Inner.addParagraph().appendText("after"); ExpectedMarkdown = R"md(- foo - bar -- foo - baz - - foo - - baz +- foo + + baz + + - foo + + - baz + baz - + after)md"; EXPECT_EQ(L.asMarkdown(), ExpectedMarkdown); ExpectedPlainText = R"pt(- foo - bar - foo + baz + - foo + - baz + baz + after)pt"; EXPECT_EQ(L.asPlainText(), ExpectedPlainText); } >From 1fe20072222fd5c752292634b9b8d4b23b17b602 Mon Sep 17 00:00:00 2001 From: Tim Cottin <timcot...@gmx.de> Date: Fri, 30 May 2025 19:56:26 +0000 Subject: [PATCH 2/3] [clangd] fix formatting --- clang-tools-extra/clangd/support/Markup.cpp | 16 ++++++++-------- .../clangd/unittests/CodeCompleteTests.cpp | 9 +++++---- .../clangd/unittests/HoverTests.cpp | 1 - .../clangd/unittests/support/MarkupTests.cpp | 3 ++- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp index b1e6252e473f5..63b8f98580bd8 100644 --- a/clang-tools-extra/clangd/support/Markup.cpp +++ b/clang-tools-extra/clangd/support/Markup.cpp @@ -66,12 +66,13 @@ bool looksLikeTag(llvm::StringRef Contents) { /// /// **FIXME:** handle the case when the client does support HTML rendering in /// markdown. For this, the LSP server needs to check the -/// [supportsHtml capability](https://github.com/microsoft/language-server-protocol/issues/1344) +/// [supportsHtml +/// capability](https://github.com/microsoft/language-server-protocol/issues/1344) /// of the client. /// /// \param C The character to check. -/// \param After The string that follows \p C . This is used to determine if \p C is -/// part of a tag or an entity reference. +/// \param After The string that follows \p C . +// This is used to determine if \p C is part of a tag or an entity reference. /// \returns true if \p C should be escaped, false otherwise. bool needsLeadingEscape(char C, llvm::StringRef After) { switch (C) { @@ -245,7 +246,8 @@ class CodeBlock : public Block { std::string indentLines(llvm::StringRef Input) { assert(!Input.ends_with("\n") && "Input should've been trimmed."); std::string IndentedR; - // We'll add 2 spaces after each new line which is not followed by another new line. + // We'll add 2 spaces after each new line which is not followed by another new + // line. IndentedR.reserve(Input.size() + Input.count('\n') * 2); for (size_t I = 0; I < Input.size(); ++I) { char C = Input[I]; @@ -326,7 +328,7 @@ llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, return Options.front(); } -bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line) const{ +bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line) const { constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt"; Line = Line.rtrim(); @@ -515,9 +517,7 @@ Paragraph &Document::addParagraph() { return *static_cast<Paragraph *>(Children.back().get()); } -void Document::addRuler() { - Children.push_back(std::make_unique<Ruler>()); -} +void Document::addRuler() { Children.push_back(std::make_unique<Ruler>()); } void Document::addCodeBlock(std::string Code, std::string Language) { Children.emplace_back( diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp index db9626bee300e..22c5ff6e44c46 100644 --- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp @@ -1095,10 +1095,11 @@ TEST(CompletionTest, Documentation) { int x = ^ )cpp"); - EXPECT_THAT(Results.Completions, - Contains(AllOf( - named("foo"), - doc("Annotation: custom_annotation\n\nNon-doxygen comment.")))); + EXPECT_THAT( + Results.Completions, + Contains( + AllOf(named("foo"), + doc("Annotation: custom_annotation\n\nNon-doxygen comment.")))); EXPECT_THAT( Results.Completions, Contains(AllOf(named("bar"), doc("Doxygen comment.\n\\param int a")))); diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 0047eed03d8d9..6baeb835f2b8f 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -3847,7 +3847,6 @@ TEST(Hover, ParseDocumentation) { "`not parsed`", }}; - //Case C = Cases[2]; for (const auto &C : Cases) { markup::Document Output; parseDocumentation(C.Documentation, Output); diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp index f1a4211997c9c..cef8944e89053 100644 --- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp +++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp @@ -187,7 +187,8 @@ TEST(Paragraph, SeparationOfChunks) { EXPECT_EQ(P.asPlainText(), "after foobar batno space text"); P.appendSpace().appendCode("code").appendText(".\n newline"); - EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space` text `code`.\n newline"); + EXPECT_EQ(P.asMarkdown(), + "after `foobar` bat`no` `space` text `code`.\n newline"); EXPECT_EQ(P.asPlainText(), "after foobar batno space text code.\nnewline"); } >From bf5a9255f2fb7e5570fa7848ae90ee59fe6fe35c Mon Sep 17 00:00:00 2001 From: Tim Cottin <timcot...@gmx.de> Date: Sun, 29 Jun 2025 18:14:28 +0000 Subject: [PATCH 3/3] [clangd] introduce CommentFormat option --- clang-tools-extra/clangd/ClangdLSPServer.cpp | 6 +- clang-tools-extra/clangd/Config.h | 13 + clang-tools-extra/clangd/ConfigCompile.cpp | 17 ++ clang-tools-extra/clangd/ConfigFragment.h | 11 + clang-tools-extra/clangd/ConfigYAML.cpp | 10 + clang-tools-extra/clangd/Hover.cpp | 20 ++ clang-tools-extra/clangd/Hover.h | 2 + clang-tools-extra/clangd/support/Markup.cpp | 173 ++++++++++++- clang-tools-extra/clangd/support/Markup.h | 6 + .../clangd/unittests/HoverTests.cpp | 17 +- .../clangd/unittests/support/MarkupTests.cpp | 228 +++++++++++++++--- 11 files changed, 450 insertions(+), 53 deletions(-) diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp index 1e981825c7c15..e19ac7718469f 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -1262,11 +1262,9 @@ void ClangdLSPServer::onHover(const TextDocumentPositionParams &Params, R.contents.kind = HoverContentFormat; R.range = (*H)->SymRange; switch (HoverContentFormat) { - case MarkupKind::PlainText: - R.contents.value = (*H)->present().asPlainText(); - return Reply(std::move(R)); case MarkupKind::Markdown: - R.contents.value = (*H)->present().asMarkdown(); + case MarkupKind::PlainText: + R.contents.value = (*H)->present(HoverContentFormat); return Reply(std::move(R)); }; llvm_unreachable("unhandled MarkupKind"); diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h index 586d031d58481..2f1cb86f68d4d 100644 --- a/clang-tools-extra/clangd/Config.h +++ b/clang-tools-extra/clangd/Config.h @@ -177,6 +177,19 @@ struct Config { /// Controls highlighting modifiers that are disabled. std::vector<std::string> DisabledModifiers; } SemanticTokens; + + enum class CommentFormatPolicy { + /// Treat comments as plain text. + PlainText, + /// Treat comments as Markdown. + Markdown, + /// Treat comments as doxygen. + Doxygen, + }; + + struct { + CommentFormatPolicy CommentFormat = CommentFormatPolicy::PlainText; + } Documentation; }; } // namespace clangd diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp index aa2561e081047..6b61eed092003 100644 --- a/clang-tools-extra/clangd/ConfigCompile.cpp +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -198,6 +198,7 @@ struct FragmentCompiler { compile(std::move(F.InlayHints)); compile(std::move(F.SemanticTokens)); compile(std::move(F.Style)); + compile(std::move(F.Documentation)); } void compile(Fragment::IfBlock &&F) { @@ -760,6 +761,22 @@ struct FragmentCompiler { } } + void compile(Fragment::DocumentationBlock &&F) { + if (F.CommentFormat) { + if (auto Val = + compileEnum<Config::CommentFormatPolicy>("CommentFormat", + *F.CommentFormat) + .map("Plaintext", Config::CommentFormatPolicy::PlainText) + .map("Markdown", + Config::CommentFormatPolicy::Markdown) + .map("Doxygen", Config::CommentFormatPolicy::Doxygen) + .value()) + Out.Apply.push_back([Val](const Params &, Config &C) { + C.Documentation.CommentFormat = *Val; + }); + } + } + constexpr static llvm::SourceMgr::DiagKind Error = llvm::SourceMgr::DK_Error; constexpr static llvm::SourceMgr::DiagKind Warning = llvm::SourceMgr::DK_Warning; diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h index 9535b20253b13..de20356e97ec2 100644 --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -372,6 +372,17 @@ struct Fragment { std::vector<Located<std::string>> DisabledModifiers; }; SemanticTokensBlock SemanticTokens; + + /// Configures documentation style and behaviour. + struct DocumentationBlock { + /// Specifies the format of comments in the code. + /// Valid values are enum Config::CommentFormatPolicy values: + /// - Plaintext: Treat comments as plain text. + /// - Markdown: Treat comments as Markdown. + /// - Doxygen: Treat comments as doxygen. + std::optional<Located<std::string>> CommentFormat; + }; + DocumentationBlock Documentation; }; } // namespace config diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp index 95cc5c1f9f1cf..1fe55fbcaadf1 100644 --- a/clang-tools-extra/clangd/ConfigYAML.cpp +++ b/clang-tools-extra/clangd/ConfigYAML.cpp @@ -68,6 +68,7 @@ class Parser { Dict.handle("Hover", [&](Node &N) { parse(F.Hover, N); }); Dict.handle("InlayHints", [&](Node &N) { parse(F.InlayHints, N); }); Dict.handle("SemanticTokens", [&](Node &N) { parse(F.SemanticTokens, N); }); + Dict.handle("Documentation", [&](Node &N) { parse(F.Documentation, N); }); Dict.parse(N); return !(N.failed() || HadError); } @@ -299,6 +300,15 @@ class Parser { Dict.parse(N); } + void parse(Fragment::DocumentationBlock &F, Node &N) { + DictParser Dict("Documentation", this); + Dict.handle("CommentFormat", [&](Node &N) { + if (auto Value = scalarValue(N, "CommentFormat")) + F.CommentFormat = *Value; + }); + Dict.parse(N); + } + // Helper for parsing mapping nodes (dictionaries). // We don't use YamlIO as we want to control over unknown keys. class DictParser { diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index 88755733aa67c..609a7f91b1c4c 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -15,6 +15,7 @@ #include "Headers.h" #include "IncludeCleaner.h" #include "ParsedAST.h" +#include "Protocol.h" #include "Selection.h" #include "SourceCode.h" #include "clang-include-cleaner/Analysis.h" @@ -1535,6 +1536,25 @@ markup::Document HoverInfo::present() const { return Output; } +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::PlainText) + // If the user prefers plain text, we use the present() method to generate + // the plain text output. + return present().asEscapedMarkdown(); + } + + return present().asPlainText(); +} + // If the backtick at `Offset` starts a probable quoted range, return the range // (including the quotes). std::optional<llvm::StringRef> getBacktickQuoteRange(llvm::StringRef Line, diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h index fe689de44732e..2f65431bd72de 100644 --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -120,6 +120,8 @@ struct HoverInfo { /// Produce a user-readable information. markup::Document present() const; + + std::string present(MarkupKind Kind) const; }; inline bool operator==(const HoverInfo::PrintedType &LHS, diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp index 63b8f98580bd8..5af85754bebd0 100644 --- a/clang-tools-extra/clangd/support/Markup.cpp +++ b/clang-tools-extra/clangd/support/Markup.cpp @@ -6,6 +6,7 @@ // //===----------------------------------------------------------------------===// #include "support/Markup.h" +#include "clang/Basic/CharInfo.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" @@ -55,6 +56,101 @@ bool looksLikeTag(llvm::StringRef Contents) { return true; // Potentially incomplete tag. } +// Tests whether C should be backslash-escaped in markdown. +// The string being escaped is Before + C + After. This is part of a paragraph. +// StartsLine indicates whether `Before` is the start of the line. +// After may not be everything until the end of the line. +// +// It's always safe to escape punctuation, but want minimal escaping. +// The strategy is to escape the first character of anything that might start +// a markdown grammar construct. +bool needsLeadingEscapePlaintext(char C, llvm::StringRef Before, + llvm::StringRef After, bool StartsLine) { + assert(Before.take_while(llvm::isSpace).empty()); + auto RulerLength = [&]() -> /*Length*/ unsigned { + if (!StartsLine || !Before.empty()) + return false; + llvm::StringRef A = After.rtrim(); + return llvm::all_of(A, [C](char D) { return C == D; }) ? 1 + A.size() : 0; + }; + auto IsBullet = [&]() { + return StartsLine && Before.empty() && + (After.empty() || After.starts_with(" ")); + }; + auto SpaceSurrounds = [&]() { + return (After.empty() || llvm::isSpace(After.front())) && + (Before.empty() || llvm::isSpace(Before.back())); + }; + auto WordSurrounds = [&]() { + return (!After.empty() && llvm::isAlnum(After.front())) && + (!Before.empty() && llvm::isAlnum(Before.back())); + }; + + switch (C) { + case '\\': // Escaped character. + return true; + case '`': // Code block or inline code + // Any number of backticks can delimit an inline code block that can end + // anywhere (including on another line). We must escape them all. + return true; + case '~': // Code block + return StartsLine && Before.empty() && After.starts_with("~~"); + case '#': { // ATX heading. + if (!StartsLine || !Before.empty()) + return false; + llvm::StringRef Rest = After.ltrim(C); + return Rest.empty() || Rest.starts_with(" "); + } + case ']': // Link or link reference. + // We escape ] rather than [ here, because it's more constrained: + // ](...) is an in-line link + // ]: is a link reference + // The following are only links if the link reference exists: + // ] by itself is a shortcut link + // ][...] is an out-of-line link + // Because we never emit link references, we don't need to handle these. + return After.starts_with(":") || After.starts_with("("); + case '=': // Setex heading. + return RulerLength() > 0; + case '_': // Horizontal ruler or matched delimiter. + if (RulerLength() >= 3) + return true; + // Not a delimiter if surrounded by space, or inside a word. + // (The rules at word boundaries are subtle). + return !(SpaceSurrounds() || WordSurrounds()); + case '-': // Setex heading, horizontal ruler, or bullet. + if (RulerLength() > 0) + return true; + return IsBullet(); + case '+': // Bullet list. + return IsBullet(); + case '*': // Bullet list, horizontal ruler, or delimiter. + return IsBullet() || RulerLength() >= 3 || !SpaceSurrounds(); + case '<': // HTML tag (or autolink, which we choose not to escape) + return looksLikeTag(After); + case '>': // Quote marker. Needs escaping at start of line. + return StartsLine && Before.empty(); + case '&': { // HTML entity reference + auto End = After.find(';'); + if (End == llvm::StringRef::npos) + return false; + llvm::StringRef Content = After.substr(0, End); + if (Content.consume_front("#")) { + if (Content.consume_front("x") || Content.consume_front("X")) + return llvm::all_of(Content, llvm::isHexDigit); + return llvm::all_of(Content, llvm::isDigit); + } + return llvm::all_of(Content, llvm::isAlpha); + } + case '.': // Numbered list indicator. Escape 12. -> 12\. at start of line. + case ')': + return StartsLine && !Before.empty() && + llvm::all_of(Before, llvm::isDigit) && After.starts_with(" "); + default: + return false; + } +} + /// \brief Tests whether \p C should be backslash-escaped in markdown. /// /// The MarkupContent LSP specification defines that `markdown` content needs to @@ -74,7 +170,7 @@ bool looksLikeTag(llvm::StringRef Contents) { /// \param After The string that follows \p C . // This is used to determine if \p C is part of a tag or an entity reference. /// \returns true if \p C should be escaped, false otherwise. -bool needsLeadingEscape(char C, llvm::StringRef After) { +bool needsLeadingEscapeMarkdown(char C, llvm::StringRef After) { switch (C) { case '<': // HTML tag (or autolink, which we choose not to escape) return looksLikeTag(After); @@ -95,12 +191,22 @@ bool needsLeadingEscape(char C, llvm::StringRef After) { } } +bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, + bool StartsLine, bool EscapeMarkdown) { + if (EscapeMarkdown) + return needsLeadingEscapePlaintext(C, Before, After, StartsLine); + return needsLeadingEscapeMarkdown(C, After); +} + /// Escape a markdown text block. Ensures the punctuation will not introduce /// any of the markdown constructs. -std::string renderText(llvm::StringRef Input, bool StartsLine) { +std::string renderText(llvm::StringRef Input, bool StartsLine, bool EscapeMarkdown = false) { std::string R; for (unsigned I = 0; I < Input.size(); ++I) { - if (needsLeadingEscape(Input[I], Input.substr(I + 1))) + if (Input.substr(0, I).take_while(llvm::isSpace).empty() && + !isWhitespace(Input[I]) && + needsLeadingEscape(Input[I], Input.substr(0, I), Input.substr(I + 1), + StartsLine, EscapeMarkdown)) R.push_back('\\'); R.push_back(Input[I]); } @@ -204,6 +310,9 @@ std::string renderBlocks(llvm::ArrayRef<std::unique_ptr<Block>> Children, // https://github.com/microsoft/vscode/issues/88416 for details. class Ruler : public Block { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override { + renderMarkdown(OS); + } void renderMarkdown(llvm::raw_ostream &OS) const override { // Note that we need an extra new line before the ruler, otherwise we might // make previous block a title instead of introducing a ruler. @@ -218,6 +327,9 @@ class Ruler : public Block { class CodeBlock : public Block { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override { + renderMarkdown(OS); + } 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. @@ -261,6 +373,12 @@ std::string indentLines(llvm::StringRef Input) { class Heading : public Paragraph { public: Heading(size_t Level) : Level(Level) {} + + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override { + OS << std::string(Level, '#') << ' '; + Paragraph::renderEscapedMarkdown(OS); + } + void renderMarkdown(llvm::raw_ostream &OS) const override { OS << std::string(Level, '#') << ' '; Paragraph::renderMarkdown(OS); @@ -272,6 +390,13 @@ class Heading : public Paragraph { } // namespace +std::string Block::asEscapedMarkdown() const { + std::string R; + llvm::raw_string_ostream OS(R); + renderEscapedMarkdown(OS); + return llvm::StringRef(OS.str()).trim().str(); +} + std::string Block::asMarkdown() const { std::string R; llvm::raw_string_ostream OS(R); @@ -286,6 +411,33 @@ std::string Block::asPlainText() const { return llvm::StringRef(OS.str()).trim().str(); } +void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const { + bool NeedsSpace = false; + bool HasChunks = false; + for (auto &C : Chunks) { + if (C.SpaceBefore || NeedsSpace) + OS << " "; + switch (C.Kind) { + case ChunkKind::PlainText: + OS << renderText(C.Contents, !HasChunks, true); + break; + case ChunkKind::InlineCode: + OS << renderInlineBlock(C.Contents); + break; + case ChunkKind::Bold: + OS << renderText("**" + C.Contents + "**", !HasChunks, true); + break; + case ChunkKind::Emphasized: + OS << renderText("*" + C.Contents + "*", !HasChunks, true); + break; + } + HasChunks = true; + NeedsSpace = C.SpaceAfter; + } + // A paragraph in markdown is separated by a blank line. + OS << "\n\n"; +} + void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const { bool NeedsSpace = false; bool HasChunks = false; @@ -421,6 +573,17 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { BulletList::BulletList() = default; BulletList::~BulletList() = default; +void BulletList::renderEscapedMarkdown(llvm::raw_ostream &OS) const { + for (auto &D : Items) { + std::string M = D.asEscapedMarkdown(); + // Instead of doing this we might prefer passing Indent to children to get + // rid of the copies, if it turns out to be a bottleneck. + OS << "- " << indentLines(M) << '\n'; + } + // We need a new line after list to terminate it in markdown. + OS << "\n\n"; +} + void BulletList::renderMarkdown(llvm::raw_ostream &OS) const { for (auto &D : Items) { std::string M = D.asMarkdown(); @@ -524,6 +687,10 @@ void Document::addCodeBlock(std::string Code, std::string Language) { std::make_unique<CodeBlock>(std::move(Code), std::move(Language))); } +std::string Document::asEscapedMarkdown() const { + return renderBlocks(Children, &Block::renderEscapedMarkdown); +} + std::string Document::asMarkdown() const { return renderBlocks(Children, &Block::renderMarkdown); } diff --git a/clang-tools-extra/clangd/support/Markup.h b/clang-tools-extra/clangd/support/Markup.h index a74fade13d115..23dcf9c7fad1d 100644 --- a/clang-tools-extra/clangd/support/Markup.h +++ b/clang-tools-extra/clangd/support/Markup.h @@ -27,9 +27,11 @@ namespace markup { /// should trim them if need be. class Block { public: + virtual void renderEscapedMarkdown(llvm::raw_ostream &OS) const = 0; virtual void renderMarkdown(llvm::raw_ostream &OS) const = 0; virtual void renderPlainText(llvm::raw_ostream &OS) const = 0; virtual std::unique_ptr<Block> clone() const = 0; + std::string asEscapedMarkdown() const; std::string asMarkdown() const; std::string asPlainText() const; @@ -42,6 +44,7 @@ class Block { /// One must introduce different paragraphs to create separate blocks. class Paragraph : public Block { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override; void renderMarkdown(llvm::raw_ostream &OS) const override; void renderPlainText(llvm::raw_ostream &OS) const override; std::unique_ptr<Block> clone() const override; @@ -89,6 +92,7 @@ class Paragraph : public Block { class ListItemParagraph : public Paragraph { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override; void renderMarkdown(llvm::raw_ostream &OS) const override; }; @@ -102,6 +106,7 @@ class BulletList : public Block { // A BulletList rendered in markdown is a tight list if it is not a nested // list and no item contains multiple paragraphs. Otherwise, it is a loose // list. + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override; void renderMarkdown(llvm::raw_ostream &OS) const override; void renderPlainText(llvm::raw_ostream &OS) const override; std::unique_ptr<Block> clone() const override; @@ -137,6 +142,7 @@ class Document { BulletList &addBulletList(); + std::string asEscapedMarkdown() const; /// Doesn't contain any trailing newlines. /// It is expected that the result of this function /// is rendered as markdown. diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 6baeb835f2b8f..48a0d5773e226 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -10,6 +10,7 @@ #include "Annotations.h" #include "Config.h" #include "Hover.h" +#include "Protocol.h" #include "TestFS.h" #include "TestIndex.h" #include "TestTU.h" @@ -3125,7 +3126,7 @@ TEST(Hover, All) { Expected.SymRange = T.range(); Case.ExpectedBuilder(Expected); - SCOPED_TRACE(H->present().asPlainText()); + SCOPED_TRACE(H->present(MarkupKind::PlainText)); EXPECT_EQ(H->NamespaceScope, Expected.NamespaceScope); EXPECT_EQ(H->LocalScope, Expected.LocalScope); EXPECT_EQ(H->Name, Expected.Name); @@ -3217,7 +3218,7 @@ TEST(Hover, Providers) { ASSERT_TRUE(H); HoverInfo Expected; Case.ExpectedBuilder(Expected); - SCOPED_TRACE(H->present().asMarkdown()); + SCOPED_TRACE(H->present(MarkupKind::Markdown)); EXPECT_EQ(H->Provider, Expected.Provider); } } @@ -3237,7 +3238,7 @@ TEST(Hover, ParseProviderInfo) { {HIFooBar, "### `foo`\n\nprovided by `<bar.h>`"}}; for (const auto &Case : Cases) - EXPECT_EQ(Case.HI.present().asMarkdown(), Case.ExpectedMarkdown); + EXPECT_EQ(Case.HI.present(MarkupKind::Markdown), Case.ExpectedMarkdown); } TEST(Hover, UsedSymbols) { @@ -3287,7 +3288,7 @@ TEST(Hover, UsedSymbols) { ASSERT_TRUE(H); HoverInfo Expected; Case.ExpectedBuilder(Expected); - SCOPED_TRACE(H->present().asMarkdown()); + SCOPED_TRACE(H->present(MarkupKind::Markdown)); EXPECT_EQ(H->UsedSymbolNames, Expected.UsedSymbolNames); } } @@ -3757,7 +3758,7 @@ provides Foo, Bar, Baz, Foobar, Qux and 1 more)"}}; Config Cfg; Cfg.Hover.ShowAKA = true; WithContextValue WithCfg(Config::Key, std::move(Cfg)); - EXPECT_EQ(HI.present().asPlainText(), C.ExpectedRender); + EXPECT_EQ(HI.present(MarkupKind::PlainText), C.ExpectedRender); } } @@ -3863,7 +3864,7 @@ TEST(Hover, PresentHeadings) { HI.Kind = index::SymbolKind::Variable; HI.Name = "foo"; - EXPECT_EQ(HI.present().asMarkdown(), "### variable `foo`"); + EXPECT_EQ(HI.present(MarkupKind::Markdown), "### variable `foo`"); } // This is a separate test as rulers behave differently in markdown vs @@ -3885,14 +3886,14 @@ TEST(Hover, PresentRulers) { "```cpp\n" "def\n" "```"; - EXPECT_EQ(HI.present().asMarkdown(), ExpectedMarkdown); + EXPECT_EQ(HI.present(MarkupKind::Markdown), ExpectedMarkdown); llvm::StringRef ExpectedPlaintext = R"pt(variable foo Value = val def)pt"; - EXPECT_EQ(HI.present().asPlainText(), ExpectedPlaintext); + EXPECT_EQ(HI.present(MarkupKind::PlainText), ExpectedPlaintext); } TEST(Hover, SpaceshipTemplateNoCrash) { diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp index cef8944e89053..6cd7127943a58 100644 --- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp +++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp @@ -17,6 +17,10 @@ namespace markup { namespace { std::string escape(llvm::StringRef Text) { + return Paragraph().appendText(Text.str()).asEscapedMarkdown(); +} + +std::string dontEscape(llvm::StringRef Text) { return Paragraph().appendText(Text.str()).asMarkdown(); } @@ -33,25 +37,26 @@ MATCHER(escapedNone, "") { TEST(Render, Escaping) { // Check all ASCII punctuation. std::string Punctuation = R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; - EXPECT_EQ(escape(Punctuation), Punctuation); + std::string EscapedPunc = R"txt(!"#$%&'()\*+,-./:;<=>?@[\\]^\_\`{|}~)txt"; + EXPECT_EQ(escape(Punctuation), EscapedPunc); // Inline code - EXPECT_THAT(escape("`foo`"), escapedNone()); - EXPECT_THAT(escape("`foo"), escapedNone()); - EXPECT_THAT(escape("foo`"), escapedNone()); - EXPECT_THAT(escape("``foo``"), escapedNone()); + EXPECT_EQ(escape("`foo`"), R"(\`foo\`)"); + EXPECT_EQ(escape("`foo"), R"(\`foo)"); + EXPECT_EQ(escape("foo`"), R"(foo\`)"); + EXPECT_EQ(escape("``foo``"), R"(\`\`foo\`\`)"); // Code blocks - EXPECT_THAT(escape("```"), escapedNone()); - EXPECT_THAT(escape("~~~"), escapedNone()); + EXPECT_EQ(escape("```"), R"(\`\`\`)"); // This could also be inline code! + EXPECT_EQ(escape("~~~"), R"(\~~~)"); // Rulers and headings - EXPECT_THAT(escape("## Heading"), escapedNone()); + EXPECT_THAT(escape("## Heading"), escaped('#')); EXPECT_THAT(escape("Foo # bar"), escapedNone()); - EXPECT_THAT(escape("---"), escapedNone()); - EXPECT_THAT(escape("-"), escapedNone()); - EXPECT_THAT(escape("==="), escapedNone()); - EXPECT_THAT(escape("="), escapedNone()); - EXPECT_THAT(escape("***"), escapedNone()); // \** could start emphasis! + EXPECT_EQ(escape("---"), R"(\---)"); + EXPECT_EQ(escape("-"), R"(\-)"); + EXPECT_EQ(escape("==="), R"(\===)"); + EXPECT_EQ(escape("="), R"(\=)"); + EXPECT_EQ(escape("***"), R"(\*\*\*)"); // \** could start emphasis! // HTML tags. EXPECT_THAT(escape("<pre"), escaped('<')); @@ -67,24 +72,24 @@ TEST(Render, Escaping) { EXPECT_THAT(escape("Website <http://foo.bar>"), escapedNone()); // Bullet lists. - EXPECT_THAT(escape("- foo"), escapedNone()); - EXPECT_THAT(escape("* foo"), escapedNone()); - EXPECT_THAT(escape("+ foo"), escapedNone()); - EXPECT_THAT(escape("+"), escapedNone()); + EXPECT_THAT(escape("- foo"), escaped('-')); + EXPECT_THAT(escape("* foo"), escaped('*')); + EXPECT_THAT(escape("+ foo"), escaped('+')); + EXPECT_THAT(escape("+"), escaped('+')); EXPECT_THAT(escape("a + foo"), escapedNone()); EXPECT_THAT(escape("a+ foo"), escapedNone()); - EXPECT_THAT(escape("1. foo"), escapedNone()); + EXPECT_THAT(escape("1. foo"), escaped('.')); EXPECT_THAT(escape("a. foo"), escapedNone()); // Emphasis. - EXPECT_THAT(escape("*foo*"), escapedNone()); - EXPECT_THAT(escape("**foo**"), escapedNone()); - EXPECT_THAT(escape("*foo"), escapedNone()); + EXPECT_EQ(escape("*foo*"), R"(\*foo\*)"); + EXPECT_EQ(escape("**foo**"), R"(\*\*foo\*\*)"); + EXPECT_THAT(escape("*foo"), escaped('*')); EXPECT_THAT(escape("foo *"), escapedNone()); EXPECT_THAT(escape("foo * bar"), escapedNone()); EXPECT_THAT(escape("foo_bar"), escapedNone()); - EXPECT_THAT(escape("foo _bar"), escapedNone()); - EXPECT_THAT(escape("foo_ bar"), escapedNone()); + EXPECT_THAT(escape("foo _bar"), escaped('_')); + EXPECT_THAT(escape("foo_ bar"), escaped('_')); EXPECT_THAT(escape("foo _ bar"), escapedNone()); // HTML entities. @@ -96,8 +101,8 @@ TEST(Render, Escaping) { EXPECT_THAT(escape("foo &?; bar"), escapedNone()); // Links. - EXPECT_THAT(escape("[foo](bar)"), escapedNone()); - EXPECT_THAT(escape("[foo]: bar"), escapedNone()); + EXPECT_THAT(escape("[foo](bar)"), escaped(']')); + EXPECT_THAT(escape("[foo]: bar"), escaped(']')); // No need to escape these, as the target never exists. EXPECT_THAT(escape("[foo][]"), escapedNone()); EXPECT_THAT(escape("[foo][bar]"), escapedNone()); @@ -106,15 +111,132 @@ TEST(Render, Escaping) { // In code blocks we don't need to escape ASCII punctuation. Paragraph P = Paragraph(); P.appendCode("* foo !+ bar * baz"); - EXPECT_EQ(P.asMarkdown(), "`* foo !+ bar * baz`"); + EXPECT_EQ(P.asEscapedMarkdown(), "`* foo !+ bar * baz`"); // But we have to escape the backticks. P = Paragraph(); P.appendCode("foo`bar`baz", /*Preserve=*/true); - EXPECT_EQ(P.asMarkdown(), "`foo``bar``baz`"); + EXPECT_EQ(P.asEscapedMarkdown(), "`foo``bar``baz`"); // In plain-text, we fall back to different quotes. EXPECT_EQ(P.asPlainText(), "'foo`bar`baz'"); + // Inline code blocks starting or ending with backticks should add spaces. + P = Paragraph(); + P.appendCode("`foo"); + EXPECT_EQ(P.asEscapedMarkdown(), "` ``foo `"); + P = Paragraph(); + P.appendCode("foo`"); + EXPECT_EQ(P.asEscapedMarkdown(), "` foo`` `"); + P = Paragraph(); + P.appendCode("`foo`"); + EXPECT_EQ(P.asEscapedMarkdown(), "` ``foo`` `"); + + // Code blocks might need more than 3 backticks. + Document D; + D.addCodeBlock("foobarbaz `\nqux"); + EXPECT_EQ(D.asEscapedMarkdown(), "```cpp\n" + "foobarbaz `\nqux\n" + "```"); + D = Document(); + D.addCodeBlock("foobarbaz ``\nqux"); + EXPECT_THAT(D.asEscapedMarkdown(), "```cpp\n" + "foobarbaz ``\nqux\n" + "```"); + D = Document(); + D.addCodeBlock("foobarbaz ```\nqux"); + EXPECT_EQ(D.asEscapedMarkdown(), "````cpp\n" + "foobarbaz ```\nqux\n" + "````"); + D = Document(); + D.addCodeBlock("foobarbaz ` `` ``` ```` `\nqux"); + EXPECT_EQ(D.asEscapedMarkdown(), "`````cpp\n" + "foobarbaz ` `` ``` ```` `\nqux\n" + "`````"); +} + +TEST(Render, NoEscaping) { + // Check all ASCII punctuation. + std::string Punctuation = R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; + EXPECT_EQ(dontEscape(Punctuation), Punctuation); + + // Inline code + EXPECT_THAT(dontEscape("`foo`"), escapedNone()); + EXPECT_THAT(dontEscape("`foo"), escapedNone()); + EXPECT_THAT(dontEscape("foo`"), escapedNone()); + EXPECT_THAT(dontEscape("``foo``"), escapedNone()); + // Code blocks + EXPECT_THAT(dontEscape("```"), escapedNone()); + EXPECT_THAT(dontEscape("~~~"), escapedNone()); + + // Rulers and headings + EXPECT_THAT(dontEscape("## Heading"), escapedNone()); + EXPECT_THAT(dontEscape("Foo # bar"), escapedNone()); + EXPECT_THAT(dontEscape("---"), escapedNone()); + EXPECT_THAT(dontEscape("-"), escapedNone()); + EXPECT_THAT(dontEscape("==="), escapedNone()); + EXPECT_THAT(dontEscape("="), escapedNone()); + EXPECT_THAT(dontEscape("***"), escapedNone()); // \** could start emphasis! + + // HTML tags. + EXPECT_THAT(dontEscape("<pre"), escaped('<')); + EXPECT_THAT(dontEscape("< pre"), escapedNone()); + EXPECT_THAT(dontEscape("if a<b then"), escaped('<')); + EXPECT_THAT(dontEscape("if a<b then c."), escapedNone()); + EXPECT_THAT(dontEscape("if a<b then c='foo'."), escaped('<')); + EXPECT_THAT(dontEscape("std::vector<T>"), escaped('<')); + EXPECT_THAT(dontEscape("std::vector<std::string>"), escaped('<')); + EXPECT_THAT(dontEscape("std::map<int, int>"), escapedNone()); + // Autolinks + EXPECT_THAT(dontEscape("Email <f...@bar.com>"), escapedNone()); + EXPECT_THAT(dontEscape("Website <http://foo.bar>"), escapedNone()); + + // Bullet lists. + EXPECT_THAT(dontEscape("- foo"), escapedNone()); + EXPECT_THAT(dontEscape("* foo"), escapedNone()); + EXPECT_THAT(dontEscape("+ foo"), escapedNone()); + EXPECT_THAT(dontEscape("+"), escapedNone()); + EXPECT_THAT(dontEscape("a + foo"), escapedNone()); + EXPECT_THAT(dontEscape("a+ foo"), escapedNone()); + EXPECT_THAT(dontEscape("1. foo"), escapedNone()); + EXPECT_THAT(dontEscape("a. foo"), escapedNone()); + + // Emphasis. + EXPECT_THAT(dontEscape("*foo*"), escapedNone()); + EXPECT_THAT(dontEscape("**foo**"), escapedNone()); + EXPECT_THAT(dontEscape("*foo"), escapedNone()); + EXPECT_THAT(dontEscape("foo *"), escapedNone()); + EXPECT_THAT(dontEscape("foo * bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo_bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo _bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo_ bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo _ bar"), escapedNone()); + + // HTML entities. + EXPECT_THAT(dontEscape("fish &chips;"), escaped('&')); + EXPECT_THAT(dontEscape("fish & chips;"), escapedNone()); + EXPECT_THAT(dontEscape("fish &chips"), escapedNone()); + EXPECT_THAT(dontEscape("foo * bar"), escaped('&')); + EXPECT_THAT(dontEscape("foo ¯ bar"), escaped('&')); + EXPECT_THAT(dontEscape("foo &?; bar"), escapedNone()); + + // Links. + EXPECT_THAT(dontEscape("[foo](bar)"), escapedNone()); + EXPECT_THAT(dontEscape("[foo]: bar"), escapedNone()); + // No need to escape these, as the target never exists. + EXPECT_THAT(dontEscape("[foo][]"), escapedNone()); + EXPECT_THAT(dontEscape("[foo][bar]"), escapedNone()); + EXPECT_THAT(dontEscape("[foo]"), escapedNone()); + + // In code blocks we don't need to escape ASCII punctuation. + Paragraph P = Paragraph(); + P.appendCode("* foo !+ bar * baz"); + EXPECT_EQ(P.asMarkdown(), "`* foo !+ bar * baz`"); + + // But we have to escape the backticks. + P = Paragraph(); + P.appendCode("foo`bar`baz", /*Preserve=*/true); + EXPECT_EQ(P.asMarkdown(), "`foo``bar``baz`"); + // Inline code blocks starting or ending with backticks should add spaces. P = Paragraph(); P.appendCode("`foo"); @@ -149,17 +271,6 @@ TEST(Render, Escaping) { "`````"); } -TEST(Paragraph, Chunks) { - Paragraph P = Paragraph(); - P.appendText("One "); - P.appendCode("fish"); - P.appendText(", two "); - P.appendCode("fish", /*Preserve=*/true); - - EXPECT_EQ(P.asMarkdown(), "One `fish`, two `fish`"); - EXPECT_EQ(P.asPlainText(), "One fish, two `fish`"); -} - TEST(Paragraph, SeparationOfChunks) { // This test keeps appending contents to a single Paragraph and checks // expected accumulated contents after each one. @@ -167,26 +278,33 @@ TEST(Paragraph, SeparationOfChunks) { Paragraph P; P.appendText("after "); + EXPECT_EQ(P.asEscapedMarkdown(), "after"); EXPECT_EQ(P.asMarkdown(), "after"); EXPECT_EQ(P.asPlainText(), "after"); P.appendCode("foobar").appendSpace(); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar`"); EXPECT_EQ(P.asMarkdown(), "after `foobar`"); EXPECT_EQ(P.asPlainText(), "after foobar"); P.appendText("bat"); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar` bat"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat"); EXPECT_EQ(P.asPlainText(), "after foobar bat"); P.appendCode("no").appendCode("space"); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar` bat`no` `space`"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space`"); EXPECT_EQ(P.asPlainText(), "after foobar batno space"); P.appendText(" text"); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar` bat`no` `space` text"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space` text"); EXPECT_EQ(P.asPlainText(), "after foobar batno space text"); P.appendSpace().appendCode("code").appendText(".\n newline"); + EXPECT_EQ(P.asEscapedMarkdown(), + "after `foobar` bat`no` `space` text `code`.\n newline"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space` text `code`.\n newline"); EXPECT_EQ(P.asPlainText(), "after foobar batno space text code.\nnewline"); @@ -200,30 +318,37 @@ TEST(Paragraph, SeparationOfChunks2) { Paragraph P; P.appendText("after "); + EXPECT_EQ(P.asEscapedMarkdown(), "after"); EXPECT_EQ(P.asMarkdown(), "after"); EXPECT_EQ(P.asPlainText(), "after"); P.appendText("foobar"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar"); EXPECT_EQ(P.asMarkdown(), "after foobar"); EXPECT_EQ(P.asPlainText(), "after foobar"); P.appendText(" bat"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar bat"); EXPECT_EQ(P.asMarkdown(), "after foobar bat"); EXPECT_EQ(P.asPlainText(), "after foobar bat"); P.appendText("baz"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz"); EXPECT_EQ(P.asMarkdown(), "after foobar batbaz"); EXPECT_EQ(P.asPlainText(), "after foobar batbaz"); P.appendText(" faz "); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz"); EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz"); EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz"); P.appendText(" bar "); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz bar"); EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz bar"); EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz bar"); P.appendText("qux"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz bar qux"); EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz bar qux"); EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz bar qux"); } @@ -236,22 +361,27 @@ TEST(Paragraph, SeparationOfChunks3) { Paragraph P; P.appendText("after \n"); + EXPECT_EQ(P.asEscapedMarkdown(), "after"); EXPECT_EQ(P.asMarkdown(), "after"); EXPECT_EQ(P.asPlainText(), "after"); P.appendText(" foobar\n"); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar"); EXPECT_EQ(P.asMarkdown(), "after \n foobar"); EXPECT_EQ(P.asPlainText(), "after\nfoobar"); P.appendText("- bat\n"); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n- bat"); EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat"); EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat"); P.appendText("- baz"); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n- bat\n- baz"); EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat\n- baz"); EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat\n- baz"); P.appendText(" faz "); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n- bat\n- baz faz"); EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat\n- baz faz"); EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat\n- baz faz"); } @@ -262,6 +392,7 @@ TEST(Paragraph, ExtraSpaces) { Paragraph P; P.appendText("foo\n \t baz"); P.appendCode(" bar\n"); + EXPECT_EQ(P.asEscapedMarkdown(), "foo\n \t baz`bar`"); EXPECT_EQ(P.asMarkdown(), "foo\n \t baz`bar`"); EXPECT_EQ(P.asPlainText(), "foo bazbar"); } @@ -270,6 +401,7 @@ TEST(Paragraph, SpacesCollapsed) { Paragraph P; P.appendText(" foo bar "); P.appendText(" baz "); + EXPECT_EQ(P.asEscapedMarkdown(), "foo bar baz"); EXPECT_EQ(P.asMarkdown(), "foo bar baz"); EXPECT_EQ(P.asPlainText(), "foo bar baz"); } @@ -279,6 +411,7 @@ TEST(Paragraph, NewLines) { Paragraph P; P.appendText(" \n foo\nbar\n "); P.appendCode(" \n foo\nbar \n "); + EXPECT_EQ(P.asEscapedMarkdown(), "foo\nbar\n `foo bar`"); EXPECT_EQ(P.asMarkdown(), "foo\nbar\n `foo bar`"); EXPECT_EQ(P.asPlainText(), "foo bar foo bar"); } @@ -286,14 +419,17 @@ TEST(Paragraph, NewLines) { TEST(Paragraph, BoldText) { Paragraph P; P.appendBoldText(""); + EXPECT_EQ(P.asEscapedMarkdown(), ""); EXPECT_EQ(P.asMarkdown(), ""); EXPECT_EQ(P.asPlainText(), ""); P.appendBoldText(" \n foo\nbar\n "); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*\\*foo bar\\*\\*"); EXPECT_EQ(P.asMarkdown(), "**foo bar**"); EXPECT_EQ(P.asPlainText(), "**foo bar**"); P.appendSpace().appendBoldText("foobar"); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*\\*foo bar\\*\\* \\*\\*foobar\\*\\*"); EXPECT_EQ(P.asMarkdown(), "**foo bar** **foobar**"); EXPECT_EQ(P.asPlainText(), "**foo bar** **foobar**"); } @@ -301,14 +437,17 @@ TEST(Paragraph, BoldText) { TEST(Paragraph, EmphasizedText) { Paragraph P; P.appendEmphasizedText(""); + EXPECT_EQ(P.asEscapedMarkdown(), ""); EXPECT_EQ(P.asMarkdown(), ""); EXPECT_EQ(P.asPlainText(), ""); P.appendEmphasizedText(" \n foo\nbar\n "); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*foo bar\\*"); EXPECT_EQ(P.asMarkdown(), "*foo bar*"); EXPECT_EQ(P.asPlainText(), "*foo bar*"); P.appendSpace().appendEmphasizedText("foobar"); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*foo bar\\* \\*foobar\\*"); EXPECT_EQ(P.asMarkdown(), "*foo bar* *foobar*"); EXPECT_EQ(P.asPlainText(), "*foo bar* *foobar*"); } @@ -325,6 +464,7 @@ TEST(Document, Separators) { test ``` bar)md"; + EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); const char ExpectedText[] = R"pt(foo @@ -342,6 +482,7 @@ TEST(Document, Ruler) { // Ruler followed by paragraph. D.addParagraph().appendText("bar"); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\nbar"); EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nbar"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar"); @@ -350,6 +491,7 @@ TEST(Document, Ruler) { D.addRuler(); D.addCodeBlock("bar"); // Ruler followed by a codeblock. + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\n```cpp\nbar\n```"); EXPECT_EQ(D.asMarkdown(), "foo\n\n---\n```cpp\nbar\n```"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar"); @@ -358,12 +500,14 @@ TEST(Document, Ruler) { D.addParagraph().appendText("foo"); D.addRuler(); D.addRuler(); + EXPECT_EQ(D.asEscapedMarkdown(), "foo"); EXPECT_EQ(D.asMarkdown(), "foo"); EXPECT_EQ(D.asPlainText(), "foo"); // Multiple rulers between blocks D.addRuler(); D.addParagraph().appendText("foo"); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\nfoo"); EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nfoo"); EXPECT_EQ(D.asPlainText(), "foo\n\nfoo"); } @@ -376,6 +520,7 @@ TEST(Document, Append) { E.addRuler(); E.addParagraph().appendText("bar"); D.append(std::move(E)); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\nbar"); EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nbar"); } @@ -384,6 +529,7 @@ TEST(Document, Heading) { D.addHeading(1).appendText("foo"); D.addHeading(2).appendText("bar"); D.addParagraph().appendText("baz"); + EXPECT_EQ(D.asEscapedMarkdown(), "# foo\n\n## bar\n\nbaz"); EXPECT_EQ(D.asMarkdown(), "# foo\n\n## bar\n\nbaz"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar\n\nbaz"); } @@ -403,6 +549,7 @@ foo R"pt(foo bar baz)pt"; + EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asPlainText(), ExpectedPlainText); D.addCodeBlock("foo"); @@ -415,6 +562,7 @@ foo ```cpp foo ```)md"; + EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); ExpectedPlainText = R"pt(foo @@ -429,12 +577,14 @@ TEST(BulletList, Render) { BulletList L; // Flat list L.addItem().addParagraph().appendText("foo"); + EXPECT_EQ(L.asEscapedMarkdown(), "- foo"); EXPECT_EQ(L.asMarkdown(), "- foo"); EXPECT_EQ(L.asPlainText(), "- foo"); L.addItem().addParagraph().appendText("bar"); llvm::StringRef Expected = R"md(- foo - bar)md"; + EXPECT_EQ(L.asEscapedMarkdown(), Expected); EXPECT_EQ(L.asMarkdown(), Expected); EXPECT_EQ(L.asPlainText(), Expected); @@ -465,6 +615,7 @@ TEST(BulletList, Render) { - baz baz)md"; + EXPECT_EQ(L.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(L.asMarkdown(), ExpectedMarkdown); StringRef ExpectedPlainText = R"pt(- foo - bar @@ -494,6 +645,7 @@ TEST(BulletList, Render) { baz after)md"; + EXPECT_EQ(L.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(L.asMarkdown(), ExpectedMarkdown); ExpectedPlainText = R"pt(- foo - bar _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits