[clang-tools-extra] [clangd] Doxygen Parsing: Add proper handling of \note, \warning and \retval command + change render kind of command arguments + add preprocessing for markdown code blocks/spans (PR #156365)

via cfe-commits cfe-commits at lists.llvm.org
Sat Sep 27 06:20:24 PDT 2025


https://github.com/tcottin updated https://github.com/llvm/llvm-project/pull/156365

>From 5e5b87c6985c12b8afeea077a723c969e2153f49 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Mon, 1 Sep 2025 17:54:53 +0000
Subject: [PATCH 1/5] fix note, warning and retval command. Change arguments of
 doxygen commands to be rendered as monospaced

---
 clang-tools-extra/clangd/Hover.cpp            |   6 +-
 .../clangd/SymbolDocumentation.cpp            | 111 ++++++++-------
 .../clangd/SymbolDocumentation.h              |  33 +----
 .../clangd/unittests/HoverTests.cpp           |  43 +++++-
 .../unittests/SymbolDocumentationTests.cpp    | 126 ++++++++++++++++--
 5 files changed, 227 insertions(+), 92 deletions(-)

diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 9eec322fe5963..3b9baed1c2838 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1589,13 +1589,11 @@ markup::Document HoverInfo::presentDoxygen() const {
       P.appendText(" - ");
       SymbolDoc.returnToMarkup(P);
     }
+
+    SymbolDoc.retvalsToMarkup(Output);
     Output.addRuler();
   }
 
-  // add specially handled doxygen commands.
-  SymbolDoc.warningsToMarkup(Output);
-  SymbolDoc.notesToMarkup(Output);
-
   // add any other documentation.
   SymbolDoc.docToMarkup(Output);
 
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
index 9ae1ef3f828e0..86bd6162b6f72 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.cpp
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -34,10 +34,20 @@ void commandToMarkup(markup::Paragraph &Out, StringRef Command,
                      StringRef Args) {
   Out.appendBoldText(commandMarkerAsString(CommandMarker) + Command.str());
   Out.appendSpace();
-  if (!Args.empty()) {
-    Out.appendEmphasizedText(Args.str());
+  if (!Args.empty())
+    Out.appendCode(Args.str());
+}
+
+template <typename T> std::string getArgText(const T *Command) {
+  std::string ArgText;
+  for (unsigned I = 0; I < Command->getNumArgs(); ++I) {
+    if (!ArgText.empty())
+      ArgText += " ";
+    ArgText += Command->getArgText(I);
   }
+  return ArgText;
 }
+
 } // namespace
 
 class ParagraphToMarkupDocument
