Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,708 changes: 101 additions & 1,607 deletions lua/opencode/api.lua

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions lua/opencode/commands/completion_providers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
local M = {}

---@type table<string, fun(): string[]>
local providers = {
user_commands = function()
local config_file = require('opencode.config_file')
local user_commands = config_file.get_user_commands():wait()
if not user_commands then
return {}
end

local names = vim.tbl_keys(user_commands)
table.sort(names)
return names
end,
}

---@param provider_id string
---@return (fun(): string[])|nil
function M.get(provider_id)
return providers[provider_id]
end

return M
194 changes: 194 additions & 0 deletions lua/opencode/commands/dispatch.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
local handlers = require('opencode.commands.handlers')
local config = require('opencode.config')
local registry = require('opencode.registry')
local state = require('opencode.state')

local M = {}

local lifecycle_hook_keys = {
before = 'on_command_before',
after = 'on_command_after',
error = 'on_command_error',
finally = 'on_command_finally',
}

local lifecycle_event_names = {
before = 'custom.command.before',
after = 'custom.command.after',
error = 'custom.command.error',
finally = 'custom.command.finally',
}

---@param event_name string
---@param payload table
local function emit_lifecycle_event(event_name, payload)
local manager = state.event_manager
if manager and type(manager.emit) == 'function' then
pcall(manager.emit, manager, event_name, payload)
end
end

---@param stage OpencodeCommandLifecycleStage
---@param hook_id string
---@param hook_fn OpencodeCommandDispatchHook
---@param ctx OpencodeCommandDispatchContext
---@return OpencodeCommandDispatchContext
local function run_hook(stage, hook_id, hook_fn, ctx)
local ok, next_ctx_or_err = pcall(hook_fn, ctx)
if not ok then
emit_lifecycle_event('custom.command.hook_error', {
stage = stage,
hook_id = hook_id,
error = tostring(next_ctx_or_err),
context = ctx,
})
return ctx
end

if type(next_ctx_or_err) == 'table' then
return next_ctx_or_err
end

return ctx
end

---@param stage OpencodeCommandLifecycleStage
---@return { id: string, fn: OpencodeCommandDispatchHook }[]
local function collect_registry_stage_hooks(stage)
local hooks = registry.get_hooks()
local hook_names = vim.tbl_keys(hooks)
table.sort(hook_names)

local stage_hooks = {}
for _, hook_name in ipairs(hook_names) do
local hook_spec = hooks[hook_name]
local hook_fn

if type(hook_spec) == 'table' then
hook_fn = hook_spec[stage]
if type(hook_fn) ~= 'function' then
hook_fn = hook_spec[lifecycle_hook_keys[stage]]
end
elseif type(hook_spec) == 'function' and (hook_name == stage or hook_name == lifecycle_hook_keys[stage]) then
hook_fn = hook_spec
end

if type(hook_fn) == 'function' then
stage_hooks[#stage_hooks + 1] = {
id = 'registry:' .. hook_name,
fn = hook_fn,
}
end
end

return stage_hooks
end

---@param stage OpencodeCommandLifecycleStage
---@param ctx OpencodeCommandDispatchContext
---@return OpencodeCommandDispatchContext
local function run_hook_pipeline(stage, ctx)
local next_ctx = ctx

for _, hook in ipairs(collect_registry_stage_hooks(stage)) do
next_ctx = run_hook(stage, hook.id, hook.fn, next_ctx)
end

local hooks = config.hooks
if hooks then
local config_hook_name = lifecycle_hook_keys[stage]
local config_hook = hooks[config_hook_name]
if type(config_hook) == 'function' then
next_ctx = run_hook(stage, 'config:' .. config_hook_name, config_hook, next_ctx)
end
end

emit_lifecycle_event(lifecycle_event_names[stage], next_ctx)

return next_ctx
end

---@param parsed OpencodeCommandParseResult
---@param api OpencodeCommandApi
---@return OpencodeCommandDispatchResult
function M.command(parsed, api)
---@type OpencodeCommandDispatchContext
local ctx = {
parsed = parsed,
intent = parsed.intent,
args = parsed.intent and parsed.intent.args or nil,
range = parsed.intent and parsed.intent.range or nil,
}

