[clang-tools-extra] [clangd] Use plaintext newline handling for escaped markdown hover style (PR #185197)
via cfe-commits
cfe-commits at lists.llvm.org
Sun Mar 8 05:37:29 PDT 2026
https://github.com/tcottin updated https://github.com/llvm/llvm-project/pull/185197
>From d9714b7edf3b56ace9ed90aba3afb66891f7673f Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sun, 8 Mar 2026 12:37:10 +0000
Subject: [PATCH] use plaintext newline handling for escaped markdown hover
style
---
clang-tools-extra/clangd/support/Markup.cpp | 31 +++++++++++++++----
clang-tools-extra/clangd/support/Markup.h | 15 ++++++---
.../clangd/unittests/HoverTests.cpp | 28 ++++++++---------
.../unittests/SymbolDocumentationTests.cpp | 11 +++----
.../clangd/unittests/support/MarkupTests.cpp | 25 +++++++--------
5 files changed, 65 insertions(+), 45 deletions(-)
diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp
index 9ba993a04709c..e65263e6d1f7c 100644
--- a/clang-tools-extra/clangd/support/Markup.cpp
+++ b/clang-tools-extra/clangd/support/Markup.cpp
@@ -491,7 +491,12 @@ void Paragraph::renderNewlinesMarkdown(llvm::raw_ostream &OS,
OS << Line;
- if (!Rest.empty() && isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/true))
+ if (Rest.empty())
+ // In this case the paragraph ends and we don't need to add another
+ // newline
+ continue;
+
+ if (isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/true))
// In markdown, 2 spaces before a line break forces a line break.
OS << " ";
OS << '\n';
@@ -528,7 +533,11 @@ void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
NeedsSpace = C.SpaceAfter;
}
- renderNewlinesMarkdown(OS, ParagraphText);
+ // We use the same newline handling as for plaintext to "escape" markdown
+ // whitespace rendering.
+ // But we need to use Markdown linebreaks since the client requested Markdown
+ // content.
+ renderNewlinesPlaintext(OS, ParagraphText, /*UseMarkdownLinebreaks=*/true);
// A paragraph in markdown is separated by a blank line.
OS << "\n\n";
@@ -635,7 +644,8 @@ bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest,
}
void Paragraph::renderNewlinesPlaintext(llvm::raw_ostream &OS,
- llvm::StringRef ParagraphText) const {
+ llvm::StringRef ParagraphText,
+ bool UseMarkdownLinebreaks) const {
llvm::StringRef Line, Rest;
for (std::tie(Line, Rest) = ParagraphText.trim().split('\n');
@@ -659,13 +669,22 @@ void Paragraph::renderNewlinesPlaintext(llvm::raw_ostream &OS,
OS << canonicalizeSpaces(Line);
- if (isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/false))
+ if (Rest.empty())
+ // In this case the paragraph ends and we don't need to add another
+ // newline
+ continue;
+
+ if (isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/false)) {
+ if (UseMarkdownLinebreaks)
+ // In markdown, 2 spaces before a line break forces a line break.
+ OS << " ";
OS << '\n';
- else if (!Rest.empty())
+ } else {
// 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 << ' ';
+ }
}
}
@@ -700,7 +719,7 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
NeedsSpace = C.SpaceAfter;
}
- renderNewlinesPlaintext(OS, ParagraphText);
+ renderNewlinesPlaintext(OS, ParagraphText, /*UseMarkdownLinebreaks=*/false);
// Paragraphs are separated by a blank line.
OS << "\n\n";
diff --git a/clang-tools-extra/clangd/support/Markup.h b/clang-tools-extra/clangd/support/Markup.h
index 219a7dad1e175..bf22abd82622c 100644
--- a/clang-tools-extra/clangd/support/Markup.h
+++ b/clang-tools-extra/clangd/support/Markup.h
@@ -161,15 +161,20 @@ class Paragraph : public Block {
///
/// Lines containing only whitespace are ignored.
///
- /// This newline handling is only used when the client requests plain
- /// text for hover/signature help content.
- /// Therefore with this approach we mimic the behavior of markdown rendering
- /// for these clients.
+ /// This newline handling is used when the server is configured to provide
+ /// Plaintext hover content.
+ ///
+ /// In case the client requests Markdown content, the newline "character"
+ /// needs to be a Markdown line break (two spaces followed by `\n`).
+ /// Otherwise the linebreak will not be rendered correctly.
+ /// This can be controlled by the \p UseMarkdownLinebreaks parameter.
///
/// \param OS The stream to render to.
/// \param ParagraphText The text of the paragraph to render.
+ /// \param UseMarkdownLinebreaks Indicates whether to use markdown linebreaks
void renderNewlinesPlaintext(llvm::raw_ostream &OS,
- llvm::StringRef ParagraphText) const;
+ llvm::StringRef ParagraphText,
+ bool UseMarkdownLinebreaks) const;
};
/// Represents a sequence of one or more documents. Knows how to print them in a
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 7bff20e6f5635..aeff0206a01e0 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -4362,13 +4362,13 @@ TEST(Hover, ParseDocumentation) {
llvm::StringRef ExpectedRenderPlainText;
} Cases[] = {{
" \n foo\nbar",
- "foo\nbar",
+ "foo bar",
"foo\nbar",
"foo bar",
},
{
"foo\nbar \n ",
- "foo\nbar",
+ "foo bar",
"foo\nbar",
"foo bar",
},
@@ -4380,7 +4380,7 @@ TEST(Hover, ParseDocumentation) {
},
{
"foo \nbar",
- "foo \nbar",
+ "foo \nbar",
"foo \nbar",
"foo\nbar",
},
@@ -4392,25 +4392,25 @@ TEST(Hover, ParseDocumentation) {
},
{
"foo\n\n\n\tbar",
- "foo\n\n\tbar",
+ "foo\n\nbar",
"foo\n\n\tbar",
"foo\n\nbar",
},
{
"foo\n\n\n bar",
- "foo\n\n bar",
+ "foo\n\nbar",
"foo\n\n bar",
"foo\n\nbar",
},
{
"foo\n\n\n bar",
- "foo\n\n bar",
+ "foo\n\nbar",
"foo\n\n bar",
"foo\n\nbar",
},
{
"foo\n\n\n bar",
- "foo\n\n bar",
+ "foo\n\nbar",
"foo\n\n bar",
"foo\n\nbar",
},
@@ -4422,25 +4422,25 @@ TEST(Hover, ParseDocumentation) {
},
{
"foo\n\n\n\n\tbar",
- "foo\n\n\tbar",
+ "foo\n\nbar",
"foo\n\n\tbar",
"foo\n\nbar",
},
{
"foo\n\n\n\n bar",
- "foo\n\n bar",
+ "foo\n\nbar",
"foo\n\n bar",
"foo\n\nbar",
},
{
"foo\n\n\n\n bar",
- "foo\n\n bar",
+ "foo\n\nbar",
"foo\n\n bar",
"foo\n\nbar",
},
{
"foo\n\n\n\n bar",
- "foo\n\n bar",
+ "foo\n\nbar",
"foo\n\n bar",
"foo\n\nbar",
},
@@ -4452,7 +4452,7 @@ TEST(Hover, ParseDocumentation) {
},
{
"foo. \nbar",
- "foo. \nbar",
+ "foo. \nbar",
"foo. \nbar",
"foo.\nbar",
},
@@ -4464,7 +4464,7 @@ TEST(Hover, ParseDocumentation) {
},
{
"foo\nbar",
- "foo\nbar",
+ "foo bar",
"foo\nbar",
"foo bar",
},
@@ -4482,7 +4482,7 @@ TEST(Hover, ParseDocumentation) {
},
{
"`not\nparsed`",
- "\\`not\nparsed\\`",
+ "\\`not parsed\\`",
"`not\nparsed`",
"`not parsed`",
},
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
index 676f7dfc74483..236ac1d3ccc5c 100644
--- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -33,7 +33,7 @@ TEST(SymbolDocumentation, DetailedDocToMarkup) {
},
{
"brief\n\nfoo\nbar\n",
- "foo\nbar",
+ "foo bar",
"foo\nbar",
"foo bar",
},
@@ -174,8 +174,7 @@ documentation)",
these are details
-More description
-documentation)",
+More description documentation)",
R"(**\brief** another brief?
these are details
@@ -194,8 +193,7 @@ More description documentation)",
<b>this is a bold text</b>
normal text<i>this is an italic text</i>
<code>this is a code block</code>)",
- R"(\<b>this is a bold text\</b>
-normal text\<i>this is an italic text\</i>
+ R"(\<b>this is a bold text\</b> normal text\<i>this is an italic text\</i>
\<code>this is a code block\</code>)",
R"(\<b>this is a bold text\</b>
normal text\<i>this is an italic text\</i>
@@ -711,8 +709,7 @@ TEST(SymbolDocumentation, MarkdownCodeSpans) {
{R"(`multi
line
\c span`)",
- R"(\`multi
-line
+ R"(\`multi line
\\c span\`)",
R"(`multi
line
diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
index af4782c07ae52..59746c6f3dcab 100644
--- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
+++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
@@ -304,7 +304,7 @@ TEST(Paragraph, SeparationOfChunks) {
P.appendSpace().appendCode("code").appendText(".\n newline");
EXPECT_EQ(P.asEscapedMarkdown(),
- "after `foobar` bat`no` `space` text `code`. \n newline");
+ "after `foobar` bat`no` `space` text `code`. \nnewline");
EXPECT_EQ(P.asMarkdown(),
"after `foobar` bat`no` `space` text `code`. \n newline");
EXPECT_EQ(P.asPlainText(), "after foobar batno space text code.\nnewline");
@@ -343,12 +343,12 @@ TEST(Paragraph, SeparationOfChunks2) {
EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz");
P.appendText(" bar ");
- EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz 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.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");
}
@@ -366,23 +366,22 @@ TEST(Paragraph, SeparationOfChunks3) {
EXPECT_EQ(P.asPlainText(), "after");
P.appendText(" foobar\n");
- EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar");
+ EXPECT_EQ(P.asEscapedMarkdown(), "after \nfoobar");
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.asEscapedMarkdown(), "after \nfoobar \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 \nfoobar \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 \nfoobar \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");
}
@@ -461,8 +460,8 @@ TEST(Paragraph, LineBreakIndicators) {
"Visual linebreak for\n>blockquoute line 1\n> blockquoute line 2"},
{"Visual linebreak for\n# Heading 1\ntext under heading\n## Heading "
"2\ntext under heading 2",
- "Visual linebreak for \n\\# Heading 1\ntext under heading \n\\## "
- "Heading 2\ntext under heading 2",
+ "Visual linebreak for \n\\# Heading 1 text under heading \n\\## "
+ "Heading 2 text under heading 2",
"Visual linebreak for\n# Heading 1\ntext under heading\n## Heading "
"2\ntext under heading 2",
"Visual linebreak for\n# Heading 1 text under heading\n## Heading 2 "
@@ -488,7 +487,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.asEscapedMarkdown(), "foo baz`bar`");
EXPECT_EQ(P.asMarkdown(), "foo\n \t baz`bar`");
EXPECT_EQ(P.asPlainText(), "foo bazbar");
}
@@ -497,7 +496,7 @@ TEST(Paragraph, SpacesCollapsed) {
Paragraph P;
P.appendText(" foo bar ");
P.appendText(" baz ");
- EXPECT_EQ(P.asEscapedMarkdown(), "foo bar baz");
+ EXPECT_EQ(P.asEscapedMarkdown(), "foo bar baz");
EXPECT_EQ(P.asMarkdown(), "foo bar baz");
EXPECT_EQ(P.asPlainText(), "foo bar baz");
}
@@ -507,7 +506,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.asEscapedMarkdown(), "foo bar \n`foo bar`");
EXPECT_EQ(P.asMarkdown(), "foo\nbar\n `foo bar`");
EXPECT_EQ(P.asPlainText(), "foo bar foo bar");
}
More information about the cfe-commits
mailing list