Skip to content

Commit 8dd1332

Browse files
committed
fix(ui/completion): refactor completion engine integration and source priority handling
- Standardized trigger, setup, and completion callbacks for all engines. - Fix blink opening path completion sometimes this should fix #130
1 parent f2d3d9e commit 8dd1332

5 files changed

Lines changed: 304 additions & 160 deletions

File tree

lua/opencode/ui/completion.lua

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
11
local M = {}
22

33
local completion_sources = {}
4+
local _current_engine = nil
5+
6+
-- Engine configuration mapping
7+
local ENGINE_CONFIG = {
8+
['nvim-cmp'] = {
9+
module = 'opencode.ui.completion.engines.nvim_cmp',
10+
constructor = 'new',
11+
},
12+
['blink'] = {
13+
module = 'opencode.ui.completion.engines.blink_cmp',
14+
constructor = 'create', -- Special case for blink
15+
},
16+
['vim_complete'] = {
17+
module = 'opencode.ui.completion.engines.vim_complete',
18+
constructor = 'new',
19+
},
20+
}
21+
22+
---Load and create an engine instance
23+
---@param engine_name string
24+
---@return table|nil engine
25+
local function load_engine(engine_name)
26+
local config = ENGINE_CONFIG[engine_name]
27+
if not config then
28+
vim.notify('Unknown completion engine: ' .. tostring(engine_name), vim.log.levels.WARN)
29+
return nil
30+
end
31+
32+
local ok, EngineClass = pcall(require, config.module)
33+
if not ok then
34+
vim.notify('Failed to load ' .. engine_name .. ' engine: ' .. tostring(EngineClass), vim.log.levels.ERROR)
35+
return nil
36+
end
37+
38+
local constructor = EngineClass[config.constructor]
39+
if not constructor then
40+
vim.notify('Engine ' .. engine_name .. ' missing ' .. config.constructor .. ' method', vim.log.levels.ERROR)
41+
return nil
42+
end
43+
44+
return constructor()
45+
end
446

547
function M.setup()
648
local files_source = require('opencode.ui.completion.files')
@@ -17,23 +59,21 @@ function M.setup()
1759
return (a.priority or 0) > (b.priority or 0)
1860
end)
1961

62+
local engine_name = M.get_completion_engine()
63+
local engine = load_engine(engine_name)
2064
local setup_success = false
2165

22-
local engine = M.get_completion_engine()
23-
24-
if engine == 'nvim-cmp' then
25-
require('opencode.ui.completion.engines.nvim_cmp').setup(completion_sources)
26-
setup_success = true
27-
elseif engine == 'blink' then
28-
require('opencode.ui.completion.engines.blink_cmp').setup(completion_sources)
29-
setup_success = true
30-
elseif engine == 'vim_complete' then
31-
require('opencode.ui.completion.engines.vim_complete').setup(completion_sources)
32-
setup_success = true
66+
if engine and engine.setup then
67+
setup_success = engine:setup(completion_sources)
3368
end
3469

70+
M._current_engine = engine
71+
3572
if not setup_success then
36-
vim.notify('Opencode: No completion engine available', vim.log.levels.WARN)
73+
vim.notify(
74+
'Opencode: No completion engine available (engine: ' .. tostring(engine_name) .. ')',
75+
vim.log.levels.WARN
76+
)
3777
end
3878
end
3979

@@ -83,17 +123,16 @@ end
83123

84124
function M.trigger_completion(trigger_char)
85125
return function()
86-
local engine = M.get_completion_engine()
87-
88-
if engine == 'vim_complete' then
89-
require('opencode.ui.completion.engines.vim_complete').trigger(trigger_char)
90-
elseif engine == 'blink' then
91-
vim.api.nvim_feedkeys(trigger_char, 'in', true)
92-
require('blink.cmp').show({ providers = { 'opencode_mentions' } })
93-
else
94-
vim.api.nvim_feedkeys(trigger_char, 'in', true)
126+
if M._current_engine and M._current_engine.trigger then
127+
M._current_engine:trigger(trigger_char)
95128
end
96129
end
97130
end
98131

