Skip to content

Commit cb23223

Browse files
committed
feat(api): auto complete user commands
Add autocomplete to `:Opencode command` and add user commands to slash commands list.
1 parent c1b728a commit cb23223

File tree

2 files changed

+194
-11
lines changed

2 files changed

+194
-11
lines changed

lua/opencode/api.lua

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,16 @@ M.commands = {
10221022

10231023
command = {
10241024
desc = 'Run user-defined command',
1025+
completions = function()
1026+
local config_file = require('opencode.config_file')
1027+
local user_commands = config_file.get_user_commands()
1028+
if not user_commands then
1029+
return {}
1030+
end
1031+
local names = vim.tbl_keys(user_commands)
1032+
table.sort(names)
1033+
return names
1034+
end,
10251035
fn = function(args)
10261036
local name = args[1]
10271037
if not name or name == '' then
@@ -1071,7 +1081,7 @@ M.slash_commands_map = {
10711081
['/agent'] = { fn = M.select_agent, desc = 'Select agent mode' },
10721082
['/agents_init'] = { fn = M.initialize, desc = 'Initialize AGENTS.md session' },
10731083
['/child-sessions'] = { fn = M.select_child_session, desc = 'Select child session' },
1074-
['/commands'] = { fn = M.commands_list, desc = 'Show user-defined commands' },
1084+
['/command-list'] = { fn = M.commands_list, desc = 'Show user-defined commands' },
10751085
['/compact'] = { fn = M.compact_session, desc = 'Compact current session' },
10761086
['/mcp'] = { fn = M.mcp, desc = 'Show MCP server configuration' },
10771087
['/models'] = { fn = M.configure_provider, desc = 'Switch provider/model' },
@@ -1166,9 +1176,13 @@ function M.complete_command(arg_lead, cmd_line, cursor_pos)
11661176
end
11671177

11681178
if num_parts <= 3 and subcmd_def.completions then
1179+
local completions = subcmd_def.completions
1180+
if type(completions) == 'function' then
1181+
completions = completions()
1182+
end
11691183
return vim.tbl_filter(function(opt)
11701184
return vim.startswith(opt, arg_lead)
1171-
end, subcmd_def.completions)
1185+
end, completions)
11721186
end
11731187

11741188
if num_parts <= 4 and subcmd_def.sub_completions then
@@ -1209,6 +1223,22 @@ function M.get_slash_commands()
12091223
fn = def.fn,
12101224
})
12111225
end
1226+
1227+
local config_file = require('opencode.config_file')
1228+
local user_commands = config_file.get_user_commands()
1229+
if user_commands then
1230+
for name, def in pairs(user_commands) do
1231+
table.insert(result, {
1232+
slash_cmd = '/' .. name,
1233+
desc = def.description or 'User command',
1234+
fn = function()
1235+
M.run_user_command(name, {})
1236+
end,
1237+
args = false,
1238+
})
1239+
end
1240+
end
1241+
12121242
return result
12131243
end
12141244

tests/unit/api_spec.lua

Lines changed: 162 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,13 @@ describe('opencode.api', function()
174174
end)
175175

