#include #include #include "test/annotation.h" #include "test/test.h" #include "feature/feature.h" namespace clice::testing { namespace { namespace protocol = kota::ipc::protocol; TEST_SUITE(CodeCompletion) { std::vector items; llvm::IntrusiveRefCntPtr vfs; std::string main_path; void code_complete(llvm::StringRef code, feature::CodeCompletionOptions options = {}) { vfs = llvm::makeIntrusiveRefCnt(); 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 struct Foo { Foo() {} Foo(T x) {} Foo(T x, T y) {} }; template Foo(T) -> Foo; 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(ConstructorLabelNoTemplateArgs) { // Constructors of class templates should use plain class name as label, // not "Foo" or "Foo<_Tp, _Alloc>". This ensures dedup works and // insertion text is correct. feature::CodeCompletionOptions opts; opts.bundle_overloads = false; code_complete(R"cpp( template struct Bazzz { Bazzz() {} Bazzz(T x) {} Bazzz(T x, U y) {} }; template Bazzz(T) -> Bazzz; void bar() { Ba$(pos) } )cpp", opts); // Non-bundled mode should produce multiple "Bazzz" items (class + constructors + guide). auto count = std::ranges::count_if(items, [](const protocol::CompletionItem& item) { return item.label == "Bazzz"; }); ASSERT_TRUE(count > 1); // Every item's label must be plain "Bazzz", never "Bazzz". // And the insertion text must also be "Bazzz" (not the templated form). for(auto& item: items) { if(item.label.find("Bazzz") != std::string::npos) { ASSERT_EQ(item.label, "Bazzz"); auto& edit = std::get(*item.text_edit); ASSERT_TRUE(edit.new_text.starts_with("Bazzz")); ASSERT_TRUE(edit.new_text.find("<") == std::string::npos); } } } TEST_CASE(DeprecatedTag) { code_complete(R"cpp( [[deprecated]] int foooo(int x); int z = fo$(pos) )cpp"); auto it = find_item("foooo"); ASSERT_TRUE(it != items.end()); ASSERT_TRUE(it->tags.has_value()); auto& tags = *it->tags; ASSERT_TRUE(std::ranges::find(tags, protocol::CompletionItemTag::Deprecated) != tags.end()); } TEST_CASE(NotDeprecated) { code_complete(R"cpp( int foooo(int x); int z = fo$(pos) )cpp"); auto it = find_item("foooo"); ASSERT_TRUE(it != items.end()); // Non-deprecated should have no Deprecated tag. ASSERT_TRUE(!it->tags.has_value() || std::ranges::find(*it->tags, protocol::CompletionItemTag::Deprecated) == it->tags->end()); } 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(*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(*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