diff --git a/src/feature/code_completion.cpp b/src/feature/code_completion.cpp index 33eb2ed8..c5b41aca 100644 --- a/src/feature/code_completion.cpp +++ b/src/feature/code_completion.cpp @@ -158,6 +158,86 @@ auto extract_signature(const clang::CodeCompletionString& ccs) -> std::string { return signature; } +/// Find the first non-whitespace character after the given offset in content. +/// Returns '\0' if none found (end of content). +auto next_token_char(llvm::StringRef content, std::uint32_t offset) -> char { + for(auto i = offset; i < content.size(); ++i) { + char c = content[i]; + if(c != ' ' && c != '\t' && c != '\n' && c != '\r') { + return c; + } + } + return '\0'; +} + +/// 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. +/// If skip_parens is true, omits everything from '(' onward (when the next +/// token after the cursor is already '('). +auto build_snippet(const clang::CodeCompletionString& ccs, bool skip_parens = false) + -> std::string { + std::string snippet; + unsigned placeholder_index = 0; + bool in_parens = false; + + 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(in_parens && skip_parens) { + break; + } + if(chunk.Text) { + snippet += std::format("${{{0}:{1}}}", ++placeholder_index, chunk.Text); + } + break; + case CK::CK_LeftParen: + in_parens = true; + if(!skip_parens) { + snippet += '('; + } + break; + case CK::CK_RightParen: + in_parens = false; + if(!skip_parens) { + snippet += ')'; + } + break; + case CK::CK_LeftAngle: snippet += '<'; break; + case CK::CK_RightAngle: snippet += '>'; break; + case CK::CK_Comma: + if(!(in_parens && skip_parens)) { + snippet += ", "; + } + break; + case CK::CK_Text: + if(!(in_parens && skip_parens) && chunk.Text) { + snippet += chunk.Text; + } + break; + case CK::CK_Optional: break; + case CK::CK_Informative: + case CK::CK_ResultType: + case CK::CK_CurrentParameter: break; + default: break; + } + } + + // If no placeholders were generated and parens were skipped, + // 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) { @@ -179,9 +259,11 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { CodeCompletionCollector(std::uint32_t offset, PositionEncoding encoding, std::vector& output, - const CodeCompletionOptions& options) : + const CodeCompletionOptions& options, + llvm::StringRef original_content) : clang::CodeCompleteConsumer({}), offset(offset), encoding(encoding), output(output), - options(options), info(std::make_shared()) {} + options(options), original_content(original_content), + info(std::make_shared()) {} clang::CodeCompletionAllocator& getAllocator() final { return info.getAllocator(); @@ -220,27 +302,33 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { 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 +347,7 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { 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 +375,7 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { 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 +432,7 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { std::string signature; std::string return_type; + std::string snippet; auto* ccs = candidate.CreateCodeCompletionString(sema, context, @@ -353,9 +442,23 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { 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) { + bool next_is_paren = next_token_char(original_content, offset) == '('; + snippet = build_snippet(*ccs, /*skip_parens=*/next_is_paren); + } } - 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; } } @@ -413,6 +516,7 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { std::uint32_t offset; PositionEncoding encoding; std::vector& output; + llvm::StringRef original_content; const CodeCompletionOptions& options; clang::CodeCompletionTUInfo info; }; @@ -427,7 +531,15 @@ auto code_complete(CompilationParams& params, auto& [file, offset] = params.completion; (void)file; - auto* consumer = new CodeCompletionCollector(offset, encoding, items, options); + // Get the original file content for lookahead (smart parens detection). + llvm::StringRef original_content; + auto buf_it = params.buffers.find(file); + if(buf_it != params.buffers.end()) { + original_content = buf_it->second->getBuffer(); + } + + auto* consumer = + new CodeCompletionCollector(offset, encoding, items, options, original_content); auto unit = complete(params, consumer); (void)unit; diff --git a/tests/unit/feature/code_completion_tests.cpp b/tests/unit/feature/code_completion_tests.cpp index 7e996abb..341d19f7 100644 --- a/tests/unit/feature/code_completion_tests.cpp +++ b/tests/unit/feature/code_completion_tests.cpp @@ -237,6 +237,142 @@ 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(SmartParensSkip) { + // When next token after cursor is '(', snippet should not insert parens. + 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)(1, 2.0f); +)cpp", + opts); + + auto it = find_item("foooo"); + ASSERT_TRUE(it != items.end()); + // With parens already present, snippet should degrade to plain text + // (no placeholders → build_snippet returns empty → label used). + auto& edit = std::get(*it->text_edit); + ASSERT_TRUE(edit.new_text.find("(") == std::string::npos); +} + +TEST_CASE(SmartParensInsert) { + // When next token is NOT '(', snippet should include parens normally. + 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()); + auto& edit = std::get(*it->text_edit); + // Should contain '(' since there's no existing paren. + ASSERT_TRUE(edit.new_text.find("(") != std::string::npos); + ASSERT_TRUE(edit.new_text.find("${1:") != std::string::npos); +} + +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 {