Skip to content

Commit adc7619

Browse files
committed
WIP: lsp completion (Kind of buggy)
1 parent 3fd1bd6 commit adc7619

7 files changed

Lines changed: 374 additions & 14 deletions

File tree

ftplugin/opencode.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
if vim.b.did_ftplugin then
2+
return
3+
end
4+
vim.b.did_ftplugin = true
5+
-- Auto-attach opencode LSP to opencode input buffers
6+
7+
-- This provides completion for files, subagents, commands, and context items
8+
-- Works with any LSP-compatible completion plugin (blink.cmp, nvim-cmp, etc.)
9+
10+
local bufnr = vim.api.nvim_get_current_buf()
11+
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)
20+
21+
if client_id then
22+
local completion = require('opencode.ui.completion')
23+
-- track insert start state
24+
vim.api.nvim_create_autocmd('InsertEnter', {
25+
buffer = bufnr,
26+
callback = function()
27+
completion.on_insert_enter()
28+
end,
29+
})
30+
31+
vim.api.nvim_create_autocmd('TextChangedI', {
32+
buffer = bufnr,
33+
callback = function(e)
34+
completion.on_text_changed()
35+
end,
36+
})
37+
end

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+
-- require('opencode.ui.completion').trigger_completion(char)()
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+
-- require('opencode.ui.completion').trigger_completion(char)()
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+
-- require('opencode.ui.completion').trigger_completion(char)()
375375
end
376376

377377
function M.focus_input()

lua/opencode/keymap.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ 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
2829
elseif config_entry then
2930
local func_name = config_entry[1]
3031
local callback = type(func_name) == 'function' and func_name or api[func_name]

