From 6870597044157de9a31cba1bc676d05e79667443 Mon Sep 17 00:00:00 2001 From: zaragoza-xu Date: Fri, 10 Jul 2026 14:58:40 +0800 Subject: [PATCH 1/2] feat(server): hover on includes --- src/feature/hover.cpp | 57 ++++++++++++++++++++++++++++++ tests/unit/feature/hover_tests.cpp | 38 ++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/feature/hover.cpp b/src/feature/hover.cpp index cbb5a1855..6be0d6477 100644 --- a/src/feature/hover.cpp +++ b/src/feature/hover.cpp @@ -10,6 +10,7 @@ #include "semantic/ast_utility.h" #include "semantic/find_target.h" #include "semantic/selection.h" +#include "syntax/lexer.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringExtras.h" @@ -1044,6 +1045,57 @@ auto attr_hover(const clang::Attr* attr, clang::ASTContext& context) -> std::opt return info; } +auto include_hover(CompilationUnitRef unit, std::uint32_t offset) -> std::optional { + auto interested = unit.interested_file(); + auto directives_it = unit.directives().find(interested); + if(directives_it == unit.directives().end()) { + return std::nullopt; + } + + auto content = unit.interested_content(); + auto* lang_opts = &unit.lang_options(); + + auto header_name = [&](LocalSourceRange range) -> std::string { + auto arg = content.substr(range.begin, range.end - range.begin).trim(); + if(arg.size() >= 2 && ((arg.front() == '"' && arg.back() == '"') || + (arg.front() == '<' && arg.back() == '>'))) { + arg = arg.drop_front().drop_back(); + } + return arg.str(); + }; + + auto try_directive = [&](clang::SourceLocation loc, + clang::FileID target) -> std::optional { + if(!target.isValid()) + return std::nullopt; + auto [fid, directive_offset] = unit.decompose_location(loc); + if(fid != interested || directive_offset >= content.size()) + return std::nullopt; + auto range = find_directive_argument(content, directive_offset, lang_opts); + if(!range || !range->contains(offset)) + return std::nullopt; + + HoverInfo info; + info.name = header_name(*range); + info.kind = SymbolKind::Header; + info.definition = unit.file_path(target); + info.symbol_range = *range; + return info; + }; + + for(const auto& include: directives_it->second.includes) { + if(auto info = try_directive(include.location, include.fid)) { + return info; + } + } + for(const auto& has_include: directives_it->second.has_includes) { + if(auto info = try_directive(has_include.location, has_include.fid)) { + return info; + } + } + return std::nullopt; +} + void add_layout_info(const clang::NamedDecl& decl, HoverInfo& info) { if(decl.isInvalidDecl()) { return; @@ -1504,6 +1556,11 @@ llvm::raw_ostream& operator<<(llvm::raw_ostream& os, const HoverInfo::Param& par auto hover_info(CompilationUnitRef unit, std::uint32_t offset, const HoverOptions& options) -> std::optional { + /// The hover is over an include directive. + if(auto info = include_hover(unit, offset)) { + return info; + } + auto& context = unit.context(); auto policy = hover_printing_policy(context.getPrintingPolicy()); diff --git a/tests/unit/feature/hover_tests.cpp b/tests/unit/feature/hover_tests.cpp index c1d0fb6b8..f25ba0c03 100644 --- a/tests/unit/feature/hover_tests.cpp +++ b/tests/unit/feature/hover_tests.cpp @@ -8,6 +8,7 @@ #include "test/test.h" #include "test/tester.h" #include "feature/feature.h" +#include "support/filesystem.h" #include "kota/meta/enum.h" @@ -1011,6 +1012,43 @@ int $foo = 1; ASSERT_EQ(result->range->end.character, 7U); } +TEST_CASE(include_header) { + add_file("test.h", "#pragma once\n"); + add_main("main.cpp", R"cpp( +#include @arg["test.h"] +$(outside) +int x = 0; +)cpp"); + ASSERT_TRUE(compile()); + + auto arg = range("arg", "main.cpp"); + info = feature::hover_info(*unit, arg.begin + 1); + ASSERT_TRUE(info.has_value()); + EXPECT_EQ(info->kind, SymbolKind::Header); + EXPECT_EQ(info->name, "test.h"); + + llvm::SmallString<128> path(info->definition); + path::remove_dots(path); + EXPECT_EQ(path, TestVFS::path("test.h")); + ASSERT_TRUE(info->symbol_range.has_value()); + EXPECT_EQ(info->symbol_range->begin, arg.begin); + EXPECT_EQ(info->symbol_range->end, arg.end); + + result = feature::hover(*unit, arg.begin + 1, {}, feature::PositionEncoding::UTF8); + ASSERT_TRUE(result.has_value()); + auto* content = std::get_if(&result->contents); + ASSERT_TRUE(content != nullptr); + EXPECT_TRUE(content->value.contains("test.h")); + EXPECT_TRUE(content->value.contains(TestVFS::path("test.h"))); + ASSERT_TRUE(result->range.has_value()); + EXPECT_EQ(result->range->start.line, 1U); + EXPECT_EQ(result->range->start.character, 9U); + EXPECT_EQ(result->range->end.line, 1U); + EXPECT_EQ(result->range->end.character, 17U); + + EXPECT_FALSE(feature::hover_info(*unit, point("outside")).has_value()); +} + TEST_CASE(scoped_attribute) { run_info(R"cpp( [[gnu::no$inline]] void foo(); From d3f5b539c23358b46d3fa5a8de4bb392ec7d63e8 Mon Sep 17 00:00:00 2001 From: zaragoza-xu Date: Fri, 10 Jul 2026 14:58:40 +0800 Subject: [PATCH 2/2] feat(hover): show members of tag decls --- docs/en/features/hover.md | 4 +- docs/zh/features/hover.md | 4 +- src/feature/hover.cpp | 60 ++++++++ tests/data/hover/tag_decls.cpp | 76 ++++++++++ .../hover/snapshot/tag_decls.cpp.snap.yml | 138 +++++++++++++++++- 5 files changed, 274 insertions(+), 8 deletions(-) diff --git a/docs/en/features/hover.md b/docs/en/features/hover.md index 55640b7ae..41d106641 100644 --- a/docs/en/features/hover.md +++ b/docs/en/features/hover.md @@ -237,7 +237,7 @@ ## Special Hover Targets -- [ ] Show struct/enum members on type-level hover ([clangd#959](https://github.com/clangd/clangd/issues/959)) +- [x] Show struct/enum members on type-level hover ([clangd#959](https://github.com/clangd/clangd/issues/959)) ```cpp enum Color { Red, Green, Blue }; @@ -266,7 +266,7 @@ // hover on "nodiscard" → description of the attribute ``` -- [ ] `#include` directive hover showing resolved header path +- [x] `#include` directive hover showing resolved header path - [x] `this` expression hover showing pointed-to type - [x] `__func__` and related predefined identifier hover diff --git a/docs/zh/features/hover.md b/docs/zh/features/hover.md index 1098759b7..074f049f0 100644 --- a/docs/zh/features/hover.md +++ b/docs/zh/features/hover.md @@ -237,7 +237,7 @@ ## 特殊悬停目标 -- [ ] 类型级悬停显示结构体/枚举成员([clangd#959](https://github.com/clangd/clangd/issues/959)) +- [x] 类型级悬停显示结构体/枚举成员([clangd#959](https://github.com/clangd/clangd/issues/959)) ```cpp enum Color { Red, Green, Blue }; @@ -266,7 +266,7 @@ // 悬停 "nodiscard" → 显示该属性的说明 ``` -- [ ] `#include` 指令悬停显示解析后的头文件路径 +- [x] `#include` 指令悬停显示解析后的头文件路径 - [x] `this` 表达式悬停显示指向的类型 - [x] `__func__` 及相关预定义标识符悬停 diff --git a/src/feature/hover.cpp b/src/feature/hover.cpp index 6be0d6477..29997ab27 100644 --- a/src/feature/hover.cpp +++ b/src/feature/hover.cpp @@ -118,6 +118,58 @@ auto namespace_scope(const clang::Decl* decl) -> std::string { return ""; } +void print_tag_name(std::string& definition, + const clang::TagDecl& decl, + clang::PrintingPolicy policy) { + /// We only need the full tag name, so truncate the body at the opening brace for later fill-in. + llvm::raw_string_ostream os(definition); + decl.print(os, policy); + auto body = definition.rfind('{'); + if(body == std::string::npos) { + return; + } + definition.resize(body); +} + +auto print_record_definition(const clang::RecordDecl& decl, clang::PrintingPolicy policy) + -> std::string { + std::string definition; + + print_tag_name(definition, decl, policy); + + llvm::raw_string_ostream os(definition); + os << " {"; + for(const clang::FieldDecl* field: decl.fields()) { + os << '\n'; + field->print(os, policy); + os << ';'; + } + os << "\n}"; + + return definition; +} + +auto print_enum_definition(const clang::EnumDecl& decl, clang::PrintingPolicy policy) + -> std::string { + std::string definition; + + print_tag_name(definition, decl, policy); + + llvm::raw_string_ostream os(definition); + os << " {"; + for(const clang::EnumConstantDecl* enumerator: decl.enumerators()) { + os << '\n'; + enumerator->print(os, policy); + if(!enumerator->getInitExpr() && !enumerator->getType()->isDependentType()) { + os << " = " << llvm::toString(enumerator->getInitVal(), 10); + } + os << ','; + } + os << "\n}"; + + return definition; +} + auto print_definition(const clang::Decl* decl, clang::PrintingPolicy policy, const clang::syntax::TokenBuffer& tb) -> std::string { @@ -130,6 +182,14 @@ auto print_definition(const clang::Decl* decl, policy.SuppressInitializers = true; } } + } else if(auto* record = llvm::dyn_cast(decl)) { + if(auto* definition = record->getDefinition()) { + return print_record_definition(*definition, policy); + } + } else if(auto* enum_decl = llvm::dyn_cast(decl)) { + if(auto* definition = enum_decl->getDefinition()) { + return print_enum_definition(*definition, policy); + } } std::string definition; diff --git a/tests/data/hover/tag_decls.cpp b/tests/data/hover/tag_decls.cpp index 84ab6ee44..c29131c7d 100644 --- a/tests/data/hover/tag_decls.cpp +++ b/tests/data/hover/tag_decls.cpp @@ -152,3 +152,79 @@ namespace nested_templates_class { template struct cls {}; $(17_nested_template_class)cls>> foo; } + +namespace enum_explicit { +enum Color { + RED = -123, + GREEN, + BLUE = 5, +}; +void foo() { + $(18_enum_explicit)Color color = GREEN; +} +} + +namespace struct_fields { +struct Point { + int x; + int y; +}; +void foo() { + $(19_struct_fields)Point point; +} +} + +namespace inherited_struct { +struct base { + int a; +}; + +struct Point : base { + int x; + int y; +}; +void foo() { + $(20_inherited_struct)Point point; +} +} + +namespace inherited_class { +struct first {}; +struct second {}; + +class Point : public first, virtual protected second { + int value; +}; +void foo() { + $(21_inherited_class)Point point; +} +} + +namespace template_fields { +template +struct Box { + T value; +}; +void foo() { + $(22_template_fields)Box box; +} +} + +namespace inline_method_body { +struct Widget { + int value; + int get() const { return value; } +}; +void foo() { + $(23_inline_method_body)Widget widget; +} +} + +namespace lambda_field_body { +struct Widget { + int value = [] { return 42; }(); +}; +void foo() { + $(24_lambda_field_body)Widget widget; +} +} \ No newline at end of file diff --git a/tests/snapshots/hover/snapshot/tag_decls.cpp.snap.yml b/tests/snapshots/hover/snapshot/tag_decls.cpp.snap.yml index f353af947..3daa9a8b7 100644 --- a/tests/snapshots/hover/snapshot/tag_decls.cpp.snap.yml +++ b/tests/snapshots/hover/snapshot/tag_decls.cpp.snap.yml @@ -37,7 +37,7 @@ input_file: tag_decls.cpp name: "MyUnion" kind: Union namespace_scope: "union_decl::ns1::" - definition: "union MyUnion {}" + definition: "union MyUnion {\n int x;\n int y;\n}" symbol_range: "[541, 548)" markdown: | ### union `MyUnion` @@ -45,7 +45,10 @@ input_file: tag_decls.cpp --- ```cpp // In namespace union_decl::ns1 - union MyUnion {} + union MyUnion { + int x; + int y; + } ``` 04_forward_class: @@ -72,7 +75,7 @@ input_file: tag_decls.cpp kind: Enum namespace_scope: "enum_def::" documentation: "Enum declaration" - definition: "enum Hello {}" + definition: "enum Hello {\n ONE = 0,\n TWO = 1,\n THREE = 2,\n}" symbol_range: "[747, 752)" markdown: | ### enum `Hello` @@ -83,7 +86,11 @@ input_file: tag_decls.cpp --- ```cpp // In namespace enum_def - enum Hello {} + enum Hello { + ONE = 0, + TWO = 1, + THREE = 2, + } ``` 06_enumerator: @@ -335,4 +342,127 @@ input_file: tag_decls.cpp struct cls>> {} ``` +18_enum_explicit: + name: "Color" + kind: Enum + namespace_scope: "enum_explicit::" + definition: "enum Color {\n RED = -123,\n GREEN = -122,\n BLUE = 5,\n}" + symbol_range: "[2394, 2399)" + markdown: | + ### enum `Color` + + --- + ```cpp + // In namespace enum_explicit + enum Color { + RED = -123, + GREEN = -122, + BLUE = 5, + } + ``` + +19_struct_fields: + name: "Point" + kind: Struct + namespace_scope: "struct_fields::" + definition: "struct Point {\n int x;\n int y;\n}" + symbol_range: "[2497, 2502)" + markdown: | + ### struct `Point` + + --- + ```cpp + // In namespace struct_fields + struct Point { + int x; + int y; + } + ``` + +20_inherited_struct: + name: "Point" + kind: Struct + namespace_scope: "inherited_struct::" + definition: "struct Point : base {\n int x;\n int y;\n}" + symbol_range: "[2629, 2634)" + markdown: | + ### struct `Point` + + --- + ```cpp + // In namespace inherited_struct + struct Point : base { + int x; + int y; + } + ``` + +21_inherited_class: + name: "Point" + kind: Class + namespace_scope: "inherited_class::" + definition: "class Point : public first, virtual protected second {\n int value;\n}" + symbol_range: "[2797, 2802)" + markdown: | + ### class `Point` + + --- + ```cpp + // In namespace inherited_class + class Point : public first, virtual protected second { + int value; + } + ``` + +22_template_fields: + name: "Box" + kind: Struct + namespace_scope: "template_fields::" + definition: "template <> struct Box {\n int value;\n}" + symbol_range: "[2907, 2910)" + markdown: | + ### struct `Box` + + --- + ```cpp + // In namespace template_fields + template <> struct Box { + int value; + } + ``` + +23_inline_method_body: + name: "Widget" + kind: Struct + namespace_scope: "inline_method_body::" + definition: "struct Widget {\n int value;\n}" + symbol_range: "[3040, 3046)" + markdown: | + ### struct `Widget` + + --- + ```cpp + // In namespace inline_method_body + struct Widget { + int value; + } + ``` + +24_lambda_field_body: + name: "Widget" + kind: Struct + namespace_scope: "lambda_field_body::" + definition: "struct Widget {\n int value = [] {}();\n}" + symbol_range: "[3159, 3165)" + markdown: | + ### struct `Widget` + + --- + ```cpp + // In namespace lambda_field_body + struct Widget { + int value = [] {}(); + } + ``` +