@@ -70,12 +80,7 @@ class ParagraphToMarkupDocument
   void visitInlineCommandComment(const comments::InlineCommandComment *C) {
 
     if (C->getNumArgs() > 0) {
-      std::string ArgText;
-      for (unsigned I = 0; I < C->getNumArgs(); ++I) {
-        if (!ArgText.empty())
-          ArgText += " ";
-        ArgText += C->getArgText(I);
-      }
+      std::string ArgText = getArgText(C);
 
       switch (C->getRenderKind()) {
       case comments::InlineCommandRenderKind::Monospaced:
@@ -158,10 +163,9 @@ class ParagraphToString
   void visitInlineCommandComment(const comments::InlineCommandComment *C) {
     Out << commandMarkerAsString(C->getCommandMarker());
     Out << C->getCommandName(Traits);
-    if (C->getNumArgs() > 0) {
-      for (unsigned I = 0; I < C->getNumArgs(); ++I)
-        Out << " " << C->getArgText(I);
-    }
+    std::string ArgText = getArgText(C);
+    if (!ArgText.empty())
+      Out << " " << ArgText;
     Out << " ";
   }
 
@@ -210,16 +214,27 @@ class BlockCommentToMarkupDocument
                                 Traits)
           .visit(B->getParagraph());
       break;
+    case comments::CommandTraits::KCI_note:
+    case comments::CommandTraits::KCI_warning:
+      commandToHeadedParagraph(B);
+      break;
+    case comments::CommandTraits::KCI_retval: {
+      // The \retval command describes the return value given as its single
+      // argument in the corresponding paragraph.
+      // Note: We know that we have exactly one argument but not if it has an
+      // associated paragraph.
+      auto &P = Out.addParagraph().appendCode(getArgText(B));
+      if (B->getParagraph() && !B->getParagraph()->isWhitespace()) {
+        P.appendText(" - ");
+        ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
+      }
+      return;
+    }
     default: {
       // Some commands have arguments, like \throws.
       // The arguments are not part of the paragraph.
       // We need reconstruct them here.
-      std::string ArgText;
-      for (unsigned I = 0; I < B->getNumArgs(); ++I) {
-        if (!ArgText.empty())
-          ArgText += " ";
-        ArgText += B->getArgText(I);
-      }
+      std::string ArgText = getArgText(B);
       auto &P = Out.addParagraph();
       commandToMarkup(P, B->getCommandName(Traits), B->getCommandMarker(),
                       ArgText);
@@ -262,6 +277,19 @@ class BlockCommentToMarkupDocument
   markup::Document &Out;
   const comments::CommandTraits &Traits;
   StringRef CommentEscapeMarker;
+
+  /// Emphasize the given command in a paragraph.
+  /// Uses the command name with the first letter capitalized as the heading.
+  void commandToHeadedParagraph(const comments::BlockCommandComment *B) {
+    Out.addRuler();
+    auto &P = Out.addParagraph();
+    std::string Heading = B->getCommandName(Traits).slice(0, 1).upper() +
+                          B->getCommandName(Traits).drop_front().str();
+    P.appendBoldText(Heading + ":");
+    P.appendText("  \n");
+    ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
+    Out.addRuler();
+  }
 };
 
 void SymbolDocCommentVisitor::visitBlockCommandComment(
@@ -282,36 +310,22 @@ void SymbolDocCommentVisitor::visitBlockCommandComment(
     }
     break;
   case comments::CommandTraits::KCI_retval:
-    RetvalParagraphs.push_back(B->getParagraph());
-    return;
-  case comments::CommandTraits::KCI_warning:
-    WarningParagraphs.push_back(B->getParagraph());
-    return;
-  case comments::CommandTraits::KCI_note:
-    NoteParagraphs.push_back(B->getParagraph());
+    // Only consider retval commands having an argument.
+    // The argument contains the described return value which is needed to
+    // convert it to markup.
+    if (B->getNumArgs() == 1)
+      RetvalCommands.push_back(B);
     return;
   default:
     break;
   }
 
-  // For all other commands, we store them in the UnhandledCommands map.
+  // For all other commands, we store them in the BlockCommands map.
   // This allows us to keep the order of the comments.
-  UnhandledCommands[CommentPartIndex] = B;
+  BlockCommands[CommentPartIndex] = B;
   CommentPartIndex++;
 }
 
-void SymbolDocCommentVisitor::paragraphsToMarkup(
-    markup::Document &Out,
-    const llvm::SmallVectorImpl<const comments::ParagraphComment *> &Paragraphs)
-    const {
-  if (Paragraphs.empty())
-    return;
-
-  for (const auto *P : Paragraphs) {
-    ParagraphToMarkupDocument(Out.addParagraph(), Traits).visit(P);
-  }
-}
-
 void SymbolDocCommentVisitor::briefToMarkup(markup::Paragraph &Out) const {
   if (!BriefParagraph)
     return;
@@ -324,14 +338,6 @@ void SymbolDocCommentVisitor::returnToMarkup(markup::Paragraph &Out) const {
   ParagraphToMarkupDocument(Out, Traits).visit(ReturnParagraph);
 }
 
-void SymbolDocCommentVisitor::notesToMarkup(markup::Document &Out) const {
-  paragraphsToMarkup(Out, NoteParagraphs);
-}
-
-void SymbolDocCommentVisitor::warningsToMarkup(markup::Document &Out) const {
-  paragraphsToMarkup(Out, WarningParagraphs);
-}
-
 void SymbolDocCommentVisitor::parameterDocToMarkup(
     StringRef ParamName, markup::Paragraph &Out) const {
   if (ParamName.empty())
@@ -354,7 +360,7 @@ void SymbolDocCommentVisitor::parameterDocToString(
 
 void SymbolDocCommentVisitor::docToMarkup(markup::Document &Out) const {
   for (unsigned I = 0; I < CommentPartIndex; ++I) {
-    if (const auto *BC = UnhandledCommands.lookup(I)) {
+    if (const auto *BC = BlockCommands.lookup(I)) {
       BlockCommentToMarkupDocument(Out, Traits).visit(BC);
     } else if (const auto *P = FreeParagraphs.lookup(I)) {
       ParagraphToMarkupDocument(Out.addParagraph(), Traits).visit(P);
@@ -382,5 +388,14 @@ void SymbolDocCommentVisitor::templateTypeParmDocToString(
   }
 }
 
+void SymbolDocCommentVisitor::retvalsToMarkup(markup::Document &Out) const {
+  if (RetvalCommands.empty())
+    return;
+  markup::BulletList &BL = Out.addBulletList();
+  for (const auto *P : RetvalCommands) {
+    BlockCommentToMarkupDocument(BL.addItem(), Traits).visit(P);
+  }
+}
+
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h
index b0d3428dfce20..1d48ee6d4d3f9 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.h
+++ b/clang-tools-extra/clangd/SymbolDocumentation.h
@@ -114,22 +114,14 @@ class SymbolDocCommentVisitor
 
   bool hasReturnCommand() const { return ReturnParagraph; }
 
-  bool hasRetvalCommands() const { return !RetvalParagraphs.empty(); }
-
-  bool hasNoteCommands() const { return !NoteParagraphs.empty(); }
-
-  bool hasWarningCommands() const { return !WarningParagraphs.empty(); }
-
   /// Converts all unhandled comment commands to a markup document.
   void docToMarkup(markup::Document &Out) const;
   /// Converts the "brief" command(s) to a markup document.
   void briefToMarkup(markup::Paragraph &Out) const;
   /// Converts the "return" command(s) to a markup document.
   void returnToMarkup(markup::Paragraph &Out) const;
-  /// Converts the "note" command(s) to a markup document.
-  void notesToMarkup(markup::Document &Out) const;
-  /// Converts the "warning" command(s) to a markup document.
-  void warningsToMarkup(markup::Document &Out) const;
+  /// Converts the "retval" command(s) to a markup document.
+  void retvalsToMarkup(markup::Document &Out) const;
 
   void visitBlockCommandComment(const comments::BlockCommandComment *B);
 
@@ -173,19 +165,13 @@ class SymbolDocCommentVisitor
   /// Paragraph of the "return" command.
   const comments::ParagraphComment *ReturnParagraph = nullptr;
 
-  /// Paragraph(s) of the "note" command(s)
-  llvm::SmallVector<const comments::ParagraphComment *> RetvalParagraphs;
+  /// All the "retval" command(s)
+  llvm::SmallVector<const comments::BlockCommandComment *> RetvalCommands;
 
-  /// Paragraph(s) of the "note" command(s)
-  llvm::SmallVector<const comments::ParagraphComment *> NoteParagraphs;
-
-  /// Paragraph(s) of the "warning" command(s)
-  llvm::SmallVector<const comments::ParagraphComment *> WarningParagraphs;
-
-  /// All the paragraphs we don't have any special handling for,
-  /// e.g. "details".
+  /// All the parsed doxygen block commands.
+  /// They might have special handling internally like \\note or \\warning
   llvm::SmallDenseMap<unsigned, const comments::BlockCommandComment *>
-      UnhandledCommands;
+      BlockCommands;
 
   /// Parsed paragaph(s) of the "param" comamnd(s)
   llvm::SmallDenseMap<StringRef, const comments::ParamCommandComment *>
@@ -198,11 +184,6 @@ class SymbolDocCommentVisitor
   /// All "free" text paragraphs.
   llvm::SmallDenseMap<unsigned, const comments::ParagraphComment *>
       FreeParagraphs;
-
-  void paragraphsToMarkup(
-      markup::Document &Out,
-      const llvm::SmallVectorImpl<const comments::ParagraphComment *>
-          &Paragraphs) const;
 };
 
 } // namespace clangd
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 743c0dc0d0187..fbcc2992cf2b7 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -4045,9 +4045,19 @@ brief doc
 longer doc)"},
       {[](HoverInfo &HI) {
          HI.Kind = index::SymbolKind::Function;
-         HI.Documentation = "@brief brief doc\n\n"
-                            "longer doc\n at param a this is a param\n at return it "
-                            "returns something";
+         HI.Documentation = R"(@brief brief doc
+
+longer doc
+ at note this is a note
+
+As you see, notes are "inlined".
+ at warning this is a warning
+
+As well as warnings
+ at param a this is a param
+ at return it returns something
+ at retval 0 if successful
+ at retval 1 if failed)";
          HI.Definition = "int foo(int a)";
          HI.ReturnType = "int";
          HI.Name = "foo";
@@ -4068,8 +4078,16 @@ longer doc)"},
 @brief brief doc
 
 longer doc
+ at note this is a note
+
+As you see, notes are "inlined".
+ at warning this is a warning
+
+As well as warnings
 @param a this is a param
 @return it returns something
+ at retval 0 if successful
+ at retval 1 if failed
 
 ---
 ```cpp
@@ -4095,8 +4113,25 @@ brief doc
 
 `int` - it returns something
 
+- `0` - if successful
+- `1` - if failed
+
 ---
-longer doc)"},
+longer doc
+
+---
+**Note:**  
+this is a note
+
+---
+As you see, notes are "inlined".
+
+---
+**Warning:**  
+this is a warning
+
+---
+As well as warnings)"},
       {[](HoverInfo &HI) {
          HI.Kind = index::SymbolKind::Function;
          HI.Documentation = "@brief brief doc\n\n"
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
index 31a6a149e33c4..ba692399a7093 100644
--- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -15,7 +15,7 @@
 namespace clang {
 namespace clangd {
 
-TEST(SymbolDocumentation, UnhandledDocs) {
+TEST(SymbolDocumentation, DocToMarkup) {
 
   CommentOptions CommentOpts;
 
@@ -63,15 +63,15 @@ TEST(SymbolDocumentation, UnhandledDocs) {
       },
       {
           "foo \\ref bar baz",
-          "foo \\*\\*\\\\ref\\*\\* \\*bar\\* baz",
-          "foo **\\ref** *bar* baz",
-          "foo **\\ref** *bar* baz",
+          "foo \\*\\*\\\\ref\\*\\* `bar` baz",
+          "foo **\\ref** `bar` baz",
+          "foo **\\ref** bar baz",
       },
       {
           "foo @ref bar baz",
-          "foo \\*\\*@ref\\*\\* \\*bar\\* baz",
-          "foo **@ref** *bar* baz",
-          "foo **@ref** *bar* baz",
+          "foo \\*\\*@ref\\*\\* `bar` baz",
+          "foo **@ref** `bar` baz",
+          "foo **@ref** bar baz",
       },
       {
           "\\brief this is a \\n\nbrief description",
@@ -81,9 +81,9 @@ TEST(SymbolDocumentation, UnhandledDocs) {
       },
       {
           "\\throw exception foo",
-          "\\*\\*\\\\throw\\*\\* \\*exception\\* foo",
-          "**\\throw** *exception* foo",
-          "**\\throw** *exception* foo",
+          "\\*\\*\\\\throw\\*\\* `exception` foo",
+          "**\\throw** `exception` foo",
+          "**\\throw** exception foo",
       },
       {
           R"(\brief this is a brief description
@@ -198,6 +198,72 @@ normal text\<i>this is an italic text\</i>
           "<b>this is a bold text</b> normal text<i>this is an italic text</i> "
           "<code>this is a code block</code>",
       },
+      {"@note This is a note",
+       R"(\*\*Note:\*\*  
+This is a note)",
+       R"(**Note:**  
+This is a note)",
+       R"(**Note:**
+This is a note)"},
+      {R"(Paragraph 1
+ at note This is a note
+
+Paragraph 2)",
+       R"(Paragraph 1
+
+---
+\*\*Note:\*\*  
+This is a note
+
+---
+Paragraph 2)",
+       R"(Paragraph 1
+
+---
+**Note:**  
+This is a note
+
+---
+Paragraph 2)",
+       R"(Paragraph 1
+
+**Note:**
+This is a note
+
+Paragraph 2)"},
+      {"@warning This is a warning",
+       R"(\*\*Warning:\*\*  
+This is a warning)",
+       R"(**Warning:**  
+This is a warning)",
+       R"(**Warning:**
+This is a warning)"},
+      {R"(Paragraph 1
+ at warning This is a warning
+
+Paragraph 2)",
+       R"(Paragraph 1
+
+---
+\*\*Warning:\*\*  
+This is a warning
+
+---
+Paragraph 2)",
+       R"(Paragraph 1
+
+---
+**Warning:**  
+This is a warning
+
+---
+Paragraph 2)",
+       R"(Paragraph 1
+
+**Warning:**
+This is a warning
+
+Paragraph 2)"},
   };
   for (const auto &C : Cases) {
     markup::Document Doc;
@@ -211,5 +277,45 @@ normal text\<i>this is an italic text\</i>
   }
 }
 
+TEST(SymbolDocumentation, RetvalCommand) {
+
+  CommentOptions CommentOpts;
+
+  struct Case {
+    llvm::StringRef Documentation;
+    llvm::StringRef ExpectedRenderEscapedMarkdown;
+    llvm::StringRef ExpectedRenderMarkdown;
+    llvm::StringRef ExpectedRenderPlainText;
+  } Cases[] = {
+      {"@retval", "", "", ""},
+      {R"(@retval MyReturnVal
+ at retval MyOtherReturnVal)",
+       R"(- `MyReturnVal`
+- `MyOtherReturnVal`)",
+       R"(- `MyReturnVal`
+- `MyOtherReturnVal`)",
+       R"(- MyReturnVal
+- MyOtherReturnVal)"},
+      {R"(@retval MyReturnVal if foo
+ at retval MyOtherReturnVal if bar)",
+       R"(- `MyReturnVal` - if foo
+- `MyOtherReturnVal` - if bar)",
+       R"(- `MyReturnVal` - if foo
+- `MyOtherReturnVal` - if bar)",
+       R"(- MyReturnVal - if foo
+- MyOtherReturnVal - if bar)"},
+  };
+  for (const auto &C : Cases) {
+    markup::Document Doc;
+    SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
+
+    SymbolDoc.retvalsToMarkup(Doc);
+
+    EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
+    EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
+    EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
+  }
+}
+
 } // namespace clangd
 } // namespace clang

>From 078294bf08f600262b369a6a855e63eaf004e9be Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Mon, 15 Sep 2025 11:42:02 +0000
Subject: [PATCH 2/5] add markdown preprocessing to handle code blocks and code
 spans correctly

---
 .../clangd/SymbolDocumentation.cpp            | 151 +++++++
 .../clangd/SymbolDocumentation.h              |  49 ++-
 clang-tools-extra/clangd/support/Markup.cpp   |  29 +-
 .../unittests/SymbolDocumentationTests.cpp    | 393 ++++++++++++++++++
 4 files changed, 595 insertions(+), 27 deletions(-)

diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
index 86bd6162b6f72..ee67f9ae66443 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.cpp
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -13,6 +13,7 @@
 #include "clang/AST/CommentCommandTraits.h"
 #include "clang/AST/CommentVisitor.h"
 #include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringRef.h"
 
 namespace clang {
@@ -249,7 +250,53 @@ class BlockCommentToMarkupDocument
     }
   }
 
+  void visitCodeCommand(const comments::VerbatimBlockComment *VB) {
+    std::string CodeLang = "";
+    auto *FirstLine = VB->child_begin();
+    // The \\code command has an optional language argument.
+    // This argument is currently not parsed by the clang doxygen parser.
+    // Therefore we try to extract it from the first line of the verbatim
+    // block.
+    if (VB->getNumLines() > 0) {
+      if (const auto *Line =
+              cast<comments::VerbatimBlockLineComment>(*FirstLine)) {
+        llvm::StringRef Text = Line->getText();
+        // Language is a single word enclosed in {}.
+        if (llvm::none_of(Text, llvm::isSpace) && Text.consume_front("{") &&
+            Text.consume_back("}")) {
+          // drop a potential . since this is not supported in Markdown
+          // fenced code blocks.
+          Text.consume_front(".");
+          // Language is alphanumeric or '+'.
+          CodeLang = Text.take_while([](char C) {
+                           return llvm::isAlnum(C) || C == '+';
+                         })
+                         .str();
+          // Skip the first line for the verbatim text.
+          ++FirstLine;
+        }
+      }
+    }
+
+    std::string CodeBlockText;
+
+    for (const auto *LI = FirstLine; LI != VB->child_end(); ++LI) {
+      if (const auto *Line = cast<comments::VerbatimBlockLineComment>(*LI)) {
+        CodeBlockText += Line->getText().str() + "\n";
+      }
+    }
+
+    Out.addCodeBlock(CodeBlockText, CodeLang);
+  }
+
   void visitVerbatimBlockComment(const comments::VerbatimBlockComment *VB) {
+    // The \\code command is a special verbatim block command which we handle
+    // separately.
+    if (VB->getCommandID() == comments::CommandTraits::KCI_code) {
+      visitCodeCommand(VB);
+      return;
+    }
+
     commandToMarkup(Out.addParagraph(), VB->getCommandName(Traits),
                     VB->getCommandMarker(), "");
 
@@ -292,6 +339,110 @@ class BlockCommentToMarkupDocument
   }
 };
 
+void SymbolDocCommentVisitor::preprocessDocumentation(StringRef Doc) {
+  enum State {
+    Normal,
+    FencedCodeblock,
+  } State = Normal;
+  std::string CodeFence;
+
+  llvm::raw_string_ostream OS(CommentWithMarkers);
+
+  // The documentation string is processed line by line.
+  // The raw documentation string does not contain the comment markers
+  // (e.g. /// or /** */).
+  // But the comment lexer expects doxygen markers, so add them back.
+  // We need to use the /// style doxygen markers because the comment could
+  // contain the closing tag "*/" of a C Style "/** */" comment
+  // which would break the parsing if we would just enclose the comment text
+  // with "/** */".
+
+  // Escape doxygen commands inside markdown inline code spans.
+  // This is required to not let the doxygen parser interpret them as
+  // commands.
+  // Note: This is a heuristic which may fail in some cases.
+  bool InCodeSpan = false;
+
+  llvm::StringRef Line, Rest;
+  for (std::tie(Line, Rest) = Doc.split('\n'); !(Line.empty() && Rest.empty());
+       std::tie(Line, Rest) = Rest.split('\n')) {
+
+    // Detect code fence (``` or ~~~)
+    if (State == Normal) {
+      llvm::StringRef Trimmed = Line.ltrim();
+      if (Trimmed.starts_with("```") || Trimmed.starts_with("~~~")) {
+        // https://www.doxygen.nl/manual/markdown.html#md_fenced
+        CodeFence =
+            Trimmed.take_while([](char C) { return C == '`' || C == '~'; })
+                .str();
+        // Try to detect language: first word after fence. Could also be
+        // enclosed in {}
+        llvm::StringRef AfterFence =
+            Trimmed.drop_front(CodeFence.size()).ltrim();
+        // ignore '{' at the beginning of the language name to not duplicate it
+        // for the doxygen command
+        AfterFence.consume_front("{");
+        // The name is alphanumeric or '.' or '+'
+        StringRef CodeLang = AfterFence.take_while(
+            [](char C) { return llvm::isAlnum(C) || C == '.' || C == '+'; });
+
+        OS << "///@code";
+
+        if (!CodeLang.empty())
+          OS << "{" << CodeLang.str() << "}";
+
+        OS << "\n";
+
+        State = FencedCodeblock;
+        continue;
+      }
+
+      // FIXME: handle indented code blocks too?
+      // In doxygen, the indentation which triggers a code block depends on the
+      // indentation of the previous paragraph.
+      // https://www.doxygen.nl/manual/markdown.html#mddox_code_blocks
+    } else if (State == FencedCodeblock) {
+      // End of code fence
+      if (Line.ltrim().starts_with(CodeFence)) {
+        OS << "///@endcode\n";
+        State = Normal;
+        continue;
+      }
+      OS << "///" << Line << "\n";
+      continue;
+    }
+
+    // Normal line preprocessing (add doxygen markers, handle escaping)
+    OS << "///";
+
+    if (Line.empty() || Line.trim().empty()) {
+      OS << "\n";
+      // Empty lines reset the InCodeSpan state.
+      InCodeSpan = false;
+      continue;
+    }
+
+    if (Line.starts_with("<"))
+      // A comment line starting with '///<' is treated as a doxygen
+      // command. To avoid this, we add a space before the '<'.
+      OS << ' ';
+
+    for (char C : Line) {
+      if (C == '`')
+        InCodeSpan = !InCodeSpan;
+      else if (InCodeSpan && (C == '@' || C == '\\'))
+        OS << '\\';
+      OS << C;
+    }
+
+    OS << "\n";
+  }
+
+  // Close any unclosed code block
+  if (State == FencedCodeblock)
+    OS << "///@endcode\n";
+}
+
 void SymbolDocCommentVisitor::visitBlockCommandComment(
     const comments::BlockCommandComment *B) {
   switch (B->getCommandID()) {
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h
index 1d48ee6d4d3f9..825c9a296df70 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.h
+++ b/clang-tools-extra/clangd/SymbolDocumentation.h
@@ -21,6 +21,7 @@
 #include "clang/AST/CommentSema.h"
 #include "clang/AST/CommentVisitor.h"
 #include "clang/Basic/SourceManager.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/raw_ostream.h"
 #include <string>
 
@@ -51,31 +52,8 @@ class SymbolDocCommentVisitor
     CommentWithMarkers.reserve(Documentation.size() +
                                Documentation.count('\n') * 3);
 
-    // The comment lexer expects doxygen markers, so add them back.
-    // We need to use the /// style doxygen markers because the comment could
-    // contain the closing the closing tag "*/" of a C Style "/** */" comment
-    // which would break the parsing if we would just enclose the comment text
-    // with "/** */".
-    CommentWithMarkers = "///";
-    bool NewLine = true;
-    for (char C : Documentation) {
-      if (C == '\n') {
-        CommentWithMarkers += "\n///";
-        NewLine = true;
-      } else {
-        if (NewLine && (C == '<')) {
-          // A comment line starting with '///<' is treated as a doxygen
-          // comment. Therefore add a space to separate the '<' from the comment
-          // marker. This allows to parse html tags at the beginning of a line
-          // and the escape marker prevents adding the artificial space in the
-          // markup documentation. The extra space will not be rendered, since
-          // we render it as markdown.
-          CommentWithMarkers += ' ';
-        }
-        CommentWithMarkers += C;
-        NewLine = false;
-      }
-    }
+    preprocessDocumentation(Documentation);
+
     SourceManagerForFile SourceMgrForFile("mock_file.cpp", CommentWithMarkers);
 
     SourceManager &SourceMgr = SourceMgrForFile.get();
@@ -149,6 +127,27 @@ class SymbolDocCommentVisitor
     TemplateParameters[TP->getParamNameAsWritten()] = std::move(TP);
   }
 
+  /// \brief Preprocesses the raw documentation string to prepare it for doxygen
+  /// parsing.
+  ///
+  /// This is a workaround to provide better support for markdown in
+  /// doxygen. Clang's doxygen parser e.g. does not handle markdown code blocks.
+  ///
+  /// The documentation string is preprocessed to replace some markdown
+  /// constructs with parsable doxygen commands. E.g. markdown code blocks are
+  /// replaced with doxygen \\code{.lang} ...
+  /// \\endcode blocks.
+  ///
+  /// Additionally, potential doxygen commands inside markdown
+  /// inline code spans are escaped to avoid that doxygen tries to interpret
+  /// them as commands.
+  ///
+  /// \note Although this is a workaround, it is very similar to what
+  /// doxygen itself does for markdown. In doxygen, the first parsing step is
+  /// also a markdown preprocessing step.
+  /// See https://www.doxygen.nl/manual/markdown.html
+  void preprocessDocumentation(StringRef Doc);
+
 private:
   comments::CommandTraits Traits;
   llvm::BumpPtrAllocator Allocator;
diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp
index 89bdc656d440f..304917de252bf 100644
--- a/clang-tools-extra/clangd/support/Markup.cpp
+++ b/clang-tools-extra/clangd/support/Markup.cpp
@@ -199,10 +199,16 @@ bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After,
   return needsLeadingEscapeMarkdown(C, After);
 }
 
-/// Escape a markdown text block.
+/// \brief Render text for markdown output.
+///
 /// If \p EscapeMarkdown is true it ensures the punctuation will not introduce
 /// any of the markdown constructs.
+///
 /// Else, markdown syntax is not escaped, only HTML tags and entities.
+/// HTML is escaped because usually clients do not support HTML rendering by
+/// default. Passing unescaped HTML will therefore often result in not showing
+/// the HTML at all.
+/// \note In markdown code spans, we do not escape anything.
 std::string renderText(llvm::StringRef Input, bool StartsLine,
                        bool EscapeMarkdown) {
   std::string R;
@@ -213,6 +219,10 @@ std::string renderText(llvm::StringRef Input, bool StartsLine,
 
   bool IsFirstLine = true;
 
+  // Inside markdown code spans, we do not escape anything when EscapeMarkdown
+  // is false.
+  bool InCodeSpan = false;
+
   for (std::tie(Line, Rest) = Input.split('\n');
        !(Line.empty() && Rest.empty());
        std::tie(Line, Rest) = Rest.split('\n')) {
@@ -226,12 +236,27 @@ std::string renderText(llvm::StringRef Input, bool StartsLine,
       R.append(LeadingSpaces);
     }
 
+    // Handle the case where the user escaped a character themselves.
+    // This is relevant for markdown code spans if EscapeMarkdown is false,
+    // because if the user escaped a backtick, we must treat the enclosed text
+    // as normal markdown text.
+    bool UserEscape = false;
     for (unsigned I = LeadingSpaces.size(); I < Line.size(); ++I) {
-      if (needsLeadingEscape(Line[I], Line.substr(LeadingSpaces.size(), I),
+
+      if (!EscapeMarkdown && !UserEscape && Line[I] == '`')
+        InCodeSpan = !InCodeSpan;
+
+      if (!InCodeSpan &&
+          needsLeadingEscape(Line[I], Line.substr(LeadingSpaces.size(), I),
                              Line.substr(I + 1), StartsLineIntern,
                              EscapeMarkdown))
         R.push_back('\\');
       R.push_back(Line[I]);
+
+      if (Line[I] == '\\')
+        UserEscape = !UserEscape;
+      else
+        UserEscape = false;
     }
 
     IsFirstLine = false;
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
index ba692399a7093..a2e7f00c933a7 100644
--- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -317,5 +317,398 @@ TEST(SymbolDocumentation, RetvalCommand) {
   }
 }
 
+TEST(SymbolDocumentation, DoxygenCodeBlocks) {
+  CommentOptions CommentOpts;
+
+  struct Case {
+    llvm::StringRef Documentation;
+    llvm::StringRef ExpectedRenderEscapedMarkdown;
+    llvm::StringRef ExpectedRenderMarkdown;
+    llvm::StringRef ExpectedRenderPlainText;
+  } Cases[] = {
+      {R"(@code
+int code() { return 0; }
+ at endcode
+ at code{.cpp}
+int code_lang() { return 0; }
+ at endcode
+ at code{.c++}
+int code_lang_plus() { return 0; }
+ at endcode
+ at code{.py}
+class A:
+    pass
+ at endcode
+ at code{nolang}
+class B:
+    pass
+ at endcode)",
+       R"(```
+int code() { return 0; }
+```
+
+```cpp
+int code_lang() { return 0; }
+```
+
+```c++
+int code_lang_plus() { return 0; }
+```
+
+```py
+class A:
+    pass
+```
+
+```nolang
+class B:
+    pass
+```)",
+       R"(```
+int code() { return 0; }
+```
+
+```cpp
+int code_lang() { return 0; }
+```
+
+```c++
+int code_lang_plus() { return 0; }
+```
+
+```py
+class A:
+    pass
+```
+
+```nolang
+class B:
+    pass
+```)",
+       R"(int code() { return 0; }
+
+int code_lang() { return 0; }
+
+int code_lang_plus() { return 0; }
+
+class A:
+    pass
+
+class B:
+    pass)"},
+  };
+  for (const auto &C : Cases) {
+    markup::Document Doc;
+    SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
+
+    SymbolDoc.docToMarkup(Doc);
+
+    EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
+    EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
+    EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
+  }
+}
+
+TEST(SymbolDocumentation, MarkdownCodeBlocks) {
+  CommentOptions CommentOpts;
+
+  struct Case {
+    llvm::StringRef Documentation;
+    llvm::StringRef ExpectedRenderEscapedMarkdown;
+    llvm::StringRef ExpectedRenderMarkdown;
+    llvm::StringRef ExpectedRenderPlainText;
+  } Cases[] = {
+      {R"(```
+int backticks() { return 0; }
+```
+```cpp
+int backticks_lang() { return 0; }
+```
+```c++
+int backticks_lang_plus() { return 0; }
+```
+~~~
+int tilde() { return 0; }
+~~~
+~~~~~~~~~~~~~~~~~~~~~~~~
+int tilde_many() { return 0; }
+~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~{.c++}
+int tilde_many_lang() { return 0; }
+~~~~~~~~~~~~~~~~~~~~~~~~
+```py
+class A:
+    pass
+```
+```python
+class B:
+    pass
+```
+~~~{.python}
+class C:
+    pass
+~~~
+)",
+       R"(```
+int backticks() { return 0; }
+```
+
+```cpp
+int backticks_lang() { return 0; }
+```
+
+```c++
+int backticks_lang_plus() { return 0; }
+```
+
+```
+int tilde() { return 0; }
+```
+
+```
+int tilde_many() { return 0; }
+```
+
+```c++
+int tilde_many_lang() { return 0; }
+```
+
+```py
+class A:
+    pass
+```
+
+```python
+class B:
+    pass
+```
+
+```python
+class C:
+    pass
+```)",
+       R"(```
+int backticks() { return 0; }
+```
+
+```cpp
+int backticks_lang() { return 0; }
+```
+
+```c++
+int backticks_lang_plus() { return 0; }
+```
+
+```
+int tilde() { return 0; }
+```
+
+```
+int tilde_many() { return 0; }
+```
+
+```c++
+int tilde_many_lang() { return 0; }
+```
+
+```py
+class A:
+    pass
+```
+
+```python
+class B:
+    pass
+```
+
+```python
+class C:
+    pass
+```)",
+       R"(int backticks() { return 0; }
+
+int backticks_lang() { return 0; }
+
+int backticks_lang_plus() { return 0; }
+
+int tilde() { return 0; }
+
+int tilde_many() { return 0; }
+
+int tilde_many_lang() { return 0; }
+
+class A:
+    pass
+
+class B:
+    pass
+
+class C:
+    pass)"},
+      {R"(```
+// this code block is missing end backticks
+
+)",
+       R"(```
+// this code block is missing end backticks
+```)",
+       R"(```
+// this code block is missing end backticks
+```)",
+       R"(// this code block is missing end backticks)"},
+  };
+  for (const auto &C : Cases) {
+    markup::Document Doc;
+    SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
+
+    SymbolDoc.docToMarkup(Doc);
+
+    EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
+    EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
+    EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
+  }
+}
+
+TEST(SymbolDocumentation, MarkdownCodeBlocksSeparation) {
+  CommentOptions CommentOpts;
+
+  struct Case {
+    llvm::StringRef Documentation;
+    llvm::StringRef ExpectedRenderEscapedMarkdown;
+    llvm::StringRef ExpectedRenderMarkdown;
+    llvm::StringRef ExpectedRenderPlainText;
+  } Cases[] = {
+      {R"(@note Show that code blocks are correctly separated
+```
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; }
+```)",
+       R"(\*\*Note:\*\*  
+Show that code blocks are correctly separated
+
+---
+```
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; }
+```)",
+       R"(**Note:**  
+Show that code blocks are correctly separated
+
+---
+```
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; }
+```)",
+       R"(**Note:**
+Show that code blocks are correctly separated
+
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; })"},
+      {R"(@note Show that code blocks are correctly separated
+~~~~~~~~~
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; }
+~~~~~~~~~)",
+       R"(\*\*Note:\*\*  
+Show that code blocks are correctly separated
+
+---
+```
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; }
+```)",
+       R"(**Note:**  
+Show that code blocks are correctly separated
+
+---
+```
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; }
+```)",
+       R"(**Note:**
+Show that code blocks are correctly separated
+
+/// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
+
+/// With preprocessing, the code block is correctly separated from the @note paragraph.
+/// Also note that without preprocessing, all doxygen commands inside code blocks, like @p would be incorrectly interpreted.
+int function() { return 0; })"},
+  };
+  for (const auto &C : Cases) {
+    markup::Document Doc;
+    SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
+
+    SymbolDoc.docToMarkup(Doc);
+
+    EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
+    EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
+    EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
+  }
+}
+
+TEST(SymbolDocumentation, MarkdownCodeSpans) {
+  CommentOptions CommentOpts;
+
+  struct Case {
+    llvm::StringRef Documentation;
+    llvm::StringRef ExpectedRenderEscapedMarkdown;
+    llvm::StringRef ExpectedRenderMarkdown;
+    llvm::StringRef ExpectedRenderPlainText;
+  } Cases[] = {
+      {R"(`this is a code span with @p and \c inside`)",
+       R"(\`this is a code span with @p and \\c inside\`)",
+       R"(`this is a code span with @p and \c inside`)",
+       R"(`this is a code span with @p and \c inside`)"},
+      {R"(<escaped> `<not-escaped>`)", R"(\<escaped> \`\<not-escaped>\`)",
+       R"(\<escaped> `<not-escaped>`)", R"(<escaped> `<not-escaped>`)"},
+      {R"(<escaped> \`<escaped> doxygen commands not parsed @p, \c, @note, \warning \`)",
+       R"(\<escaped> \\\`\<escaped> doxygen commands not parsed @p, \\c, @note, \\warning \\\`)",
+       R"(\<escaped> \`\<escaped> doxygen commands not parsed @p, \c, @note, \warning \`)",
+       R"(<escaped> \`<escaped> doxygen commands not parsed @p, \c, @note, \warning \`)"},
+      {R"(`multi
+line
+\c span`)",
+       R"(\`multi
+line
+\\c span\`)",
+       R"(`multi
+line
+\c span`)",
+       R"(`multi line
+\c span`)"},
+  };
+  for (const auto &C : Cases) {
+    markup::Document Doc;
+    SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
+
+    SymbolDoc.docToMarkup(Doc);
+
+    EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
+    EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
+    EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
+  }
+}
+
 } // namespace clangd
 } // namespace clang

>From cfbda5a162b6be52ceefed05f32212d830106bdc Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sun, 21 Sep 2025 10:10:56 +0000
Subject: [PATCH 3/5] separate brief and details by using first paragraph as
 brief

---
 clang-tools-extra/clangd/Hover.cpp            |  6 +-
 .../clangd/SymbolDocumentation.cpp            | 13 ++-
 .../clangd/SymbolDocumentation.h              | 14 +++-
 .../clangd/unittests/HoverTests.cpp           | 14 +++-
 .../unittests/SymbolDocumentationTests.cpp    | 80 +++++++++++++------
 5 files changed, 94 insertions(+), 33 deletions(-)

diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 3b9baed1c2838..205bc93e82a4f 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1594,8 +1594,10 @@ markup::Document HoverInfo::presentDoxygen() const {
     Output.addRuler();
   }
 
-  // add any other documentation.
-  SymbolDoc.docToMarkup(Output);
+  if (SymbolDoc.hasDetailedDoc()) {
+    Output.addParagraph().appendBoldText("Details:");
+    SymbolDoc.detailedDocToMarkup(Output);
+  }
 
   Output.addRuler();
 
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
index ee67f9ae66443..48926c842552f 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.cpp
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -231,6 +231,17 @@ class BlockCommentToMarkupDocument
       }
       return;
     }
+    case comments::CommandTraits::KCI_details: {
+      // The \details command is just used to separate the brief from the
+      // detailed description. This separation is already done in the
+      // SymbolDocCommentVisitor. Therefore we can omit the command itself
+      // here and just process the paragraph.
+      if (B->getParagraph() && !B->getParagraph()->isWhitespace()) {
+        ParagraphToMarkupDocument(Out.addParagraph(), Traits)
+            .visit(B->getParagraph());
+      }
+      return;
+    }
     default: {
       // Some commands have arguments, like \throws.
       // The arguments are not part of the paragraph.
@@ -509,7 +520,7 @@ void SymbolDocCommentVisitor::parameterDocToString(
   }
 }
 
-void SymbolDocCommentVisitor::docToMarkup(markup::Document &Out) const {
+void SymbolDocCommentVisitor::detailedDocToMarkup(markup::Document &Out) const {
   for (unsigned I = 0; I < CommentPartIndex; ++I) {
     if (const auto *BC = BlockCommands.lookup(I)) {
       BlockCommentToMarkupDocument(Out, Traits).visit(BC);
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h
index 825c9a296df70..bbd45fdb781f7 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.h
+++ b/clang-tools-extra/clangd/SymbolDocumentation.h
@@ -78,6 +78,14 @@ class SymbolDocCommentVisitor
     for (auto *Block : FC->getBlocks()) {
       visit(Block);
     }
+
+    // If we have not seen a brief command, use the very first free paragraph as
+    // the brief.
+    if (!BriefParagraph && !FreeParagraphs.empty() &&
+        FreeParagraphs.contains(0)) {
+      BriefParagraph = FreeParagraphs.lookup(0);
+      FreeParagraphs.erase(0);
+    }
   }
 
   bool isParameterDocumented(StringRef ParamName) const {
@@ -92,8 +100,12 @@ class SymbolDocCommentVisitor
 
   bool hasReturnCommand() const { return ReturnParagraph; }
 
+  bool hasDetailedDoc() const {
+    return !FreeParagraphs.empty() || !BlockCommands.empty();
+  }
+
   /// Converts all unhandled comment commands to a markup document.
-  void docToMarkup(markup::Document &Out) const;
+  void detailedDocToMarkup(markup::Document &Out) const;
   /// Converts the "brief" command(s) to a markup document.
   void briefToMarkup(markup::Paragraph &Out) const;
   /// Converts the "return" command(s) to a markup document.
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index fbcc2992cf2b7..b3874414c7f1a 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -3456,15 +3456,15 @@ template <typename T, typename C = bool> class Foo {})",
 template <typename T, typename C = bool> class Foo {}
 ```
 
+---
+documentation
+
 ---
 **Template Parameters:**
 
 - `typename T`
 - `typename C = bool`
 
----
-documentation
-
 ---
 Size: 10 bytes)",
       },
