diff --git a/clang-tools-extra/clangd/FormattedString.h b/clang-tools-extra/clangd/FormattedString.h --- a/clang-tools-extra/clangd/FormattedString.h +++ b/clang-tools-extra/clangd/FormattedString.h @@ -13,38 +13,52 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMATTEDSTRING_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMATTEDSTRING_H +#include #include #include namespace clang { namespace clangd { +namespace markup { /// A structured string representation that could be converted to markdown or /// plaintext upon requrest. -class FormattedString { +class RenderableBlock { public: + virtual std::string renderAsMarkdown() const = 0; + virtual std::string renderAsPlainText() const = 0; + + virtual ~RenderableBlock() = default; +}; + +/// Represents parts of the markup that can contain strings, like inline code, +/// code block or plain text. +/// Only CodeBlocks guarantees conservation of new lines within text. One must +/// introduce different paragraphs to create separate blocks. +class Paragraph : public RenderableBlock { +public: + std::string renderAsMarkdown() const override; + std::string renderAsPlainText() const override; + /// Append plain text to the end of the string. - void appendText(std::string Text); - /// Append a block of C++ code. This translates to a ``` block in markdown. - /// In a plain text representation, the code block will be surrounded by - /// newlines. - void appendCodeBlock(std::string Code, std::string Language = "cpp"); + Paragraph &appendText(std::string Text); + /// Append an inline block of C++ code. This translates to the ` block in /// markdown. - void appendInlineCode(std::string Code); + Paragraph &appendInlineCode(std::string Code); - std::string renderAsMarkdown() const; - std::string renderAsPlainText() const; - std::string renderForTests() const; + /// Append a block of C++ code. This translates to a ``` block in markdown. + /// In a plain text representation, the code block will be surrounded by + /// newlines. + Paragraph &appendCodeBlock(std::string Code, std::string Language = "cpp"); private: - enum class ChunkKind { - PlainText, /// A plain text paragraph. - CodeBlock, /// A block of code. - InlineCodeBlock, /// An inline block of code. - }; struct Chunk { - ChunkKind Kind = ChunkKind::PlainText; + enum { + PlainText, + InlineCode, + CodeBlock, + } Kind = PlainText; std::string Contents; /// Language for code block chunks. Ignored for other chunks. std::string Language; @@ -52,6 +66,17 @@ std::vector Chunks; }; +class Document { +public: + Paragraph &addParagraph(); + std::string renderAsMarkdown() const; + std::string renderAsPlainText() const; + +private: + std::vector> Children; +}; + +} // namespace markup } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/FormattedString.cpp b/clang-tools-extra/clangd/FormattedString.cpp --- a/clang-tools-extra/clangd/FormattedString.cpp +++ b/clang-tools-extra/clangd/FormattedString.cpp @@ -7,14 +7,19 @@ //===----------------------------------------------------------------------===// #include "FormattedString.h" #include "clang/Basic/CharInfo.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FormatVariadic.h" #include +#include #include +#include namespace clang { namespace clangd { +namespace markup { namespace { /// Escape a markdown text block. Ensures the punctuation will not introduce @@ -86,114 +91,158 @@ return BlockMarker + Language.str() + "\n" + Input.str() + "\n" + BlockMarker; } -} // namespace - -void FormattedString::appendText(std::string Text) { - Chunk C; - C.Kind = ChunkKind::PlainText; - C.Contents = Text; - Chunks.push_back(C); -} - -void FormattedString::appendCodeBlock(std::string Code, std::string Language) { - Chunk C; - C.Kind = ChunkKind::CodeBlock; - C.Contents = std::move(Code); - C.Language = std::move(Language); - Chunks.push_back(std::move(C)); -} - -void FormattedString::appendInlineCode(std::string Code) { - Chunk C; - C.Kind = ChunkKind::InlineCodeBlock; - C.Contents = std::move(Code); - Chunks.push_back(std::move(C)); +// Concatanates whitespace blocks into a single ` `. +std::string canonicalizeSpaces(std::string Input) { + // Goes over the string and preserves only a single ` ` for any whitespace + // chunks, the rest is moved to the end of the string and dropped in the end. + auto WritePtr = Input.begin(); + bool InsideWhitespaceChunk = false; + for (auto I = Input.begin(), E = Input.end(); I != E; ++I) { + if (isWhitespace(*I)) { + // If already inside a whitespace chunk, skip. + if (InsideWhitespaceChunk) + continue; + InsideWhitespaceChunk = true; + // Canonicalize whitespace. + *I = ' '; + } else { + // Moved out of the whitespace chunk. + InsideWhitespaceChunk = false; + } + *WritePtr++ = *I; + } + Input.resize(WritePtr - Input.begin()); + return Input; } -std::string FormattedString::renderAsMarkdown() const { - std::string R; - auto EnsureWhitespace = [&R]() { - // Adds a space for nicer rendering. - if (!R.empty() && !isWhitespace(R.back())) - R += " "; - }; - for (const auto &C : Chunks) { - switch (C.Kind) { - case ChunkKind::PlainText: - if (!C.Contents.empty() && !isWhitespace(C.Contents.front())) - EnsureWhitespace(); - R += renderText(C.Contents); - continue; - case ChunkKind::InlineCodeBlock: - EnsureWhitespace(); - R += renderInlineBlock(C.Contents); - continue; - case ChunkKind::CodeBlock: - if (!R.empty() && !llvm::StringRef(R).endswith("\n")) - R += "\n"; - R += renderCodeBlock(C.Contents, C.Language); - R += "\n"; - continue; +enum RenderType { + PlainText, + Markdown, +}; +std::string +renderBlocks(llvm::ArrayRef> Children, + RenderType RT) { + std::vector Items; + for (auto &C : Children) { + switch (RT) { + case PlainText: + Items.emplace_back(C->renderAsPlainText()); + break; + case Markdown: + Items.emplace_back(C->renderAsMarkdown()); + break; } - llvm_unreachable("unhanlded ChunkKind"); } - return R; + return llvm::join(Items, "\n\n"); } -std::string FormattedString::renderAsPlainText() const { +} // namespace + +std::string Paragraph::renderAsMarkdown() const { std::string R; + bool WasBlock = false; auto EnsureWhitespace = [&]() { - if (R.empty() || isWhitespace(R.back())) - return; - R += " "; + // Adds a space for nicer rendering. + if (!R.empty() && !isWhitespace(R.back())) + R += WasBlock ? '\n' : ' '; }; - Optional LastWasBlock; - for (const auto &C : Chunks) { - bool IsBlock = C.Kind == ChunkKind::CodeBlock; - if (LastWasBlock.hasValue() && (IsBlock || *LastWasBlock)) - R += "\n\n"; - LastWasBlock = IsBlock; - + for (auto &C : Chunks) { + llvm::StringRef TrimmedContents = llvm::StringRef(C.Contents).trim(); switch (C.Kind) { - case ChunkKind::PlainText: + case Chunk::PlainText: EnsureWhitespace(); - R += C.Contents; + R += renderText(TrimmedContents); break; - case ChunkKind::InlineCodeBlock: + case Chunk::InlineCode: EnsureWhitespace(); - R += C.Contents; + R += renderInlineBlock(TrimmedContents); break; - case ChunkKind::CodeBlock: - R += C.Contents; + case Chunk::CodeBlock: + // Codeblocks must start on a newline. + if (!R.empty() && R.back() != '\n') + R += '\n'; + // Note that codeblocks preserve whitespaces at the beginning/end of the + // string. + R += renderCodeBlock(C.Contents, C.Language); break; } - // Trim trailing whitespace in chunk. - while (!R.empty() && isWhitespace(R.back())) - R.pop_back(); + WasBlock = C.Kind == Chunk::CodeBlock; } return R; } -std::string FormattedString::renderForTests() const { +std::string Paragraph::renderAsPlainText() const { std::string R; - for (const auto &C : Chunks) { + auto EnsureWhitespace = [&R]() { + // Adds a space for nicer rendering. + if (!R.empty() && !isWhitespace(R.back())) + R += " "; + }; + for (auto &C : Chunks) { + llvm::StringRef TrimmedContents = llvm::StringRef(C.Contents).trim(); switch (C.Kind) { - case ChunkKind::PlainText: - R += "text[" + C.Contents + "]"; + case Chunk::PlainText: + EnsureWhitespace(); + R += TrimmedContents; break; - case ChunkKind::InlineCodeBlock: - R += "code[" + C.Contents + "]"; + case Chunk::InlineCode: + EnsureWhitespace(); + R += TrimmedContents; break; - case ChunkKind::CodeBlock: - if (!R.empty()) + case Chunk::CodeBlock: + // Make sure we have an empty line before a code block. + if (!R.empty() && !llvm::StringRef(R).endswith("\n\n")) { + if (R.back() != '\n') + R += "\n"; R += "\n"; - R += llvm::formatv("codeblock({0}) [\n{1}\n]\n", C.Language, C.Contents); + } + R += TrimmedContents.str() + "\n\n"; break; } } + // Trim trailing whitespace in chunk. while (!R.empty() && isWhitespace(R.back())) R.pop_back(); return R; } + +Paragraph &Paragraph::appendText(std::string Text) { + Chunks.emplace_back(); + Chunk &C = Chunks.back(); + C.Contents = canonicalizeSpaces(std::move(Text)); + C.Kind = Chunk::PlainText; + return *this; +} + +Paragraph &Paragraph::appendInlineCode(std::string Code) { + Chunks.emplace_back(); + Chunk &C = Chunks.back(); + C.Contents = canonicalizeSpaces(std::move(Code)); + C.Kind = Chunk::InlineCode; + return *this; +} + +Paragraph &Paragraph::appendCodeBlock(std::string Code, std::string Language) { + Chunks.emplace_back(); + Chunk &C = Chunks.back(); + C.Contents = std::move(Code); + C.Language = std::move(Language); + C.Kind = Chunk::CodeBlock; + return *this; +} + +Paragraph &Document::addParagraph() { + Children.emplace_back(std::make_unique()); + return *static_cast(Children.back().get()); +} + +std::string Document::renderAsMarkdown() const { + return renderBlocks(Children, RenderType::Markdown); +} + +std::string Document::renderAsPlainText() const { + return renderBlocks(Children, RenderType::PlainText); +} +} // namespace markup } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -72,7 +72,7 @@ llvm::Optional Value; /// Produce a user-readable information. - FormattedString present() const; + markup::Document present() const; }; llvm::raw_ostream &operator<<(llvm::raw_ostream &, const HoverInfo::Param &); inline bool operator==(const HoverInfo::Param &LHS, diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -11,6 +11,7 @@ #include "AST.h" #include "CodeCompletionStrings.h" #include "FindTarget.h" +#include "FormattedString.h" #include "Logger.h" #include "Selection.h" #include "SourceCode.h" @@ -440,28 +441,29 @@ return HI; } -FormattedString HoverInfo::present() const { - FormattedString Output; +markup::Document HoverInfo::present() const { + markup::Document Output; if (NamespaceScope) { - Output.appendText("Declared in"); + auto &P = Output.addParagraph(); + P.appendText("Declared in"); // Drop trailing "::". if (!LocalScope.empty()) - Output.appendInlineCode(llvm::StringRef(LocalScope).drop_back(2)); + P.appendInlineCode(llvm::StringRef(LocalScope).drop_back(2)); else if (NamespaceScope->empty()) - Output.appendInlineCode("global namespace"); + P.appendInlineCode("global namespace"); else - Output.appendInlineCode(llvm::StringRef(*NamespaceScope).drop_back(2)); + P.appendInlineCode(llvm::StringRef(*NamespaceScope).drop_back(2)); } if (!Definition.empty()) { - Output.appendCodeBlock(Definition); + Output.addParagraph().appendCodeBlock(Definition); } else { // Builtin types - Output.appendCodeBlock(Name); + Output.addParagraph().appendCodeBlock(Name); } if (!Documentation.empty()) - Output.appendText(Documentation); + Output.addParagraph().appendText(Documentation); return Output; } diff --git a/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp b/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp --- a/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp +++ b/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp @@ -14,186 +14,233 @@ namespace clang { namespace clangd { +namespace markup { namespace { -TEST(FormattedString, Basic) { - FormattedString S; - EXPECT_EQ(S.renderAsPlainText(), ""); - EXPECT_EQ(S.renderAsMarkdown(), ""); - - S.appendText("foobar "); - S.appendText("baz"); - EXPECT_EQ(S.renderAsPlainText(), "foobar baz"); - EXPECT_EQ(S.renderAsMarkdown(), "foobar baz"); - - S = FormattedString(); - S.appendInlineCode("foobar"); - EXPECT_EQ(S.renderAsPlainText(), "foobar"); - EXPECT_EQ(S.renderAsMarkdown(), "`foobar`"); - - S = FormattedString(); - S.appendCodeBlock("foobar"); - EXPECT_EQ(S.renderAsPlainText(), "foobar"); - EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n" - "foobar\n" - "```\n"); -} - -TEST(FormattedString, CodeBlocks) { - FormattedString S; - S.appendCodeBlock("foobar"); - S.appendCodeBlock("bazqux", "javascript"); - S.appendText("after"); - - std::string ExpectedText = R"(foobar - -bazqux - -after)"; - EXPECT_EQ(S.renderAsPlainText(), ExpectedText); - std::string ExpectedMarkdown = R"md(```cpp -foobar -``` -```javascript -bazqux -``` -after)md"; - EXPECT_EQ(S.renderAsMarkdown(), ExpectedMarkdown); - - S = FormattedString(); - S.appendInlineCode("foobar"); - S.appendInlineCode("bazqux"); - EXPECT_EQ(S.renderAsPlainText(), "foobar bazqux"); - EXPECT_EQ(S.renderAsMarkdown(), "`foobar` `bazqux`"); - - S = FormattedString(); - S.appendText("foo"); - S.appendInlineCode("bar"); - S.appendText("baz"); - - EXPECT_EQ(S.renderAsPlainText(), "foo bar baz"); - EXPECT_EQ(S.renderAsMarkdown(), "foo `bar` baz"); -} - -TEST(FormattedString, Escaping) { +TEST(Render, Escaping) { // Check some ASCII punctuation - FormattedString S; - S.appendText("*!`"); - EXPECT_EQ(S.renderAsMarkdown(), "\\*\\!\\`"); + Paragraph P; + P.appendText("*!`"); + EXPECT_EQ(P.renderAsMarkdown(), "\\*\\!\\`"); // Check all ASCII punctuation. - S = FormattedString(); + P = Paragraph(); std::string Punctuation = R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; // Same text, with each character escaped. std::string EscapedPunctuation; EscapedPunctuation.reserve(2 * Punctuation.size()); for (char C : Punctuation) EscapedPunctuation += std::string("\\") + C; - S.appendText(Punctuation); - EXPECT_EQ(S.renderAsMarkdown(), EscapedPunctuation); + P.appendText(Punctuation); + EXPECT_EQ(P.renderAsMarkdown(), EscapedPunctuation); // In code blocks we don't need to escape ASCII punctuation. - S = FormattedString(); - S.appendInlineCode("* foo !+ bar * baz"); - EXPECT_EQ(S.renderAsMarkdown(), "`* foo !+ bar * baz`"); - S = FormattedString(); - S.appendCodeBlock("#define FOO\n* foo !+ bar * baz"); - EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n" + P = Paragraph(); + P.appendInlineCode("* foo !+ bar * baz"); + EXPECT_EQ(P.renderAsMarkdown(), "`* foo !+ bar * baz`"); + P = Paragraph(); + P.appendCodeBlock("#define FOO\n* foo !+ bar * baz"); + EXPECT_EQ(P.renderAsMarkdown(), "```cpp\n" "#define FOO\n* foo !+ bar * baz\n" - "```\n"); + "```"); // But we have to escape the backticks. - S = FormattedString(); - S.appendInlineCode("foo`bar`baz"); - EXPECT_EQ(S.renderAsMarkdown(), "`foo``bar``baz`"); + P = Paragraph(); + P.appendInlineCode("foo`bar`baz"); + EXPECT_EQ(P.renderAsMarkdown(), "`foo``bar``baz`"); - S = FormattedString(); - S.appendCodeBlock("foo`bar`baz"); - EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n" + P = Paragraph(); + P.appendCodeBlock("foo`bar`baz"); + EXPECT_EQ(P.renderAsMarkdown(), "```cpp\n" "foo`bar`baz\n" - "```\n"); + "```"); // Inline code blocks starting or ending with backticks should add spaces. - S = FormattedString(); - S.appendInlineCode("`foo"); - EXPECT_EQ(S.renderAsMarkdown(), "` ``foo `"); - S = FormattedString(); - S.appendInlineCode("foo`"); - EXPECT_EQ(S.renderAsMarkdown(), "` foo`` `"); - S = FormattedString(); - S.appendInlineCode("`foo`"); - EXPECT_EQ(S.renderAsMarkdown(), "` ``foo`` `"); - - // Should also add extra spaces if the block stars and ends with spaces. - S = FormattedString(); - S.appendInlineCode(" foo "); - EXPECT_EQ(S.renderAsMarkdown(), "` foo `"); - S = FormattedString(); - S.appendInlineCode("foo "); - EXPECT_EQ(S.renderAsMarkdown(), "`foo `"); - S = FormattedString(); - S.appendInlineCode(" foo"); - EXPECT_EQ(S.renderAsMarkdown(), "` foo`"); + P = Paragraph(); + P.appendInlineCode("`foo"); + EXPECT_EQ(P.renderAsMarkdown(), "` ``foo `"); + P = Paragraph(); + P.appendInlineCode("foo`"); + EXPECT_EQ(P.renderAsMarkdown(), "` foo`` `"); + P = Paragraph(); + P.appendInlineCode("`foo`"); + EXPECT_EQ(P.renderAsMarkdown(), "` ``foo`` `"); // Code blocks might need more than 3 backticks. - S = FormattedString(); - S.appendCodeBlock("foobarbaz `\nqux"); - EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n" + P = Paragraph(); + P.appendCodeBlock("foobarbaz `\nqux"); + EXPECT_EQ(P.renderAsMarkdown(), "```cpp\n" "foobarbaz `\nqux\n" - "```\n"); - S = FormattedString(); - S.appendCodeBlock("foobarbaz ``\nqux"); - EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n" + "```"); + P = Paragraph(); + P.appendCodeBlock("foobarbaz ``\nqux"); + EXPECT_EQ(P.renderAsMarkdown(), "```cpp\n" "foobarbaz ``\nqux\n" - "```\n"); - S = FormattedString(); - S.appendCodeBlock("foobarbaz ```\nqux"); - EXPECT_EQ(S.renderAsMarkdown(), "````cpp\n" + "```"); + P = Paragraph(); + P.appendCodeBlock("foobarbaz ```\nqux"); + EXPECT_EQ(P.renderAsMarkdown(), "````cpp\n" "foobarbaz ```\nqux\n" - "````\n"); - S = FormattedString(); - S.appendCodeBlock("foobarbaz ` `` ``` ```` `\nqux"); - EXPECT_EQ(S.renderAsMarkdown(), "`````cpp\n" + "````"); + P = Paragraph(); + P.appendCodeBlock("foobarbaz ` `` ``` ```` `\nqux"); + EXPECT_EQ(P.renderAsMarkdown(), "`````cpp\n" "foobarbaz ` `` ``` ```` `\nqux\n" - "`````\n"); + "`````"); } -TEST(FormattedString, MarkdownWhitespace) { - // Whitespace should be added as separators between blocks. - FormattedString S; - S.appendText("foo"); - S.appendText("bar"); - EXPECT_EQ(S.renderAsMarkdown(), "foo bar"); - - S = FormattedString(); - S.appendInlineCode("foo"); - S.appendInlineCode("bar"); - EXPECT_EQ(S.renderAsMarkdown(), "`foo` `bar`"); - - // However, we don't want to add any extra whitespace. - S = FormattedString(); - S.appendText("foo "); - S.appendInlineCode("bar"); - EXPECT_EQ(S.renderAsMarkdown(), "foo `bar`"); - - S = FormattedString(); - S.appendText("foo\n"); - S.appendInlineCode("bar"); - EXPECT_EQ(S.renderAsMarkdown(), "foo\n`bar`"); - - S = FormattedString(); - S.appendInlineCode("foo"); - S.appendText(" bar"); - EXPECT_EQ(S.renderAsMarkdown(), "`foo` bar"); - - S = FormattedString(); - S.appendText("foo"); - S.appendCodeBlock("bar"); - S.appendText("baz"); - EXPECT_EQ(S.renderAsMarkdown(), "foo\n```cpp\nbar\n```\nbaz"); +TEST(Paragraph, SeparationOfBlocks) { + // 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. + struct Test { + enum { + PlainText, + InlineBlock, + CodeBlock, + } Kind; + llvm::StringRef Contents; + llvm::StringRef Language; + llvm::StringRef ExpectedMarkdown; + llvm::StringRef ExpectedPlainText; + } Tests[] = { + { + Test::CodeBlock, + "foobar", + "cpp", + R"md(```cpp +foobar +```)md", + "foobar", + }, + { + Test::PlainText, + "after", + "", + R"md(```cpp +foobar +``` +after)md", + R"pt(foobar + +after)pt", + }, + { + Test::CodeBlock, + "bazqux", + "javascript", + R"md(```cpp +foobar +``` +after +```javascript +bazqux +```)md", + R"pt(foobar + +after + +bazqux)pt", + }, + { + Test::InlineBlock, + "foobar", + "", + R"md(```cpp +foobar +``` +after +```javascript +bazqux +``` +`foobar`)md", + R"pt(foobar + +after + +bazqux + +foobar)pt", + }, + { + Test::PlainText, + "bat", + "", + R"md(```cpp +foobar +``` +after +```javascript +bazqux +``` +`foobar` bat)md", + R"pt(foobar + +after + +bazqux + +foobar bat)pt", + }, + }; + Paragraph P; + for (const auto &T : Tests) { + switch (T.Kind) { + case Test::CodeBlock: + P.appendCodeBlock(T.Contents, T.Language); + break; + case Test::InlineBlock: + P.appendInlineCode(T.Contents); + break; + case Test::PlainText: + P.appendText(T.Contents); + break; + } + EXPECT_EQ(P.renderAsMarkdown(), T.ExpectedMarkdown); + EXPECT_EQ(P.renderAsPlainText(), T.ExpectedPlainText); + } } +TEST(Paragraph, ExtraSpaces) { + // Make sure spaces inside chunks are dropped. + Paragraph P; + P.appendText("foo\n \t baz"); + P.appendInlineCode(" bar\n"); + EXPECT_EQ(P.renderAsMarkdown(), R"md(foo baz `bar`)md"); + EXPECT_EQ(P.renderAsPlainText(), R"pt(foo baz bar)pt"); + + P = Paragraph(); + // Code blocks preserves any extra spaces. + P.appendCodeBlock("foo\n bar\n baz"); + EXPECT_EQ(P.renderAsMarkdown(), R"md(```cpp +foo + bar + baz +```)md"); + EXPECT_EQ(P.renderAsPlainText(), R"pt(foo + bar + baz)pt"); +} + +TEST(Paragraph, NewLines) { + // New lines before and after chunks are dropped. + Paragraph P; + P.appendText(" \n foo\nbar\n "); + P.appendInlineCode(" \n foo\nbar \n "); + // Only code blocks preserves newlines. + P.appendCodeBlock("\nbat\n"); + EXPECT_EQ(P.renderAsMarkdown(), R"md(foo bar `foo bar` +```cpp + +bat +```)md"); + // Plaintext would trim the output in the end. + EXPECT_EQ(P.renderAsPlainText(), R"pt(foo bar foo bar + +bat)pt"); +} } // namespace +} // namespace markup } // namespace clangd } // namespace clang