diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..a1de830 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,5 @@ +{ + "diagnostics": { + "globals": ["vim"] + } +} diff --git a/lua/opencode-context/config.lua b/lua/opencode-context/config.lua new file mode 100644 index 0000000..1584776 --- /dev/null +++ b/lua/opencode-context/config.lua @@ -0,0 +1,29 @@ +local default_config = { + -- Tmux settings + tmux_target = nil, -- Manual override: "session:window.pane" + auto_detect_pane = true, -- Auto-detect opencode pane in current window + + -- Prompt window configuration + win = { + icon = " ", + input = { + width = 0.60, + height = 0.30, + }, + persistent = { + height = 2, + }, + }, +} + +local M = vim.deepcopy(default_config) + +M.update = function(opts) + local newconf = vim.tbl_deep_extend("force", default_config, opts or {}) + + for k, v in pairs(newconf) do + M[k] = v + end +end + +return M diff --git a/lua/opencode-context/init.lua b/lua/opencode-context/init.lua index 239cb0d..75c523d 100644 --- a/lua/opencode-context/init.lua +++ b/lua/opencode-context/init.lua @@ -1,11 +1,6 @@ local M = {} local ui = require("opencode-context.ui") - -M.config = { - -- Tmux settings - tmux_target = nil, -- Manual override: "session:window.pane" - auto_detect_pane = true, -- Auto-detect opencode pane in current window -} +local config = require("opencode-context.config") local function get_current_file_path() local bufnr = vim.api.nvim_get_current_buf() @@ -42,8 +37,8 @@ end -- returns the buffer, relative file path and cursor local function get_cursor() local function is_floating(winid) - local config = vim.api.nvim_win_get_config(winid) - return config.relative ~= "" + local win_cfg = vim.api.nvim_win_get_config(winid) + return win_cfg.relative ~= "" end local current_win = vim.api.nvim_get_current_win() @@ -186,11 +181,11 @@ end local function find_opencode_pane() -- If manual target is set, use it - if M.config.tmux_target then - return M.config.tmux_target + if config.tmux_target then + return config.tmux_target end - if not M.config.auto_detect_pane then + if not config.auto_detect_pane then return nil end @@ -281,19 +276,12 @@ function M.send_prompt() local default_text = "" if mode == "v" or mode == "V" or mode == "\22" then -- \22 is visual block mode default_text = "@selection " + else + default_text = "@cursor " end - vim.ui.input({ - prompt = "Enter prompt for opencode (use @file, @buffers, @cursor, @selection, @diagnostics): ", - default = default_text, - }, function(input) - if not input or input == "" then - return - end - - local processed_prompt = replace_placeholders(input) - send_to_opencode(processed_prompt) - end) + M.show_input_prompt() + M.add_text_to_prompt(default_text) end function M.toggle_mode() @@ -328,19 +316,27 @@ local function create_send_callback() end function M.show_persistent_prompt() - ui.show_persistent_prompt(create_send_callback()) + ui.show_prompt(create_send_callback(), "persistent") end function M.hide_persistent_prompt() - ui.hide_persistent_prompt() + ui.hide_prompt() end function M.toggle_persistent_prompt() - ui.toggle_persistent_prompt(create_send_callback()) + ui.toggle_prompt(create_send_callback(), "persistent") +end + +function M.show_input_prompt() + ui.show_prompt(create_send_callback(), "input") +end + +function M.add_text_to_prompt(text) + return ui.add_text_at_cursor(text) end function M.setup(opts) - M.config = vim.tbl_deep_extend("force", M.config, opts or {}) + require("opencode-context.config").update(opts) end return M diff --git a/lua/opencode-context/ui.lua b/lua/opencode-context/ui.lua index 3be8990..4887dde 100644 --- a/lua/opencode-context/ui.lua +++ b/lua/opencode-context/ui.lua @@ -1,9 +1,41 @@ +local config = require("opencode-context.config") + local M = {} -- UI state local prompt_win = nil local prompt_buf = nil +local safe_size = function(size, min, max) + local safe_size = size + + -- If less or equal to zero, return minimum allowed + -- size. + if safe_size <= 0 then + return min + end + + -- If size is between 0 and 1, assume is a percentage of + -- max allowed size. + if safe_size < 1 then + safe_size = math.floor(max * safe_size) + end + + return safe_size +end + +local function safe_height(height) + local min_height = 1 + local max_height = math.floor(0.9 * vim.o.lines) + return safe_size(height, min_height, max_height) +end + +local function safe_width(width) + local min_width = math.floor(0.1 * vim.o.columns) + local max_width = math.floor(0.9 * vim.o.columns) + return safe_size(width, min_width, max_width) +end + local function create_prompt_buffer() if prompt_buf and vim.api.nvim_buf_is_valid(prompt_buf) then return prompt_buf @@ -20,7 +52,7 @@ local function create_prompt_buffer() return prompt_buf end -local function setup_prompt_keymaps(bufnr, send_callback) +local function setup_prompt_keymaps(bufnr, send_callback, type) local opts = { buffer = bufnr, silent = true } -- Enter to send prompt @@ -41,20 +73,32 @@ local function setup_prompt_keymaps(bufnr, send_callback) vim.api.nvim_win_set_cursor(prompt_win, { 1, 0 }) end end + + if type == "input" then + vim.cmd("stopinsert") + M.hide_prompt() + end end, opts) -- Escape to return to normal mode vim.keymap.set("i", "", function() vim.cmd("stopinsert") + if type == "input" then + if prompt_buf and vim.api.nvim_buf_is_valid(prompt_buf) then + -- Clear prompt buffer + vim.api.nvim_buf_set_lines(prompt_buf, 0, -1, false, { "" }) + end + M.hide_prompt() + end end, opts) -- Close prompt window with 'q' in normal mode vim.keymap.set("n", "q", function() - M.hide_persistent_prompt() + M.hide_prompt() end, opts) end -function M.show_persistent_prompt(send_callback) +function M.show_prompt(send_callback, type) if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then vim.api.nvim_set_current_win(prompt_win) vim.cmd("startinsert") @@ -63,21 +107,32 @@ function M.show_persistent_prompt(send_callback) local bufnr = create_prompt_buffer() - -- Get editor dimensions - local width = vim.o.columns - local height = 1 -- Increased to accommodate 2-line title - local row = vim.o.lines - height - 3 -- Position above statusline/powerline + local relative = "cursor" + local width = safe_width(config.win.input.width) + local height = safe_height(config.win.input.height) + local row = 0 + local column = 0 + local border = "rounded" + + if type == "persistent" then + relative = "editor" + width = safe_width(vim.o.columns) + height = safe_height(config.win.persistent.height) + row = vim.o.lines - height - 3 -- Position above statusline/powerline + column = 2 + border = "solid" + end -- Create floating window prompt_win = vim.api.nvim_open_win(bufnr, true, { - relative = "editor", + relative = relative, width = width, height = height, row = row, - col = 2, + col = column, style = "minimal", - border = "solid", - title = " OpenCode - use @file, @buffers, @cursor, @selection, @diagnostics, @here", + border = border, + title = config.win.icon .. " OpenCode - use @file, @buffers, @cursor, @selection, @diagnostics, @here", title_pos = "left", }) @@ -86,7 +141,7 @@ function M.show_persistent_prompt(send_callback) vim.wo[prompt_win].linebreak = true -- Setup keymaps for this buffer - setup_prompt_keymaps(bufnr, send_callback) + setup_prompt_keymaps(bufnr, send_callback, type) -- Start in insert mode vim.cmd("startinsert") @@ -99,7 +154,7 @@ function M.show_persistent_prompt(send_callback) if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then local current_win = vim.api.nvim_get_current_win() if current_win ~= prompt_win then - M.hide_persistent_prompt() + M.hide_prompt() end end end, 100) @@ -108,18 +163,18 @@ function M.show_persistent_prompt(send_callback) }) end -function M.hide_persistent_prompt() +function M.hide_prompt() if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then vim.api.nvim_win_close(prompt_win, false) prompt_win = nil end end -function M.toggle_persistent_prompt(send_callback) +function M.toggle_prompt(send_callback, type) if prompt_win and vim.api.nvim_win_is_valid(prompt_win) then - M.hide_persistent_prompt() + M.hide_prompt() else - M.show_persistent_prompt(send_callback) + M.show_prompt(send_callback, type) end end @@ -127,4 +182,82 @@ function M.is_prompt_visible() return prompt_win and vim.api.nvim_win_is_valid(prompt_win) end +function M.add_text_at_cursor(text) + if not prompt_buf or not vim.api.nvim_buf_is_valid(prompt_buf) then + return false + end + + if not prompt_win or not vim.api.nvim_win_is_valid(prompt_win) then + return false + end + + if not text or text == "" then + return false + end + + -- Clear prompt buffer + vim.api.nvim_buf_set_lines(prompt_buf, 0, -1, false, { "" }) + + -- Current cursor position + local row = 0 + local col = 0 + + -- Get current buffer content + local lines = vim.api.nvim_buf_get_lines(prompt_buf, 0, -1, false) + + -- Split the text to insert into lines + local text_lines = vim.split(text, "\n", { plain = true }) + + -- Handle single line insertion + if #text_lines == 1 then + local current_line = lines[row + 1] or "" + local before = current_line:sub(1, col) + local after = current_line:sub(col + 1) + lines[row + 1] = before .. text .. after + + -- Update buffer + vim.api.nvim_buf_set_lines(prompt_buf, 0, -1, false, lines) + + -- Move cursor to end of inserted text + vim.api.nvim_win_set_cursor(prompt_win, { row + 1, col + #text }) + else + -- Handle multi-line insertion + local current_line = lines[row + 1] or "" + local before = current_line:sub(1, col) + local after = current_line:sub(col + 1) + + -- Create newlines array + local new_lines = {} + + -- Add lines before insertion point + for i = 1, row do + new_lines[i] = lines[i] + end + + -- Add first line of insertion (with before text) + new_lines[row + 1] = before .. text_lines[1] + + -- Add middle lines of insertion + for i = 2, #text_lines - 1 do + new_lines[row + i] = text_lines[i] + end + -- Add last line of insertion (with after text) + local last_text_line = text_lines[#text_lines] + new_lines[row + #text_lines] = last_text_line .. after + + -- Add remaining lines after insertion point + for i = row + 2, #lines do + new_lines[row + #text_lines + (i - row - 1)] = lines[i] + end + + -- Update buffer + vim.api.nvim_buf_set_lines(prompt_buf, 0, -1, false, new_lines) + + -- Move cursor to end of inserted text + vim.api.nvim_win_set_cursor(prompt_win, { row + #text_lines, #last_text_line }) + end + + return true +end + return M