@@ -4004,6 +4004,8 @@ void foo()
 brief doc
 
 ---
+**Details:**
+
 longer doc)"},
       {[](HoverInfo &HI) {
          HI.Kind = index::SymbolKind::Function;
@@ -4042,6 +4044,8 @@ brief doc
 `int`
 
 ---
+**Details:**
+
 longer doc)"},
       {[](HoverInfo &HI) {
          HI.Kind = index::SymbolKind::Function;
@@ -4117,6 +4121,8 @@ brief doc
 - `1` - if failed
 
 ---
+**Details:**
+
 longer doc
 
 ---
@@ -4186,6 +4192,8 @@ brief doc
 `int` - it returns something
 
 ---
+**Details:**
+
 longer doc)"},
   };
 
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
index a2e7f00c933a7..4e8856624f47f 100644
--- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -15,7 +15,7 @@
 namespace clang {
 namespace clangd {
 
-TEST(SymbolDocumentation, DocToMarkup) {
+TEST(SymbolDocumentation, DetailedDocToMarkup) {
 
   CommentOptions CommentOpts;
 
@@ -26,49 +26,49 @@ TEST(SymbolDocumentation, DocToMarkup) {
     llvm::StringRef ExpectedRenderPlainText;
   } Cases[] = {
       {
-          "foo bar",
+          "brief\n\nfoo bar",
           "foo bar",
           "foo bar",
           "foo bar",
       },
       {
-          "foo\nbar\n",
+          "brief\n\nfoo\nbar\n",
           "foo\nbar",
           "foo\nbar",
           "foo bar",
       },
       {
-          "foo\n\nbar\n",
+          "brief\n\nfoo\n\nbar\n",
           "foo\n\nbar",
           "foo\n\nbar",
           "foo\n\nbar",
       },
       {
-          "foo \\p bar baz",
+          "brief\n\nfoo \\p bar baz",
           "foo `bar` baz",
           "foo `bar` baz",
           "foo bar baz",
       },
       {
-          "foo \\e bar baz",
+          "brief\n\nfoo \\e bar baz",
           "foo \\*bar\\* baz",
           "foo *bar* baz",
           "foo *bar* baz",
       },
       {
-          "foo \\b bar baz",
+          "brief\n\nfoo \\b bar baz",
           "foo \\*\\*bar\\*\\* baz",
           "foo **bar** baz",
           "foo **bar** baz",
       },
       {
-          "foo \\ref bar baz",
+          "brief\n\nfoo \\ref bar baz",
           "foo \\*\\*\\\\ref\\*\\* `bar` baz",
           "foo **\\ref** `bar` baz",
           "foo **\\ref** bar baz",
       },
       {
-          "foo @ref bar baz",
+          "brief\n\nfoo @ref bar baz",
           "foo \\*\\*@ref\\*\\* `bar` baz",
           "foo **@ref** `bar` baz",
           "foo **@ref** bar baz",
@@ -80,7 +80,7 @@ TEST(SymbolDocumentation, DocToMarkup) {
           "",
       },
       {
-          "\\throw exception foo",
+          "brief\n\n\\throw exception foo",
           "\\*\\*\\\\throw\\*\\* `exception` foo",
           "**\\throw** `exception` foo",
           "**\\throw** exception foo",
@@ -108,7 +108,8 @@ TEST(SymbolDocumentation, DocToMarkup) {
 - item 3)",
       },
       {
-          "\\defgroup mygroup this is a group\nthis is not a group description",
+          "brief\n\n\\defgroup mygroup this is a group\nthis is not a group "
+          "description",
           "\\*\\*@defgroup\\*\\* `mygroup this is a group`\n\nthis is not a "
           "group "
           "description",
@@ -118,7 +119,9 @@ TEST(SymbolDocumentation, DocToMarkup) {
           "description",
       },
       {
-          R"(\verbatim
+          R"(brief
+
+\verbatim
 this is a
 verbatim block containing
 some verbatim text
@@ -150,7 +153,7 @@ some verbatim text
 **@endverbatim**)",
       },
       {
-          "@param foo this is a parameter\n at param bar this is another "
+          "brief\n\n at param foo this is a parameter\n at param bar this is another "
           "parameter",
           "",
           "",
@@ -169,24 +172,26 @@ More description
 documentation)",
           R"(\*\*\\brief\*\* another brief?
 
-\*\*\\details\*\* these are details
+these are details
 
 More description
 documentation)",
           R"(**\brief** another brief?
 
-**\details** these are details
+these are details
 
 More description
 documentation)",
           R"(**\brief** another brief?
 
-**\details** these are details
+these are details
 
 More description documentation)",
       },
       {
-          R"(<b>this is a bold text</b>
+          R"(brief
+
+<b>this is a bold text</b>
 normal text<i>this is an italic text</i>
 <code>this is a code block</code>)",
           R"(\<b>this is a bold text\</b>
@@ -198,14 +203,16 @@ normal text\<i>this is an italic text\</i>
           "<b>this is a bold text</b> normal text<i>this is an italic text</i> "
           "<code>this is a code block</code>",
       },
-      {"@note This is a note",
+      {"brief\n\n at note This is a note",
        R"(\*\*Note:\*\*  
 This is a note)",
        R"(**Note:**  
 This is a note)",
        R"(**Note:**
 This is a note)"},
-      {R"(Paragraph 1
+      {R"(brief
+
+Paragraph 1
 @note This is a note
 
 Paragraph 2)",
@@ -231,14 +238,16 @@ Paragraph 2)",
 This is a note
 
 Paragraph 2)"},
