Skip to content

Commit ae4610a

Browse files
committed
feat: implement completion through LSP
1 parent adc7619 commit ae4610a

15 files changed

Lines changed: 104 additions & 868 deletions

File tree

ftplugin/opencode.lua

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,18 @@ vim.b.did_ftplugin = true
99

1010
local bufnr = vim.api.nvim_get_current_buf()
1111

12-
-- Start the in-process LSP server
13-
local ok_lsp, opencode_ls = pcall(require, 'opencode.lsp.opencode_ls')
14-
if not ok_lsp then
15-
vim.notify('Failed to load opencode LSP server: ' .. tostring(opencode_ls), vim.log.levels.WARN)
16-
return
17-
end
18-
19-
local client_id = opencode_ls.start(bufnr)
12+
local opencode_completion_ls = require('opencode.lsp.opencode_completion_ls')
13+
local client_id = opencode_completion_ls.start(bufnr)
14+
local completion = require('opencode.ui.completion')
2015

2116
if client_id then
22-
local completion = require('opencode.ui.completion')
2317
-- track insert start state
2418
vim.api.nvim_create_autocmd('InsertEnter', {
2519
buffer = bufnr,
2620
callback = function()
21+
if not completion.has_completion_engine() then
22+
vim.lsp.completion.enable(true, client_id, bufnr, { autotrigger = true })
23+
end
2724
completion.on_insert_enter()
2825
end,
2926
})

lua/opencode/api.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,19 +359,19 @@ function M.mention()
359359
local char = config.get_key_for_function('input_window', 'mention')
360360

361361
ui.focus_input({ restore_position = false, start_insert = true })
362-
-- require('opencode.ui.completion').trigger_completion(char)()
362+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(char, true, false, true), 'n', false)
363363
end
364364

365365
function M.context_items()
366366
local char = config.get_key_for_function('input_window', 'context_items')
367367
ui.focus_input({ restore_position = false, start_insert = true })
368-
-- require('opencode.ui.completion').trigger_completion(char)()
368+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(char, true, false, true), 'n', false)
369369
end
370370

371371
function M.slash_commands()
372372
local char = config.get_key_for_function('input_window', 'slash_commands')
373373
ui.focus_input({ restore_position = false, start_insert = true })
374-
-- require('opencode.ui.completion').trigger_completion(char)()
374+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(char, true, false, true), 'n', false)
375375
end
376376

377377
function M.focus_input()

lua/opencode/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ function M.setup(opts)
1111
require('opencode.ui.highlight').setup()
1212
require('opencode.core').setup()
1313
require('opencode.api').setup()
14-
require('opencode.keymap').setup(config.keymap)
1514
require('opencode.ui.completion').setup()
15+
require('opencode.keymap').setup(config.keymap)
1616
require('opencode.event_manager').setup()
1717
require('opencode.context').setup()
1818
require('opencode.ui.context_bar').setup()

