[clang-tools-extra] [clangd] Improve Markup Rendering (PR #140498)
via cfe-commits
cfe-commits at lists.llvm.org
Sat Jul 12 13:45:58 PDT 2025
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 <timcottin at gmx.de>
Date: Mon, 19 May 2025 06:26:36 +0000
Subject: [PATCH 1/7] [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 <timcottin at gmx.de>
Date: Fri, 30 May 2025 19:56:26 +0000
Subject: [PATCH 2/7] [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 39b891de33872162473e28748b7ce5e4a04dcf92 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Tue, 1 Jul 2025 17:32:07 +0000
Subject: [PATCH 3/7] [clangd] introduce CommentFormat option
---
clang-tools-extra/clangd/ClangdLSPServer.cpp | 6 +-
clang-tools-extra/clangd/Config.h | 13 +
clang-tools-extra/clangd/ConfigCompile.cpp | 16 ++
clang-tools-extra/clangd/ConfigFragment.h | 11 +
clang-tools-extra/clangd/ConfigYAML.cpp | 10 +
clang-tools-extra/clangd/Hover.cpp | 21 ++
clang-tools-extra/clangd/Hover.h | 2 +
clang-tools-extra/clangd/support/Markup.cpp | 174 ++++++++++++-
clang-tools-extra/clangd/support/Markup.h | 6 +
.../clangd/unittests/HoverTests.cpp | 17 +-
.../clangd/unittests/support/MarkupTests.cpp | 228 +++++++++++++++---
11 files changed, 451 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..ca8033e5df98d 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,21 @@ 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..3a55cbd3078ec 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,26 @@ 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..50f7d8ee4102d 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,23 @@ 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 +311,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 +328,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 +374,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 +391,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 +412,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 +574,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 +688,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..efb3408d0eae3 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 <foo at 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
>From dd5659e4155c2b3a4296740869ba21ac2438e1d8 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Thu, 3 Jul 2025 07:41:21 +0000
Subject: [PATCH 4/7] [clangd] Escaping adaption due to whitespace preservation
---
clang-tools-extra/clangd/CodeComplete.cpp | 6 ++-
clang-tools-extra/clangd/Hover.cpp | 7 +++-
clang-tools-extra/clangd/support/Markup.cpp | 39 +++++++++++++++----
.../clangd/test/signature-help-unescaped.test | 36 +++++++++++++++++
.../clangd/test/signature-help.test | 4 +-
.../clangd/unittests/HoverTests.cpp | 20 +++++++++-
.../clangd/unittests/support/MarkupTests.cpp | 6 +--
7 files changed, 103 insertions(+), 15 deletions(-)
create mode 100644 clang-tools-extra/clangd/test/signature-help-unescaped.test
diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp
index a8182ce98ebe0..cd46d76e8b37b 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -193,7 +193,11 @@ MarkupContent renderDoc(const markup::Document &Doc, MarkupKind Kind) {
Result.value.append(Doc.asPlainText());
break;
case MarkupKind::Markdown:
- Result.value.append(Doc.asMarkdown());
+ if (Config::current().Documentation.CommentFormat ==
+ Config::CommentFormatPolicy::PlainText)
+ Result.value.append(Doc.asEscapedMarkdown());
+ else
+ Result.value.append(Doc.asMarkdown());
break;
}
return Result;
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 3a55cbd3078ec..c92c69ec0205d 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1569,9 +1569,14 @@ std::optional<llvm::StringRef> getBacktickQuoteRange(llvm::StringRef Line,
return std::nullopt;
// The quoted string must be nonempty and usually has no leading/trailing ws.
- auto Next = Line.find('`', Offset + 1);
+ auto Next = Line.find_first_of("`\n", Offset + 1);
if (Next == llvm::StringRef::npos)
return std::nullopt;
+
+ // There should be no newline in the quoted string.
+ if (Line[Next] == '\n')
+ return std::nullopt;
+
llvm::StringRef Contents = Line.slice(Offset + 1, Next);
if (Contents.empty() || isWhitespace(Contents.front()) ||
isWhitespace(Contents.back()))
diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp
index 50f7d8ee4102d..dce20d1544097 100644
--- a/clang-tools-extra/clangd/support/Markup.cpp
+++ b/clang-tools-extra/clangd/support/Markup.cpp
@@ -203,14 +203,39 @@ bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After,
std::string renderText(llvm::StringRef Input, bool StartsLine,
bool EscapeMarkdown = false) {
std::string R;
- for (unsigned I = 0; I < Input.size(); ++I) {
- 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]);
+
+ // split the input into lines, and escape each line separately.
+ llvm::StringRef Line, Rest;
+
+ bool StartsLineIntern = StartsLine;
+ bool IsFirstLine = true;
+
+ for (std::tie(Line, Rest) = Input.split('\n');
+ !(Line.empty() && Rest.empty());
+ std::tie(Line, Rest) = Rest.split('\n')) {
+
+ StartsLineIntern = IsFirstLine ? StartsLine : true;
+
+ // Ignore leading spaces for the escape logic, but preserve them in the
+ // output.
+ StringRef LeadingSpaces = Line.take_while(llvm::isSpace);
+ if (!LeadingSpaces.empty()) {
+ R.append(LeadingSpaces);
+ }
+
+ for (unsigned I = LeadingSpaces.size(); I < Line.size(); ++I) {
+ if (needsLeadingEscape(Line[I], Line.substr(LeadingSpaces.size(), I),
+ Line.substr(I + 1), StartsLineIntern,
+ EscapeMarkdown))
+ R.push_back('\\');
+ R.push_back(Line[I]);
+ }
+
+ IsFirstLine = false;
+ if (!Rest.empty())
+ R.push_back('\n');
}
+
return R;
}
diff --git a/clang-tools-extra/clangd/test/signature-help-unescaped.test b/clang-tools-extra/clangd/test/signature-help-unescaped.test
new file mode 100644
index 0000000000000..a79b2587422af
--- /dev/null
+++ b/clang-tools-extra/clangd/test/signature-help-unescaped.test
@@ -0,0 +1,36 @@
+# RUN: mkdir -p %t/clangd
+
+# Create a config file that configures to use CommentFormat Markdown.
+# RUN: echo 'Documentation:' > %t/clangd/config.yaml
+# RUN: echo ' CommentFormat: Markdown' >> %t/clangd/config.yaml
+# RUN: env XDG_CONFIG_HOME=%t clangd -lit-test -enable-config < %s | FileCheck -strict-whitespace %s
+# 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` _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,
+# CHECK-NEXT: "jsonrpc": "2.0",
+# CHECK-NEXT: "result": {
+# CHECK-NEXT: "activeParameter": 0,
+# CHECK-NEXT: "activeSignature": 0,
+# CHECK-NEXT: "signatures": [
+# CHECK-NEXT: {
+# CHECK-NEXT: "documentation": {
+# CHECK-NEXT: "kind": "markdown",
+# CHECK-NEXT: "value": "comment `markdown` _noescape_"
+# CHECK-NEXT: },
+# CHECK-NEXT: "label": "x(int) -> void",
+# CHECK-NEXT: "parameters": [
+# CHECK-NEXT: {
+# CHECK-NEXT: "label": "int"
+# CHECK-NEXT: }
+# CHECK-NEXT: ]
+# CHECK-NEXT: }
+# CHECK-NEXT: ]
+# CHECK-NEXT: }
+---
+{"jsonrpc":"2.0","id":100000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
diff --git a/clang-tools-extra/clangd/test/signature-help.test b/clang-tools-extra/clangd/test/signature-help.test
index cc6f3a09cee71..a642574571cc3 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` _noescape_\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` _escape_\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` _noescape_"
+# CHECK-NEXT: "value": "comment `markdown` \\_escape\\_"
# CHECK-NEXT: },
# CHECK-NEXT: "label": "x(int) -> void",
# CHECK-NEXT: "parameters": [
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 48a0d5773e226..cc6063e15b525 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -3765,24 +3765,29 @@ provides Foo, Bar, Baz, Foobar, Qux and 1 more)"}};
TEST(Hover, ParseDocumentation) {
struct Case {
llvm::StringRef Documentation;
+ llvm::StringRef ExpectedRenderEscapedMarkdown;
llvm::StringRef ExpectedRenderMarkdown;
llvm::StringRef ExpectedRenderPlainText;
} Cases[] = {{
" \n foo\nbar",
"foo\nbar",
+ "foo\nbar",
"foo bar",
},
{
"foo\nbar \n ",
"foo\nbar",
+ "foo\nbar",
"foo bar",
},
{
+ "foo \nbar",
"foo \nbar",
"foo \nbar",
"foo\nbar",
},
{
+ "foo \nbar",
"foo \nbar",
"foo \nbar",
"foo\nbar",
@@ -3791,43 +3796,52 @@ TEST(Hover, ParseDocumentation) {
"foo\n\n\nbar",
"foo\n\nbar",
"foo\n\nbar",
+ "foo\n\nbar",
},
{
"foo\n\n\n\tbar",
"foo\n\n\tbar",
+ "foo\n\n\tbar",
"foo\n\nbar",
},
{
"foo\n\n\n bar",
"foo\n\n bar",
+ "foo\n\n bar",
"foo\n\nbar",
},
{
"foo\n\n\n bar",
"foo\n\n bar",
+ "foo\n\n bar",
"foo\n\nbar",
},
{
"foo\n\n\n bar",
"foo\n\n bar",
+ "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\nbar",
"foo\nbar",
"foo bar",
@@ -3836,15 +3850,18 @@ TEST(Hover, ParseDocumentation) {
"Tests primality of `p`.",
"Tests primality of `p`.",
"Tests primality of `p`.",
+ "Tests primality of `p`.",
},
{
"'`' 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\nparsed\\`",
+ "`not\nparsed`",
"`not parsed`",
}};
@@ -3852,6 +3869,7 @@ TEST(Hover, ParseDocumentation) {
markup::Document Output;
parseDocumentation(C.Documentation, Output);
+ EXPECT_EQ(Output.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
EXPECT_EQ(Output.asMarkdown(), C.ExpectedRenderMarkdown);
EXPECT_EQ(Output.asPlainText(), C.ExpectedRenderPlainText);
}
diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
index efb3408d0eae3..482f230fb86fe 100644
--- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
+++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
@@ -371,17 +371,17 @@ TEST(Paragraph, SeparationOfChunks3) {
EXPECT_EQ(P.asPlainText(), "after\nfoobar");
P.appendText("- bat\n");
- EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n- bat");
+ 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.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.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");
}
>From 6d81ab09fab3dd34187e922183bb511c15ebb936 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Fri, 11 Jul 2025 20:42:46 +0200
Subject: [PATCH 5/7] [clangd] Set test as unsupported for Windows
---
clang-tools-extra/clangd/test/signature-help-unescaped.test | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/clang-tools-extra/clangd/test/signature-help-unescaped.test b/clang-tools-extra/clangd/test/signature-help-unescaped.test
index a79b2587422af..c8a7df81580c9 100644
--- a/clang-tools-extra/clangd/test/signature-help-unescaped.test
+++ b/clang-tools-extra/clangd/test/signature-help-unescaped.test
@@ -1,3 +1,7 @@
+# We specify a custom path in XDG_CONFIG_HOME, which only works on some systems.
+# UNSUPPORTED: system-windows
+# UNSUPPORTED: system-darwin
+
# RUN: mkdir -p %t/clangd
# Create a config file that configures to use CommentFormat Markdown.
>From d1b3dffc40c664d8cf543051a3fc5264d7a86478 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sat, 12 Jul 2025 19:50:48 +0000
Subject: [PATCH 6/7] [clangd] Fix review findings
---
clang-tools-extra/clangd/Hover.cpp | 8 ++
clang-tools-extra/clangd/support/Markup.cpp | 78 +++++++++++++++----
clang-tools-extra/clangd/support/Markup.h | 19 +++--
.../clangd/unittests/HoverTests.cpp | 30 +++++++
4 files changed, 111 insertions(+), 24 deletions(-)
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index c92c69ec0205d..b0d560999da59 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1608,11 +1608,19 @@ void parseDocumentationParagraph(llvm::StringRef Text, markup::Paragraph &Out) {
}
void parseDocumentation(llvm::StringRef Input, markup::Document &Output) {
+ // A documentation string is treated as a sequence of paragraphs,
+ // where the paragraphs are seperated by at least one empty line
+ // (meaning 2 consecutive newline characters).
+ // Possible leading empty lines (introduced by an even number > 1 of
+ // empty lines between 2 paragraphs) will be removed later in the Markup
+ // renderer.
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")) {
+ // The Paragraph will be empty if there is an even number of newline
+ // characters between two paragraphs, so we skip it.
if (!Paragraph.empty())
parseDocumentationParagraph(Paragraph, Output.addParagraph());
}
diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp
index dce20d1544097..7be98e6855845 100644
--- a/clang-tools-extra/clangd/support/Markup.cpp
+++ b/clang-tools-extra/clangd/support/Markup.cpp
@@ -168,7 +168,8 @@ bool needsLeadingEscapePlaintext(char C, llvm::StringRef Before,
///
/// \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.
+/// 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 needsLeadingEscapeMarkdown(char C, llvm::StringRef After) {
switch (C) {
@@ -198,23 +199,25 @@ bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After,
return needsLeadingEscapeMarkdown(C, After);
}
-/// Escape a markdown text block. Ensures the punctuation will not introduce
+/// Escape a markdown text block.
+/// If \p EscapeMarkdown is true it ensures the punctuation will not introduce
/// any of the markdown constructs.
+/// Else, markdown syntax is not escaped, only HTML tags and entities.
std::string renderText(llvm::StringRef Input, bool StartsLine,
- bool EscapeMarkdown = false) {
+ bool EscapeMarkdown) {
std::string R;
+ R.reserve(Input.size());
// split the input into lines, and escape each line separately.
llvm::StringRef Line, Rest;
- bool StartsLineIntern = StartsLine;
bool IsFirstLine = true;
for (std::tie(Line, Rest) = Input.split('\n');
!(Line.empty() && Rest.empty());
std::tie(Line, Rest) = Rest.split('\n')) {
- StartsLineIntern = IsFirstLine ? StartsLine : true;
+ bool StartsLineIntern = IsFirstLine ? StartsLine : true;
// Ignore leading spaces for the escape logic, but preserve them in the
// output.
@@ -243,6 +246,7 @@ std::string renderText(llvm::StringRef Input, bool StartsLine,
/// is surrounded by backticks and the inner contents are properly escaped.
std::string renderInlineBlock(llvm::StringRef Input) {
std::string R;
+ R.reserve(Input.size());
// Double all backticks to make sure we don't close the inline block early.
for (size_t From = 0; From < Input.size();) {
size_t Next = Input.find("`", From);
@@ -401,17 +405,21 @@ class Heading : public Paragraph {
Heading(size_t Level) : Level(Level) {}
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
- OS << std::string(Level, '#') << ' ';
+ insertHeadingMarkers(OS);
Paragraph::renderEscapedMarkdown(OS);
}
void renderMarkdown(llvm::raw_ostream &OS) const override {
- OS << std::string(Level, '#') << ' ';
+ insertHeadingMarkers(OS);
Paragraph::renderMarkdown(OS);
}
private:
size_t Level;
+
+ void insertHeadingMarkers(llvm::raw_ostream &OS) const {
+ OS << std::string(Level, '#') << ' ';
+ }
};
} // namespace
@@ -445,16 +453,18 @@ void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
OS << " ";
switch (C.Kind) {
case ChunkKind::PlainText:
- OS << renderText(C.Contents, !HasChunks, true);
+ OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/true);
break;
case ChunkKind::InlineCode:
OS << renderInlineBlock(C.Contents);
break;
case ChunkKind::Bold:
- OS << renderText("**" + C.Contents + "**", !HasChunks, true);
+ OS << renderText("**" + C.Contents + "**", !HasChunks,
+ /*EscapeMarkdown=*/true);
break;
case ChunkKind::Emphasized:
- OS << renderText("*" + C.Contents + "*", !HasChunks, true);
+ OS << renderText("*" + C.Contents + "*", !HasChunks,
+ /*EscapeMarkdown=*/true);
break;
}
HasChunks = true;
@@ -472,16 +482,18 @@ void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const {
OS << " ";
switch (C.Kind) {
case ChunkKind::PlainText:
- OS << renderText(C.Contents, !HasChunks);
+ OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false);
break;
case ChunkKind::InlineCode:
OS << renderInlineBlock(C.Contents);
break;
case ChunkKind::Bold:
- OS << "**" << renderText(C.Contents, !HasChunks) << "**";
+ OS << "**" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false)
+ << "**";
break;
case ChunkKind::Emphasized:
- OS << "*" << renderText(C.Contents, !HasChunks) << "*";
+ OS << "*" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false)
+ << "*";
break;
}
HasChunks = true;
@@ -545,6 +557,8 @@ bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line,
void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
bool NeedsSpace = false;
std::string ConcatenatedText;
+ ConcatenatedText.reserve(EstimatedStringSize);
+
llvm::raw_string_ostream ConcatenatedOS(ConcatenatedText);
for (auto &C : Chunks) {
@@ -573,6 +587,18 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
// We go through the contents line by line to handle the newlines
// and required spacing correctly.
+ //
+ // Newlines are added if:
+ // - the line ends with 2 spaces and a newline follows
+ // - the line ends with punctuation that indicates a line break (.:,;!?)
+ // - the next line starts with a hard line break indicator (-@>#`, or a digit
+ // followed by '.' or ')'), ignoring leading whitespace.
+ //
+ // Otherwise, newlines in the input are replaced with a single space.
+ //
+ // Multiple spaces are collapsed into a single space.
+ //
+ // Lines containing only whitespace are ignored.
llvm::StringRef Line, Rest;
for (std::tie(Line, Rest) =
@@ -580,6 +606,17 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
!(Line.empty() && Rest.empty());
std::tie(Line, Rest) = Rest.split('\n')) {
+ // Remove lines which only contain whitespace.
+ //
+ // Note: this also handles the case when there are multiple newlines
+ // in a row, since all leading newlines are removed.
+ //
+ // The documentation parsing treats multiple newlines as paragraph
+ // separators, hence it will create a new Paragraph instead of adding
+ // multiple newlines to the same Paragraph.
+ // Therfore multiple newlines are never added to a paragraph
+ // except if the user explicitly adds them using
+ // e.g. appendText("user text\n\nnext text").
Line = Line.ltrim();
if (Line.empty())
continue;
@@ -589,6 +626,9 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
if (isHardLineBreakAfter(Line, Rest))
OS << '\n';
else if (!Rest.empty())
+ // Since we removed any trailing whitespace from the input using trim(),
+ // we know that the next line contains non-whitespace characters.
+ // Therefore, we can add a space without worrying about trailing spaces.
OS << ' ';
}
@@ -606,7 +646,7 @@ void BulletList::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
// 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.
+ // We add 2 newlines after list to terminate it in markdown.
OS << "\n\n";
}
@@ -617,7 +657,7 @@ void BulletList::renderMarkdown(llvm::raw_ostream &OS) const {
// 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.
+ // We add 2 newlines after list to terminate it in markdown.
OS << "\n\n";
}
@@ -641,8 +681,10 @@ Paragraph &Paragraph::appendChunk(llvm::StringRef Contents, ChunkKind K) {
return *this;
Chunks.emplace_back();
Chunk &C = Chunks.back();
- C.Contents = std::move(Contents);
+ C.Contents = Contents;
C.Kind = K;
+
+ EstimatedStringSize += Contents.size();
return *this;
}
@@ -652,7 +694,7 @@ Paragraph &Paragraph::appendText(llvm::StringRef Text) {
return *this;
}
- return appendChunk(Text, ChunkKind::PlainText);
+ return appendChunk(std::move(Text), ChunkKind::PlainText);
}
Paragraph &Paragraph::appendEmphasizedText(llvm::StringRef Text) {
@@ -677,6 +719,8 @@ Paragraph &Paragraph::appendCode(llvm::StringRef Code, bool Preserve) {
C.Preserve = Preserve;
// Disallow adjacent code spans without spaces, markdown can't render them.
C.SpaceBefore = AdjacentCode;
+
+ EstimatedStringSize += Norm.size();
return *this;
}
diff --git a/clang-tools-extra/clangd/support/Markup.h b/clang-tools-extra/clangd/support/Markup.h
index 23dcf9c7fad1d..b6399b1ec6136 100644
--- a/clang-tools-extra/clangd/support/Markup.h
+++ b/clang-tools-extra/clangd/support/Markup.h
@@ -37,6 +37,14 @@ class Block {
virtual bool isRuler() const { return false; }
virtual ~Block() = default;
+
+protected:
+ /// Estimated size of the string representation of this block.
+ /// Used to reserve space in the output string.
+ /// Each time block content is added, this value is updated.
+ /// This is an estimate, so it may not be accurate but can help
+ /// reducing dynamically reallocating string memory.
+ unsigned EstimatedStringSize = 0;
};
/// Represents parts of the markup that can contain strings, like inline code,
@@ -67,7 +75,7 @@ class Paragraph : public Block {
Paragraph &appendSpace();
private:
- typedef enum { PlainText, InlineCode, Bold, Emphasized } ChunkKind;
+ enum ChunkKind { PlainText, InlineCode, Bold, Emphasized };
struct Chunk {
ChunkKind Kind = PlainText;
// Preserve chunk markers in plaintext.
@@ -90,12 +98,6 @@ class Paragraph : public Block {
bool isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest) const;
};
-class ListItemParagraph : public Paragraph {
-public:
- void renderEscapedMarkdown(llvm::raw_ostream &OS) const override;
- void renderMarkdown(llvm::raw_ostream &OS) const override;
-};
-
/// Represents a sequence of one or more documents. Knows how to print them in a
/// list like format, e.g. by prepending with "- " and indentation.
class BulletList : public Block {
@@ -142,6 +144,9 @@ class Document {
BulletList &addBulletList();
+ /// Doesn't contain any trailing newlines and escaped markdown syntax.
+ /// It is expected that the result of this function
+ /// is rendered as markdown.
std::string asEscapedMarkdown() const;
/// Doesn't contain any trailing newlines.
/// It is expected that the result of this function
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index cc6063e15b525..6b6b0d1e87c5b 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -3822,6 +3822,36 @@ TEST(Hover, ParseDocumentation) {
"foo\n\n bar",
"foo\n\nbar",
},
+ {
+ "foo\n\n\n\nbar",
+ "foo\n\nbar",
+ "foo\n\nbar",
+ "foo\n\nbar",
+ },
+ {
+ "foo\n\n\n\n\tbar",
+ "foo\n\n\tbar",
+ "foo\n\n\tbar",
+ "foo\n\nbar",
+ },
+ {
+ "foo\n\n\n\n bar",
+ "foo\n\n bar",
+ "foo\n\n bar",
+ "foo\n\nbar",
+ },
+ {
+ "foo\n\n\n bar",
+ "foo\n\n bar",
+ "foo\n\n bar",
+ "foo\n\nbar",
+ },
+ {
+ "foo\n\n\n bar",
+ "foo\n\n bar",
+ "foo\n\n bar",
+ "foo\n\nbar",
+ },
{
"foo.\nbar",
"foo.\nbar",
>From 2d5faec894dfbb501ed9bd43d7ba50f5a1f97972 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sat, 12 Jul 2025 20:45:41 +0000
Subject: [PATCH 7/7] [clangd] fix typo
---
clang-tools-extra/clangd/Hover.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index b0d560999da59..6d2b6c3df09ae 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1611,7 +1611,7 @@ void parseDocumentation(llvm::StringRef Input, markup::Document &Output) {
// A documentation string is treated as a sequence of paragraphs,
// where the paragraphs are seperated by at least one empty line
// (meaning 2 consecutive newline characters).
- // Possible leading empty lines (introduced by an even number > 1 of
+ // Possible leading empty lines (introduced by an odd number > 1 of
// empty lines between 2 paragraphs) will be removed later in the Markup
// renderer.
llvm::StringRef Paragraph, Rest;
More information about the cfe-commits
mailing list