-      {"@warning This is a warning",
+      {"brief\n\n at warning This is a warning",
        R"(\*\*Warning:\*\*  
 This is a warning)",
        R"(**Warning:**  
 This is a warning)",
        R"(**Warning:**
 This is a warning)"},
-      {R"(Paragraph 1
+      {R"(brief
+
+Paragraph 1
 @warning This is a warning
 
 Paragraph 2)",
@@ -264,12 +273,31 @@ Paragraph 2)",
 This is a warning
 
 Paragraph 2)"},
+      {R"(@note this is not treated as brief
+
+ at brief this is the brief
+
+Another paragraph)",
+       R"(\*\*Note:\*\*  
+this is not treated as brief
+
+---
+Another paragraph)",
+       R"(**Note:**  
+this is not treated as brief
+
+---
+Another paragraph)",
+       R"(**Note:**
+this is not treated as brief
+
+Another paragraph)"},
   };
   for (const auto &C : Cases) {
     markup::Document Doc;
     SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
 
-    SymbolDoc.docToMarkup(Doc);
+    SymbolDoc.detailedDocToMarkup(Doc);
 
     EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
     EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
@@ -401,7 +429,7 @@ class B:
     markup::Document Doc;
     SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
 
-    SymbolDoc.docToMarkup(Doc);
+    SymbolDoc.detailedDocToMarkup(Doc);
 
     EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
     EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
@@ -561,7 +589,7 @@ class C:
     markup::Document Doc;
     SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
 
-    SymbolDoc.docToMarkup(Doc);
+    SymbolDoc.detailedDocToMarkup(Doc);
 
     EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
     EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
@@ -659,7 +687,7 @@ int function() { return 0; })"},
     markup::Document Doc;
     SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
 
