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; }; +/// 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 { +public: + void renderMarkdown(llvm::raw_ostream &OS) const override; + void renderPlainText(llvm::raw_ostream &OS) const override; + + class Document &addItem(); + +private: + std::vector Items; +}; + /// A format-agnostic representation for structured text. Allows rendering into /// markdown and plaintext. class Document { @@ -74,13 +87,16 @@ /// text representation, the code block will be surrounded by newlines. void addCodeBlock(std::string Code, std::string Language = "cpp"); + BulletList &addBulletList(); + + /// Doesn't contain any trailing newlines. std::string asMarkdown() const; + /// Doesn't contain any trailing newlines. std::string asPlainText() 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 @@ -149,6 +149,21 @@ std::string Contents; std::string Language; }; + +// Inserts two spaces after each `\n` to indent each line. First line is not +// indented. +std::string indentLines(llvm::StringRef Input) { + assert(!Input.endswith("\n") && "Input should've been trimmed."); + std::string IndentedR; + // We'll add 2 spaces after each new line. + IndentedR.reserve(Input.size() + Input.count('\n') * 2); + for (char C : Input) { + IndentedR += C; + if (C == '\n') + IndentedR.append(" "); + } + return IndentedR; +} } // namespace std::string Block::asMarkdown() const { @@ -193,6 +208,24 @@ OS << '\n'; } +void BulletList::renderMarkdown(llvm::raw_ostream &OS) const { + for (auto &D : Items) { + // 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'; + } + // 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 : Items) { + // 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.asPlainText()) << '\n'; + } +} + Paragraph &Paragraph::appendText(std::string Text) { Text = canonicalizeSpaces(std::move(Text)); if (Text.empty()) @@ -215,6 +248,11 @@ return *this; } +class Document &BulletList::addItem() { + Items.emplace_back(); + return Items.back(); +} + Paragraph &Document::addParagraph() { Children.push_back(std::make_unique()); return *static_cast(Children.back().get()); @@ -234,6 +272,11 @@ std::string Document::asPlainText() const { return renderBlocks(Children, &Block::renderPlainText); } + +BulletList &Document::addBulletList() { + Children.emplace_back(std::make_unique()); + return *static_cast(Children.back().get()); +} } // namespace markup } // namespace clangd } // namespace clang 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 @@ -186,6 +186,71 @@ EXPECT_EQ(D.asPlainText(), ExpectedPlainText); } +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