-
Notifications
You must be signed in to change notification settings - Fork 71
feat(completion): smart paren detection to avoid duplicate parens #415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 @@ 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>()) {} | ||
|
Comment on lines
+262
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm the member declaration order vs initializer order in the file under review.
rg -nP -C1 '^\s*(std::uint32_t offset;|PositionEncoding encoding;|std::vector<protocol::CompletionItem>& output;|llvm::StringRef original_content;|const CodeCompletionOptions& options;|clang::CodeCompletionTUInfo info;)' src/feature/code_completion.cpp
rg -nP -C2 'clang::CodeCompleteConsumer\(\{\}\), offset\(offset\)' src/feature/code_completion.cppRepository: clice-io/clice Length of output: 881 Member initializer order does not match declaration order (-Wreorder). The data members are declared as 🔧 Proposed fix private:
std::uint32_t offset;
PositionEncoding encoding;
std::vector<protocol::CompletionItem>& output;
- llvm::StringRef original_content;
const CodeCompletionOptions& options;
+ llvm::StringRef original_content;
clang::CodeCompletionTUInfo info;Or swap the initializers on lines 265-266 to match declaration order. 🤖 Prompt for AI Agents |
||
|
|
||
| clang::CodeCompletionAllocator& getAllocator() final { | ||
| return info.getAllocator(); | ||
|
|
@@ -425,7 +457,8 @@ class CodeCompletionCollector final : public clang::CodeCompleteConsumer { | |
| // 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 @@ 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; | ||
| }; | ||
|
|
@@ -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; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lookahead does not skip comments.
next_token_charonly skips whitespace. A cursor followed by a comment before the(, such asfoo|/* comment */(args)orfoo| // comment\n(args), will return/and fail to trigger smart-paren skipping, causing duplicate parens in those (uncommon but real) cases. If you want this to be robust, consider skipping//line comments and/* */block comments too. Otherwise this is an acceptable limitation worth a doc note.🤖 Prompt for AI Agents