-
Notifications
You must be signed in to change notification settings - Fork 29
fix(ui/completion): refactor completion engine integration and source… #165
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,88 @@ | ||||||||||
| local Promise = require('opencode.promise') | ||||||||||
| local M = {} | ||||||||||
| local state = require('opencode.state') | ||||||||||
| local CompletionEngine = require('opencode.ui.completion.engines.base') | ||||||||||
|
|
||||||||||
| ---@class BlinkCmpEngine : CompletionEngine | ||||||||||
| local BlinkCmpEngine = setmetatable({}, { __index = CompletionEngine }) | ||||||||||
| BlinkCmpEngine.__index = BlinkCmpEngine | ||||||||||
|
|
||||||||||
| ---Create a new blink-cmp completion engine | ||||||||||
| ---@return BlinkCmpEngine | ||||||||||
| function BlinkCmpEngine.new() | ||||||||||
| local self = CompletionEngine.new('blink_cmp') | ||||||||||
| return setmetatable(self, BlinkCmpEngine) | ||||||||||
| end | ||||||||||
|
|
||||||||||
| ---Check if blink-cmp is available | ||||||||||
| ---@return boolean | ||||||||||
| function BlinkCmpEngine:is_available() | ||||||||||
| local ok = pcall(require, 'blink.cmp') | ||||||||||
| return ok and CompletionEngine.is_available() | ||||||||||
| end | ||||||||||
|
|
||||||||||
| ---Setup blink-cmp completion engine | ||||||||||
| ---@param completion_sources table[] | ||||||||||
| ---@return boolean | ||||||||||
| function BlinkCmpEngine:setup(completion_sources) | ||||||||||
| local ok, blink = pcall(require, 'blink.cmp') | ||||||||||
| if not ok then | ||||||||||
| return false | ||||||||||
| end | ||||||||||
|
|
||||||||||
| CompletionEngine.setup(self, completion_sources) | ||||||||||
|
|
||||||||||
| blink.add_source_provider('opencode_mentions', { | ||||||||||
| module = 'opencode.ui.completion.engines.blink_cmp', | ||||||||||
| async = true, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| -- Hide blink-cmp menu on certain trigger characters when opened via other completion sources | ||||||||||
| vim.api.nvim_create_autocmd('User', { | ||||||||||
| group = vim.api.nvim_create_augroup('OpencodeBlinkCmp', { clear = true }), | ||||||||||
| pattern = 'BlinkCmpMenuOpen', | ||||||||||
| callback = function() | ||||||||||
| local current_buf = vim.api.nvim_get_current_buf() | ||||||||||
| local input_buf = vim.tbl_get(state, 'windows', 'input_buf') | ||||||||||
| if not state.windows or current_buf ~= input_buf then | ||||||||||
| return | ||||||||||
| end | ||||||||||
|
|
||||||||||
| local blink = require('blink.cmp') | ||||||||||
| local ctx = blink.get_context() | ||||||||||
|
|
||||||||||
| local triggers = CompletionEngine.get_trigger_characters() | ||||||||||
| if ctx.trigger.initial_kind == 'trigger_character' and vim.tbl_contains(triggers, ctx.trigger.character) then | ||||||||||
| blink.hide() | ||||||||||
| end | ||||||||||
| end, | ||||||||||
| }) | ||||||||||
| return true | ||||||||||
| end | ||||||||||
|
|
||||||||||
| ---Trigger completion manually for blink-cmp | ||||||||||
| ---@param trigger_char string | ||||||||||
| function BlinkCmpEngine:trigger(trigger_char) | ||||||||||
| local blink = require('blink.cmp') | ||||||||||
|
|
||||||||||
| vim.api.nvim_feedkeys(trigger_char, 'in', true) | ||||||||||
| if blink.is_visible() then | ||||||||||
| blink.hide() | ||||||||||
| end | ||||||||||
|
|
||||||||||
| blink.show({ | ||||||||||
| providers = { 'opencode_mentions' }, | ||||||||||
| trigger_character = trigger_char, | ||||||||||
| }) | ||||||||||
| end | ||||||||||
|
|
||||||||||
| function BlinkCmpEngine:hide() | ||||||||||
| local blink = require('blink.cmp') | ||||||||||
| if blink.is_visible() then | ||||||||||
| blink.hide() | ||||||||||
| end | ||||||||||
| end | ||||||||||
|
|
||||||||||
| -- Source implementation for blink-cmp provider (when this module is loaded by blink.cmp) | ||||||||||
| local Source = {} | ||||||||||
| Source.__index = Source | ||||||||||
|
|
||||||||||
|
|
@@ -10,19 +92,13 @@ function Source.new() | |||||||||
| end | ||||||||||
|
|
||||||||||
| function Source:get_trigger_characters() | ||||||||||
| local config = require('opencode.config') | ||||||||||
| local mention_key = config.get_key_for_function('input_window', 'mention') | ||||||||||
| local slash_key = config.get_key_for_function('input_window', 'slash_commands') | ||||||||||
| local context_key = config.get_key_for_function('input_window', 'context_items') | ||||||||||
| return { | ||||||||||
| slash_key or '', | ||||||||||
| mention_key or '', | ||||||||||
| context_key or '', | ||||||||||
| } | ||||||||||
| local CompletionEngine = require('opencode.ui.completion.engines.base') | ||||||||||
| return CompletionEngine.get_trigger_characters() | ||||||||||
| end | ||||||||||
|
|
||||||||||
| function Source:enabled() | ||||||||||
| return vim.bo.filetype == 'opencode' | ||||||||||
| local CompletionEngine = require('opencode.ui.completion.engines.base') | ||||||||||
| return CompletionEngine.is_available() | ||||||||||
| end | ||||||||||
|
|
||||||||||
| function Source:get_completions(ctx, callback) | ||||||||||
|
|
@@ -34,7 +110,9 @@ function Source:get_completions(ctx, callback) | |||||||||
| local col = ctx.cursor[2] + 1 | ||||||||||
| local before_cursor = line:sub(1, col - 1) | ||||||||||
|
|
||||||||||
| local trigger_chars = table.concat(vim.tbl_map(vim.pesc, self:get_trigger_characters()), '') | ||||||||||
| local CompletionEngine = require('opencode.ui.completion.engines.base') | ||||||||||
| local triggers = CompletionEngine.get_trigger_characters() | ||||||||||
| local trigger_chars = table.concat(vim.tbl_map(vim.pesc, triggers), '') | ||||||||||
| local trigger_char, trigger_match = before_cursor:match('([' .. trigger_chars .. '])([%w_/%-%.]*)$') | ||||||||||
|
|
||||||||||
|
Comment on lines
+114
to
117
|
||||||||||
| local triggers = CompletionEngine.get_trigger_characters() | |
| local trigger_chars = table.concat(vim.tbl_map(vim.pesc, triggers), '') | |
| local trigger_char, trigger_match = before_cursor:match('([' .. trigger_chars .. '])([%w_/%-%.]*)$') | |
| local trigger_char, trigger_match = CompletionEngine.parse_trigger(before_cursor) |
Copilot
AI
Jan 6, 2026
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.
Creating a new engine instance here is incorrect. This creates a fresh engine with no completion sources, which means on_complete won't be able to find the appropriate source to call. The engine should be passed or accessed as a shared instance (similar to how nvim_cmp.lua handles this with the 'engine' variable captured in the closure). This could lead to completion callbacks not working properly.
Copilot
AI
Jan 6, 2026
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.
The module export pattern here is confusing and could lead to unexpected behavior. Setting M = BlinkCmpEngine makes the module return the class itself, then overriding M.new changes the class's new method. This means when someone imports this module and calls .new(), they get a Source instance instead of a BlinkCmpEngine instance, unless they use the .create() method. This dual interface is documented in the comment but is error-prone. Consider using a more explicit pattern like returning a table with separate engine and source fields, or documenting this pattern more prominently.
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.
Both the local variable _current_engine (line 4) and the module field M._current_engine (line 70) are defined but only M._current_engine is used. The local variable declaration at the module level serves no purpose and could be confusing. Consider removing the local variable declaration and only using M._current_engine, or document why both exist.