-
Notifications
You must be signed in to change notification settings - Fork 15.3k
[clangd] Fix regression regarding new line handling for hover/signature help content #162029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b1b2251
handle new lines in paragraphs correctly for markdown and escaped mar…
tcottin edb53f1
Merge branch 'main' into fix-newline-regression
tcottin b98a2c7
fix failing tests after branch update
tcottin f2e9154
Merge branch 'main' into fix-newline-regression
tcottin 2ea98a0
fix review findings
tcottin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -475,60 +475,102 @@ std::string Block::asPlainText() const { | |
| return llvm::StringRef(OS.str()).trim().str(); | ||
| } | ||
|
|
||
| void Paragraph::renderNewlinesMarkdown(llvm::raw_ostream &OS, | ||
| std::string &ParagraphText) const { | ||
| llvm::StringRef Line, Rest; | ||
|
|
||
| for (std::tie(Line, Rest) = | ||
| llvm::StringRef(ParagraphText).ltrim("\n").rtrim().split('\n'); | ||
| !(Line.empty() && Rest.empty()); | ||
| std::tie(Line, Rest) = Rest.split('\n')) { | ||
|
|
||
| if (Line.empty()) { | ||
| // Blank lines are preserved in markdown. | ||
| OS << '\n'; | ||
| continue; | ||
| } | ||
|
|
||
| OS << Line; | ||
|
|
||
| if (!Rest.empty() && isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/true)) | ||
| // In markdown, 2 spaces before a line break forces a line break. | ||
| OS << " "; | ||
| OS << '\n'; | ||
| } | ||
| } | ||
|
|
||
| void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const { | ||
| bool NeedsSpace = false; | ||
| bool HasChunks = false; | ||
| std::string ParagraphText; | ||
| ParagraphText.reserve(EstimatedStringSize); | ||
| llvm::raw_string_ostream ParagraphTextOS(ParagraphText); | ||
| for (auto &C : Chunks) { | ||
| if (C.SpaceBefore || NeedsSpace) | ||
| OS << " "; | ||
| ParagraphTextOS << " "; | ||
| switch (C.Kind) { | ||
| case ChunkKind::PlainText: | ||
| OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/true); | ||
| ParagraphTextOS << renderText(C.Contents, !HasChunks, | ||
| /*EscapeMarkdown=*/true); | ||
| break; | ||
| case ChunkKind::InlineCode: | ||
| OS << renderInlineBlock(C.Contents); | ||
| ParagraphTextOS << renderInlineBlock(C.Contents); | ||
| break; | ||
| case ChunkKind::Bold: | ||
| OS << renderText("**" + C.Contents + "**", !HasChunks, | ||
| /*EscapeMarkdown=*/true); | ||
| ParagraphTextOS << renderText("**" + C.Contents + "**", !HasChunks, | ||
| /*EscapeMarkdown=*/true); | ||
| break; | ||
| case ChunkKind::Emphasized: | ||
| OS << renderText("*" + C.Contents + "*", !HasChunks, | ||
| /*EscapeMarkdown=*/true); | ||
| ParagraphTextOS << renderText("*" + C.Contents + "*", !HasChunks, | ||
| /*EscapeMarkdown=*/true); | ||
| break; | ||
| } | ||
| HasChunks = true; | ||
| NeedsSpace = C.SpaceAfter; | ||
| } | ||
|
|
||
| renderNewlinesMarkdown(OS, ParagraphText); | ||
|
|
||
| // A paragraph in markdown is separated by a blank line. | ||
| OS << "\n\n"; | ||
| } | ||
|
|
||
| void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const { | ||
| bool NeedsSpace = false; | ||
| bool HasChunks = false; | ||
| std::string ParagraphText; | ||
| ParagraphText.reserve(EstimatedStringSize); | ||
| llvm::raw_string_ostream ParagraphTextOS(ParagraphText); | ||
| for (auto &C : Chunks) { | ||
| if (C.SpaceBefore || NeedsSpace) | ||
| OS << " "; | ||
| ParagraphTextOS << " "; | ||
| switch (C.Kind) { | ||
| case ChunkKind::PlainText: | ||
| OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false); | ||
| ParagraphTextOS << renderText(C.Contents, !HasChunks, | ||
| /*EscapeMarkdown=*/false); | ||
| break; | ||
| case ChunkKind::InlineCode: | ||
| OS << renderInlineBlock(C.Contents); | ||
| ParagraphTextOS << renderInlineBlock(C.Contents); | ||
| break; | ||
| case ChunkKind::Bold: | ||
| OS << "**" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false) | ||
| << "**"; | ||
| ParagraphTextOS << "**" | ||
| << renderText(C.Contents, !HasChunks, | ||
| /*EscapeMarkdown=*/false) | ||
| << "**"; | ||
| break; | ||
| case ChunkKind::Emphasized: | ||
| OS << "*" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false) | ||
| << "*"; | ||
| ParagraphTextOS << "*" | ||
| << renderText(C.Contents, !HasChunks, | ||
| /*EscapeMarkdown=*/false) | ||
| << "*"; | ||
| break; | ||
| } | ||
| HasChunks = true; | ||
| NeedsSpace = C.SpaceAfter; | ||
| } | ||
|
|
||
| renderNewlinesMarkdown(OS, ParagraphText); | ||
|
|
||
| // A paragraph in markdown is separated by a blank line. | ||
| OS << "\n\n"; | ||
| } | ||
|
|
@@ -537,8 +579,6 @@ std::unique_ptr<Block> Paragraph::clone() const { | |
| return std::make_unique<Paragraph>(*this); | ||
| } | ||
|
|
||
| /// Choose a marker to delimit `Text` from a prioritized list of options. | ||
| /// This is more readable than escaping for plain-text. | ||
| llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, | ||
| llvm::StringRef Text) const { | ||
| // Prefer a delimiter whose characters don't appear in the text. | ||
|
|
@@ -548,23 +588,39 @@ llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef<llvm::StringRef> Options, | |
| return Options.front(); | ||
| } | ||
|
|
||
| bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line) const { | ||
| bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line, | ||
| bool IsMarkdown) const { | ||
| constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt"; | ||
|
|
||
| if (!IsMarkdown && Line.ends_with(" ")) | ||
| return true; | ||
|
|
||
| Line = Line.rtrim(); | ||
| return !Line.empty() && Punctuation.contains(Line.back()); | ||
| } | ||
|
|
||
| bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest) const { | ||
| bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest, | ||
| bool IsMarkdown) const { | ||
| // Plaintext indicators: | ||
| // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote, | ||
| // '#' headings, '`' code blocks, two spaces (markdown force newline) | ||
| constexpr llvm::StringLiteral LinebreakIndicators = R"txt(-*@\>#`)txt"; | ||
| // '#' headings, '`' code blocks | ||
| constexpr llvm::StringLiteral LinebreakIndicatorsPlainText = | ||
| R"txt(-*@\>#`)txt"; | ||
| // Markdown indicators: | ||
| // Only '@' and '\' documentation commands/escaped markdown syntax. | ||
| constexpr llvm::StringLiteral LinebreakIndicatorsMarkdown = R"txt(@\)txt"; | ||
|
|
||
| Rest = Rest.ltrim(" \t"); | ||
| if (Rest.empty()) | ||
| return false; | ||
|
|
||
| if (LinebreakIndicators.contains(Rest.front())) | ||
| if (IsMarkdown) { | ||
| if (LinebreakIndicatorsMarkdown.contains(Rest.front())) | ||
| return true; | ||
| return false; | ||
| } | ||
|
|
||
| if (LinebreakIndicatorsPlainText.contains(Rest.front())) | ||
| return true; | ||
|
|
||
| if (llvm::isDigit(Rest.front())) { | ||
|
|
@@ -575,64 +631,18 @@ bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest) const { | |
| return false; | ||
| } | ||
|
|
||
| bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line, | ||
| llvm::StringRef Rest) const { | ||
| // In Markdown, 2 spaces before a line break forces a line break. | ||
| // Add a line break for plaintext in this case too. | ||
| bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest, | ||
| bool IsMarkdown) const { | ||
| // Should we also consider whether Line is short? | ||
| return Line.ends_with(" ") || punctuationIndicatesLineBreak(Line) || | ||
| isHardLineBreakIndicator(Rest); | ||
| return (punctuationIndicatesLineBreak(Line, IsMarkdown) || | ||
|
||
| isHardLineBreakIndicator(Rest, IsMarkdown)); | ||
| } | ||
|
|
||
| void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { | ||
| bool NeedsSpace = false; | ||
| std::string ConcatenatedText; | ||
| ConcatenatedText.reserve(EstimatedStringSize); | ||
|
|
||
| llvm::raw_string_ostream ConcatenatedOS(ConcatenatedText); | ||
|
|
||
| for (auto &C : Chunks) { | ||
|
|
||
| if (C.Kind == ChunkKind::PlainText) { | ||
| if (C.SpaceBefore || NeedsSpace) | ||
| ConcatenatedOS << ' '; | ||
|
|
||
| ConcatenatedOS << C.Contents; | ||
| NeedsSpace = llvm::isSpace(C.Contents.back()) || C.SpaceAfter; | ||
| continue; | ||
| } | ||
|
|
||
| if (C.SpaceBefore || NeedsSpace) | ||
| ConcatenatedOS << ' '; | ||
| llvm::StringRef Marker = ""; | ||
| if (C.Preserve && C.Kind == ChunkKind::InlineCode) | ||
| Marker = chooseMarker({"`", "'", "\""}, C.Contents); | ||
| else if (C.Kind == ChunkKind::Bold) | ||
| Marker = "**"; | ||
| else if (C.Kind == ChunkKind::Emphasized) | ||
| Marker = "*"; | ||
| ConcatenatedOS << Marker << C.Contents << Marker; | ||
| NeedsSpace = C.SpaceAfter; | ||
| } | ||
|
|
||
| // We go through the contents line by line to handle the newlines | ||
| // and required spacing correctly. | ||
| // | ||
| // Newlines are added if: | ||
| // - the line ends with 2 spaces and a newline follows | ||
| // - the line ends with punctuation that indicates a line break (.:,;!?) | ||
| // - the next line starts with a hard line break indicator (-@>#`, or a digit | ||
| // followed by '.' or ')'), ignoring leading whitespace. | ||
| // | ||
| // Otherwise, newlines in the input are replaced with a single space. | ||
| // | ||
| // Multiple spaces are collapsed into a single space. | ||
| // | ||
| // Lines containing only whitespace are ignored. | ||
| void Paragraph::renderNewlinesPlaintext(llvm::raw_ostream &OS, | ||
| std::string &ParagraphText) const { | ||
|
||
| llvm::StringRef Line, Rest; | ||
|
|
||
| for (std::tie(Line, Rest) = | ||
| llvm::StringRef(ConcatenatedText).trim().split('\n'); | ||
| for (std::tie(Line, Rest) = llvm::StringRef(ParagraphText).trim().split('\n'); | ||
| !(Line.empty() && Rest.empty()); | ||
| std::tie(Line, Rest) = Rest.split('\n')) { | ||
|
|
||
|
|
@@ -653,14 +663,48 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { | |
|
|
||
| OS << canonicalizeSpaces(Line); | ||
|
|
||
| if (isHardLineBreakAfter(Line, Rest)) | ||
| if (isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/false)) | ||
| OS << '\n'; | ||
| else if (!Rest.empty()) | ||
| // Since we removed any trailing whitespace from the input using trim(), | ||
| // we know that the next line contains non-whitespace characters. | ||
| // Therefore, we can add a space without worrying about trailing spaces. | ||
| OS << ' '; | ||
| } | ||
| } | ||
|
|
||
| void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { | ||
| bool NeedsSpace = false; | ||
| std::string ParagraphText; | ||
| ParagraphText.reserve(EstimatedStringSize); | ||
|
|
||
| llvm::raw_string_ostream ParagraphTextOS(ParagraphText); | ||
|
|
||
| for (auto &C : Chunks) { | ||
|
|
||
| if (C.Kind == ChunkKind::PlainText) { | ||
| if (C.SpaceBefore || NeedsSpace) | ||
| ParagraphTextOS << ' '; | ||
|
|
||
| ParagraphTextOS << C.Contents; | ||
| NeedsSpace = llvm::isSpace(C.Contents.back()) || C.SpaceAfter; | ||
| continue; | ||
| } | ||
|
|
||
| if (C.SpaceBefore || NeedsSpace) | ||
| ParagraphTextOS << ' '; | ||
| llvm::StringRef Marker = ""; | ||
| if (C.Preserve && C.Kind == ChunkKind::InlineCode) | ||
| Marker = chooseMarker({"`", "'", "\""}, C.Contents); | ||
| else if (C.Kind == ChunkKind::Bold) | ||
| Marker = "**"; | ||
| else if (C.Kind == ChunkKind::Emphasized) | ||
| Marker = "*"; | ||
| ParagraphTextOS << Marker << C.Contents << Marker; | ||
| NeedsSpace = C.SpaceAfter; | ||
| } | ||
|
|
||
| renderNewlinesPlaintext(OS, ParagraphText); | ||
|
|
||
| // Paragraphs are separated by a blank line. | ||
| OS << "\n\n"; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
return LinebreakIndicatorsMarkdown.contains...