diff --git a/lua/opencode/api_client.lua b/lua/opencode/api_client.lua index 91b1c9a0..a09218a0 100644 --- a/lua/opencode/api_client.lua +++ b/lua/opencode/api_client.lua @@ -37,6 +37,10 @@ function OpencodeApiClient:_ensure_base_url() end end + if not state.opencode_server.url then + return false + end + self.base_url = state.opencode_server.url:gsub('/$', '') return true end @@ -49,7 +53,7 @@ end --- @return Promise promise function OpencodeApiClient:_call(endpoint, method, body, query) if not self:_ensure_base_url() then - return nil + return require('opencode.promise').new():reject('No server base url') end local url = self.base_url .. endpoint @@ -221,7 +225,7 @@ end --- Create and send a new message to a session --- @param id string Session ID (required) ---- @param message_data {messageID?: string, model?: {providerID: string, modelID: string}, agent?: string, system?: string, tools?: table, parts: Part[]} Message creation data +--- @param message_data {messageID?: string, model?: {providerID: string, modelID: string}, agent?: string, system?: string, tools?: table, parts: OpencodeMessagePart[]} Message creation data --- @param directory string|nil Directory path --- @return Promise<{info: MessageInfo, parts: OpencodeMessagePart[]}> function OpencodeApiClient:create_message(id, message_data, directory) @@ -393,7 +397,7 @@ function OpencodeApiClient:subscribe_to_events(directory, on_event) chunk = chunk:gsub('^data:%s*', '') local ok, event = pcall(vim.json.decode, vim.trim(chunk)) if ok and event then - on_event(event) + on_event(event --[[@as table]]) end end) end diff --git a/lua/opencode/config_file.lua b/lua/opencode/config_file.lua index d5aa6687..6cd0ccf7 100644 --- a/lua/opencode/config_file.lua +++ b/lua/opencode/config_file.lua @@ -59,15 +59,15 @@ end function M.get_model_info(provider, model) local config_file = require('opencode.config_file') local providers = config_file.get_opencode_providers() or {} - providers = vim.tbl_filter(function(p) + local filtered_providers = vim.tbl_filter(function(p) return p.id == provider - end, providers) + end, providers.providers) - if #providers == 0 then + if #filtered_providers == 0 then return nil end - return providers[1] and providers[1].models and providers[1].models[model] or nil + return filtered_providers[1] and filtered_providers[1].models and filtered_providers[1].models[model] or nil end function M.get_opencode_agents() diff --git a/lua/opencode/context.lua b/lua/opencode/context.lua index 926af7e4..7e2b43da 100644 --- a/lua/opencode/context.lua +++ b/lua/opencode/context.lua @@ -56,7 +56,7 @@ end ---@param context_key string ---@return boolean function M.is_context_enabled(context_key) - local is_enabled = vim.tbl_get(config, 'context', context_key, 'enabled') + local is_enabled = vim.tbl_get(config --[[@as table]], 'context', context_key, 'enabled') local is_state_enabled = vim.tbl_get(state, 'current_context_config', context_key, 'enabled') if is_state_enabled ~= nil then @@ -76,7 +76,7 @@ function M.get_diagnostics(buf) return {} end - local global_conf = vim.tbl_get(config, 'context', 'diagnostics') or {} + local global_conf = vim.tbl_get(config --[[@as table]], 'context', 'diagnostics') or {} local diagnostic_conf = vim.tbl_deep_extend('force', global_conf, current_conf) or {} local severity_levels = {} @@ -264,7 +264,8 @@ function M.get_current_cursor_data(buf, win) end local cursor_pos = vim.fn.getcurpos(win) - local cursor_content = vim.trim(vim.api.nvim_buf_get_lines(buf, cursor_pos[2] - 1, cursor_pos[2], false)[1] or '') + local start_line = (cursor_pos[2] - 1) --[[@as integer]] + local cursor_content = vim.trim(vim.api.nvim_buf_get_lines(buf, start_line, cursor_pos[2], false)[1] or '') return { line = cursor_pos[2], column = cursor_pos[3], line_content = cursor_content } end @@ -384,7 +385,6 @@ end function M.format_message(prompt, opts) opts = opts or config.context local context = M.delta_context(opts) - context.prompt = prompt local parts = { { type = 'text', text = prompt } } @@ -430,7 +430,7 @@ end --- Extracts context from an OpencodeMessage (with parts) ---@param message { parts: OpencodeMessagePart[] } ----@return { prompt: string, selected_text: string|nil, current_file: string|nil, mentioned_files: string[]|nil} +---@return { prompt: string|nil, selected_text: string|nil, current_file: string|nil, mentioned_files: string[]|nil} function M.extract_from_opencode_message(message) local ctx = { prompt = nil, selected_text = nil, current_file = nil } diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index 026b134a..5d4ea805 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -99,6 +99,10 @@ end --- @param prompt string The message prompt to send. --- @param opts? SendMessageOpts function M.send_message(prompt, opts) + if not state.active_session or not state.active_session.id then + return false + end + local mentioned_files = context.context.mentioned_files or {} local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files) diff --git a/lua/opencode/health.lua b/lua/opencode/health.lua index 3f73ac0a..0c77949d 100644 --- a/lua/opencode/health.lua +++ b/lua/opencode/health.lua @@ -111,6 +111,8 @@ local function check_configuration() return end + ---@cast config OpencodeConfig + local valid_positions = { 'left', 'right', 'top', 'bottom' } if not vim.tbl_contains(valid_positions, config.ui.position) then health.warn( diff --git a/lua/opencode/keymap.lua b/lua/opencode/keymap.lua index 03de87fc..502b6dbf 100644 --- a/lua/opencode/keymap.lua +++ b/lua/opencode/keymap.lua @@ -34,7 +34,7 @@ end -- Setup window-specific keymaps (shared helper for input/output windows) ---@param keymap_config table Window keymap configuration ----@param buf_id number Buffer ID to set keymaps for +---@param buf_id integer Buffer ID to set keymaps for function M.setup_window_keymaps(keymap_config, buf_id) if not vim.api.nvim_buf_is_valid(buf_id) then return diff --git a/lua/opencode/state.lua b/lua/opencode/state.lua index f143e031..36730b54 100644 --- a/lua/opencode/state.lua +++ b/lua/opencode/state.lua @@ -11,9 +11,9 @@ ---@field input_content table ---@field is_opencode_focused boolean ---@field last_focused_opencode_window string|nil ----@field last_input_window_position number|nil ----@field last_output_window_position number|nil ----@field last_code_win_before_opencode number|nil +---@field last_input_window_position integer[]|nil +---@field last_output_window_position integer[]|nil +---@field last_code_win_before_opencode integer|nil ---@field current_code_buf number|nil ---@field display_route any|nil ---@field current_mode string diff --git a/lua/opencode/throttling_emitter.lua b/lua/opencode/throttling_emitter.lua index 79d9b1ae..482932b8 100644 --- a/lua/opencode/throttling_emitter.lua +++ b/lua/opencode/throttling_emitter.lua @@ -4,7 +4,7 @@ local M = {} --- @field queue table[] Queue of pending items to be processed --- @field drain_scheduled boolean Whether a drain is already scheduled --- @field process_fn fun(table): nil Function to process the queue of events ---- @field drain_interval_ms number Interval between drains in milliseconds +--- @field drain_interval_ms integer Interval between drains in milliseconds --- @field enqueue fun(self: ThrottlingEmitter, item: any) Enqueue an item for batch processing --- @field clear fun(self: ThrottlingEmitter) Clear the queue and cancel any pending drain local ThrottlingEmitter = {} diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 5b9a1e16..7e8f864d 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -125,7 +125,7 @@ ---@class OpencodeDebugConfig ---@field enabled boolean ----@field capture_streamed_events any[] +---@field capture_streamed_events boolean --- @class OpencodeProviders --- @field [string] string[] diff --git a/lua/opencode/ui/autocmds.lua b/lua/opencode/ui/autocmds.lua index c42dc388..2457c867 100644 --- a/lua/opencode/ui/autocmds.lua +++ b/lua/opencode/ui/autocmds.lua @@ -59,7 +59,7 @@ function M.setup_resize_handler(windows) vim.api.nvim_create_autocmd('WinResized', { group = resize_group, callback = function(args) - local win = tonumber(args.match) + local win = tonumber(args.match) --[[@as integer]] if not win or not vim.api.nvim_win_is_valid(win) or not output_window.mounted() then return end diff --git a/lua/opencode/ui/footer.lua b/lua/opencode/ui/footer.lua index af972709..2622bb09 100644 --- a/lua/opencode/ui/footer.lua +++ b/lua/opencode/ui/footer.lua @@ -10,10 +10,10 @@ local loading_animation = require('opencode.ui.loading_animation') local M = {} function M.render() - local windows = state.windows - if not output_window.mounted(windows) or not M.mounted(windows) then + if not output_window.mounted() or not M.mounted() then return end + ---@cast state.windows { output_win: integer } local segments = {} @@ -46,23 +46,22 @@ function M.render() append_to_footer(restore_point_text) end - ---@diagnostic disable-next-line: need-check-nil - local win_width = vim.api.nvim_win_get_width(windows.output_win) + local win_width = vim.api.nvim_win_get_width(state.windows.output_win) local footer_text = table.concat(segments, ' | ') .. ' ' footer_text = string.rep(' ', win_width - #footer_text) .. footer_text M.set_content({ footer_text }) end ----@param windows OpencodeWindowState -function M._build_footer_win_config(windows) +---@param output_win integer +function M._build_footer_win_config(output_win) return { relative = 'win', - win = windows.output_win, + win = output_win, anchor = 'SW', - width = vim.api.nvim_win_get_width(windows.output_win), + width = vim.api.nvim_win_get_width(output_win), height = 1, - row = vim.api.nvim_win_get_height(windows.output_win) - 1, + row = vim.api.nvim_win_get_height(output_win) - 1, col = 0, focusable = false, style = 'minimal', @@ -82,7 +81,11 @@ local function on_job_count_changed(_, new, old) end function M.setup(windows) - windows.footer_win = vim.api.nvim_open_win(windows.footer_buf, false, M._build_footer_win_config(windows)) + if not windows.output_win then + return false + end + + windows.footer_win = vim.api.nvim_open_win(windows.footer_buf, false, M._build_footer_win_config(windows.output_win)) vim.api.nvim_set_option_value('winhl', 'Normal:OpenCodeHint', { win = windows.footer_win }) -- for stats changes @@ -95,7 +98,9 @@ function M.setup(windows) end function M.close() - if state.windows then + if M.mounted() then + ---@cast state.windows { footer_win: integer, footer_buf: integer } + pcall(vim.api.nvim_win_close, state.windows.footer_win, true) pcall(vim.api.nvim_buf_delete, state.windows.footer_buf, { force = true }) end @@ -112,7 +117,9 @@ function M.mounted(windows) return windows and windows.footer_win and vim.api.nvim_win_is_valid(windows.footer_win) + and windows.output_win and vim.api.nvim_win_is_valid(windows.output_win) + and windows.footer_buf end function M.update_window(windows) @@ -120,7 +127,7 @@ function M.update_window(windows) return end - vim.api.nvim_win_set_config(windows.footer_win, M._build_footer_win_config(windows)) + vim.api.nvim_win_set_config(windows.footer_win, M._build_footer_win_config(windows.output_win)) M.render() end @@ -132,29 +139,26 @@ function M.create_buf() end function M.clear() - local windows = state.windows - if not M.mounted() or not windows then + if not M.mounted() then return end + ---@cast state.windows { footer_buf: integer } local foot_ns_id = vim.api.nvim_create_namespace('opencode_footer') - vim.api.nvim_buf_clear_namespace(windows.footer_buf, foot_ns_id, 0, -1) + vim.api.nvim_buf_clear_namespace(state.windows.footer_buf, foot_ns_id, 0, -1) M.set_content({}) - -- - -- state.tokens_count = 0 - -- state.cost = 0 end function M.set_content(lines) - local windows = state.windows - if not M.mounted() or not windows then + if not M.mounted() then return end + ---@cast state.windows { footer_buf: integer } - vim.api.nvim_set_option_value('modifiable', true, { buf = windows.footer_buf }) - vim.api.nvim_buf_set_lines(windows.footer_buf, 0, -1, false, lines) - vim.api.nvim_set_option_value('modifiable', false, { buf = windows.footer_buf }) + vim.api.nvim_set_option_value('modifiable', true, { buf = state.windows.footer_buf }) + vim.api.nvim_buf_set_lines(state.windows.footer_buf, 0, -1, false, lines) + vim.api.nvim_set_option_value('modifiable', false, { buf = state.windows.footer_buf }) end return M diff --git a/lua/opencode/ui/mention.lua b/lua/opencode/ui/mention.lua index f45c4eee..88916ee4 100644 --- a/lua/opencode/ui/mention.lua +++ b/lua/opencode/ui/mention.lua @@ -1,5 +1,3 @@ -local config = require('opencode.config') - local M = {} local mentions_namespace = vim.api.nvim_create_namespace('OpencodeMentions') @@ -25,6 +23,7 @@ function M.highlight_all_mentions(buf, callback) if not mention_start then break end + ---@cast mention_start integer if callback then callback(line:sub(mention_start + 1, mention_end), row, mention_start, mention_end) @@ -82,7 +81,7 @@ function M.highlight_mentions_in_output(output, text, mentions, start_line) end_col = col_end, hl_group = 'OpencodeMention', priority = 1000, - }) + } --[[@as OutputExtmark]]) break end @@ -91,9 +90,14 @@ function M.highlight_mentions_in_output(output, text, mentions, start_line) end end end + local function insert_mention(windows, row, col, name) local current_line = vim.api.nvim_buf_get_lines(windows.input_buf, row - 1, row, false)[1] + if not current_line then + return + end + local insert_name = '@' .. name .. ' ' local new_line = current_line:sub(1, col) .. insert_name .. current_line:sub(col + 2) @@ -111,7 +115,7 @@ function M.mention(get_name) get_name(function(name) vim.schedule(function() - if not windows or not name then + if not windows or not windows.input_win or not name then return end local cursor_pos = vim.api.nvim_win_get_cursor(windows.input_win) diff --git a/lua/opencode/ui/output_window.lua b/lua/opencode/ui/output_window.lua index 82cd2cb1..c69b754f 100644 --- a/lua/opencode/ui/output_window.lua +++ b/lua/opencode/ui/output_window.lua @@ -24,16 +24,7 @@ end function M.mounted(windows) windows = windows or state.windows - if - not state.windows - or not state.windows.output_buf - or not state.windows.output_win - or not vim.api.nvim_win_is_valid(windows.output_win) - then - return false - end - - return true + return windows and windows.output_buf and windows.output_win and vim.api.nvim_win_is_valid(windows.output_win) end function M.setup(windows) @@ -66,6 +57,7 @@ function M.get_buf_line_count() if not M.mounted() then return 0 end + ---@cast state.windows { output_buf: integer } return vim.api.nvim_buf_line_count(state.windows.output_buf) end @@ -78,18 +70,14 @@ function M.set_lines(lines, start_line, end_line) if not M.mounted() then return end + ---@cast state.windows { output_buf: integer } start_line = start_line or 0 end_line = end_line or -1 - local windows = state.windows - if not windows or not windows.output_buf then - return - end - - vim.api.nvim_set_option_value('modifiable', true, { buf = windows.output_buf }) - vim.api.nvim_buf_set_lines(windows.output_buf, start_line, end_line, false, lines) - vim.api.nvim_set_option_value('modifiable', false, { buf = windows.output_buf }) + vim.api.nvim_set_option_value('modifiable', true, { buf = state.windows.output_buf }) + vim.api.nvim_buf_set_lines(state.windows.output_buf, start_line, end_line, false, lines) + vim.api.nvim_set_option_value('modifiable', false, { buf = state.windows.output_buf }) end ---Clear output buf extmarks @@ -97,9 +85,10 @@ end ---@param end_line? integer Line to clear until, defaults to -1 ---@param clear_all? boolean If true, clears all extmarks in the buffer function M.clear_extmarks(start_line, end_line, clear_all) - if not M.mounted() or not state.windows.output_buf then + if not M.mounted() then return end + ---@cast state.windows { output_buf: integer } start_line = start_line or 0 end_line = end_line or -1 @@ -108,12 +97,13 @@ function M.clear_extmarks(start_line, end_line, clear_all) end ---Apply extmarks to the output buffer ----@param extmarks table Extmarks indexed by line +---@param extmarks table Extmarks indexed by line ---@param line_offset? integer Line offset to apply to extmarks, defaults to 0 function M.set_extmarks(extmarks, line_offset) if not M.mounted() or not extmarks or type(extmarks) ~= 'table' then return end + ---@cast state.windows { output_buf: integer } line_offset = line_offset or 0 @@ -122,7 +112,7 @@ function M.set_extmarks(extmarks, line_offset) for line_idx, marks in pairs(extmarks) do for _, mark in ipairs(marks) do local actual_mark = type(mark) == 'function' and mark() or mark - local target_line = line_offset + line_idx + local target_line = line_offset + line_idx --[[@as integer]] if actual_mark.end_row then actual_mark.end_row = actual_mark.end_row + line_offset end @@ -130,12 +120,18 @@ function M.set_extmarks(extmarks, line_offset) if actual_mark.start_col then actual_mark.start_col = nil end + ---@cast actual_mark vim.api.keyset.set_extmark pcall(vim.api.nvim_buf_set_extmark, output_buf, M.namespace, target_line, start_col or 0, actual_mark) end end end function M.focus_output(should_stop_insert) + if not M.mounted() then + return + end + ---@cast state.windows { output_win: integer } + if should_stop_insert then vim.cmd('stopinsert') end @@ -146,6 +142,8 @@ function M.close() if M.mounted() then return end + ---@cast state.windows { output_win: integer, output_buf: integer } + pcall(vim.api.nvim_win_close, state.windows.output_win, true) pcall(vim.api.nvim_buf_delete, state.windows.output_buf, { force = true }) end diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 527a7c3a..22c42ca7 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -22,7 +22,7 @@ local trigger_on_data_rendered = require('opencode.util').debounce(function() return end - if not state.windows then + if not state.windows or not state.windows.output_buf or not state.windows.output_win then return end diff --git a/lua/opencode/ui/ui.lua b/lua/opencode/ui/ui.lua index 5410428c..ae76267b 100644 --- a/lua/opencode/ui/ui.lua +++ b/lua/opencode/ui/ui.lua @@ -23,6 +23,7 @@ function M.close_windows(windows) pcall(vim.api.nvim_del_augroup_by_name, 'OpencodeResize') pcall(vim.api.nvim_del_augroup_by_name, 'OpencodeWindows') + ---@cast windows { input_win: integer, output_win: integer, input_buf: integer, output_buf: integer } pcall(vim.api.nvim_win_close, windows.input_win, true) pcall(vim.api.nvim_win_close, windows.output_win, true) pcall(vim.api.nvim_buf_delete, windows.input_buf, { force = true }) @@ -111,7 +112,7 @@ end function M.focus_input(opts) opts = opts or {} local windows = state.windows - if not windows then + if not windows or not windows.input_win then return end @@ -130,7 +131,7 @@ end function M.focus_output(opts) opts = opts or {} local windows = state.windows - if not windows then + if not windows or not windows.output_win then return end @@ -225,7 +226,7 @@ end function M.toggle_pane() local current_win = vim.api.nvim_get_current_win() - if current_win == state.windows.input_win then + if state.windows and current_win == state.windows.input_win then output_window.focus_output(true) else input_window.focus_input() diff --git a/lua/opencode/util.lua b/lua/opencode/util.lua index 8498bff2..0bc8e6d5 100644 --- a/lua/opencode/util.lua +++ b/lua/opencode/util.lua @@ -66,72 +66,6 @@ function M.indent_code_block(text) return vim.trim(table.concat(content, '\n')) end --- Get timezone offset in seconds for various timezone formats -function M.get_timezone_offset(timezone) - -- Handle numeric timezone formats (+HHMM, -HHMM) - if timezone:match('^[%+%-]%d%d:?%d%d$') then - local sign = timezone:sub(1, 1) == '+' and 1 or -1 - local hours = tonumber(timezone:match('^[%+%-](%d%d)')) - local mins = tonumber(timezone:match('^[%+%-]%d%d:?(%d%d)$') or '00') - return sign * (hours * 3600 + mins * 60) - end - - -- Map of common timezone abbreviations to their offset in seconds from UTC - local timezone_map = { - -- Zero offset timezones - ['UTC'] = 0, - ['GMT'] = 0, - - -- North America - ['EST'] = -5 * 3600, - ['EDT'] = -4 * 3600, - ['CST'] = -6 * 3600, - ['CDT'] = -5 * 3600, - ['MST'] = -7 * 3600, - ['MDT'] = -6 * 3600, - ['PST'] = -8 * 3600, - ['PDT'] = -7 * 3600, - ['AKST'] = -9 * 3600, - ['AKDT'] = -8 * 3600, - ['HST'] = -10 * 3600, - - -- Europe - ['WET'] = 0, - ['WEST'] = 1 * 3600, - ['CET'] = 1 * 3600, - ['CEST'] = 2 * 3600, - ['EET'] = 2 * 3600, - ['EEST'] = 3 * 3600, - ['MSK'] = 3 * 3600, - ['BST'] = 1 * 3600, - - -- Asia & Middle East - ['IST'] = 5.5 * 3600, - ['PKT'] = 5 * 3600, - ['HKT'] = 8 * 3600, - ['PHT'] = 8 * 3600, - ['JST'] = 9 * 3600, - ['KST'] = 9 * 3600, - - -- Australia & Pacific - ['AWST'] = 8 * 3600, - ['ACST'] = 9.5 * 3600, - ['AEST'] = 10 * 3600, - ['AEDT'] = 11 * 3600, - ['NZST'] = 12 * 3600, - ['NZDT'] = 13 * 3600, - } - - -- Handle special cases for ambiguous abbreviations - if timezone == 'CST' and not timezone_map[timezone] then - -- In most contexts, CST refers to Central Standard Time (US) - return -6 * 3600 - end - - -- Return the timezone offset or default to UTC (0) - return timezone_map[timezone] or 0 -end - -- Reset all ANSI styling function M.ansi_reset() return '\27[0m' @@ -199,17 +133,14 @@ function M.format_time(timestamp) timestamp = math.floor(timestamp / 1000) end - local now = os.time() - local today_start = os.time(os.date('*t', now)) - - os.date('*t', now).hour * 3600 - - os.date('*t', now).min * 60 - - os.date('*t', now).sec + local local_t = os.date('*t') --[[@as std.osdateparam]] + local today_start = os.time({ year = local_t.year, month = local_t.month, day = local_t.day }) if timestamp >= today_start then - return os.date('%I:%M %p', timestamp) - else - return os.date('%d %b %Y %I:%M %p', timestamp) + return os.date('%I:%M %p', timestamp) --[[@as string]] end + + return os.date('%d %b %Y %I:%M %p', timestamp) --[[@as string]] end function M.index_of(tbl, value) @@ -410,6 +341,7 @@ function M.check_prompt_allowed(guard_callback, mentioned_files) return false, 'prompt_guard must return a boolean' end + ---@cast result boolean return result, nil end