132+
function M.hide_completion()
133+
if M._current_engine and M._current_engine.hide then
134+
M._current_engine:hide()
135+
end
136+
end
137+
99138
return M
Lines changed: 105 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,88 @@
11
local Promise = require('opencode.promise')
2-
local M = {}
2+
local state = require('opencode.state')
3+
local CompletionEngine = require('opencode.ui.completion.engines.base')
4+
5+
---@class BlinkCmpEngine : CompletionEngine
6+
local BlinkCmpEngine = setmetatable({}, { __index = CompletionEngine })
7+
BlinkCmpEngine.__index = BlinkCmpEngine
8+
9+
---Create a new blink-cmp completion engine
10+
---@return BlinkCmpEngine
11+
function BlinkCmpEngine.new()
12+
local self = CompletionEngine.new('blink_cmp')
13+
return setmetatable(self, BlinkCmpEngine)
14+
end
15+
16+
---Check if blink-cmp is available
17+
---@return boolean
18+
function BlinkCmpEngine:is_available()
19+
local ok = pcall(require, 'blink.cmp')
20+
return ok and CompletionEngine.is_available()
21+
end
22+
23+
---Setup blink-cmp completion engine
24+
---@param completion_sources table[]
25+
---@return boolean
26+
function BlinkCmpEngine:setup(completion_sources)
27+
local ok, blink = pcall(require, 'blink.cmp')
28+
if not ok then
29+
return false
30+
end
31+
32+
CompletionEngine.setup(self, completion_sources)
33+
34+
blink.add_source_provider('opencode_mentions', {
35+
module = 'opencode.ui.completion.engines.blink_cmp',
36+
async = true,
37+
})
38+
39+
-- Hide blink-cmp menu on certain trigger characters when opened via other completion sources
40+
vim.api.nvim_create_autocmd('User', {
41+
group = vim.api.nvim_create_augroup('OpencodeBlinkCmp', { clear = true }),
42+
pattern = 'BlinkCmpMenuOpen',
43+
callback = function()
44+
local current_buf = vim.api.nvim_get_current_buf()
45+
local input_buf = vim.tbl_get(state, 'windows', 'input_buf')
46+
if not state.windows or current_buf ~= input_buf then
47+
return
48+
end
49+
50+
local blink = require('blink.cmp')
51+
local ctx = blink.get_context()
52+
53+
local triggers = CompletionEngine.get_trigger_characters()
54+
if ctx.trigger.initial_kind == 'trigger_character' and vim.tbl_contains(triggers, ctx.trigger.character) then
55+
blink.hide()
56+
end
57+
end,
58+
})
59+
return true
60+
end
361

62+
---Trigger completion manually for blink-cmp
63+
---@param trigger_char string
64+
function BlinkCmpEngine:trigger(trigger_char)
65+
local blink = require('blink.cmp')
66+
67+
vim.api.nvim_feedkeys(trigger_char, 'in', true)
68+
if blink.is_visible() then
69+
blink.hide()
70+
end
71+
72+
blink.show({
73+
providers = { 'opencode_mentions' },
74+
trigger_character = trigger_char,
75+
})
76+
end
77+
78+
function BlinkCmpEngine:hide()
79+
local blink = require('blink.cmp')
80+
if blink.is_visible() then
81+
blink.hide()
82+
end
83+
end
84+
85+
-- Source implementation for blink-cmp provider (when this module is loaded by blink.cmp)
486
local Source = {}
587
Source.__index = Source
688

@@ -10,19 +92,13 @@ function Source.new()
1092
end
1193

1294
function Source:get_trigger_characters()
13-
local config = require('opencode.config')
14-
local mention_key = config.get_key_for_function('input_window', 'mention')
15-
local slash_key = config.get_key_for_function('input_window', 'slash_commands')
16-
local context_key = config.get_key_for_function('input_window', 'context_items')
17-
return {
18-
slash_key or '',
19-
mention_key or '',
20-
context_key or '',
21-
}
95+
local CompletionEngine = require('opencode.ui.completion.engines.base')
96+
return CompletionEngine.get_trigger_characters()
2297
end
2398