if not parsed.ok then
ctx.error = parsed.error
ctx = run_hook_pipeline('error', ctx)
ctx = run_hook_pipeline('finally', ctx)

return {
ok = false,
error = ctx.error,
}
end

ctx = run_hook_pipeline('before', ctx)

local intent = ctx.intent or parsed.intent
local args = ctx.args
if args == nil and intent then
args = intent.args
end

local range = ctx.range
if range == nil and intent then
range = intent.range
end

if intent then
intent.args = args or {}
intent.range = range
ctx.intent = intent
end

local ok, result, handler_error = handlers.execute(intent.handler_id, api, intent.args, intent.range)

if not ok then
ctx.error = {
code = 'unknown_handler',
message = 'Unknown command handler: ' .. intent.handler_id,
handler_id = intent.handler_id,
}
ctx = run_hook_pipeline('error', ctx)
ctx = run_hook_pipeline('finally', ctx)

return {
ok = false,
intent = ctx.intent,
error = ctx.error,
}
end

if handler_error then
ctx.error = handler_error
ctx = run_hook_pipeline('error', ctx)
ctx = run_hook_pipeline('finally', ctx)

return {
ok = false,
intent = ctx.intent,
error = ctx.error,
}
end

ctx.result = result
ctx = run_hook_pipeline('after', ctx)
ctx = run_hook_pipeline('finally', ctx)

return {
ok = true,
result = ctx.result,
intent = ctx.intent,
}
end

return M
104 changes: 104 additions & 0 deletions lua/opencode/commands/handlers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
local M = {}
local registry = require('opencode.registry')

local handler_group_modules = {
'opencode.commands.handlers.window',
'opencode.commands.handlers.agent',
'opencode.commands.handlers.workflow',
'opencode.commands.handlers.session',
'opencode.commands.handlers.diff',
'opencode.commands.handlers.permission',
}

---@return OpencodeCommandHandlerMap
local function get_handlers()
local all_handlers = {}
local handler_sources = {}

for _, module_name in ipairs(handler_group_modules) do
local module_exports = require(module_name)
local module_handlers = module_exports.handlers or module_exports
for handler_id, handler in pairs(module_handlers) do
if handler_sources[handler_id] then
error(
string.format(
"Duplicate handler_id '%s' in modules '%s' and '%s'",
handler_id,
handler_sources[handler_id],
module_name
)
)
end

handler_sources[handler_id] = module_name
all_handlers[handler_id] = handler
end
end

for handler_id, handler in pairs(registry.get_handlers()) do
if handler_sources[handler_id] then
error(
string.format(
"Duplicate handler_id '%s' in modules '%s' and extension registry",
handler_id,
handler_sources[handler_id]
)
)
end

handler_sources[handler_id] = 'extension registry'
all_handlers[handler_id] = handler
end

return all_handlers
end

-- Validate handler graph during module load for fast-fail behavior.
get_handlers()

---@param handler_id string
---@return OpencodeCommandHandler|nil
function M.get(handler_id)
return get_handlers()[handler_id]
end

---@return string[]
function M.ids()
local ids = vim.tbl_keys(get_handlers())
table.sort(ids)
return ids
end

---@param handler_id string
---@param api OpencodeCommandApi
---@param args string[]
---@param range? OpencodeSelectionRange
---@return boolean, any, OpencodeCommandDispatchError|nil
function M.execute(handler_id, api, args, range)
local handler = M.get(handler_id)
if not handler then
return false, nil, nil
end

local ok, result = pcall(handler, api, args or {}, range)
if ok then
return true, result, nil
end

if type(result) == 'table' and type(result.code) == 'string' and type(result.message) == 'string' then
if not result.handler_id then
result.handler_id = handler_id
end
return true, nil, result
end

return true,
nil,
{
code = 'handler_exception',
message = 'Command handler failed: ' .. handler_id,
handler_id = handler_id,
}
end

return M
Loading
Loading