-    SymbolDoc.docToMarkup(Doc);
+    SymbolDoc.detailedDocToMarkup(Doc);
 
     EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
     EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
@@ -702,7 +730,7 @@ line
     markup::Document Doc;
     SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
 
-    SymbolDoc.docToMarkup(Doc);
+    SymbolDoc.briefToMarkup(Doc.addParagraph());
 
     EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
     EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);

>From d77969f5efb4a08480020f7f420bab36b9a9087c Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sat, 27 Sep 2025 12:32:38 +0000
Subject: [PATCH 4/5] ignore empty freestanding paragraphs

---
 clang-tools-extra/clangd/SymbolDocumentation.h            | 6 ++++--
 .../clangd/unittests/SymbolDocumentationTests.cpp         | 8 ++++++++
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h
index bbd45fdb781f7..88c7ade633516 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.h
+++ b/clang-tools-extra/clangd/SymbolDocumentation.h
@@ -127,8 +127,10 @@ class SymbolDocCommentVisitor
                             llvm::raw_string_ostream &Out) const;
 
   void visitParagraphComment(const comments::ParagraphComment *P) {
-    FreeParagraphs[CommentPartIndex] = P;
-    CommentPartIndex++;
+    if (!P->isWhitespace()) {
+      FreeParagraphs[CommentPartIndex] = P;
+      CommentPartIndex++;
+    }
   }
 
   void visitParamCommandComment(const comments::ParamCommandComment *P) {
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
index 4e8856624f47f..5078a6778ead3 100644
--- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -292,6 +292,14 @@ Another paragraph)",
 this is not treated as brief
 
 Another paragraph)"},