176176
it('parses multiple prefixes and passes all to send_message', function()
177-
api.commands.run.fn({ 'agent=plan', 'model=openai/gpt-4', 'context=current_file.enabled=false', 'analyze', 'code' })
177+
api.commands.run.fn({
178+
'agent=plan',
179+
'model=openai/gpt-4',
180+
'context=current_file.enabled=false',
181+
'analyze',
182+
'code',
183+
})
178184
assert.stub(core.send_message).was_called()
179185
assert.stub(core.send_message).was_called_with('analyze code', {
180186
new_session = false,
@@ -267,10 +273,9 @@ describe('opencode.api', function()
267273

268274
api.mcp()
269275

270-
assert.stub(notify_stub).was_called_with(
271-
'No MCP configuration found. Please check your opencode config file.',
272-
vim.log.levels.WARN
273-
)
276+
assert
277+
.stub(notify_stub)
278+
.was_called_with('No MCP configuration found. Please check your opencode config file.', vim.log.levels.WARN)
274279

275280
config_file.get_mcp_servers = original_get_mcp_servers
276281
notify_stub:revert()
@@ -325,13 +330,161 @@ describe('opencode.api', function()
325330

326331
api.commands_list()
327332

328-
assert.stub(notify_stub).was_called_with(
329-
'No user commands found. Please check your opencode config file.',
330-
vim.log.levels.WARN
331-
)
333+
assert
334+
.stub(notify_stub)
335+
.was_called_with('No user commands found. Please check your opencode config file.', vim.log.levels.WARN)
332336

333337
config_file.get_user_commands = original_get_user_commands
334338
notify_stub:revert()
335339
end)
336340
end)
341+
342+
describe('command autocomplete', function()
343+
it('provides user command names for completion', function()
344+
local config_file = require('opencode.config_file')
345+
local original_get_user_commands = config_file.get_user_commands
346+
347+
config_file.get_user_commands = function()
348+
return {
349+
['build'] = { description = 'Build the project' },
350+
['test'] = { description = 'Run tests' },
351+
['deploy'] = { description = 'Deploy to production' },
352+
}
353+
end
354+
355+
local completions = api.commands.command.completions()
356+
357+
assert.truthy(vim.tbl_contains(completions, 'build'))
358+
assert.truthy(vim.tbl_contains(completions, 'test'))
359+
assert.truthy(vim.tbl_contains(completions, 'deploy'))
360+
361+
config_file.get_user_commands = original_get_user_commands
362+
end)
363+
364+
it('returns empty array when no user commands exist', function()
365+
local config_file = require('opencode.config_file')
366+
local original_get_user_commands = config_file.get_user_commands
367+
368+
config_file.get_user_commands = function()
369+
return nil
370+
end
371+
372+
local completions = api.commands.command.completions()
373+
374+
assert.same({}, completions)
375+
376+
config_file.get_user_commands = original_get_user_commands
377+
end)
378+
379+
it('integrates with complete_command for Opencode command <TAB>', function()
380+
local config_file = require('opencode.config_file')
381+
local original_get_user_commands = config_file.get_user_commands
382+
383+
config_file.get_user_commands = function()
384+
return {
385+
['build'] = { description = 'Build the project' },
386+
['test'] = { description = 'Run tests' },
387+
}
388+
end
389+
390+
local results = api.complete_command('b', 'Opencode command b', 18)
391+
392+
assert.truthy(vim.tbl_contains(results, 'build'))
393+
assert.falsy(vim.tbl_contains(results, 'test'))
394+
395+
config_file.get_user_commands = original_get_user_commands
396+
end)
397+
end)
398+
399+
describe('slash commands with user commands', function()
400+
it('includes user commands in get_slash_commands', function()
401+
local config_file = require('opencode.config_file')
402+
local original_get_user_commands = config_file.get_user_commands
403+
404+
config_file.get_user_commands = function()
405+
return {
406+
['build'] = { description = 'Build the project' },
407+
['test'] = { description = 'Run tests' },
408+
}
409+
end
410+
411+
local slash_commands = api.get_slash_commands()
412+
413+
local build_found = false
414+
local test_found = false
415+
416+
for _, cmd in ipairs(slash_commands) do
417+
if cmd.slash_cmd == '/build' then
418+
build_found = true
419+
assert.equal('Build the project', cmd.desc)
420+
assert.is_function(cmd.fn)
421+
assert.truthy(cmd.args)
422+
elseif cmd.slash_cmd == '/test' then
423+
test_found = true
424+
assert.equal('Run tests', cmd.desc)
425+
assert.is_function(cmd.fn)
426+
assert.falsy(cmd.args)
427+
end
428+
end
429+
430+
assert.truthy(build_found, 'Should include /build command')
431+
assert.truthy(test_found, 'Should include /test command')
432+
433+
config_file.get_user_commands = original_get_user_commands
434+
end)
435+
436+
it('uses default description when none provided', function()
437+
local config_file = require('opencode.config_file')
438+
local original_get_user_commands = config_file.get_user_commands
439+
440+
config_file.get_user_commands = function()
441+
return {
442+
['custom'] = {},
443+
}
444+
end
445+
446+
local slash_commands = api.get_slash_commands()
447+
448+
local custom_found = false
449+
for _, cmd in ipairs(slash_commands) do
450+
if cmd.slash_cmd == '/custom' then
451+
custom_found = true
452+
assert.equal('User command', cmd.desc)
453+
end
454+
end
455+
456+
assert.truthy(custom_found, 'Should include /custom command')
457+
458+
config_file.get_user_commands = original_get_user_commands
459+
end)
460+
461+
it('includes built-in slash commands alongside user commands', function()
462+
local config_file = require('opencode.config_file')
463+
local original_get_user_commands = config_file.get_user_commands
464+
465+
config_file.get_user_commands = function()
466+
return {
467+
['build'] = { description = 'Build the project' },
468+
}
469+
end
470+
471+
local slash_commands = api.get_slash_commands()
472+
473+
local help_found = false
474+
local build_found = false
475+
476+
for _, cmd in ipairs(slash_commands) do
477+
if cmd.slash_cmd == '/help' then
478+
help_found = true
479+
elseif cmd.slash_cmd == '/build' then
480+
build_found = true
481+
end
482+
end
483+
484+
assert.truthy(help_found, 'Should include built-in /help command')
485+
assert.truthy(build_found, 'Should include user /build command')
486+
487+
config_file.get_user_commands = original_get_user_commands
488+
end)
489+
end)
337490
end)

0 commit comments

Comments
 (0)