[clangd] Change line break behaviour for hoverinfo

`parseDocumentation` retains hard line breaks and removes soft line
breaks inside documentation comments.
Wether a line break is hard or soft is determined by the following rules
(some of which have been discussed in
https://github.com/clangd/clangd/issues/95):

Line breaks that are preceded by a punctuation are retained
Line breaks that are followed by "interesting characters" (e.g. Markdown
syntax, doxygen commands) are retained
All other line breaks are removed

Related issue: https://github.com/clangd/clangd/issues/95

Differential Revision: https://reviews.llvm.org/D76094
This commit is contained in:
Lorenz Junglas 2020-03-24 12:30:51 +01:00 committed by Sam McCall
parent 68687e75e7
commit b194e7d631
3 changed files with 152 additions and 1 deletions

View File

@ -520,6 +520,49 @@ llvm::Optional<HoverInfo> getHoverContents(const Expr *E, ParsedAST &AST) {
}
return llvm::None;
}
bool isParagraphLineBreak(llvm::StringRef Str, size_t LineBreakIndex) {
return Str.substr(LineBreakIndex + 1)
.drop_while([](auto C) { return C == ' ' || C == '\t'; })
.startswith("\n");
};
bool isPunctuationLineBreak(llvm::StringRef Str, size_t LineBreakIndex) {
constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt";
return LineBreakIndex > 0 && Punctuation.contains(Str[LineBreakIndex - 1]);
};
bool isFollowedByHardLineBreakIndicator(llvm::StringRef Str,
size_t LineBreakIndex) {
// '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote,
// '#' headings, '`' code blocks
constexpr llvm::StringLiteral LinbreakIdenticators = R"txt(-*@\>#`)txt";
auto NextNonSpaceCharIndex = Str.find_first_not_of(' ', LineBreakIndex + 1);
if (NextNonSpaceCharIndex == llvm::StringRef::npos) {
return false;
}
auto FollowedBySingleCharIndicator =
LinbreakIdenticators.find(Str[NextNonSpaceCharIndex]) !=
llvm::StringRef::npos;
auto FollowedByNumberedListIndicator =
llvm::isDigit(Str[NextNonSpaceCharIndex]) &&
NextNonSpaceCharIndex + 1 < Str.size() &&
(Str[NextNonSpaceCharIndex + 1] == '.' ||
Str[NextNonSpaceCharIndex + 1] == ')');
return FollowedBySingleCharIndicator || FollowedByNumberedListIndicator;
};
bool isHardLineBreak(llvm::StringRef Str, size_t LineBreakIndex) {
return isPunctuationLineBreak(Str, LineBreakIndex) ||
isFollowedByHardLineBreakIndicator(Str, LineBreakIndex);
}
} // namespace
llvm::Optional<HoverInfo> getHover(ParsedAST &AST, Position Pos,
@ -652,7 +695,7 @@ markup::Document HoverInfo::present() const {
}
if (!Documentation.empty())
Output.addParagraph().appendText(Documentation);
parseDocumentation(Documentation, Output);
if (!Definition.empty()) {
Output.addRuler();
@ -675,6 +718,45 @@ markup::Document HoverInfo::present() const {
return Output;
}
void parseDocumentation(llvm::StringRef Input, markup::Document &Output) {
constexpr auto WhiteSpaceChars = "\t\n\v\f\r ";
auto TrimmedInput = Input.trim();
std::string CurrentLine;
for (size_t CharIndex = 0; CharIndex < TrimmedInput.size();) {
if (TrimmedInput[CharIndex] == '\n') {
// Trim whitespace infront of linebreak
const auto LastNonSpaceCharIndex =
CurrentLine.find_last_not_of(WhiteSpaceChars) + 1;
CurrentLine.erase(LastNonSpaceCharIndex);
if (isParagraphLineBreak(TrimmedInput, CharIndex) ||
isHardLineBreak(TrimmedInput, CharIndex)) {
// FIXME: maybe distinguish between line breaks and paragraphs
Output.addParagraph().appendText(CurrentLine);
CurrentLine = "";
} else {
// Ommit linebreak
CurrentLine += ' ';
}
CharIndex++;
// After a linebreak always remove spaces to avoid 4 space markdown code
// blocks, also skip all additional linebreaks since they have no effect
CharIndex = TrimmedInput.find_first_not_of(WhiteSpaceChars, CharIndex);
} else {
CurrentLine += TrimmedInput[CharIndex];
CharIndex++;
}
}
if (!CurrentLine.empty()) {
Output.addParagraph().appendText(CurrentLine);
}
}
llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
const HoverInfo::Param &P) {
std::vector<llvm::StringRef> Output;

View File

@ -74,6 +74,10 @@ struct HoverInfo {
/// Produce a user-readable information.
markup::Document present() const;
};
// Try to infer structure of a documentation comment (e.g. line breaks).
void parseDocumentation(llvm::StringRef Input, markup::Document &Output);
llvm::raw_ostream &operator<<(llvm::raw_ostream &, const HoverInfo::Param &);
inline bool operator==(const HoverInfo::Param &LHS,
const HoverInfo::Param &RHS) {

View File

@ -1883,6 +1883,71 @@ def)",
}
}
TEST(Hover, DocCommentLineBreakConversion) {
struct Case {
llvm::StringRef Documentation;
llvm::StringRef ExpectedRenderMarkdown;
llvm::StringRef ExpectedRenderPlainText;
} Cases[] = {{
" \n foo\nbar",
"foo bar",
"foo bar",
},
{
"foo\nbar \n ",
"foo bar",
"foo bar",
},
{
"foo \nbar",
"foo bar",
"foo bar",
},
{
"foo \nbar",
"foo bar",
"foo bar",
},
{
"foo\n\n\nbar",
"foo \nbar",
"foo\nbar",
},
{
"foo\n\n\n\tbar",
"foo \nbar",
"foo\nbar",
},
{
"foo\n\n\n bar",
"foo \nbar",
"foo\nbar",
},
{
"foo.\nbar",
"foo. \nbar",
"foo.\nbar",
},
{
"foo\n*bar",
"foo \n\\*bar",
"foo\n*bar",
},
{
"foo\nbar",
"foo bar",
"foo bar",
}};
for (const auto &C : Cases) {
markup::Document Output;
parseDocumentation(C.Documentation, Output);
EXPECT_EQ(Output.asMarkdown(), C.ExpectedRenderMarkdown);
EXPECT_EQ(Output.asPlainText(), C.ExpectedRenderPlainText);
}
}
// This is a separate test as headings don't create any differences in plaintext
// mode.
TEST(Hover, PresentHeadings) {