72 changes: 51 additions & 21 deletions clang-tools-extra/clangd/FormattedString.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,75 @@
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMATTEDSTRING_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMATTEDSTRING_H

#include "llvm/Support/raw_ostream.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 {
/// Holds text and knows how to lay it out. Multiple blocks can be grouped to
/// form a document. Blocks include their own trailing newlines, container
/// should trim them if need be.
class Block {
public:
virtual void renderMarkdown(llvm::raw_ostream &OS) const = 0;
virtual void renderPlainText(llvm::raw_ostream &OS) const = 0;
std::string asMarkdown() const;
std::string asPlainText() const;

virtual ~Block() = default;
};

/// Represents parts of the markup that can contain strings, like inline code,
/// code block or plain text.
/// One must introduce different paragraphs to create separate blocks.
class Paragraph : public Block {
public:
void renderMarkdown(llvm::raw_ostream &OS) const override;
void renderPlainText(llvm::raw_ostream &OS) 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");
/// Append an inline block of C++ code. This translates to the ` block in
/// markdown.
void appendInlineCode(std::string Code);

std::string renderAsMarkdown() const;
std::string renderAsPlainText() const;
std::string renderForTests() const;
Paragraph &appendText(std::string Text);

/// Append inline code, this translates to the ` block in markdown.
Paragraph &appendCode(std::string Code);

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,
} Kind = PlainText;
std::string Contents;
/// Language for code block chunks. Ignored for other chunks.
std::string Language;
};
std::vector<Chunk> Chunks;
};

/// A format-agnostic representation for structured text. Allows rendering into
/// markdown and plaintext.
class Document {
public:
/// Adds a semantical block that will be separate from others.
Paragraph &addParagraph();
/// Inserts a vertical space into the document.
void addSpacer();
/// Adds a block of code. This translates to a ``` block in markdown. In plain
/// text representation, the code block will be surrounded by newlines.
void addCodeBlock(std::string Code, std::string Language = "cpp");

std::string asMarkdown() const;
std::string asPlainText() const;

private:
std::vector<std::unique_ptr<Block>> Children;
};

} // namespace markup
} // namespace clangd
} // namespace clang

Expand Down
22 changes: 12 additions & 10 deletions clang-tools-extra/clangd/Hover.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -94,7 +95,7 @@ std::string printDefinition(const Decl *D) {
}

void printParams(llvm::raw_ostream &OS,
const std::vector<HoverInfo::Param> &Params) {
const std::vector<HoverInfo::Param> &Params) {
for (size_t I = 0, E = Params.size(); I != E; ++I) {
if (I)
OS << ", ";
Expand Down Expand Up @@ -441,28 +442,29 @@ llvm::Optional<HoverInfo> getHover(ParsedAST &AST, Position Pos,
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.appendCode(llvm::StringRef(LocalScope).drop_back(2));
else if (NamespaceScope->empty())
Output.appendInlineCode("global namespace");
P.appendCode("global namespace");
else
Output.appendInlineCode(llvm::StringRef(*NamespaceScope).drop_back(2));
P.appendCode(llvm::StringRef(*NamespaceScope).drop_back(2));
}

if (!Definition.empty()) {
Output.appendCodeBlock(Definition);
Output.addCodeBlock(Definition);
} else {
// Builtin types
Output.appendCodeBlock(Name);
Output.addCodeBlock(Name);
}

if (!Documentation.empty())
Output.appendText(Documentation);
Output.addParagraph().appendText(Documentation);
return Output;
}

Expand Down
2 changes: 1 addition & 1 deletion clang-tools-extra/clangd/Hover.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct HoverInfo {
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,
Expand Down
294 changes: 137 additions & 157 deletions clang-tools-extra/clangd/unittests/FormattedStringTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,192 +8,172 @@
#include "FormattedString.h"
#include "clang/Basic/LLVM.h"
#include "llvm/ADT/StringRef.h"

#include "llvm/Support/raw_ostream.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

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.asMarkdown(), "\\*\\!\\`");

// 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.asMarkdown(), 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"
"#define FOO\n* foo !+ bar * baz\n"
"```\n");
P = Paragraph();
P.appendCode("* foo !+ bar * baz");
EXPECT_EQ(P.asMarkdown(), "`* foo !+ bar * baz`");

// But we have to escape the backticks.
S = FormattedString();
S.appendInlineCode("foo`bar`baz");
EXPECT_EQ(S.renderAsMarkdown(), "`foo``bar``baz`");

S = FormattedString();
S.appendCodeBlock("foo`bar`baz");
EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n"
"foo`bar`baz\n"
"```\n");
P = Paragraph();
P.appendCode("foo`bar`baz");
EXPECT_EQ(P.asMarkdown(), "`foo``bar``baz`");