lua/opencode/keymap.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ local function process_keymap_entry(keymap_config, default_modes, base_opts, def
2525
for key_binding, config_entry in pairs(keymap_config) do
2626
if config_entry == false then
2727
-- Skip keymap if explicitly set to false (disabled)
28-
elseif key_binding == '@' or key_binding == '/' or key_binding == '#' then
2928
elseif config_entry then
3029
local func_name = config_entry[1]
3130
local callback = type(func_name) == 'function' and func_name or api[func_name]

lua/opencode/lsp/opencode_ls.lua renamed to lua/opencode/lsp/opencode_completion_ls.lua

Lines changed: 30 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,22 @@ local M = {}
77
local handlers = {}
88
local ms = vim.lsp.protocol.Methods
99

10-
---Parse trigger characters from all registered completion sources
11-
---@return string[]
12-
local function get_trigger_characters()
13-
local chars = {}
14-
local config = require('opencode.config')
15-
16-
-- Get trigger characters from keymaps
17-
local triggers = {
18-
config.get_key_for_function('input_window', 'mention'), -- @ for subagents
19-
config.get_key_for_function('input_window', 'slash_commands'), -- / for commands
20-
config.get_key_for_function('input_window', 'context_items'), -- # for context
21-
}
22-
23-
for _, trigger in ipairs(triggers) do
24-
if trigger and not vim.tbl_contains(chars, trigger) then
25-
table.insert(chars, trigger)
26-
end
27-
end
28-
29-
return chars
30-
end
31-
3210
---Initialize handler - negotiates capabilities with the client
3311
---@param params lsp.InitializeParams
3412
---@param callback fun(err?: lsp.ResponseError, result: lsp.InitializeResult)
3513
handlers[ms.initialize] = function(params, callback)
36-
local trigger_chars = get_trigger_characters()
14+
local completion = require('opencode.ui.completion')
15+
local triggers = completion.get_trigger_characters()
3716

3817
callback(nil, {
3918
capabilities = {
4019
completionProvider = {
4120
resolveProvider = true,
42-
triggerCharacters = trigger_chars,
21+
triggerCharacters = triggers,
4322
},
4423
},
4524
serverInfo = {
46-
name = 'opencode_ls',
25+
name = 'opencode_completion_ls',
4726
version = '1.0.0',
4827
},
4928
})
@@ -55,6 +34,7 @@ end
5534
---@return string trigger_char
5635
---@return string full_line
5736
local function get_completion_context(params)
37+
local completion = require('opencode.ui.completion')
5838
local bufnr = vim.api.nvim_get_current_buf()
5939
local line_num = params.position.line + 1 -- LSP is 0-indexed
6040
local col = params.position.character
@@ -64,14 +44,7 @@ local function get_completion_context(params)
6444
local line_to_cursor = line:sub(1, col)
6545

6646
-- Find the trigger character
67-
local trigger_char = ''
68-
local config = require('opencode.config')
69-
local triggers = {
70-
config.get_key_for_function('input_window', 'mention'),
71-
config.get_key_for_function('input_window', 'slash_commands'),
72-
config.get_key_for_function('input_window', 'context_items'),
73-
}
74-
47+
local triggers = completion.get_trigger_characters()
7548
for _, t in ipairs(triggers) do
7649
if t and line_to_cursor:match(vim.pesc(t) .. '[^%s]*$') then
7750
trigger_char = t
@@ -88,24 +61,23 @@ local function get_completion_context(params)
8861
return word, trigger_char, line
8962
end
9063

64+
local function supports_kind_icons()
65+
-- only blink.cmp supports kind icons currently, so we check for its presence
66+
local has_blink_cmp = pcall(require, 'blink.cmp')
67+
return has_blink_cmp
68+
end
69+
9170
---Convert opencode CompletionItem to LSP CompletionItem
9271
---@param item CompletionItem
9372
---@param index integer
9473
---@return lsp.CompletionItem
9574
local function to_lsp_item(item, index)
96-
-- Map opencode kinds to LSP kinds
97-
local kind_map = {
98-
file = vim.lsp.protocol.CompletionItemKind.File,
99-
subagent = vim.lsp.protocol.CompletionItemKind.Class,
100-
command = vim.lsp.protocol.CompletionItemKind.Function,
101-
context = vim.lsp.protocol.CompletionItemKind.Variable,
102-
}
10375
local source = require('opencode.ui.completion').get_source_by_name(item.source_name)
10476

10577
local lsp_item = {
106-
label = item.kind_icon .. item.label,
107-
kind = 0,
108-
kind_icon = '',
78+
label = (supports_kind_icons() and '' or (item.kind_icon .. ' ')) .. item.label,
79+
kind = vim.lsp.protocol.CompletionItemKind.Text,
80+
kind_icon = supports_kind_icons() and item.kind_icon or nil, -- Only include kind_icon if supported
10981
kind_hl = item.kind_hl,
11082
detail = item.detail,
11183
documentation = item.documentation and {
@@ -139,39 +111,41 @@ handlers[ms.textDocument_completion] = function(params, callback)
139111
line = line,
140112
}
141113

142-
-- Get all registered sources
143114
local completion = require('opencode.ui.completion')
144115
local sources = completion.get_sources()
145116

146-
-- Collect promises from all sources
147117
local Promise = require('opencode.promise')
148118
local promises = {}
149119

150120
for _, source in ipairs(sources) do
151-
if source.complete then
152-
table.insert(promises, source.complete(completion_context))
153-
end
121+
table.insert(promises, source.complete(completion_context))
154122
end
155123

156-
-- Wait for all sources to complete in parallel
157124
Promise.all(promises)
158125
:and_then(function(results)
159126
local all_items = {}
160127

161-
-- Flatten results from all sources
128+
local is_incomplete = false
162129
for i, items in ipairs(results) do
163-
if type(items) == 'table' then
164-
for _, item in ipairs(items) do
165-
table.insert(all_items, to_lsp_item(item, i))
130+
for _, item in ipairs(items or {}) do
131+
local source = completion.get_source_by_name(item.source_name)
132+
if source and source.is_incomplete then
133+
is_incomplete = true
166134
end
135+
136+
table.insert(all_items, to_lsp_item(item, i))
167137
end
168138
end
169139

170-
callback(nil, all_items)
140+
callback(nil, {
141+
isIncomplete = is_incomplete,
142+
items = all_items,
143+
})
171144
completion.store_completion_items(all_items)
172145
end)
173146
:catch(function(err)
174-
vim.notify('Opencode LSP completion error: ' .. tostring(err), vim.log.levels.ERROR)
147+
local log = require('opencode.log')
148+
log.error('Error in completion handler: ' .. tostring(err))
175149
callback(nil, {})
176150
end)
177151
end
@@ -182,17 +156,14 @@ end
182156
handlers[ms.completionItem_resolve] = function(params, callback)
183157
local item = vim.deepcopy(params)
184158

185-
-- Additional resolution can be done here if needed
186-
-- For now, documentation is already attached in textDocument_completion
187-
188159
callback(nil, item)
189160
end
190161

191162
---Create the LSP server configuration
192163
---@return vim.lsp.ClientConfig
193164
function M.create_config()
194165
return {
195-
name = 'opencode_ls',
166+
name = 'opencode_completion_ls',
196167
cmd = function(dispatchers, config)
197168
return {
198169
request = function(method, params, callback)
@@ -219,19 +190,4 @@ function M.start(bufnr)
219190
return vim.lsp.start(config, { bufnr = bufnr, silent = false })
220191
end
221192

222-
---Hook into completion item selection to trigger on_complete callbacks
223-
---This is called when a completion item is confirmed/selected
224-
---@param item lsp.CompletionItem
225-
function M.on_completion_done(item)
226-
if not item or not item.data or not item.data._opencode_item then
227-
return
228-
end
229-
230-
local completion = require('opencode.ui.completion')
231-
local original_item = item.data._opencode_item
232-
233-
-- Call the source's on_complete callback
234-
completion.on_complete(original_item)
235-
end
236-
237193
return M

lua/opencode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@
411411
---@field priority number Priority for ordering sources
412412
---@field complete fun(context: CompletionContext): Promise<CompletionItem[]> Function to generate completion items
413413
---@field on_complete fun(item: CompletionItem): nil Optional callback when item is selected
414+
---@field is_incomplete? boolean Whether the completion results are incomplete (for sources that support pagination)
414415

415416
---@class OpencodeContext
416417
---@field current_file OpencodeContextFile|nil

0 commit comments

Comments
 (0)