+      {R"(
+ at brief Some brief
+)",
+       "", "", ""},
+      {R"(
+Some brief
+)",
+       "", "", ""},
   };
   for (const auto &C : Cases) {
     markup::Document Doc;

>From ff0418a545dfdefd8ecde536d7f725a65e264c95 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sat, 27 Sep 2025 13:19:54 +0000
Subject: [PATCH 5/5] itroduce brief section, use h3 headings for all section,
 remove rulers for highlighted commands

---
 clang-tools-extra/clangd/Hover.cpp            | 14 +++++--
 .../clangd/SymbolDocumentation.cpp            |  2 -
 .../clangd/unittests/HoverTests.cpp           | 42 +++++++++++--------
 .../unittests/SymbolDocumentationTests.cpp    | 14 -------
 4 files changed, 34 insertions(+), 38 deletions(-)

diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 205bc93e82a4f..3ab00c5147317 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1535,6 +1535,12 @@ markup::Document HoverInfo::presentDoxygen() const {
   SymbolDocCommentVisitor SymbolDoc(Documentation, CommentOpts);
 
   if (SymbolDoc.hasBriefCommand()) {
+    if (Kind != index::SymbolKind::Parameter &&
+        Kind != index::SymbolKind::TemplateTypeParm)
+      // Only add a "Brief" heading if we are not documenting a parameter.
+      // Parameters only have a brief section and adding the brief header would
+      // be redundant.
+      Output.addHeading(3).appendText("Brief");
     SymbolDoc.briefToMarkup(Output.addParagraph());
     Output.addRuler();
   }
@@ -1548,7 +1554,7 @@ markup::Document HoverInfo::presentDoxygen() const {
   // Returns
   // `type` - description
   if (TemplateParameters && !TemplateParameters->empty()) {
-    Output.addParagraph().appendBoldText("Template Parameters:");
+    Output.addHeading(3).appendText("Template Parameters");
     markup::BulletList &L = Output.addBulletList();
     for (const auto &Param : *TemplateParameters) {
       markup::Paragraph &P = L.addItem().addParagraph();
@@ -1562,7 +1568,7 @@ markup::Document HoverInfo::presentDoxygen() const {
   }
 
   if (Parameters && !Parameters->empty()) {
-    Output.addParagraph().appendBoldText("Parameters:");
+    Output.addHeading(3).appendText("Parameters");
     markup::BulletList &L = Output.addBulletList();
     for (const auto &Param : *Parameters) {
       markup::Paragraph &P = L.addItem().addParagraph();
@@ -1581,7 +1587,7 @@ markup::Document HoverInfo::presentDoxygen() const {
   if (ReturnType &&
       ((ReturnType->Type != "void" && !ReturnType->AKA.has_value()) ||
        (ReturnType->AKA.has_value() && ReturnType->AKA != "void"))) {
-    Output.addParagraph().appendBoldText("Returns:");
+    Output.addHeading(3).appendText("Returns");
     markup::Paragraph &P = Output.addParagraph();
     P.appendCode(llvm::to_string(*ReturnType));
 
@@ -1595,7 +1601,7 @@ markup::Document HoverInfo::presentDoxygen() const {
   }
 
   if (SymbolDoc.hasDetailedDoc()) {
-    Output.addParagraph().appendBoldText("Details:");
+    Output.addHeading(3).appendText("Details");
     SymbolDoc.detailedDocToMarkup(Output);
   }
 
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
index 48926c842552f..a50d7a565b1bc 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.cpp
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -339,14 +339,12 @@ class BlockCommentToMarkupDocument
   /// Emphasize the given command in a paragraph.
   /// Uses the command name with the first letter capitalized as the heading.
   void commandToHeadedParagraph(const comments::BlockCommandComment *B) {
-    Out.addRuler();
     auto &P = Out.addParagraph();
     std::string Heading = B->getCommandName(Traits).slice(0, 1).upper() +
                           B->getCommandName(Traits).drop_front().str();
     P.appendBoldText(Heading + ":");
     P.appendText("  \n");
     ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
-    Out.addRuler();
   }
 };
 
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index b3874414c7f1a..a54f366c7a862 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -3457,10 +3457,12 @@ template <typename T, typename C = bool> class Foo {}
 ```
 
 ---
+### Brief
+
 documentation
 
 ---
-**Template Parameters:**
+### Template Parameters
 
 - `typename T`
 - `typename C = bool`
@@ -3506,7 +3508,7 @@ ret_type foo(params) {}
 ```
 
 ---
-**Parameters:**
+### Parameters
 
 - 
 - `type (aka can_type)`
@@ -3514,7 +3516,7 @@ ret_type foo(params) {}
 - `type foo = default (aka can_type)`
 
 ---
-**Returns:**
+### Returns
 
 `ret_type (aka can_ret_type)`)",
       },
@@ -3649,7 +3651,7 @@ protected: size_t method()
 ```
 
 ---
-**Returns:**
+### Returns
 
 `size_t (aka unsigned long)`)",
       },
@@ -3688,7 +3690,7 @@ public: cls(int a, int b = 5)
 ```
 
 ---
-**Parameters:**
+### Parameters
 
 - `int a`
 - `int b = 5`)",
@@ -4001,10 +4003,12 @@ void foo()
 ```
 
 ---
+### Brief
+
 brief doc
 
 ---
-**Details:**
+### Details
 
 longer doc)"},
       {[](HoverInfo &HI) {
@@ -4036,15 +4040,17 @@ int foo()
 ```
 
 ---
+### Brief
+
 brief doc
 
 ---
-**Returns:**
+### Returns
 
 `int`
 
 ---
-**Details:**
+### Details
 
 longer doc)"},
       {[](HoverInfo &HI) {
@@ -4105,15 +4111,17 @@ int foo(int a)
 ```
 
 ---
+### Brief
+
 brief doc
 
 ---
-**Parameters:**
+### Parameters
 
 - `int a` - this is a param
 
 ---
-**Returns:**
+### Returns
 
 `int` - it returns something
 
@@ -4121,22 +4129,18 @@ brief doc
 - `1` - if failed
 
 ---
-**Details:**
+### Details
 
 longer doc
 
----
 **Note:**  
 this is a note
 
----
 As you see, notes are "inlined".
 
----
 **Warning:**  
 this is a warning
 
----
 As well as warnings)"},
       {[](HoverInfo &HI) {
          HI.Kind = index::SymbolKind::Function;
@@ -4179,20 +4183,22 @@ int foo(int a)
 ```
 
 ---
+### Brief
+
 brief doc
 
 ---
-**Parameters:**
+### Parameters
 
 - `int a` - this is a param
 
 ---
-**Returns:**
+### Returns
 
 `int` - it returns something
 
 ---
-**Details:**
+### Details
 
 longer doc)"},
   };
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
index 5078a6778ead3..b3185cc10dd5a 100644
--- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -218,19 +218,15 @@ Paragraph 1
 Paragraph 2)",
        R"(Paragraph 1
 
----
 \*\*Note:\*\*  
 This is a note
 
----
 Paragraph 2)",
        R"(Paragraph 1
 
----
 **Note:**  
 This is a note
 
----
 Paragraph 2)",
        R"(Paragraph 1
 
@@ -253,19 +249,15 @@ Paragraph 1
 Paragraph 2)",
        R"(Paragraph 1
 
