@@ -2,6 +2,7 @@ local M = {}
22
33local state = require (' opencode.state' )
44local renderer = require (' opencode.ui.renderer' )
5+ local output_window = require (' opencode.ui.output_window' )
56
67function M .goto_message_by_id (message_id )
78 require (' opencode.ui.ui' ).focus_output ()
@@ -61,4 +62,143 @@ function M.goto_prev_message()
6162 vim .api .nvim_win_set_cursor (win , { 1 , 0 })
6263end
6364
65+ --- @param raw string
66+ local function resolve_path (raw )
67+ if vim .uv .fs_stat (raw ) then
68+ return raw
69+ end
70+ local absolute = vim .fn .fnamemodify (raw , ' :p' )
71+ if vim .uv .fs_stat (absolute ) then
72+ return absolute
73+ end
74+ local found = vim .fn .findfile (raw , ' .;' )
75+ if found ~= ' ' then
76+ return found
77+ end
78+ end
79+
80+ --- Resolve file and line number at cursor position in the output buffer.
81+ --- @return { path : string , line : number ? }?
82+ function M .resolve_file_at_cursor ()
83+ local windows = state .windows or {}
84+ local win = windows .output_win
85+ local buf = windows .output_buf
86+
87+ if not win or not buf or not vim .api .nvim_win_is_valid (win ) then
88+ return nil
89+ end
90+
91+ local cursor = vim .api .nvim_win_get_cursor (win )
92+ local line_num = cursor [1 ]
93+ local line = vim .api .nvim_buf_get_lines (buf , line_num - 1 , line_num , false )[1 ]
94+
95+ if not line then
96+ return nil
97+ end
98+
99+ -- 1. Check for markdown-style file links: [`path`](path)
100+ local path = line :match (' %[`([^`]+)%`%]%([^%)]+%)' )
101+ if path then
102+ return { path = path }
103+ end
104+
105+ -- 2. Check for file:// style links: `file://path/to/file.lua:line`
106+ local f_path , f_line = line :match (' `file://([^:`]+):?(%d*)`' )
107+ if f_path then
108+ return { path = f_path , line = tonumber (f_line ) }
109+ end
110+
111+ -- 3. Check for action lines: **icon tool** `path`
112+ path = line :match (' %*%*.-%*%*%s+`([^`]+)`' )
113+ if path then
114+ return { path = path }
115+ end
116+
117+ -- 4. Check for diff hunk: look for the nearest file path upwards
118+ local file_path = nil
119+ for i = line_num , 1 , - 1 do
120+ local l = vim .api .nvim_buf_get_lines (buf , i - 1 , i , false )[1 ]
121+ if l then
122+ local p = l :match (' %[`([^`]+)%`%]%([^%)]+%)' ) or l :match (' %*%*.-%*%*%s+`([^`]+)`' )
123+ if p then
124+ file_path = p
125+ break
126+ end
127+ end
128+ end
129+
130+ if not file_path then
131+ return nil
132+ end
133+
134+ -- Check if we are on a diff line with a line number in the gutter
135+ local ns = output_window .namespace
136+ local extmarks = vim .api .nvim_buf_get_extmarks (buf , ns , { line_num - 1 , 0 }, { line_num - 1 , - 1 }, { details = true })
137+ local ln --- @type number ?
138+ for _ , extmark in ipairs (extmarks ) do
139+ local details = extmark [4 ]
140+ if details and details .virt_text then
141+ for _ , vt in ipairs (details .virt_text ) do
142+ local val = tonumber (vim .trim (vt [1 ]))
143+ if val then
144+ ln = val
145+ break
146+ end
147+ end
148+ end
149+ if ln then
150+ break
151+ end
152+ end
153+
154+ return { path = file_path , line = ln }
155+ end
156+
157+ --- Open a file in the current window without triggering BufRead/BufNew autocmds.
158+ --- Falls back to :edit if the file isn't loaded in any buffer yet.
159+ --- @param path string
160+ local function open_silent (path )
161+ local escaped = vim .fn .fnameescape (path )
162+ if not pcall (vim .cmd , ' buffer ' .. escaped ) then
163+ pcall (vim .cmd , ' edit ' .. escaped )
164+ end
165+ end
166+
167+ local function open_at (win , path , line )
168+ if not win or not vim .api .nvim_win_is_valid (win ) then
169+ return
170+ end
171+ vim .api .nvim_set_current_win (win )
172+ open_silent (path )
173+ if line then
174+ local buf = vim .api .nvim_win_get_buf (win )
175+ local line_count = vim .api .nvim_buf_line_count (buf )
176+ line = math.min (line , line_count )
177+ pcall (vim .api .nvim_win_set_cursor , win , { line , 0 })
178+ end
179+ end
180+
181+ local function best_target_win ()
182+ local w = state .last_code_win_before_opencode
183+ if w and vim .api .nvim_win_is_valid (w ) then
184+ return w
185+ end
186+ local alt = vim .fn .win_getid (vim .fn .winnr (' #' ))
187+ if alt ~= 0 and vim .api .nvim_win_is_valid (alt ) then
188+ return alt
189+ end
190+ end
191+
192+ function M .jump_to_file_at_cursor ()
193+ local resolved = M .resolve_file_at_cursor ()
194+ if not resolved then
195+ return
196+ end
197+ local path = resolve_path (resolved .path )
198+ if not path then
199+ return
200+ end
201+ open_at (best_target_win (), path , resolved .line )
202+ end
203+
64204return M
0 commit comments