From 3dab2ead9329e3df1691fb43d1e11b8f31bc0259 Mon Sep 17 00:00:00 2001 From: ykiko Date: Thu, 9 Apr 2026 16:39:46 +0800 Subject: [PATCH] feat(completion): snippet insertion for function/method parameters (#412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) ## 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. Co-authored-by: Claude Opus 4.6 --- src/feature/code_completion.cpp | 104 ++++++++++++++++--- tests/unit/feature/code_completion_tests.cpp | 98 +++++++++++++++++ 2 files changed, 185 insertions(+), 17 deletions(-) diff --git a/src/feature/code_completion.cpp b/src/feature/code_completion.cpp index 33eb2ed8..8cd9d268 100644 --- a/src/feature/code_completion.cpp +++ b/src/feature/code_completion.cpp @@ -158,6 +158,56 @@ auto extract_signature(const clang::CodeCompletionString& ccs) -> std::string { return signature; } +/// Build a snippet string from a CodeCompletionString. +/// Produces e.g. "funcName(${1:int x}, ${2:float y})" for functions, +/// or "ClassName<${1:T}>" for class templates. +auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string { + std::string snippet; + unsigned placeholder_index = 0; + + for(const auto& chunk: ccs) { + using CK = clang::CodeCompletionString::ChunkKind; + switch(chunk.Kind) { + case CK::CK_TypedText: + if(chunk.Text) { + snippet += chunk.Text; + } + break; + case CK::CK_Placeholder: + if(chunk.Text) { + snippet += std::format("${{{0}:{1}}}", ++placeholder_index, chunk.Text); + } + break; + case CK::CK_LeftParen: snippet += '('; break; + case CK::CK_RightParen: snippet += ')'; break; + case CK::CK_LeftAngle: snippet += '<'; break; + case CK::CK_RightAngle: snippet += '>'; break; + case CK::CK_Comma: snippet += ", "; break; + case CK::CK_Text: + if(chunk.Text) { + snippet += chunk.Text; + } + break; + case CK::CK_Optional: + // Optional chunks contain default arguments — skip for snippet. + break; + case CK::CK_Informative: + case CK::CK_ResultType: + case CK::CK_CurrentParameter: + // Display-only chunks, not part of insertion. + break; + default: break; + } + } + + // If no placeholders were generated, return empty to signal plain text. + if(placeholder_index == 0) { + return {}; + } + + return snippet; +} + /// Extract the return type from a CodeCompletionString. auto extract_return_type(const clang::CodeCompletionString& ccs) -> std::string { for(const auto& chunk: ccs) { @@ -220,27 +270,33 @@ public: bool prefix_starts_with_underscore = prefix.spelling.starts_with("_"); - auto build_item = - [&](llvm::StringRef label, protocol::CompletionItemKind kind, llvm::StringRef insert) { - protocol::CompletionItem item{ - .label = label.str(), - }; - item.kind = kind; - - protocol::TextEdit edit{ - .range = replace_range, - .new_text = insert.empty() ? label.str() : insert.str(), - }; - item.text_edit = std::move(edit); - return item; + auto build_item = [&](llvm::StringRef label, + protocol::CompletionItemKind kind, + llvm::StringRef insert, + bool is_snippet = false) { + protocol::CompletionItem item{ + .label = label.str(), }; + item.kind = kind; + + protocol::TextEdit edit{ + .range = replace_range, + .new_text = insert.empty() ? label.str() : insert.str(), + }; + item.text_edit = std::move(edit); + if(is_snippet) { + item.insert_text_format = protocol::InsertTextFormat::Snippet; + } + return item; + }; auto try_add = [&](llvm::StringRef label, protocol::CompletionItemKind kind, llvm::StringRef insert_text, llvm::StringRef overload_key, llvm::StringRef signature = {}, - llvm::StringRef return_type = {}) { + llvm::StringRef return_type = {}, + bool is_snippet = false) { if(label.empty()) { return; } @@ -259,7 +315,7 @@ public: auto [it, inserted] = overload_index.try_emplace(overload_key.str(), overloads.size()); if(inserted) { - auto item = build_item(label, kind, insert_text); + auto item = build_item(label, kind, insert_text, is_snippet); item.sort_text = std::format("{}", *score); if(!signature.empty() || !return_type.empty()) { protocol::CompletionItemLabelDetails details; @@ -287,7 +343,7 @@ public: return; } - auto item = build_item(label, kind, insert_text); + auto item = build_item(label, kind, insert_text, is_snippet); item.sort_text = std::format("{}", *score); if(!signature.empty() || !return_type.empty()) { protocol::CompletionItemLabelDetails details; @@ -344,6 +400,7 @@ public: std::string signature; std::string return_type; + std::string snippet; auto* ccs = candidate.CreateCodeCompletionString(sema, context, @@ -353,9 +410,22 @@ public: if(ccs) { signature = extract_signature(*ccs); return_type = extract_return_type(*ccs); + // Generate snippet for non-bundled callables. + if(is_callable && !options.bundle_overloads && + options.enable_function_arguments_snippet) { + snippet = build_snippet(*ccs); + } } - try_add(label, kind, label, qualified_name.str(), signature, return_type); + bool has_snippet = !snippet.empty(); + auto insert = has_snippet ? llvm::StringRef(snippet) : llvm::StringRef(label); + try_add(label, + kind, + insert, + qualified_name.str(), + signature, + return_type, + has_snippet); break; } } diff --git a/tests/unit/feature/code_completion_tests.cpp b/tests/unit/feature/code_completion_tests.cpp index 7e996abb..c3272f19 100644 --- a/tests/unit/feature/code_completion_tests.cpp +++ b/tests/unit/feature/code_completion_tests.cpp @@ -237,6 +237,104 @@ int x = fooo$(pos) 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 {