diff --git a/README.md b/README.md index a865ff4e..7e87a39c 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,11 @@ require('opencode').setup({ enabled = false, }, }, + logging = { + enabled = false, + level = 'warn', -- debug, info, warn, error + outfile = nil, + }, debug = { enabled = false, -- Enable debug messages in the output window capture_streamed_events = false, diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 14d131ff..21f58875 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -216,6 +216,11 @@ M.defaults = { enabled = false, }, }, + logging = { + enabled = false, + level = 'info', -- debug, info, warn, error + outfile = nil, + }, debug = { enabled = false, capture_streamed_events = false, diff --git a/lua/opencode/log.lua b/lua/opencode/log.lua new file mode 100644 index 00000000..a19a70c1 --- /dev/null +++ b/lua/opencode/log.lua @@ -0,0 +1,54 @@ +local M = {} + +local config = require('opencode.config') +local log_path = config.logging and config.logging.outfile or vim.fn.stdpath('log') .. '/opencode.log' +local level = config.logging and config.logging.level or 'warn' + +local logger = require('plenary.log').new({ + plugin = 'opencode', + level = level:lower(), + use_console = false, + outfile = log_path, +}) + +local function get_logger() + return logger +end + +function M.debug(msg, ...) + if not config.logging.enabled then + return + end + get_logger().debug(string.format(msg, ...)) +end + +function M.info(msg, ...) + if not config.logging.enabled then + return + end + get_logger().info(string.format(msg, ...)) +end + +function M.warn(msg, ...) + if not config.logging.enabled then + return + end + get_logger().warn(string.format(msg, ...)) +end + +function M.error(msg, ...) + if not config.logging.enabled then + return + end + get_logger().error(string.format(msg, ...)) +end + +--- @return string +function M.get_path() + if not config.logging.enabled then + return + end + return log_path +end + +return M diff --git a/lua/opencode/opencode_server.lua b/lua/opencode/opencode_server.lua index ea5f6116..e63a1b6d 100644 --- a/lua/opencode/opencode_server.lua +++ b/lua/opencode/opencode_server.lua @@ -23,10 +23,9 @@ local function ensure_vim_leave_autocmd() group = vim.api.nvim_create_augroup('OpencodeVimLeavePre', { clear = true }), callback = function() local state = require('opencode.state') + local log = require('opencode.log') if state.opencode_server then - pcall(function() - state.opencode_server:shutdown():wait(2000) - end) + state.opencode_server:shutdown() end end, }) @@ -35,6 +34,7 @@ end --- Create a new ServerJob instance --- @return OpencodeServer function OpencodeServer.new() + local log = require('opencode.log') ensure_vim_leave_autocmd() return setmetatable({ @@ -50,35 +50,42 @@ function OpencodeServer:is_running() return self.job and self.job.pid ~= nil end ---- Clean up this server job ---- @return Promise +local function kill_process(pid, signal, desc) + local log = require('opencode.log') + local ok, err = pcall(vim.uv.kill, pid, signal) + log.debug('shutdown: %s pid=%d sig=%d ok=%s err=%s', desc, pid, signal, tostring(ok), tostring(err)) + return ok, err +end + function OpencodeServer:shutdown() + local log = require('opencode.log') if self.shutdown_promise:is_resolved() then return self.shutdown_promise end if self.job and self.job.pid then - local job = self.job - - self.job = nil - self.url = nil - self.handle = nil + ---@cast self.job vim.SystemObj + local pid = self.job.pid + local children = vim.api.nvim_get_proc_children(pid) - pcall(function() - job:kill(15) -- SIGTERM - end) + if #children > 0 then + log.debug('shutdown: process pid=%d has %d children (%s)', pid, #children, vim.inspect(children)) - vim.defer_fn(function() - if job and job.pid then - pcall(function() - job:kill(9) -- SIGKILL - end) + for _, cid in ipairs(children) do + kill_process(cid, 15, 'SIGTERM child') end - end, 500) + end + + kill_process(pid, 15, 'SIGTERM') + kill_process(pid, 9, 'SIGKILL') else - self.shutdown_promise:resolve(true) + log.debug('shutdown: no job running') end + self.job = nil + self.url = nil + self.handle = nil + self.shutdown_promise:resolve(true) return self.shutdown_promise end @@ -93,6 +100,7 @@ end --- @return Promise function OpencodeServer:spawn(opts) opts = opts or {} + local log = require('opencode.log') self.job = vim.system({ config.opencode_executable, @@ -110,6 +118,7 @@ function OpencodeServer:spawn(opts) self.url = url self.spawn_promise:resolve(self) safe_call(opts.on_ready, self.job, url) + log.debug('spawn: server ready at url=%s', url) end end end, @@ -142,6 +151,7 @@ function OpencodeServer:spawn(opts) self.handle = self.job and self.job.pid + log.debug('spawn: started job with pid=%s', tostring(self.job and self.job.pid)) return self.spawn_promise end diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 69067c13..7763b366 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -189,6 +189,11 @@ ---@field default_agent? string -- Use current mode if nil ---@field instructions? string[] -- Custom instructions for quick chat +---@class OpencodeLoggingConfig +---@field enabled boolean +---@field level 'debug' | 'info' | 'warn' | 'error' +---@field outfile string|nil + ---@class OpencodeConfig ---@field preferred_picker 'telescope' | 'fzf' | 'mini.pick' | 'snacks' | 'select' | nil ---@field preferred_completion 'blink' | 'nvim-cmp' | 'vim_complete' | nil -- Preferred completion strategy for mentons and commands @@ -200,6 +205,7 @@ ---@field keymap OpencodeKeymap ---@field ui OpencodeUIConfig ---@field context OpencodeContextConfig +---@field logging OpencodeLoggingConfig ---@field debug OpencodeDebugConfig ---@field prompt_guard? fun(mentioned_files: string[]): boolean ---@field hooks OpencodeHooks