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 @@ -46,10 +46,10 @@ void renderPlainText(llvm::raw_ostream &OS) const override; /// Append plain text to the end of the string. - Paragraph &appendText(std::string Text); + Paragraph &appendText(llvm::StringRef Text); /// Append inline code, this translates to the ` block in markdown. - Paragraph &appendCode(std::string Code); + Paragraph &appendCode(llvm::StringRef Code); private: struct Chunk { 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 @@ -216,23 +216,10 @@ } // Trims the input and concatenates 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(); +std::string canonicalizeSpaces(llvm::StringRef Input) { llvm::SmallVector Words; llvm::SplitString(Input, Words); - if (Words.empty()) - return ""; - // Go over each word and add it to the string. - for (llvm::StringRef Word : Words) { - if (WritePtr > Input.begin()) - *WritePtr++ = ' '; // Separate from previous block. - llvm::for_each(Word, [&WritePtr](const char C) { *WritePtr++ = C; }); - } - // Get rid of extra spaces. - Input.resize(WritePtr - Input.begin()); - return Input; + return llvm::join(Words, " "); } std::string renderBlocks(llvm::ArrayRef> Children, @@ -398,24 +385,24 @@ } } -Paragraph &Paragraph::appendText(std::string Text) { - Text = canonicalizeSpaces(std::move(Text)); - if (Text.empty()) +Paragraph &Paragraph::appendText(llvm::StringRef Text) { + std::string Norm = canonicalizeSpaces(Text); + if (Norm.empty()) return *this; Chunks.emplace_back(); Chunk &C = Chunks.back(); - C.Contents = std::move(Text); + C.Contents = std::move(Norm); C.Kind = Chunk::PlainText; return *this; } -Paragraph &Paragraph::appendCode(std::string Code) { - Code = canonicalizeSpaces(std::move(Code)); - if (Code.empty()) +Paragraph &Paragraph::appendCode(llvm::StringRef Code) { + std::string Norm = canonicalizeSpaces(std::move(Code)); + if (Norm.empty()) return *this; Chunks.emplace_back(); Chunk &C = Chunks.back(); - C.Contents = std::move(Code); + C.Contents = std::move(Norm); C.Kind = Chunk::InlineCode; return *this; } diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -772,7 +772,7 @@ // https://github.com/microsoft/vscode/issues/88417 for details. markup::Paragraph &Header = Output.addHeading(3); if (Kind != index::SymbolKind::Unknown) - Header.appendText(std::string(index::getSymbolKindString(Kind))); + Header.appendText(index::getSymbolKindString(Kind)); assert(!Name.empty() && "hover triggered on a nameless symbol"); Header.appendCode(Name); @@ -809,10 +809,11 @@ if (Offset) Output.addParagraph().appendText( - llvm::formatv("Offset: {0} byte{1}", *Offset, *Offset == 1 ? "" : "s")); + llvm::formatv("Offset: {0} byte{1}", *Offset, *Offset == 1 ? "" : "s") + .str()); if (Size) Output.addParagraph().appendText( - llvm::formatv("Size: {0} byte{1}", *Size, *Size == 1 ? "" : "s")); + llvm::formatv("Size: {0} byte{1}", *Size, *Size == 1 ? "" : "s").str()); if (!Documentation.empty()) parseDocumentation(Documentation, Output); @@ -838,6 +839,52 @@ return Output; } +// If the backtick at `Offset` starts a probable quoted range, return the range +// (including the quotes). +llvm::Optional getBacktickQuoteRange(llvm::StringRef Line, + unsigned Offset) { + assert(Line[Offset] == '`'); + + // The open-quote is usually preceded by whitespace. + llvm::StringRef Prefix = Line.substr(0, Offset); + constexpr llvm::StringLiteral BeforeStartChars = " \t(="; + if (!Prefix.empty() && !BeforeStartChars.contains(Prefix.back())) + return llvm::None; + + // The quoted string must be nonempty and usually has no leading/trailing ws. + auto Next = Line.find('`', Offset + 1); + if (Next == llvm::StringRef::npos) + return llvm::None; + llvm::StringRef Contents = Line.slice(Offset + 1, Next); + if (Contents.empty() || isWhitespace(Contents.front()) || + isWhitespace(Contents.back())) + return llvm::None; + + // The close-quote is usually followed by whitespace or punctuation. + llvm::StringRef Suffix = Line.substr(Next + 1); + constexpr llvm::StringLiteral AfterEndChars = " \t)=.,;:"; + if (!Suffix.empty() && !AfterEndChars.contains(Suffix.front())) + return llvm::None; + + return Line.slice(Offset, Next+1); +} + +void parseDocumentationLine(llvm::StringRef Line, markup::Paragraph &Out) { + // Probably this is appendText(Line), but scan for something interesting. + for (unsigned I = 0; I < Line.size(); ++I) { + switch (Line[I]) { + case '`': + if (auto Range = getBacktickQuoteRange(Line, I)) { + Out.appendText(Line.substr(0, I)); + Out.appendCode(Range->trim("`")); + return parseDocumentationLine(Line.substr(I+Range->size()), Out); + } + break; + } + } + Out.appendText(Line); +} + void parseDocumentation(llvm::StringRef Input, markup::Document &Output) { std::vector ParagraphLines; auto FlushParagraph = [&] { @@ -845,7 +892,7 @@ return; auto &P = Output.addParagraph(); for (llvm::StringRef Line : ParagraphLines) - P.appendText(Line.str()); + parseDocumentationLine(Line, P); ParagraphLines.clear(); }; diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -1958,7 +1958,7 @@ } } -TEST(Hover, DocCommentLineBreakConversion) { +TEST(Hover, ParseDocumentation) { struct Case { llvm::StringRef Documentation; llvm::StringRef ExpectedRenderMarkdown; @@ -2017,6 +2017,22 @@ "foo\nbar", "foo bar", "foo bar", + }, + { + // FIXME: we insert spaces between code and text chunk. + "Tests primality of `p`.", + "Tests primality of `p` .", + "Tests primality of p .", + }, + { + "'`' should not occur in `Code`", + "'\\`' should not occur in `Code`", + "'`' should not occur in Code", + }, + { + "`not\nparsed`", + "\\`not parsed\\`", + "`not parsed`", }}; for (const auto &C : Cases) {