lua/opencode/lsp/opencode_ls.lua

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---In-process LSP server for opencode completion
2+
---Provides completion for files, subagents, commands, and context items
3+
---Works with any LSP-compatible completion plugin (blink.cmp, nvim-cmp, etc.)
4+
5+
local M = {}
6+
---@type table<vim.lsp.protocol.Method, fun(params: table, callback:fun(err: lsp.ResponseError?, result: any))>
7+
local handlers = {}
8+
local ms = vim.lsp.protocol.Methods
9+
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+
32+
---Initialize handler - negotiates capabilities with the client
33+
---@param params lsp.InitializeParams
34+
---@param callback fun(err?: lsp.ResponseError, result: lsp.InitializeResult)
35+
handlers[ms.initialize] = function(params, callback)
36+
local trigger_chars = get_trigger_characters()
37+
38+
callback(nil, {
39+
capabilities = {
40+
completionProvider = {
41+
resolveProvider = true,
42+
triggerCharacters = trigger_chars,
43+
},
44+
},
45+
serverInfo = {
46+
name = 'opencode_ls',
47+
version = '1.0.0',
48+
},
49+
})
50+
end
51+
52+
---Get word to complete from cursor position
53+
---@param params lsp.CompletionParams
54+
---@return string word_to_complete
55+
---@return string trigger_char
56+
---@return string full_line
57+
local function get_completion_context(params)
58+
local bufnr = vim.api.nvim_get_current_buf()
59+
local line_num = params.position.line + 1 -- LSP is 0-indexed
60+
local col = params.position.character
61+
62+
local lines = vim.api.nvim_buf_get_lines(bufnr, line_num - 1, line_num, false)
63+
local line = lines[1] or ''
64+
local line_to_cursor = line:sub(1, col)
65+
66+
-- 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+
75+
for _, t in ipairs(triggers) do
76+
if t and line_to_cursor:match(vim.pesc(t) .. '[^%s]*$') then
77+
trigger_char = t
78+
break
79+
end
80+
end
81+
82+
-- Extract word after trigger
83+
local word = ''
84+
if trigger_char ~= '' then
85+
word = line_to_cursor:match(vim.pesc(trigger_char) .. '([^%s]*)$') or ''
86+
end
87+
88+
return word, trigger_char, line
89+
end
90+
91+
---Convert opencode CompletionItem to LSP CompletionItem
92+
---@param item CompletionItem
93+
---@param index integer
94+
---@return lsp.CompletionItem
95+
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+
}
103+
local source = require('opencode.ui.completion').get_source_by_name(item.source_name)
104+
105+
local lsp_item = {
106+
label = item.kind_icon .. item.label,
107+
kind = 0,
108+
kind_icon = '',
109+
kind_hl = item.kind_hl,
110+
detail = item.detail,
111+
documentation = item.documentation and {
112+
kind = 'plaintext',
113+
value = item.documentation,
114+
} or nil,
115+
insertText = item.insert_text or item.label,
116+
insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText,
117+
filterText = item.label,
118+
sortText = string.format('%02d_%02d_%02d_%s', source.priority or 999, item.priority or 999, index, item.label),
119+
data = {
120+
source_name = item.source_name,
121+
original_data = item.data,
122+
_opencode_item = item,
123+
},
124+
}
125+
126+
return lsp_item
127+
end
128+
129+
---Completion handler - provides completion items
130+
---@param params lsp.CompletionParams
131+
---@param callback fun(err?: lsp.ResponseError, result: lsp.CompletionItem[])
132+
handlers[ms.textDocument_completion] = function(params, callback)
133+
local word, trigger_char, line = get_completion_context(params)
134+
135+
-- Build completion context
136+
local completion_context = {
137+
input = word,
138+
trigger_char = trigger_char,
139+
line = line,
140+
}
141+
142+
-- Get all registered sources
143+
local completion = require('opencode.ui.completion')
144+
local sources = completion.get_sources()
145+
146+
-- Collect promises from all sources
147+
local Promise = require('opencode.promise')
148+
local promises = {}
149+
150+
for _, source in ipairs(sources) do
151+
if source.complete then
152+
table.insert(promises, source.complete(completion_context))
153+
end
154+
end
155+
156+
-- Wait for all sources to complete in parallel
157+
Promise.all(promises)
158+
:and_then(function(results)
159+
local all_items = {}
160+
161+
-- Flatten results from all sources
162+
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))
166+
end
167+
end
168+
end
169+
170+
callback(nil, all_items)
171+
completion.store_completion_items(all_items)
172+
end)
173+
:catch(function(err)
174+
vim.notify('Opencode LSP completion error: ' .. tostring(err), vim.log.levels.ERROR)
175+
callback(nil, {})
176+
end)
177+
end
178+
179+
---Resolve handler - provides additional documentation for completion items
180+
---@param params lsp.CompletionItem
181+
---@param callback fun(err?: lsp.ResponseError, result: lsp.CompletionItem)
182+
handlers[ms.completionItem_resolve] = function(params, callback)
183+
local item = vim.deepcopy(params)
184+
185+
-- Additional resolution can be done here if needed
186+
-- For now, documentation is already attached in textDocument_completion
187+
188+
callback(nil, item)
189+
end
190+
191+
---Create the LSP server configuration
192+
---@return vim.lsp.ClientConfig
193+
function M.create_config()
194+
return {
195+
name = 'opencode_ls',
196+
cmd = function(dispatchers, config)
197+
return {
198+
request = function(method, params, callback)
199+
if handlers[method] then
200+
handlers[method](params, callback)
201+
end
202+
end,
203+
notify = function() end,
204+
is_closing = function()
205+
return false
206+
end,
207+
terminate = function() end,
208+
}
209+
end,
210+
root_dir = vim.fn.getcwd(),
211+
}
212+
end
213+
214+
---Start the LSP server for a buffer
215+
---@param bufnr integer
216+
---@return integer? client_id
217+
function M.start(bufnr)
218+
local config = M.create_config()
219+
return vim.lsp.start(config, { bufnr = bufnr, silent = false })
220+
end
221+
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+
237+
return M

lua/opencode/promise.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,20 @@ function Promise.system(cmd, opts)
402402
return p
403403
end
404404

405+
---Wait for all promises to resolve
406+
---Returns a promise that resolves with a table of all results
407+
---If any promise rejects, the returned promise rejects with that error
408+
---@generic T
409+
---@param promises Promise<T>[]
410+
---@return Promise<T[]>
411+
function Promise.all(promises)
412+
return Promise.spawn(function()
413+
local results = {}
414+
for i, promise in ipairs(promises) do
415+
results[i] = promise:await()
416+
end
417+
return results
418+
end)
419+
end
420+
405421
return Promise

0 commit comments

Comments
 (0)