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