Skip to content
Merged
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
8 changes: 8 additions & 0 deletions lua/opencode/commands/handlers/workflow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ function M.actions.clear_files()
vim.notify('Mentioned files cleared', vim.log.levels.INFO)
end

function M.actions.jump_to_file()
require('opencode.ui.navigation').jump_to_file_at_cursor()
end

function M.actions.toggle_tool_output()
local action_text = config.ui.output.tools.show_output and 'Hiding' or 'Showing'
vim.notify(action_text .. ' tool output display', vim.log.levels.INFO)
Expand Down Expand Up @@ -476,6 +480,10 @@ M.command_defs = {
desc = 'Clear only mentioned files from context',
execute = M.actions.clear_files,
},
jump_to_file = {
desc = 'Jump to file at cursor in output window',
execute = M.actions.jump_to_file,
},
debug_output = {
desc = 'Open raw output debug view',
execute = M.actions.debug_output,
Expand Down
140 changes: 140 additions & 0 deletions lua/opencode/ui/navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local M = {}

local state = require('opencode.state')
local renderer = require('opencode.ui.renderer')
local output_window = require('opencode.ui.output_window')

function M.goto_message_by_id(message_id)
require('opencode.ui.ui').focus_output()
Expand Down Expand Up @@ -61,4 +62,143 @@ function M.goto_prev_message()
vim.api.nvim_win_set_cursor(win, { 1, 0 })
end

---@param raw string
local function resolve_path(raw)
if vim.uv.fs_stat(raw) then
return raw
end
local absolute = vim.fn.fnamemodify(raw, ':p')
if vim.uv.fs_stat(absolute) then
return absolute
end
local found = vim.fn.findfile(raw, '.;')
if found ~= '' then
return found
end
end

---Resolve file and line number at cursor position in the output buffer.
---@return { path: string, line: number? }?
function M.resolve_file_at_cursor()
local windows = state.windows or {}
local win = windows.output_win
local buf = windows.output_buf

if not win or not buf or not vim.api.nvim_win_is_valid(win) then
return nil
end

local cursor = vim.api.nvim_win_get_cursor(win)
local line_num = cursor[1]
local line = vim.api.nvim_buf_get_lines(buf, line_num - 1, line_num, false)[1]

if not line then
return nil
end

-- 1. Check for markdown-style file links: [`path`](path)
local path = line:match('%[`([^`]+)%`%]%([^%)]+%)')
if path then
return { path = path }
end

-- 2. Check for file:// style links: `file://path/to/file.lua:line`
local f_path, f_line = line:match('`file://([^:`]+):?(%d*)`')
if f_path then
return { path = f_path, line = tonumber(f_line) }
end

-- 3. Check for action lines: **icon tool** `path`
path = line:match('%*%*.-%*%*%s+`([^`]+)`')
if path then
return { path = path }
end

-- 4. Check for diff hunk: look for the nearest file path upwards
local file_path = nil
for i = line_num, 1, -1 do
local l = vim.api.nvim_buf_get_lines(buf, i - 1, i, false)[1]
if l then
local p = l:match('%[`([^`]+)%`%]%([^%)]+%)') or l:match('%*%*.-%*%*%s+`([^`]+)`')
if p then
file_path = p
break
end
end
end

if not file_path then
return nil
end

-- Check if we are on a diff line with a line number in the gutter
local ns = output_window.namespace
local extmarks = vim.api.nvim_buf_get_extmarks(buf, ns, { line_num - 1, 0 }, { line_num - 1, -1 }, { details = true })
local ln ---@type number?
for _, extmark in ipairs(extmarks) do
local details = extmark[4]
if details and details.virt_text then
for _, vt in ipairs(details.virt_text) do
local val = tonumber(vim.trim(vt[1]))
if val then
ln = val
break
end
end
end
if ln then
break
end
end

return { path = file_path, line = ln }
end

---Open a file in the current window without triggering BufRead/BufNew autocmds.
---Falls back to :edit if the file isn't loaded in any buffer yet.
---@param path string
local function open_silent(path)
local escaped = vim.fn.fnameescape(path)
if not pcall(vim.cmd, 'buffer ' .. escaped) then
pcall(vim.cmd, 'edit ' .. escaped)
end
end

local function open_at(win, path, line)
if not win or not vim.api.nvim_win_is_valid(win) then
return
end
vim.api.nvim_set_current_win(win)
open_silent(path)
if line then
local buf = vim.api.nvim_win_get_buf(win)
local line_count = vim.api.nvim_buf_line_count(buf)
line = math.min(line, line_count)
pcall(vim.api.nvim_win_set_cursor, win, { line, 0 })
end
end

local function best_target_win()
local w = state.last_code_win_before_opencode
if w and vim.api.nvim_win_is_valid(w) then
return w
end
local alt = vim.fn.win_getid(vim.fn.winnr('#'))
if alt ~= 0 and vim.api.nvim_win_is_valid(alt) then
return alt
end
end

function M.jump_to_file_at_cursor()
local resolved = M.resolve_file_at_cursor()
if not resolved then
return
end
local path = resolve_path(resolved.path)
if not path then
return
end
open_at(best_target_win(), path, resolved.line)
end

Comment thread
sudo-tee marked this conversation as resolved.
return M
Loading