3 Commits

Author SHA1 Message Date
ykiko
f397e95b34 feat(hover): format definition code blocks with clang-format
Use the project's .clang-format style to format the code snippets shown
in hover popups, so they match the user's formatting preferences instead
of Clang's raw pretty-printer output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 20:28:04 +08:00
ykiko
2df42abd80 fix(hover): use double newline between markdown blocks for proper VSCode rendering
Single newline caused setext heading interpretation (text\n--- → H2) and
paragraph merging in CommonMark/markdown-it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:17:12 +08:00
ykiko
d55fb3d6d2 refactor(hover): fix doxygen rendering and rewrite Markup (formerly StructedText)
- Parse documentation through strip_doxygen_info() to render \param, \return,
  \brief and other doxygen commands as structured markdown instead of raw text
- Fix markdown block separation: add newline between blocks so paragraphs,
  bullet lists, and code blocks no longer merge on the same line
- Fix CodeBlock closing fence not on its own line when code lacks trailing newline
- Fix Heading::clone() slicing to Paragraph (lost heading level on copy)
- Fix BulletList multi-line items missing continuation indentation
- Fix double backtick in hover heading (name wrapped manually + InlineCode)
- Rename StructedText → Markup, fix Strikethough → Strikethrough typo
- Add DoxygenInfo::get_param_command_comments() for param iteration
- Rewrite Markup tests from 3 assertion-less smoke tests to 27 proper tests
- Add 6 doxygen-specific hover tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:17:12 +08:00
11 changed files with 2179 additions and 321 deletions

View File

@@ -104,4 +104,6 @@ auto document_format(llvm::StringRef file,
PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::TextEdit>;
auto format_code(llvm::StringRef file, llvm::StringRef code) -> std::string;
} // namespace clice::feature

View File

@@ -66,4 +66,20 @@ auto document_format(llvm::StringRef file,
return edits;
}
auto format_code(llvm::StringRef file, llvm::StringRef code) -> std::string {
auto style = clang::format::getStyle(clang::format::DefaultFormatStyle,
file,
clang::format::DefaultFallbackStyle,
code);
if(!style)
return code.str();
auto replacements = clang::format::reformat(*style, code, {tooling::Range(0, code.size())});
auto result = tooling::applyAllReplacements(code, replacements);
if(!result)
return code.str();
return llvm::StringRef(*result).trim().str();
}
} // namespace clice::feature

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,15 @@ std::vector<std::pair<llvm::StringRef, llvm::ArrayRef<DoxygenInfo::BlockCommandC
return res;
}
std::vector<std::pair<llvm::StringRef, const DoxygenInfo::ParamCommandCommentContent*>>
DoxygenInfo::get_param_command_comments() const {
std::vector<std::pair<llvm::StringRef, const ParamCommandCommentContent*>> res;
for(const auto& [name, info]: param_command_comments) {
res.emplace_back(name, &info);
}
return res;
}
/// Process inline commands, we only interested in `\b` (bold), `\e` (italic) and `\c` (inline code)
///
/// \param line The line

View File

@@ -49,6 +49,9 @@ public:
return doc_for_return;
}
std::vector<std::pair<llvm::StringRef, const ParamCommandCommentContent*>>
get_param_command_comments() const;
private:
llvm::SmallDenseMap<llvm::StringRef, std::vector<BlockCommandCommentContent>>
block_command_comments;

View File

