1 Commits

Author SHA1 Message Date
ykiko
8e9d4fa636 feat(completion): smart parenthesis detection to avoid duplicate parens
When the next non-whitespace character after the cursor is already '(',
skip inserting parentheses and parameter placeholders in the snippet.
This prevents duplicate parens when completing a function name that
already has arguments written after it.

Uses the original file content (not Clang's internal buffer) for
lookahead to correctly detect the next token.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 14:54:07 +08:00
4 changed files with 96 additions and 1080 deletions

View File

@@ -200,14 +200,6 @@ if(CLICE_ENABLE_BENCHMARK)
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
add_executable(pch_chain_benchmark
"${PROJECT_SOURCE_DIR}/benchmarks/pch_chain/pch_chain_benchmark.cpp"
)
target_include_directories(pch_chain_benchmark PRIVATE
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(pch_chain_benchmark PRIVATE clice::core)
endif()
if(CLICE_RELEASE)

File diff suppressed because it is too large Load Diff

View File

@@ -158,12 +158,28 @@ 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.
auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string {
/// 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;
@@ -174,33 +190,47 @@ auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string {
}
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: snippet += '('; break;
case CK::CK_RightParen: snippet += ')'; 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: snippet += ", "; break;
case CK::CK_Comma:
if(!(in_parens && skip_parens)) {
snippet += ", ";
}
break;
case CK::CK_Text:
if(chunk.Text) {
if(!(in_parens && skip_parens) && chunk.Text) {
snippet += chunk.Text;
}
break;
case CK::CK_Optional:
// Optional chunks contain default arguments — skip for snippet.
break;
case CK::CK_Optional: break;
case CK::CK_Informative:
case CK::CK_ResultType:
case CK::CK_CurrentParameter:
// Display-only chunks, not part of insertion.
break;
case CK::CK_CurrentParameter: break;
default: break;
}
}
// If no placeholders were generated, return empty to signal plain text.
// If no placeholders were generated and parens were skipped,
// return empty to signal plain text.
if(placeholder_index == 0) {
return {};
}
@@ -229,9 +259,11 @@ public:
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();
@@ -425,7 +457,8 @@ public:
// Generate snippet for non-bundled callables.
if(is_callable && !options.bundle_overloads &&
options.enable_function_arguments_snippet) {
snippet = build_snippet(*ccs);
bool next_is_paren = next_token_char(original_content, offset) == '(';
snippet = build_snippet(*ccs, /*skip_parens=*/next_is_paren);
}
}
@@ -495,6 +528,7 @@ private:
std::uint32_t offset;
PositionEncoding encoding;
std::vector<protocol::CompletionItem>& output;
llvm::StringRef original_content;
const CodeCompletionOptions& options;
clang::CodeCompletionTUInfo info;
};
@@ -509,7 +543,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;

View File

@@ -335,6 +335,44 @@ int z = fo$(pos)
*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;