Skip to content
Open
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
5 changes: 5 additions & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"diagnostics": {
"globals": ["vim"]
}
}
29 changes: 29 additions & 0 deletions lua/opencode-context/config.lua
Original file line number Diff line number Diff line change
@@ -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
48 changes: 22 additions & 26 deletions lua/opencode-context/init.lua
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
167 changes: 150 additions & 17 deletions lua/opencode-context/ui.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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", "<Esc>", 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")
Expand All @@ -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",
})

Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -108,23 +163,101 @@ 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

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