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 @@ -62,6 +62,19 @@ std::vector Chunks; }; +/// Prints a list of documents while prepending "- " marker. Also indents the +/// documents. +class BulletList : public Block { +public: + void renderMarkdown(llvm::raw_ostream &OS) const override; + void renderPlainText(llvm::raw_ostream &OS) const override; + + class Document &addItem(); + +private: + std::vector Documents; +}; + /// A format-agnostic representation for structured text. Allows rendering into /// markdown and plaintext. class Document { @@ -74,13 +87,20 @@ /// text representation, the code block will be surrounded by newlines. void addCodeBlock(std::string Code, std::string Language = "cpp"); + BulletList &addBulletList(); + std::string asMarkdown() const; std::string asPlainText() const; + /// Causes every line of the document to be indented by 2 spaces. Except the + /// first line, it is containers responsibility to adjust padding for first + /// line. + void indent() { Indent = true; } + private: std::vector> Children; + bool Indent = false; }; - } // 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 @@ -115,12 +115,28 @@ } std::string renderBlocks(llvm::ArrayRef> Children, - void (Block::*RenderFunc)(llvm::raw_ostream &) const) { + void (Block::*RenderFunc)(llvm::raw_ostream &) const, + bool Indent) { std::string R; llvm::raw_string_ostream OS(R); for (auto &C : Children) ((*C).*RenderFunc)(OS); - return llvm::StringRef(OS.str()).trim().str(); + llvm::StringRef Trimmed(OS.str()); + Trimmed = Trimmed.trim(); + if (!Indent) + return Trimmed.str(); + + // 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. + std::string IndentedR; + // We'll add 2 spaces after each new line. + IndentedR.reserve(Trimmed.size() + Trimmed.count('\n') * 2); + for (char C : Trimmed) { + IndentedR += C; + if (C == '\n') + IndentedR.append(" "); + } + return IndentedR; } // Puts a vertical space between blocks inside a document. @@ -194,6 +210,18 @@ OS << '\n'; } +void BulletList::renderMarkdown(llvm::raw_ostream &OS) const { + for (auto &D : Documents) + OS << "- " + D.asMarkdown() << '\n'; + // We need a new line after list to terminate it in markdown. + OS << '\n'; +} + +void BulletList::renderPlainText(llvm::raw_ostream &OS) const { + for (auto &D : Documents) + OS << "- " + D.asPlainText() << '\n'; +} + Paragraph &Paragraph::appendText(std::string Text) { Text = canonicalizeSpaces(std::move(Text)); if (Text.empty()) @@ -216,6 +244,12 @@ return *this; } +class Document &BulletList::addItem() { + Documents.emplace_back(); + Documents.back().indent(); + return Documents.back(); +} + Paragraph &Document::addParagraph() { Children.push_back(std::make_unique()); return *static_cast(Children.back().get()); @@ -229,11 +263,16 @@ } std::string Document::asMarkdown() const { - return renderBlocks(Children, &Block::renderMarkdown); + return renderBlocks(Children, &Block::renderMarkdown, Indent); } std::string Document::asPlainText() const { - return renderBlocks(Children, &Block::renderPlainText); + return renderBlocks(Children, &Block::renderPlainText, Indent); +} + +BulletList &Document::addBulletList() { + Children.emplace_back(std::make_unique()); + return *static_cast(Children.back().get()); } } // namespace markup } // namespace clangd 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 @@ -120,16 +120,23 @@ D.addParagraph().appendText("foo"); D.addCodeBlock("test"); D.addParagraph().appendText("bar"); + D.addBulletList().addItem().addParagraph().appendText("item"); + D.addParagraph().appendText("baz"); EXPECT_EQ(D.asMarkdown(), R"md(foo ```cpp test ``` -bar)md"); +bar +- item + +baz)md"); EXPECT_EQ(D.asPlainText(), R"pt(foo test -bar)pt"); +bar +- item +baz)pt"); } TEST(Document, Spacer) { @@ -173,6 +180,71 @@ foo)pt"); } +TEST(BulletList, Render) { + BulletList L; + // Flat list + L.addItem().addParagraph().appendText("foo"); + EXPECT_EQ(L.asMarkdown(), "- foo"); + EXPECT_EQ(L.asPlainText(), "- foo"); + + L.addItem().addParagraph().appendText("bar"); + EXPECT_EQ(L.asMarkdown(), R"md(- foo +- bar)md"); + EXPECT_EQ(L.asPlainText(), R"pt(- foo +- bar)pt"); + + // Nested list, with a single item. + Document &D = L.addItem(); + // First item with foo\nbaz + D.addParagraph().appendText("foo"); + D.addParagraph().appendText("baz"); + + // Nest one level. + Document &Inner = D.addBulletList().addItem(); + Inner.addParagraph().appendText("foo"); + + // Nest one more level. + BulletList &InnerList = Inner.addBulletList(); + // Single item, baz\nbaz + Document &DeepDoc = InnerList.addItem(); + DeepDoc.addParagraph().appendText("baz"); + DeepDoc.addParagraph().appendText("baz"); + EXPECT_EQ(L.asMarkdown(), R"md(- foo +- bar +- foo + baz + - foo + - baz + baz)md"); + EXPECT_EQ(L.asPlainText(), R"pt(- foo +- bar +- foo + baz + - foo + - baz + baz)pt"); + + // Termination + Inner.addParagraph().appendText("after"); + EXPECT_EQ(L.asMarkdown(), R"md(- foo +- bar +- foo + baz + - foo + - baz + baz + + after)md"); + EXPECT_EQ(L.asPlainText(), R"pt(- foo +- bar +- foo + baz + - foo + - baz + baz + after)pt"); +} + } // namespace } // namespace markup } // namespace clangd