diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -50,6 +50,7 @@ FileDistance.cpp FS.cpp FSProvider.cpp + FormattedString.cpp FuzzyMatch.cpp GlobalCompilationDatabase.cpp Headers.cpp diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -154,7 +154,10 @@ bool SupportsHierarchicalDocumentSymbol = false; /// Whether the client supports showing file status. bool SupportFileStatus = false; - // Store of the current versions of the open documents. + /// Which kind of markup should we use in textDocument/hover responses. + MarkupKind HoverContentFormat = MarkupKind::Plaintext; + + /// Store of the current versions of the open documents. DraftStore DraftMgr; // The CDB is created by the "initialize" LSP method. diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -8,6 +8,7 @@ #include "ClangdLSPServer.h" #include "Diagnostics.h" +#include "FormattedString.h" #include "Protocol.h" #include "SourceCode.h" #include "Trace.h" @@ -358,6 +359,7 @@ SupportsHierarchicalDocumentSymbol = Params.capabilities.HierarchicalDocumentSymbol; SupportFileStatus = Params.initializationOptions.FileStatus; + HoverContentFormat = Params.capabilities.HoverContentFormat; llvm::json::Object Result{ {{"capabilities", llvm::json::Object{ @@ -839,7 +841,32 @@ void ClangdLSPServer::onHover(const TextDocumentPositionParams &Params, Callback> Reply) { Server->findHover(Params.textDocument.uri.file(), Params.position, - std::move(Reply)); + Bind( + [this](decltype(Reply) Reply, + llvm::Expected> H) { + if (!H) + return Reply(H.takeError()); + if (!*H) + return Reply(llvm::None); + + // FIXME: render as markdown if client supports it. + Hover R; + switch (HoverContentFormat) { + case MarkupKind::Plaintext: + R.contents.kind = MarkupKind::Plaintext; + R.contents.value = + (*H)->Content.renderAsPlainText(); + R.range = (*H)->Range; + return Reply(std::move(R)); + case MarkupKind::Markdown: + R.contents.kind = MarkupKind::Markdown; + R.contents.value = (*H)->Content.renderAsMarkdown(); + R.range = (*H)->Range; + return Reply(std::move(R)); + }; + llvm_unreachable("unhandled MarkupKind"); + }, + std::move(Reply))); } void ClangdLSPServer::onTypeHierarchy( diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -14,6 +14,7 @@ #include "ClangdUnit.h" #include "CodeComplete.h" #include "FSProvider.h" +#include "FormattedString.h" #include "Function.h" #include "GlobalCompilationDatabase.h" #include "Protocol.h" @@ -180,7 +181,7 @@ /// Get code hover for a given position. void findHover(PathRef File, Position Pos, - Callback> CB); + Callback> CB); /// Get information about type hierarchy for a given position. void typeHierarchy(PathRef File, Position Pos, int Resolve, diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -10,11 +10,13 @@ #include "ClangdUnit.h" #include "CodeComplete.h" #include "FindSymbols.h" +#include "FormattedString.h" #include "Headers.h" #include "Protocol.h" #include "SourceCode.h" #include "TUScheduler.h" #include "Trace.h" +#include "XRefs.h" #include "index/CanonicalIncludes.h" #include "index/FileIndex.h" #include "index/Merge.h" @@ -527,14 +529,12 @@ } void ClangdServer::findHover(PathRef File, Position Pos, - Callback> CB) { - auto Action = [Pos](Callback> CB, - llvm::Expected InpAST) { + Callback> CB) { + auto Action = [Pos](decltype(CB) CB, llvm::Expected InpAST) { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getHover(InpAST->AST, Pos)); }; - WorkScheduler.runWithAST("Hover", File, Bind(Action, std::move(CB))); } diff --git a/clang-tools-extra/clangd/FormattedString.h b/clang-tools-extra/clangd/FormattedString.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/FormattedString.h @@ -0,0 +1,58 @@ +//===--- FormattedString.h ----------------------------------*- C++-*------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// A simple intermediate representation of formatted text that could be +// converted to plaintext or markdown. +// +//===----------------------------------------------------------------------===// +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMATTEDSTRING_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMATTEDSTRING_H + +#include +#include + +namespace clang { +namespace clangd { + +/// A structured string representation that could be converted to markdown or +/// plaintext upon requrest. +class FormattedString { +public: + /// 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. + /// EXPECTS: Code does not contain newlines. + void appendInlineCode(std::string Code); + + std::string renderAsMarkdown() const; + std::string renderAsPlainText() const; + +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; + std::string Contents; + /// Language for code block chunks. Ignored for other chunks. + std::string Language; + }; + std::vector Chunks; +}; + +} // namespace clangd +} // namespace clang + +#endif diff --git a/clang-tools-extra/clangd/FormattedString.cpp b/clang-tools-extra/clangd/FormattedString.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/FormattedString.cpp @@ -0,0 +1,176 @@ +//===--- FormattedString.cpp --------------------------------*- C++-*------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "FormattedString.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/ErrorHandling.h" +#include +#include + +namespace clang { +namespace clangd { + +namespace { +/// Escape a markdown text block. Ensures the punctuation will not introduce +/// any of the markdown constructs. +static std::string renderText(llvm::StringRef Input) { + // Escaping ASCII punctiation ensures we can't start a markdown construct. + constexpr llvm::StringLiteral Punctuation = + R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; + + std::string R; + for (size_t From = 0; From < Input.size();) { + size_t Next = Input.find_first_of(Punctuation, From); + R += Input.substr(From, Next - From); + if (Next == llvm::StringRef::npos) + break; + R += "\\"; + R += Input[Next]; + + From = Next + 1; + } + return R; +} + +/// Renders \p Input as an inline block of code in markdown. The returned value +/// is surrounded by backticks and the inner contents are properly escaped. +static std::string renderInlineBlock(llvm::StringRef Input) { + std::string R; + // Double all backticks to make sure we don't close the inline block early. + for (size_t From = 0; From < Input.size();) { + size_t Next = Input.find("`", From); + R += Input.substr(From, Next - From); + if (Next == llvm::StringRef::npos) + break; + R += "``"; // double the found backtick. + + From = Next + 1; + } + // If results starts with a backtick, add spaces on both sides. The spaces + // are ignored by markdown renderers. + if (llvm::StringRef(R).startswith("`") || llvm::StringRef(R).endswith("`")) + return "` " + std::move(R) + " `"; + // Markdown render should ignore first and last space if both are there. We + // add an extra pair of spaces in that case to make sure we render what the + // user intended. + if (llvm::StringRef(R).startswith(" ") && llvm::StringRef(R).endswith(" ")) + return "` " + std::move(R) + " `"; + return "`" + std::move(R) + "`"; +} +/// Render \p Input as markdown code block with a specified \p Language. The +/// result is surrounded by >= 3 backticks. Although markdown also allows to use +/// '~' for code blocks, they are never used. +static std::string renderCodeBlock(llvm::StringRef Input, + llvm::StringRef Language) { + // Count the maximum number of consecutive backticks in \p Input. We need to + // start and end the code block with more. + unsigned MaxBackticks = 0; + for (llvm::StringRef Left = Input;;) { + unsigned N = Left.find("`"); + Left = Left.substr(N); + if (Left.empty()) + break; + unsigned Backticks = 0; + for (; !Left.empty() && Left.front() == '`'; Left = Left.drop_front()) + ++Backticks; + MaxBackticks = std::max(Backticks, MaxBackticks); + } + // Use the corresponding number of backticks to start and end a code block. + std::string BlockMarker(/*Repeat=*/std::max(3u, MaxBackticks + 1), '`'); + return BlockMarker + Language.str() + "\n" + Input.str() + "\n" + BlockMarker; +} + +} // namespace + +void FormattedString::appendText(std::string Text) { + if (Chunks.empty() || Chunks.back().Kind != ChunkKind::PlainText) { + Chunk C; + C.Kind = ChunkKind::PlainText; + Chunks.push_back(C); + } + Chunks.back().Contents += Text; +} + +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) { + assert(!llvm::StringRef(Code).contains("\n")); + + Chunk C; + C.Kind = ChunkKind::InlineCodeBlock; + C.Contents = std::move(Code); + Chunks.push_back(std::move(C)); +} + +std::string FormattedString::renderAsMarkdown() const { + std::string R; + for (const auto &C : Chunks) { + switch (C.Kind) { + case ChunkKind::PlainText: + R += renderText(C.Contents); + continue; + case ChunkKind::InlineCodeBlock: + // Make sure we don't glue two backticks together. + if (llvm::StringRef(R).endswith("`")) + R += " "; + 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; + } + llvm_unreachable("unhanlded ChunkKind"); + } + return R; +} + +static bool IsWhitespace(char C) { + return llvm::StringLiteral(" \t\n").contains(C); +} + +std::string FormattedString::renderAsPlainText() const { + std::string R; + auto EnsureWhitespace = [&]() { + if (R.empty() || IsWhitespace(R.back())) + return; + R += " "; + }; + for (const auto &C : Chunks) { + switch (C.Kind) { + case ChunkKind::PlainText: + EnsureWhitespace(); + R += C.Contents; + continue; + case ChunkKind::InlineCodeBlock: + EnsureWhitespace(); + R += C.Contents; + continue; + case ChunkKind::CodeBlock: + if (!R.empty()) + R += "\n\n"; + R += C.Contents; + if (!llvm::StringRef(C.Contents).endswith("\n")) + R += "\n"; + continue; + } + llvm_unreachable("unhanlded ChunkKind"); + } + while (!R.empty() && IsWhitespace(R.back())) + R.pop_back(); + return R; +} +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -353,6 +353,15 @@ bool fromJSON(const llvm::json::Value &, OffsetEncoding &); llvm::raw_ostream &operator<<(llvm::raw_ostream &, OffsetEncoding); +// Describes the content type that a client supports in various result literals +// like `Hover`, `ParameterInfo` or `CompletionItem`. +enum class MarkupKind { + Plaintext, + Markdown, +}; +bool fromJSON(const llvm::json::Value &, MarkupKind &); +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, MarkupKind); + // This struct doesn't mirror LSP! // The protocol defines deeply nested structures for client capabilities. // Instead of mapping them all, this just parses out the bits we care about. @@ -391,6 +400,9 @@ /// Supported encodings for LSP character offsets. (clangd extension). llvm::Optional> offsetEncoding; + + /// The content format that should be used for Hover requests. + MarkupKind HoverContentFormat = MarkupKind::Plaintext; }; bool fromJSON(const llvm::json::Value &, ClientCapabilities &); @@ -861,13 +873,8 @@ }; bool fromJSON(const llvm::json::Value &, CompletionParams &); -enum class MarkupKind { - PlainText, - Markdown, -}; - struct MarkupContent { - MarkupKind kind = MarkupKind::PlainText; + MarkupKind kind = MarkupKind::Plaintext; std::string value; }; llvm::json::Value toJSON(const MarkupContent &MC); diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -17,6 +17,7 @@ #include "llvm/ADT/Hashing.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/ErrorHandling.h" #include "llvm/Support/Format.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/JSON.h" @@ -302,6 +303,17 @@ DocumentSymbol->getBoolean("hierarchicalDocumentSymbolSupport")) R.HierarchicalDocumentSymbol = *HierarchicalSupport; } + if (auto *Hover = TextDocument->getObject("hover")) { + if (auto *ContentFormat = Hover->getArray("contentFormat")) { + for (const auto &Format : *ContentFormat) { + MarkupKind K = MarkupKind::Plaintext; + if (fromJSON(Format, K)) { + R.HoverContentFormat = K; + break; + } + } + } + } } if (auto *Workspace = O->getObject("workspace")) { if (auto *Symbol = Workspace->getObject("symbol")) { @@ -675,7 +687,7 @@ static llvm::StringRef toTextKind(MarkupKind Kind) { switch (Kind) { - case MarkupKind::PlainText: + case MarkupKind::Plaintext: return "plaintext"; case MarkupKind::Markdown: return "markdown"; @@ -683,6 +695,23 @@ llvm_unreachable("Invalid MarkupKind"); } +bool fromJSON(const llvm::json::Value &V, MarkupKind &K) { + auto Str = V.getAsString(); + if (!Str) + return false; + if (*Str == "plaintext") + K = MarkupKind::Plaintext; + else if (*Str == "markdown") + K = MarkupKind::Markdown; + else + return false; + return true; +} + +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, MarkupKind K) { + return OS << toTextKind(K); +} + llvm::json::Value toJSON(const MarkupContent &MC) { if (MC.value.empty()) return nullptr; diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h --- a/clang-tools-extra/clangd/XRefs.h +++ b/clang-tools-extra/clangd/XRefs.h @@ -14,6 +14,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_XREFS_H #include "ClangdUnit.h" +#include "FormattedString.h" #include "Protocol.h" #include "index/Index.h" #include "llvm/ADT/Optional.h" @@ -46,8 +47,12 @@ std::vector findDocumentHighlights(ParsedAST &AST, Position Pos); +struct HoverInfo { + FormattedString Content; + llvm::Optional Range; +}; /// Get the hover information when hovering at \p Pos. -llvm::Optional getHover(ParsedAST &AST, Position Pos); +llvm::Optional getHover(ParsedAST &AST, Position Pos); /// Returns reference locations of the symbol at a specified \p Pos. /// \p Limit limits the number of results returned (0 means no limit). diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp --- a/clang-tools-extra/clangd/XRefs.cpp +++ b/clang-tools-extra/clangd/XRefs.cpp @@ -8,6 +8,7 @@ #include "XRefs.h" #include "AST.h" #include "FindSymbols.h" +#include "FormattedString.h" #include "Logger.h" #include "SourceCode.h" #include "URI.h" @@ -523,17 +524,16 @@ } /// Generate a \p Hover object given the declaration \p D. -static Hover getHoverContents(const Decl *D) { - Hover H; +static FormattedString getHoverContents(const Decl *D) { + FormattedString R; llvm::Optional NamedScope = getScopeName(D); // Generate the "Declared in" section. if (NamedScope) { assert(!NamedScope->empty()); - H.contents.value += "Declared in "; - H.contents.value += *NamedScope; - H.contents.value += "\n\n"; + R.appendText("Declared in "); + R.appendInlineCode(*NamedScope); } // We want to include the template in the Hover. @@ -545,29 +545,26 @@ PrintingPolicy Policy = printingPolicyForDecls(D->getASTContext().getPrintingPolicy()); - D->print(OS, Policy); - OS.flush(); - - H.contents.value += DeclText; - return H; + R.appendCodeBlock(OS.str()); + return R; } /// Generate a \p Hover object given the type \p T. -static Hover getHoverContents(QualType T, ASTContext &ASTCtx) { - Hover H; - std::string TypeText; - llvm::raw_string_ostream OS(TypeText); +static FormattedString getHoverContents(QualType T, ASTContext &ASTCtx) { + std::string Code; + llvm::raw_string_ostream OS(Code); PrintingPolicy Policy = printingPolicyForDecls(ASTCtx.getPrintingPolicy()); T.print(OS, Policy); - OS.flush(); - H.contents.value += TypeText; - return H; + + FormattedString R; + R.appendCodeBlock(OS.str()); + return R; } /// Generate a \p Hover object given the macro \p MacroDecl. -static Hover getHoverContents(MacroDecl Decl, ParsedAST &AST) { +static FormattedString getHoverContents(MacroDecl Decl, ParsedAST &AST) { SourceManager &SM = AST.getASTContext().getSourceManager(); std::string Definition = Decl.Name; @@ -587,10 +584,9 @@ } } - Hover H; - H.contents.kind = MarkupKind::PlainText; - H.contents.value = "#define " + Definition; - return H; + FormattedString S; + S.appendCodeBlock("#define " + std::move(Definition)); + return S; } namespace { @@ -705,22 +701,31 @@ return V.getDeducedType(); } -llvm::Optional getHover(ParsedAST &AST, Position Pos) { +llvm::Optional getHover(ParsedAST &AST, Position Pos) { const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, SourceMgr.getMainFileID()); // Identified symbols at a specific position. auto Symbols = getSymbolAtPosition(AST, SourceLocationBeg); - if (!Symbols.Macros.empty()) - return getHoverContents(Symbols.Macros[0], AST); + if (!Symbols.Macros.empty()) { + HoverInfo H; + H.Content = getHoverContents(Symbols.Macros[0], AST); + return H; + } - if (!Symbols.Decls.empty()) - return getHoverContents(Symbols.Decls[0]); + if (!Symbols.Decls.empty()) { + HoverInfo H; + H.Content = getHoverContents(Symbols.Decls[0]); + return H; + } auto DeducedType = getDeducedType(AST, SourceLocationBeg); - if (DeducedType && !DeducedType->isNull()) - return getHoverContents(*DeducedType, AST.getASTContext()); + if (DeducedType && !DeducedType->isNull()) { + HoverInfo H; + H.Content = getHoverContents(*DeducedType, AST.getASTContext()); + return H; + } return None; } diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -38,6 +38,7 @@ FileDistanceTests.cpp FileIndexTests.cpp FindSymbolsTests.cpp + FormattedStringTests.cpp FSTests.cpp FunctionTests.cpp FuzzyMatchTests.cpp diff --git a/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp b/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/FormattedStringTests.cpp @@ -0,0 +1,158 @@ +//===-- FormattedStringTests.cpp ------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "FormattedString.h" +#include "clang/Basic/LLVM.h" +#include "llvm/ADT/StringRef.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(FormattedString, Basic) { + FormattedString S; + EXPECT_EQ(S.renderAsPlainText(), ""); + EXPECT_EQ(S.renderAsMarkdown(), ""); + + S.appendText("foobar"); + EXPECT_EQ(S.renderAsPlainText(), "foobar"); + EXPECT_EQ(S.renderAsMarkdown(), "foobar"); + + 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"); + + S = FormattedString(); +} + +TEST(FormattedString, CodeBlocks) { + FormattedString S; + S.appendCodeBlock("foobar"); + S.appendCodeBlock("bazqux", "javascript"); + + EXPECT_EQ(S.renderAsPlainText(), "foobar\n\n\nbazqux"); + std::string ExpectedMarkdown = R"md(```cpp +foobar +``` +```javascript +bazqux +``` +)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) { + // Check some ASCII punctuation + FormattedString S; + S.appendText("*!`"); + EXPECT_EQ(S.renderAsMarkdown(), "\\*\\!\\`"); + + // Check all ASCII punctuation. + S = FormattedString(); + 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); + + // 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"); + + // 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"); + + // 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`"); + + // 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"); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/XRefsTests.cpp b/clang-tools-extra/clangd/unittests/XRefsTests.cpp --- a/clang-tools-extra/clangd/unittests/XRefsTests.cpp +++ b/clang-tools-extra/clangd/unittests/XRefsTests.cpp @@ -1172,7 +1172,9 @@ auto AST = TU.build(); if (auto H = getHover(AST, T.point())) { EXPECT_NE("", Test.ExpectedHover) << Test.Input; - EXPECT_EQ(H->contents.value, Test.ExpectedHover.str()) << Test.Input; + // FIXME: add renderForTests() and use it here. + EXPECT_EQ(H->Content.renderAsPlainText(), Test.ExpectedHover.str()) + << Test.Input; } else EXPECT_EQ("", Test.ExpectedHover.str()) << Test.Input; }