Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 132 additions & 20 deletions src/feature/code_completion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -179,9 +259,11 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer {
CodeCompletionCollector(std::uint32_t offset,
PositionEncoding encoding,
std::vector<protocol::CompletionItem>& 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<clang::GlobalCodeCompletionAllocator>()) {}
options(options), original_content(original_content),
info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}

clang::CodeCompletionAllocator& getAllocator() final {
return info.getAllocator();
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -413,6 +516,7 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer {
std::uint32_t offset;
PositionEncoding encoding;
std::vector<protocol::CompletionItem>& output;
llvm::StringRef original_content;
const CodeCompletionOptions& options;
clang::CodeCompletionTUInfo info;
};
Expand All @@ -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;

Expand Down
136 changes: 136 additions & 0 deletions tests/unit/feature/code_completion_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<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(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<protocol::TextEdit>(*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<protocol::TextEdit>(*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<protocol::TextEdit>(*it->text_edit);
ASSERT_TRUE(edit.new_text.find("${1:") != std::string::npos);
}

TEST_CASE(Unqualified) {
code_complete(R"cpp(
namespace A {
Expand Down