kadircet created this revision.
Herald added subscribers: cfe-commits, usaxena95, arphaman, jkorous, MaskRay, 
ilya-biryukov.
Herald added a project: clang.

Initial patch for new rendering structs in clangd.

Splitting implementation into smaller chunks, for a full view of the API see 
D71063 <https://reviews.llvm.org/D71063>.


Repository:
  rG LLVM Github Monorepo

https://reviews.llvm.org/D71248

Files:
  clang-tools-extra/clangd/FormattedString.cpp
  clang-tools-extra/clangd/FormattedString.h
  clang-tools-extra/clangd/Hover.cpp
  clang-tools-extra/clangd/Hover.h
  clang-tools-extra/clangd/unittests/FormattedStringTests.cpp

Index: clang-tools-extra/clangd/unittests/FormattedStringTests.cpp
===================================================================
--- clang-tools-extra/clangd/unittests/FormattedStringTests.cpp
+++ 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
Index: clang-tools-extra/clangd/Hover.h
===================================================================
--- clang-tools-extra/clangd/Hover.h
+++ clang-tools-extra/clangd/Hover.h
@@ -72,7 +72,7 @@
   llvm::Optional<std::string> 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,
Index: clang-tools-extra/clangd/Hover.cpp
===================================================================
--- clang-tools-extra/clangd/Hover.cpp
+++ 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;
 }
 
Index: clang-tools-extra/clangd/FormattedString.h
===================================================================
--- clang-tools-extra/clangd/FormattedString.h
+++ 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 <memory>
 #include <string>
 #include <vector>
 
 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<Chunk> Chunks;
 };
 
+class Document {
+public:
+  Paragraph &addParagraph();
+  std::string renderAsMarkdown() const;
+  std::string renderAsPlainText() const;
+
+private:
+  std::vector<std::unique_ptr<RenderableBlock>> Children;
+};
+
+} // namespace markup
 } // namespace clangd
 } // namespace clang
 
Index: clang-tools-extra/clangd/FormattedString.cpp
===================================================================
--- clang-tools-extra/clangd/FormattedString.cpp
+++ 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 <cstddef>
+#include <memory>
 #include <string>
+#include <vector>
 
 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<std::unique_ptr<RenderableBlock>> Children,
+             RenderType RT) {
+  std::vector<std::string> 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<bool> 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<Paragraph>());
+  return *static_cast<Paragraph *>(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
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to