## Summary
- Generate LSP snippet placeholders (`${1:param}`, `${2:param}`) for
function and method completions in non-bundle mode
- Controlled by
`CodeCompletionOptions::enable_function_arguments_snippet` (default off)
- No-arg functions produce plain text insertion (no empty snippet)
- Bundle mode is unaffected — snippets only apply when each overload is
a separate item
- Optional chunks (default arguments) are skipped in snippet generation
## Example
```
// Before: typing "fo" and selecting foooo inserts just "foooo"
// After: typing "fo" and selecting foooo inserts "foooo(${1:int x}, ${2:float y})"
```
## Test plan
- [x] `SnippetFunctionArgs` — verifies placeholders are generated
- [x] `SnippetNoArgs` — no-arg functions don't produce snippet
- [x] `SnippetDisabled` — respects the option flag
- [x] `SnippetBundleMode` — bundle mode doesn't generate snippets
- [x] `SnippetMethod` — works for member methods too
- [x] All 494 unit tests pass
- [x] `pixi run format` clean
Stacked on #411.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Release Notes
* **New Features**
* Code completion now generates function argument snippets with
interactive placeholders, helping users efficiently navigate through
parameters during autocompletion. The feature works with functions and
methods, with configurable options to control behavior for overloaded
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
377 lines
9.7 KiB
C++
377 lines
9.7 KiB
C++
#include <algorithm>
|
|
#include <vector>
|
|
|
|
#include "test/annotation.h"
|
|
#include "test/test.h"
|
|
#include "feature/feature.h"
|
|
|
|
namespace clice::testing {
|
|
|
|
namespace {
|
|
|
|
namespace protocol = eventide::ipc::protocol;
|
|
|
|
TEST_SUITE(CodeCompletion) {
|
|
|
|
std::vector<protocol::CompletionItem> items;
|
|
llvm::IntrusiveRefCntPtr<TestVFS> vfs;
|
|
std::string main_path;
|
|
|
|
void code_complete(llvm::StringRef code, feature::CodeCompletionOptions options = {}) {
|
|
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
|
|
|
|
CompilationParams params;
|
|
auto annotation = AnnotatedSource::from(code);
|
|
|
|
vfs->add("main.cpp", annotation.content);
|
|
params.vfs = vfs;
|
|
main_path = TestVFS::path("main.cpp");
|
|
params.arguments =
|
|
{"clang++", "-std=c++20", "-ffreestanding", "-Xclang", "-undef", main_path.c_str()};
|
|
params.completion = {main_path, annotation.offsets.lookup("pos")};
|
|
params.add_remapped_file(main_path, annotation.content);
|
|
|
|
items = feature::code_complete(params, options, feature::PositionEncoding::UTF8);
|
|
}
|
|
|
|
auto find_item(llvm::StringRef label) {
|
|
return std::ranges::find_if(items, [&](const protocol::CompletionItem& item) {
|
|
return item.label == label;
|
|
});
|
|
}
|
|
|
|
TEST_CASE(Score) {
|
|
code_complete(R"cpp(
|
|
int foooo(int x);
|
|
int x = fo$(pos)
|
|
)cpp");
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(it->kind.has_value());
|
|
ASSERT_EQ(*it->kind, protocol::CompletionItemKind::Function);
|
|
}
|
|
|
|
TEST_CASE(Signature) {
|
|
code_complete(R"cpp(
|
|
int foooo(int x, float y);
|
|
int x = fo$(pos)
|
|
)cpp");
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(it->label_details.has_value());
|
|
// label_details.detail should contain the parameter list.
|
|
ASSERT_TRUE(it->label_details->detail.has_value());
|
|
auto& sig = *it->label_details->detail;
|
|
ASSERT_TRUE(sig.find("int") != std::string::npos);
|
|
ASSERT_TRUE(sig.find("float") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE(ReturnType) {
|
|
code_complete(R"cpp(
|
|
double foooo(int x);
|
|
int x = fo$(pos)
|
|
)cpp");
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(it->label_details.has_value());
|
|
// label_details.description should contain the return type.
|
|
ASSERT_TRUE(it->label_details->description.has_value());
|
|
auto& ret = *it->label_details->description;
|
|
ASSERT_TRUE(ret.find("double") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE(Snippet) {
|
|
code_complete(R"cpp(
|
|
int x = tru$(pos)
|
|
)cpp");
|
|
|
|
ASSERT_TRUE(!items.empty());
|
|
}
|
|
|
|
TEST_CASE(Overload) {
|
|
code_complete(R"cpp(
|
|
int foooo(int x);
|
|
int foooo(int x, int y);
|
|
int x = fooo$(pos)
|
|
)cpp");
|
|
|
|
ASSERT_TRUE(!items.empty());
|
|
// With bundling, there should be exactly one "foooo" item.
|
|
auto count = std::ranges::count_if(items, [](const protocol::CompletionItem& item) {
|
|
return item.label == "foooo";
|
|
});
|
|
ASSERT_EQ(count, 1);
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
// Bundled overload should show count in label_details.detail.
|
|
ASSERT_TRUE(it->label_details.has_value());
|
|
ASSERT_TRUE(it->label_details->detail.has_value());
|
|
auto& detail = *it->label_details->detail;
|
|
ASSERT_TRUE(detail.find("overload") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE(FilterUnderscore) {
|
|
code_complete(R"cpp(
|
|
int _private_thing;
|
|
int public_thing;
|
|
int x = pu$(pos)
|
|
)cpp");
|
|
|
|
// _private_thing should be filtered when prefix doesn't start with _.
|
|
auto it = find_item("_private_thing");
|
|
ASSERT_TRUE(it == items.end());
|
|
|
|
auto it2 = find_item("public_thing");
|
|
ASSERT_TRUE(it2 != items.end());
|
|
}
|
|
|
|
TEST_CASE(FilterUnderscoreExplicit) {
|
|
code_complete(R"cpp(
|
|
int _private_thing;
|
|
int x = _p$(pos)
|
|
)cpp");
|
|
|
|
// When user types _, underscore-prefixed symbols should appear.
|
|
auto it = find_item("_private_thing");
|
|
ASSERT_TRUE(it != items.end());
|
|
}
|
|
|
|
TEST_CASE(MethodSignature) {
|
|
code_complete(R"cpp(
|
|
struct Foo {
|
|
int bazzzz(int a, int b);
|
|
};
|
|
|
|
void bar() {
|
|
Foo f;
|
|
f.ba$(pos);
|
|
}
|
|
)cpp");
|
|
|
|
auto it = find_item("bazzzz");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(it->kind.has_value());
|
|
ASSERT_EQ(*it->kind, protocol::CompletionItemKind::Method);
|
|
ASSERT_TRUE(it->label_details.has_value());
|
|
ASSERT_TRUE(it->label_details->detail.has_value());
|
|
auto& sig = *it->label_details->detail;
|
|
ASSERT_TRUE(sig.find("int") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE(DeduplicateByLabel) {
|
|
code_complete(R"cpp(
|
|
template <typename T>
|
|
struct Foo {
|
|
Foo() {}
|
|
Foo(T x) {}
|
|
Foo(T x, T y) {}
|
|
};
|
|
|
|
template <typename T>
|
|
Foo(T) -> Foo<T>;
|
|
|
|
void bar() {
|
|
Fo$(pos)
|
|
}
|
|
)cpp");
|
|
|
|
// In bundle mode, "Foo" should appear exactly once (as Class kind),
|
|
// not 3 times (Class + Constructor bundle + deduction guide bundle).
|
|
auto count = std::ranges::count_if(items, [](const protocol::CompletionItem& item) {
|
|
return item.label == "Foo";
|
|
});
|
|
ASSERT_EQ(count, 1);
|
|
|
|
auto it = find_item("Foo");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(it->kind.has_value());
|
|
ASSERT_EQ(*it->kind, protocol::CompletionItemKind::Class);
|
|
}
|
|
|
|
TEST_CASE(NoBundleOverloads) {
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = false;
|
|
code_complete(R"cpp(
|
|
int foooo(int x);
|
|
int foooo(int x, int y);
|
|
double foooo(double d);
|
|
int x = fooo$(pos)
|
|
)cpp",
|
|
opts);
|
|
|
|
// Without bundling, each overload should be a separate item.
|
|
auto count = std::ranges::count_if(items, [](const protocol::CompletionItem& item) {
|
|
return item.label == "foooo";
|
|
});
|
|
ASSERT_TRUE(count >= 3);
|
|
|
|
// Each should have its own signature in label_details.
|
|
for(auto& item: items) {
|
|
if(item.label == "foooo") {
|
|
ASSERT_TRUE(item.label_details.has_value());
|
|
ASSERT_TRUE(item.label_details->detail.has_value());
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE(NoBundleNoDeduplicate) {
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = false;
|
|
code_complete(R"cpp(
|
|
int foooo(int x);
|
|
int foooo(int x, int y);
|
|
double foooo(double d);
|
|
int x = fooo$(pos)
|
|
)cpp",
|
|
opts);
|
|
|
|
// Without bundling, deduplication should NOT apply — each overload
|
|
// should appear as a separate item.
|
|
auto count = std::ranges::count_if(items, [](const protocol::CompletionItem& item) {
|
|
return item.label == "foooo";
|
|
});
|
|
ASSERT_TRUE(count >= 3);
|
|
}
|
|
|
|
TEST_CASE(SnippetFunctionArgs) {
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = false;
|
|
opts.enable_function_arguments_snippet = true;
|
|
code_complete(R"cpp(
|
|
int foooo(int x, float y);
|
|
int z = fo$(pos)
|
|
)cpp",
|
|
opts);
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
// Should have snippet format.
|
|
ASSERT_TRUE(it->insert_text_format.has_value());
|
|
ASSERT_EQ(*it->insert_text_format, protocol::InsertTextFormat::Snippet);
|
|
// textEdit should contain placeholders.
|
|
auto& edit = std::get<protocol::TextEdit>(*it->text_edit);
|
|
ASSERT_TRUE(edit.new_text.find("${1:") != std::string::npos);
|
|
ASSERT_TRUE(edit.new_text.find("${2:") != std::string::npos);
|
|
ASSERT_TRUE(edit.new_text.find("(") != std::string::npos);
|
|
ASSERT_TRUE(edit.new_text.find(")") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE(SnippetNoArgs) {
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = false;
|
|
opts.enable_function_arguments_snippet = true;
|
|
code_complete(R"cpp(
|
|
void foooo();
|
|
void bar() { fo$(pos) }
|
|
)cpp",
|
|
opts);
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
// No-arg function should not generate snippet (no placeholders).
|
|
ASSERT_TRUE(!it->insert_text_format.has_value() ||
|
|
*it->insert_text_format == protocol::InsertTextFormat::PlainText);
|
|
}
|
|
|
|
TEST_CASE(SnippetDisabled) {
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = false;
|
|
opts.enable_function_arguments_snippet = false;
|
|
code_complete(R"cpp(
|
|
int foooo(int x, float y);
|
|
int z = fo$(pos)
|
|
)cpp",
|
|
opts);
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
// With snippet disabled, should be plain text.
|
|
ASSERT_TRUE(!it->insert_text_format.has_value() ||
|
|
*it->insert_text_format == protocol::InsertTextFormat::PlainText);
|
|
}
|
|
|
|
TEST_CASE(SnippetBundleMode) {
|
|
// In bundle mode, snippets should NOT be generated even if enabled.
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = true;
|
|
opts.enable_function_arguments_snippet = true;
|
|
code_complete(R"cpp(
|
|
int foooo(int x);
|
|
int foooo(int x, int y);
|
|
int z = fo$(pos)
|
|
)cpp",
|
|
opts);
|
|
|
|
auto it = find_item("foooo");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(!it->insert_text_format.has_value() ||
|
|
*it->insert_text_format == protocol::InsertTextFormat::PlainText);
|
|
}
|
|
|
|
TEST_CASE(SnippetMethod) {
|
|
feature::CodeCompletionOptions opts;
|
|
opts.bundle_overloads = false;
|
|
opts.enable_function_arguments_snippet = true;
|
|
code_complete(R"cpp(
|
|
struct Foo {
|
|
int bazzzz(int a, int b);
|
|
};
|
|
void bar() {
|
|
Foo f;
|
|
f.ba$(pos);
|
|
}
|
|
)cpp",
|
|
opts);
|
|
|
|
auto it = find_item("bazzzz");
|
|
ASSERT_TRUE(it != items.end());
|
|
ASSERT_TRUE(it->insert_text_format.has_value());
|
|
ASSERT_EQ(*it->insert_text_format, protocol::InsertTextFormat::Snippet);
|
|
auto& edit = std::get<protocol::TextEdit>(*it->text_edit);
|
|
ASSERT_TRUE(edit.new_text.find("${1:") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE(Unqualified) {
|
|
code_complete(R"cpp(
|
|
namespace A {
|
|
void fooooo();
|
|
}
|
|
|
|
void bar() {
|
|
fo$(pos)
|
|
}
|
|
)cpp");
|
|
}
|
|
|
|
TEST_CASE(Functor) {
|
|
code_complete(R"cpp(
|
|
struct X {
|
|
void operator() () {};
|
|
};
|
|
|
|
void bar() {
|
|
X foo;
|
|
fo$(pos);
|
|
}
|
|
)cpp");
|
|
}
|
|
|
|
TEST_CASE(Lambda) {
|
|
code_complete(R"cpp(
|
|
void bar() {
|
|
auto foo = [](int x){ };
|
|
fo$(pos);
|
|
}
|
|
)cpp");
|
|
}
|
|
|
|
}; // TEST_SUITE(CodeCompletion)
|
|
|
|
} // namespace
|
|
|
|
} // namespace clice::testing
|