// 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.appendCode("`foo");
EXPECT_EQ(P.asMarkdown(), "` ``foo `");
P = Paragraph();
P.appendCode("foo`");
EXPECT_EQ(P.asMarkdown(), "` foo`` `");
P = Paragraph();
P.appendCode("`foo`");
EXPECT_EQ(P.asMarkdown(), "` ``foo`` `");

// Code blocks might need more than 3 backticks.
S = FormattedString();
S.appendCodeBlock("foobarbaz `\nqux");
EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n"
"foobarbaz `\nqux\n"
"```\n");
S = FormattedString();
S.appendCodeBlock("foobarbaz ``\nqux");
EXPECT_EQ(S.renderAsMarkdown(), "```cpp\n"
"foobarbaz ``\nqux\n"
"```\n");
S = FormattedString();
S.appendCodeBlock("foobarbaz ```\nqux");
EXPECT_EQ(S.renderAsMarkdown(), "````cpp\n"
"foobarbaz ```\nqux\n"
"````\n");
S = FormattedString();
S.appendCodeBlock("foobarbaz ` `` ``` ```` `\nqux");
EXPECT_EQ(S.renderAsMarkdown(), "`````cpp\n"
"foobarbaz ` `` ``` ```` `\nqux\n"
"`````\n");
Document D;
D.addCodeBlock("foobarbaz `\nqux");
EXPECT_EQ(D.asMarkdown(), "```cpp\n"
"foobarbaz `\nqux\n"
"```");
D = Document();
D.addCodeBlock("foobarbaz ``\nqux");
EXPECT_THAT(D.asMarkdown(), "```cpp\n"
"foobarbaz ``\nqux\n"
"```");
D = Document();
D.addCodeBlock("foobarbaz ```\nqux");
EXPECT_EQ(D.asMarkdown(), "````cpp\n"
"foobarbaz ```\nqux\n"
"````");
D = Document();
D.addCodeBlock("foobarbaz ` `` ``` ```` `\nqux");
EXPECT_EQ(D.asMarkdown(), "`````cpp\n"
"foobarbaz ` `` ``` ```` `\nqux\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, SeparationOfChunks) {
// 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.
Paragraph P;

P.appendText("after");
EXPECT_EQ(P.asMarkdown(), "after");
EXPECT_EQ(P.asPlainText(), "after");

P.appendCode("foobar");
EXPECT_EQ(P.asMarkdown(), "after `foobar`");
EXPECT_EQ(P.asPlainText(), "after foobar");

P.appendText("bat");
EXPECT_EQ(P.asMarkdown(), "after `foobar` bat");
EXPECT_EQ(P.asPlainText(), "after foobar bat");
}

TEST(Paragraph, ExtraSpaces) {
// Make sure spaces inside chunks are dropped.
Paragraph P;
P.appendText("foo\n \t baz");
P.appendCode(" bar\n");
EXPECT_EQ(P.asMarkdown(), R"md(foo baz `bar`)md");
EXPECT_EQ(P.asPlainText(), R"pt(foo baz bar)pt");
}

TEST(Paragraph, NewLines) {
// New lines before and after chunks are dropped.
Paragraph P;
P.appendText(" \n foo\nbar\n ");
P.appendCode(" \n foo\nbar \n ");
EXPECT_EQ(P.asMarkdown(), R"md(foo bar `foo bar`)md");
EXPECT_EQ(P.asPlainText(), R"pt(foo bar foo bar)pt");
}

TEST(Document, Separators) {
Document D;
D.addParagraph().appendText("foo");
D.addCodeBlock("test");
D.addParagraph().appendText("bar");
EXPECT_EQ(D.asMarkdown(), R"md(foo
```cpp
test
```
bar)md");
EXPECT_EQ(D.asPlainText(), R"pt(foo
test
bar)pt");
}

TEST(Document, Spacer) {
Document D;
D.addParagraph().appendText("foo");
D.addSpacer();
D.addParagraph().appendText("bar");
EXPECT_EQ(D.asMarkdown(), "foo\n\nbar");
EXPECT_EQ(D.asPlainText(), "foo\n\nbar");
}

TEST(CodeBlock, Render) {
Document D;
// Code blocks preserves any extra spaces.
D.addCodeBlock("foo\n bar\n baz");
EXPECT_EQ(D.asMarkdown(), R"md(```cpp
foo
bar
baz
```)md");
EXPECT_EQ(D.asPlainText(), R"pt(foo
bar
baz)pt");
D.addCodeBlock("foo");
EXPECT_EQ(D.asMarkdown(), R"md(```cpp
foo
bar
baz
```
```cpp
foo
```)md");
// FIXME: we shouldn't have 2 empty lines in between. A solution might be
// having a `verticalMargin` method for blocks, and let container insert new
// lines according to that before/after blocks.
EXPECT_EQ(D.asPlainText(), R"pt(foo
bar
baz
foo)pt");
}

} // namespace
} // namespace markup
} // namespace clangd
} // namespace clang