2499
function Source:enabled()
25-
return vim.bo.filetype == 'opencode'
100+
local CompletionEngine = require('opencode.ui.completion.engines.base')
101+
return CompletionEngine.is_available()
26102
end
27103

28104
function Source:get_completions(ctx, callback)
@@ -34,7 +110,9 @@ function Source:get_completions(ctx, callback)
34110
local col = ctx.cursor[2] + 1
35111
local before_cursor = line:sub(1, col - 1)
36112

37-
local trigger_chars = table.concat(vim.tbl_map(vim.pesc, self:get_trigger_characters()), '')
113+
local CompletionEngine = require('opencode.ui.completion.engines.base')
114+
local triggers = CompletionEngine.get_trigger_characters()
115+
local trigger_chars = table.concat(vim.tbl_map(vim.pesc, triggers), '')
38116
local trigger_char, trigger_match = before_cursor:match('([' .. trigger_chars .. '])([%w_/%-%.]*)$')
39117

40118
if not trigger_match then
@@ -43,15 +121,15 @@ function Source:get_completions(ctx, callback)
43121
end
44122

45123
local context = {
46-
input = trigger_match, -- Pass input for search-based sources (e.g., files)
124+
input = trigger_match,
47125
cursor_pos = col,
48126
line = line,
49127
trigger_char = trigger_char,
50128
}
51129

52130
local items = {}
53-
for _, completion_source in ipairs(completion_sources) do
54-
local source_items = completion_source.complete(context):await()
131+
for _, source in ipairs(completion_sources) do
132+
local source_items = source.complete(context):await()
55133
for i, item in ipairs(source_items) do
56134
local insert_text = item.insert_text or item.label
57135
table.insert(items, {
@@ -61,18 +139,10 @@ function Source:get_completions(ctx, callback)
61139
kind_hl = item.kind_hl,
62140
detail = item.detail,
63141
documentation = item.documentation,
64-
-- Use filterText for fuzzy matching against the typed text after trigger char
65142
filterText = item.filter_text or item.label,
66143
insertText = insert_text,
67-
sortText = string.format(
68-
'%02d_%02d_%02d_%s',
69-
completion_source.priority or 999,
70-
item.priority or 999,
71-
i,
72-
item.label
73-
),
74-
score_offset = -(completion_source.priority or 999) * 1000 + (item.priority or 999),
75-
144+
sortText = string.format('%02d_%02d_%02d_%s', source.priority or 999, item.priority or 999, i, item.label),
145+
score_offset = -(source.priority or 999) * 1000 + (item.priority or 999),
76146
data = {
77147
original_item = item,
78148
},
@@ -88,27 +158,23 @@ function Source:execute(ctx, item, callback, default_implementation)
88158
default_implementation()
89159

90160
if item.data and item.data.original_item then
91-
local completion = require('opencode.ui.completion')
92-
completion.on_complete(item.data.original_item)
161+
local CompletionEngine = require('opencode.ui.completion.engines.base')
162+
local engine = CompletionEngine.new('blink_cmp')
163+
engine:on_complete(item.data.original_item)
93164
end
94165

95166
callback()
96167
end
97168

98-
function M.setup(completion_sources)
99-
local ok, blink = pcall(require, 'blink.cmp')
100-
if not ok then
101-
return false
102-
end
169+
-- Export module with dual interface:
170+
-- - For our engine system: use BlinkCmpEngine methods
171+
-- - For blink.cmp provider system: override 'new' to return Source instance
172+
local M = BlinkCmpEngine
103173

104-
blink.add_source_provider('opencode_mentions', {
105-
module = 'opencode.ui.completion.engines.blink_cmp',
106-
async = true,
107-
})
108-
109-
return true
110-
end
174+
-- Save the engine constructor before overriding
175+
M.create = BlinkCmpEngine.new
111176

177+
-- Override 'new' for blink.cmp compatibility (when blink loads this as a source)
112178
M.new = Source.new
113179

114180
return M

0 commit comments

Comments
 (0)