@@ -1,4 +1,4 @@
#include "support/structed_text.h"
#include "support/markup.h"
#include <algorithm>
#include <cctype>
@@ -25,22 +25,23 @@ std::unique_ptr<Block> BulletList::clone() const {
void BulletList::render_markdown(llvm::raw_ostream& os) const {
for(auto& item: items) {
os << "- " << item.as_markdown() << '\n';
auto content = item.as_markdown();
os << "- ";
for(size_t i = 0; i < content.size(); ++i) {
os << content[i];
if(content[i] == '\n' && i + 1 < content.size())
os << " ";
}
os << '\n';
}
}
StructedText& BulletList::add_item() {
Markup& BulletList::add_item() {
return items.emplace_back();
}
// Clangd inserts escape char '\' before '*', '-' and other markdown markers
// That causes markdown comments are escaped and cannot be rendered properly
// on editors
// We do nothing on it. All the left comments are regarded as markdown rather
// than plain text
void Paragraph::render_markdown(llvm::raw_ostream& os) const {
bool need_space = false;
bool has_chunks = false;
for(auto& chunk: chunks) {
if(chunk.space_ahead || need_space) {
os << ' ';
@@ -58,17 +59,15 @@ void Paragraph::render_markdown(llvm::raw_ostream& os) const {
os << '`' << chunk.content << '`';
break;
}
case Kind::Strikethough: {
case Kind::Strikethrough: {
os << "~~" << chunk.content << "~~";
break;
}
default: {
// Kind::PlainText
os << chunk.content;
break;
}
}
has_chunks = true;
need_space = chunk.space_after;
}
}
@@ -76,7 +75,6 @@ void Paragraph::render_markdown(llvm::raw_ostream& os) const {
Paragraph& Paragraph::append_text(std::string text, Kind kind) {
if(kind == Kind::PlainText) {
llvm::StringRef s{text};
// s = s.trim(" \t\v\f\r");
if(s.empty()) {
return *this;
}
@@ -112,6 +110,10 @@ public:
Paragraph::render_markdown(os);
}
std::unique_ptr<Block> clone() const override {
return std::make_unique<Heading>(*this);
}
private:
unsigned level;
};
@@ -119,7 +121,7 @@ private:
class Ruler : public Block {
public:
void render_markdown(llvm::raw_ostream& os) const override {
os << "\n---\n";
os << "---\n";
}
bool is_ruler() const override {
@@ -134,7 +136,10 @@ public:
class CodeBlock : public Block {
public:
void render_markdown(llvm::raw_ostream& os) const override {
os << "```" << lang << '\n' << code << "```\n";
os << "```" << lang << '\n' << code;
if(!code.empty() && code.back() != '\n')
os << '\n';
os << "```\n";
}
std::unique_ptr<Block> clone() const override {
@@ -160,60 +165,55 @@ static std::string render_blocks(llvm::ArrayRef<std::unique_ptr<Block>> blocks)
blocks = blocks.drop_back(blocks.end() - last.base());
bool last_block_was_ruler = true;
// render
for(const auto& b: blocks) {
if(b->is_ruler() && last_block_was_ruler) {
continue;
}
last_block_was_ruler = b->is_ruler();
b->render_markdown(os);
os << "\n\n";
}
// Get rid of redundant empty lines introduced in plaintext while imitating
// padding in markdown.
std::string adjusted_result;
llvm::StringRef trimmed_text(os.str());
trimmed_text = trimmed_text.trim(" \t\v\f\r");
// Collapse runs of 3+ newlines down to 2 (one blank line max).
std::string result;
llvm::StringRef text(os.str());
text = text.trim();
llvm::copy_if(trimmed_text,
std::back_inserter(adjusted_result),
[&trimmed_text](const char& C) {
return !llvm::StringRef(trimmed_text.data(), &C - trimmed_text.data() + 1)
// We allow at most two newlines.
.ends_with("\n\n\n");
});
llvm::copy_if(text, std::back_inserter(result), [&text](const char& C) {
return !llvm::StringRef(text.data(), &C - text.data() + 1).ends_with("\n\n\n");
});
return adjusted_result;
return result;
}
void StructedText::append(StructedText& other) {
void Markup::append(Markup& other) {
std::move(other.blocks.begin(), other.blocks.end(), std::back_inserter(blocks));
}
Paragraph& StructedText::add_paragraph() {
Paragraph& Markup::add_paragraph() {
blocks.emplace_back(std::make_unique<Paragraph>());
return *static_cast<Paragraph*>(blocks.back().get());
}
void StructedText::add_ruler() {
void Markup::add_ruler() {
blocks.push_back(std::make_unique<Ruler>());
}
void StructedText::add_code_block(std::string code, std::string lang) {
void Markup::add_code_block(std::string code, std::string lang) {
blocks.emplace_back(std::make_unique<CodeBlock>(std::move(code), std::move(lang)));
}
Paragraph& StructedText::add_heading(unsigned level) {
Paragraph& Markup::add_heading(unsigned level) {
blocks.emplace_back(std::make_unique<Heading>(level));
return *static_cast<Paragraph*>(blocks.back().get());
}
BulletList& StructedText::add_bullet_list() {
BulletList& Markup::add_bullet_list() {
blocks.push_back(std::make_unique<BulletList>());
return *static_cast<BulletList*>(blocks.back().get());
}
std::string StructedText::as_markdown() const {
std::string Markup::as_markdown() const {
return render_blocks(blocks);
}

View File

@@ -5,11 +5,11 @@
#include <string>
#include <vector>
#include "llvm/Support/raw_os_ostream.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
/// Base class of structed text
/// Base class of markup blocks
class Block {
public:
virtual void render_markdown(llvm::raw_ostream& os) const = 0;
@@ -31,7 +31,7 @@ public:
Italic,
PlainText,
InlineCode,
Strikethough,
Strikethrough,
};
void render_markdown(llvm::raw_ostream& os) const override;
@@ -54,7 +54,7 @@ private:
std::vector<Chunk> chunks;
};
class StructedText;
class Markup;
/// Allow nested structure
class BulletList : public Block {
@@ -65,23 +65,23 @@ public:
std::unique_ptr<Block> clone() const override;
StructedText& add_item();
Markup& add_item();
private:
std::vector<StructedText> items;
std::vector<Markup> items;
};
class StructedText {
class Markup {
public:
StructedText() = default;
Markup() = default;
StructedText(const StructedText& other) {
Markup(const Markup& other) {
*this = other;
}
StructedText(StructedText&&) = default;
Markup(Markup&&) = default;
StructedText& operator=(const StructedText& other) {
Markup& operator=(const Markup& other) {
blocks.clear();
for(auto& b: other.blocks) {
blocks.push_back(b->clone());
@@ -89,9 +89,9 @@ public:
return *this;
}
StructedText& operator=(StructedText&&) = default;
Markup& operator=(Markup&&) = default;
void append(StructedText& doc);
void append(Markup& doc);
Paragraph& add_paragraph();

View File

@@ -35,6 +35,18 @@ TEST_CASE(IncludeSort) {
ASSERT_NE(edits.size(), 0U);
}
TEST_CASE(FormatCode) {
auto result = feature::format_code("main.cpp", "int add( int a,int b ){return a+b;}");
EXPECT_NE(result.find("int add("), std::string::npos);
EXPECT_EQ(result.find(" int a,int"), std::string::npos);
}
TEST_CASE(FormatCodeIdempotent) {
auto first = feature::format_code("main.cpp", "int add( int a,int b ){return a+b;}");
auto second = feature::format_code("main.cpp", first);
EXPECT_EQ(first, second);
}
}; // TEST_SUITE(Formatting)
} // namespace

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
#include "test/test.h"
#include "support/markup.h"
namespace clice::testing {
namespace {
TEST_SUITE(Markup) {
TEST_CASE(EmptyDocument) {
Markup st;
EXPECT_EQ(st.as_markdown(), "");
}
TEST_CASE(SingleParagraph) {
Markup st;
st.add_paragraph().append_text("hello world");
EXPECT_EQ(st.as_markdown(), "hello world");
}
TEST_CASE(PlainTextSpacing) {
Markup st;
auto& p = st.add_paragraph();
p.append_text("hello");
p.append_text("world");
EXPECT_EQ(st.as_markdown(), "hello world");
}
TEST_CASE(InlineCode) {
Markup st;
auto& p = st.add_paragraph();
p.append_text("Type:");
p.append_text("int", Paragraph::Kind::InlineCode);
EXPECT_EQ(st.as_markdown(), "Type: `int`");
}
TEST_CASE(Bold) {
Markup st;
st.add_paragraph().append_text("important", Paragraph::Kind::Bold);
EXPECT_EQ(st.as_markdown(), "**important**");
}
TEST_CASE(Italic) {
Markup st;
st.add_paragraph().append_text("emphasis", Paragraph::Kind::Italic);
EXPECT_EQ(st.as_markdown(), "*emphasis*");
}
TEST_CASE(Strikethrough) {
Markup st;
st.add_paragraph().append_text("removed", Paragraph::Kind::Strikethrough);
EXPECT_EQ(st.as_markdown(), "~~removed~~");
}
TEST_CASE(MixedInline) {
Markup st;
auto& p = st.add_paragraph();
p.append_text("Returns:", Paragraph::Kind::Bold);
p.append_text("the result");
EXPECT_EQ(st.as_markdown(), "**Returns:** the result");
}
TEST_CASE(ConsecutiveInlineCode) {
Markup st;
auto& p = st.add_paragraph();
p.append_text("int", Paragraph::Kind::InlineCode);
p.append_text("x", Paragraph::Kind::InlineCode);
EXPECT_EQ(st.as_markdown(), "`int` `x`");
}
TEST_CASE(Heading) {
Markup st;
st.add_heading(3).append_text("Title");
EXPECT_EQ(st.as_markdown(), "### Title");
}
TEST_CASE(HeadingWithInlineCode) {
Markup st;
auto& h = st.add_heading(2);
h.append_text("function");
h.append_text("foo", Paragraph::Kind::InlineCode);
EXPECT_EQ(st.as_markdown(), "## function `foo`");
}
TEST_CASE(Ruler) {
Markup st;
st.add_paragraph().append_text("above");
st.add_ruler();
st.add_paragraph().append_text("below");
auto md = st.as_markdown();
EXPECT_NE(md.find("above"), std::string::npos);
EXPECT_NE(md.find("---"), std::string::npos);
EXPECT_NE(md.find("below"), std::string::npos);
}
TEST_CASE(ConsecutiveRulers) {
Markup st;
st.add_paragraph().append_text("text");
st.add_ruler();
st.add_ruler();
st.add_paragraph().append_text("more");
auto md = st.as_markdown();
auto first = md.find("---");
auto second = md.find("---", first + 3);
EXPECT_EQ(second, std::string::npos);
}
TEST_CASE(LeadingTrailingRulers) {
Markup st;
st.add_ruler();
st.add_paragraph().append_text("content");
st.add_ruler();
EXPECT_EQ(st.as_markdown(), "content");
}
TEST_CASE(CodeBlock) {
Markup st;
st.add_code_block("int x = 0;", "cpp");
EXPECT_EQ(st.as_markdown(), "```cpp\nint x = 0;\n```");
}
TEST_CASE(CodeBlockTrailingNewline) {
Markup st;
st.add_code_block("int x = 0;\n", "cpp");
EXPECT_EQ(st.as_markdown(), "```cpp\nint x = 0;\n```");
}
TEST_CASE(CodeBlockNoLang) {
Markup st;
st.add_code_block("hello");
EXPECT_EQ(st.as_markdown(), "```\nhello\n```");
}
TEST_CASE(BulletListSimple) {
Markup st;
auto& list = st.add_bullet_list();
list.add_item().add_paragraph().append_text("one");
list.add_item().add_paragraph().append_text("two");
list.add_item().add_paragraph().append_text("three");
EXPECT_EQ(st.as_markdown(), "- one\n- two\n- three");
}
TEST_CASE(BulletListFormatted) {
Markup st;
auto& list = st.add_bullet_list();
list.add_item().add_paragraph().append_text("bold", Paragraph::Kind::Bold);
list.add_item().add_paragraph().append_text("code", Paragraph::Kind::InlineCode);
EXPECT_EQ(st.as_markdown(), "- **bold**\n- `code`");
}
TEST_CASE(BulletListMultiline) {
Markup st;
auto& list = st.add_bullet_list();
list.add_item().add_paragraph().append_text("line1\nline2");
auto md = st.as_markdown();
EXPECT_NE(md.find("- line1\n line2"), std::string::npos);
}
TEST_CASE(BlockSeparation) {
Markup st;
st.add_paragraph().append_text("first");
st.add_paragraph().append_text("second");
auto md = st.as_markdown();
EXPECT_NE(md.find("first\n"), std::string::npos);
EXPECT_NE(md.find("second"), std::string::npos);
EXPECT_EQ(md.find("firstsecond"), std::string::npos);
}
TEST_CASE(ParagraphThenList) {
Markup st;
st.add_paragraph().append_text("Parameters:");
auto& list = st.add_bullet_list();
list.add_item().add_paragraph().append_text("int x", Paragraph::Kind::InlineCode);
auto md = st.as_markdown();
EXPECT_EQ(md.find("Parameters:- "), std::string::npos);
EXPECT_EQ(md.find("Parameters:-"), std::string::npos);
EXPECT_NE(md.find("Parameters:"), std::string::npos);
EXPECT_NE(md.find("- `int x`"), std::string::npos);
}
TEST_CASE(HeadingThenRuler) {
Markup st;
st.add_heading(3).append_text("title");
st.add_ruler();
st.add_paragraph().append_text("body");
auto md = st.as_markdown();
EXPECT_NE(md.find("### title\n"), std::string::npos);
EXPECT_NE(md.find("---"), std::string::npos);
EXPECT_NE(md.find("body"), std::string::npos);
}
TEST_CASE(TripleNewlineCollapse) {
Markup st;
st.add_paragraph().append_text("a\n\n\n\nb");
auto md = st.as_markdown();
EXPECT_EQ(md.find("\n\n\n"), std::string::npos);
}
TEST_CASE(ClonePreservesHeading) {
Markup st;
st.add_heading(2).append_text("Title");
Markup copy = st;
auto md = copy.as_markdown();
EXPECT_NE(md.find("## Title"), std::string::npos);
}
TEST_CASE(NewlineChar) {
Markup st;
auto& p = st.add_paragraph();
p.append_text("line1");
p.append_newline_char();
p.append_text("line2");
auto md = st.as_markdown();
EXPECT_NE(md.find("line1"), std::string::npos);
EXPECT_NE(md.find("line2"), std::string::npos);
}
TEST_CASE(FullHoverLike) {
Markup st;
st.add_heading(3).append_text("function").append_text("add", Paragraph::Kind::InlineCode);
st.add_ruler();
st.add_paragraph().append_text("\xe2\x86\x92").append_text("int", Paragraph::Kind::InlineCode);
st.add_paragraph().append_text("Parameters:");
auto& params = st.add_bullet_list();
params.add_item().add_paragraph().append_text("int a", Paragraph::Kind::InlineCode);
params.add_item().add_paragraph().append_text("int b", Paragraph::Kind::InlineCode);
st.add_ruler();
st.add_code_block("int add(int a, int b);\n", "cpp");
auto md = st.as_markdown();
EXPECT_NE(md.find("### function `add`"), std::string::npos);
EXPECT_NE(md.find("---"), std::string::npos);
EXPECT_NE(md.find("\xe2\x86\x92 `int`"), std::string::npos);
EXPECT_NE(md.find("Parameters:"), std::string::npos);
EXPECT_NE(md.find("- `int a`"), std::string::npos);
EXPECT_NE(md.find("- `int b`"), std::string::npos);
EXPECT_NE(md.find("```cpp"), std::string::npos);
EXPECT_EQ(md.find("`int`Parameters"), std::string::npos);
EXPECT_EQ(md.find("Parameters:- "), std::string::npos);
}
}; // TEST_SUITE(Markup)
} // namespace
} // namespace clice::testing

View File

@@ -1,121 +0,0 @@
#include "test/test.h"
#include "support/format.h"
#include "support/structed_text.h"
namespace clice::testing {
namespace {
TEST_SUITE(StructedText) {
TEST_CASE(Paragraph) {
constexpr const char* cb =
R"c(// Without processing, recommended
char *longestPalindrome_solv2(const char *s) {
int len = strlen(s);
int max_start = 0;
int max_len = 0;
for (int i = 0; i < len; ++i) {
// j = 0, max_len is odd
// j = 1, max_len is even
for (int j = 0; j <= 1; ++j) {
int l = i;
int r = i + j;
// expand the range from center
while (l >= 0 && r < len && s[l] == s[r]) {
--l;
++r;
}
++l;
--r;
if (max_len < r - l + 1) {
max_len = r - l + 1;
max_start = i;
}
}
}
char *res = (char *)malloc((max_len + 1) * sizeof(char));
memcpy(res, s + max_start, max_len);
res[max_len] = '\0';
return res;
}
)c";
StructedText st;
st.add_paragraph().append_text("CodeBlock Example:").append_newline_char();
st.add_code_block(cb, "c");
auto& para = st.add_paragraph();
para.append_text("para1").append_newline_char();
/// std::println("{}", st.as_markdown());
}
TEST_CASE(BulletList) {
StructedText st;
st.add_bullet_list().add_item().add_paragraph().append_text("Item1");
st.add_bullet_list().add_item().add_paragraph().append_text("Item2",
Paragraph::Kind::InlineCode);
st.add_bullet_list().add_item().add_paragraph().append_text("Item3", Paragraph::Kind::Bold);
st.add_bullet_list().add_item().add_paragraph().append_text("Item4", Paragraph::Kind::Italic);
st.add_bullet_list().add_item().add_paragraph().append_text("Item5",
Paragraph::Kind::Strikethough);
/// std::println("{}", st.as_markdown());
}
TEST_CASE(FullText) {
StructedText st;
st.add_heading(3)
.append_text("function")
.append_text("test_bar", Paragraph::Kind::InlineCode)
.append_newline_char()
.append_text("Provided by:")
.append_text("`foo/bar/baz.h`");
st.add_ruler();
st.add_paragraph()
.append_text("")
.append_text("int", Paragraph::Kind::InlineCode)
.append_newline_char();
st.add_paragraph().append_text("Paramaters:", Paragraph::Kind::Bold).append_newline_char();
auto& params = st.add_bullet_list();
params.add_item()
.add_paragraph()
.append_text("int foo", Paragraph::Kind::InlineCode)
.append_text("doc for foo\ndoc for foo line2");
params.add_item()
.add_paragraph()
.append_text("char** bar", Paragraph::Kind::InlineCode)
.append_text("doc for bar");
params.add_item()
.add_paragraph()
.append_text("char** baz", Paragraph::Kind::InlineCode)
.append_text("doc for baz");
st.add_paragraph().append_text(R"md(
brief block
brief line2
a b c d e f
~~~~^
This is *Italic* **Bold** ~~Striketough~~, `InlineCode`
)md");
st.add_ruler();
st.add_paragraph().append_text("Details:", Paragraph::Kind::Bold).append_newline_char();
auto& details = st.add_bullet_list();
details.add_item().add_paragraph().append_text("Detail1: blah blah...");
details.add_item().add_paragraph().append_text("Detail2: blah blah...\n Line2: ......");
details.add_item().add_paragraph().append_text("Detail3: blah blah...");
st.add_ruler();
st.add_paragraph().append_text("Details:", Paragraph::Kind::Bold).append_newline_char();
auto& warnings = st.add_bullet_list();
warnings.add_item().add_paragraph().append_text("warnings1: blah blah...");
warnings.add_item().add_paragraph().append_text("warnings2: blah blah...\n Line2: ......");
warnings.add_item().add_paragraph().append_text("warnings3: blah blah...");
st.add_ruler();
st.add_code_block("int test_bar(int foo, char **bar, char **baz);\n", "cpp");
/// std::println("{}", st.as_markdown());
}
}; // TEST_SUITE(StructedText)
} // namespace
} // namespace clice::testing