----
 \*\*Warning:\*\*  
 This is a warning
 
----
 Paragraph 2)",
        R"(Paragraph 1
 
----
 **Warning:**  
 This is a warning
 
----
 Paragraph 2)",
        R"(Paragraph 1
 
@@ -281,12 +273,10 @@ Another paragraph)",
        R"(\*\*Note:\*\*  
 this is not treated as brief
 
----
 Another paragraph)",
        R"(**Note:**  
 this is not treated as brief
 
----
 Another paragraph)",
        R"(**Note:**
 this is not treated as brief
@@ -625,7 +615,6 @@ int function() { return 0; }
        R"(\*\*Note:\*\*  
 Show that code blocks are correctly separated
 
----
 ```
 /// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
 
@@ -636,7 +625,6 @@ int function() { return 0; }
        R"(**Note:**  
 Show that code blocks are correctly separated
 
----
 ```
 /// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
 
@@ -663,7 +651,6 @@ int function() { return 0; }
        R"(\*\*Note:\*\*  
 Show that code blocks are correctly separated
 
----
 ```
 /// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
 
@@ -674,7 +661,6 @@ int function() { return 0; }
        R"(**Note:**  
 Show that code blocks are correctly separated
 
----
 ```
 /// Without the markdown preprocessing, this line and the line above would be part of the @note paragraph.
 



More information about the cfe-commits mailing list