Skip to content

Commit f23728e

Browse files
committed
refactor(reference_picker): simplify and harden patterns
1 parent 585e05b commit f23728e

12 files changed

Lines changed: 791 additions & 65 deletions

lua/opencode/ui/formatter.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -500,14 +500,14 @@ end
500500
---@param metadata FileToolMetadata Metadata for the tool use
501501
function M._format_file_tool(output, tool_type, input, metadata)
502502
local file_name = ''
503-
if input then
503+
if input and input.filePath then
504504
local cwd = vim.fn.getcwd()
505505
local absolute = vim.fn.fnamemodify(input.filePath, ':p')
506-
506+
507507
if vim.startswith(absolute, cwd .. '/') then
508508
file_name = absolute:sub(#cwd + 2)
509509
else
510-
file_name = vim.fn.fnamemodify(input.filePath, ':t')
510+
file_name = absolute
511511
end
512512
end
513513

lua/opencode/ui/reference_picker.lua

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ local icons = require('opencode.ui.icons')
66

77
local M = {}
88

9-
---Check if a file exists
10-
---@param file_path string
9+
---Check if a file reference is valid
10+
---@param path string File path
11+
---@param context string Surrounding text
1112
---@return boolean
12-
local function file_exists(file_path)
13-
local path = file_path
14-
return vim.fn.filereadable(path) == 1
13+
local function is_valid_file_reference(path, context)
14+
-- Reject URLs
15+
if context:lower():match('https?://') or context:lower():match('www%.') then
16+
return false
17+
end
18+
19+
-- Must have extension and exist
20+
return path:match('%.[%w]+$') and vim.fn.filereadable(path) == 1
1521
end
1622

1723
---@class CodeReference
@@ -79,7 +85,7 @@ local function parse_position_info(line_str, col_or_end_str, end_line_str)
7985
return line, column, end_line
8086
end
8187

82-
---Generic function to parse file references using a given pattern
88+
---Parse file references using a pattern
8389
---@param text string The text to parse
8490
---@param pattern string Lua pattern to match
8591
---@param message_id string The message ID for tracking
@@ -94,9 +100,11 @@ local function parse_references_with_pattern(text, pattern, message_id)
94100
break
95101
end
96102

97-
if file_exists(path) then
98-
local line, column, end_line = parse_position_info(line_str, col_or_end_str, end_line_str)
103+
local context_start = math.max(1, match_start - 30)
104+
local context = text:sub(context_start, match_end + 10)
99105

106+
if is_valid_file_reference(path, context) then
107+
local line, column, end_line = parse_position_info(line_str, col_or_end_str, end_line_str)
100108
local ref = create_code_reference(path, line, column, end_line, message_id, match_start, match_end)
101109
table.insert(references, ref)
102110
end
@@ -107,65 +115,34 @@ local function parse_references_with_pattern(text, pattern, message_id)
107115
return references
108116
end
109117

110-
---Parse backtick-wrapped file references like `path/file.lua:42`
111-
---@param text string
112-
---@param message_id string
113-
---@return CodeReference[]
114-
local function parse_backtick_refs(text, message_id)
115-
local pattern = '`([^`]+%.[%w]+):?(%d*):?(%d*)-?(%d*)`'
116-
return parse_references_with_pattern(text, pattern, message_id)
117-
end
118-
119-
---Parse file:// URI references (backward compatibility)
120-
---@param text string
121-
---@param message_id string
122-
---@return CodeReference[]
123-
local function parse_file_uri_refs(text, message_id)
124-
local pattern = 'file://([%w_./%-]+):?(%d*):?(%d*)-?(%d*)'
125-
return parse_references_with_pattern(text, pattern, message_id)
126-
end
127-
128-
---Parse plain file paths like path/file.lua:42 or path/file.lua
129-
---@param text string
130-
---@param message_id string
131-
---@return CodeReference[]
132-
local function parse_plain_path_refs(text, message_id)
133-
-- Match file paths with optional extension and line/column info
134-
-- Relies on file_exists() to filter out false positives like URLs
135-
local pattern = '([%w_%./-]+%.[%w]+):?(%d*):?(%d*)-?(%d*)'
136-
return parse_references_with_pattern(text, pattern, message_id)
137-
end
138-
139118
---Parse file references from text using multiple pattern strategies
140119
---@param text string The text to parse
141120
---@param message_id string The message ID for tracking
142121
---@return CodeReference[]
143122
function M.parse_references(text, message_id)
144123
local all_refs = {}
145124

146-
local backtick_refs = parse_backtick_refs(text, message_id)
147-
local file_uri_refs = parse_file_uri_refs(text, message_id)
148-
local plain_refs = parse_plain_path_refs(text, message_id)
125+
local patterns = {
126+
'`([^`\n]+%.%w+):?(%d*):?(%d*)-?(%d*)`', -- Backticks: `file.ext:line`
127+
'file://([%S]+%.%w+):?(%d*):?(%d*)-?(%d*)', -- file:// URIs
128+
'([%w_./%-]+/[%w_./%-]*%.%w+):?(%d*):?(%d*)-?(%d*)', -- Paths with /
129+
'([%w_%-]+%.%w+):?(%d*):?(%d*)-?(%d*)', -- Top-level files
130+
}
149131

150-
vim.list_extend(all_refs, backtick_refs)
151-
vim.list_extend(all_refs, file_uri_refs)
152-
vim.list_extend(all_refs, plain_refs)
132+
for _, pattern in ipairs(patterns) do
133+
local refs = parse_references_with_pattern(text, pattern, message_id)
134+
vim.list_extend(all_refs, refs)
135+
end
153136

137+
-- Sort by position and deduplicate
154138
table.sort(all_refs, function(a, b)
155139
return a.match_start < b.match_start
156140
end)
157141

158142
local deduplicated = {}
159143
for _, ref in ipairs(all_refs) do
160-
local overlaps = false
161-
for _, existing in ipairs(deduplicated) do
162-
if ref.match_start <= existing.match_end and existing.match_start <= ref.match_end then
163-
overlaps = true
164-
break
165-
end
166-
end
167-
168-
if not overlaps then
144+
local last = deduplicated[#deduplicated]
145+
if not last or ref.match_start > last.match_end then
169146
table.insert(deduplicated, ref)
170147
end
171148
end
@@ -229,7 +206,7 @@ function M._parse_message_references(msg)
229206

230207
if part.type == 'tool' then
231208
local file_path = vim.tbl_get(part, 'state', 'input', 'filePath')
232-
if file_path and file_exists(file_path) then
209+
if file_path and vim.fn.filereadable(file_path) == 1 then
233210
local relative_path = vim.fn.fnamemodify(file_path, ':~:.')
234211
local ref = create_code_reference(relative_path, nil, nil, nil, message_id, 0, 0)
235212
table.insert(refs, ref)

tests/data/diagnostics.expected.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/data/diff.expected.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"timestamp":1769093505,"actions":[{"display_line":18,"text":"[R]evert file","key":"R","args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"range":{"to":18,"from":18},"type":"diff_revert_selected_file"},{"display_line":18,"text":"Revert [A]ll","key":"A","args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"range":{"to":18,"from":18},"type":"diff_revert_all"},{"display_line":18,"text":"[D]iff","key":"D","args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"range":{"to":18,"from":18},"type":"diff_open"}],"lines":["----","","","can you add \"great\" before \"string\" in @diff-test.txt?","","[diff-test.txt](diff-test.txt)","","----","","","** edit** `diff-test.txt`","","`````txt"," this is a string"," this is a great string","","`````","","**󰻛 Created Snapshot** `1f593f7e`","","----","","",""],"extmarks":[[1,1,0,{"ns_id":3,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287269001C5gRusYfX7A1w1]","OpencodeHint"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_pos":"win_col","right_gravity":true}],[2,2,0,{"ns_id":3,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[3,3,0,{"ns_id":3,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[4,3,39,{"end_col":53,"end_right_gravity":false,"right_gravity":true,"hl_eol":false,"priority":1000,"hl_group":"OpencodeMention","ns_id":3,"end_row":3}],[5,4,0,{"ns_id":3,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[6,5,0,{"ns_id":3,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[7,8,0,{"ns_id":3,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287287001HVwpPaH7WkRVdN]","OpencodeHint"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_pos":"win_col","right_gravity":true}],[8,10,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[9,11,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[10,12,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[11,13,0,{"end_col":0,"virt_text":[["-","OpencodeDiffDelete"]],"virt_text_pos":"overlay","ns_id":3,"right_gravity":true,"hl_eol":true,"end_right_gravity":false,"virt_text_hide":false,"virt_text_repeat_linebreak":false,"end_row":14,"hl_group":"OpencodeDiffDelete","priority":5000}],[12,13,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[13,14,0,{"end_col":0,"virt_text":[["+","OpencodeDiffAdd"]],"virt_text_pos":"overlay","ns_id":3,"right_gravity":true,"hl_eol":true,"end_right_gravity":false,"virt_text_hide":false,"virt_text_repeat_linebreak":false,"end_row":15,"hl_group":"OpencodeDiffAdd","priority":5000}],[14,14,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[15,15,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[16,16,0,{"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_hide":false,"virt_text_win_col":-1,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_pos":"win_col","right_gravity":true}],[17,21,0,{"ns_id":3,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:43:03)","OpencodeHint"],[" [msg_9d7288f2f001hW6NqqhtBc72UU]","OpencodeHint"]],"virt_text_hide":false,"virt_text_win_col":-3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_pos":"win_col","right_gravity":true}]]}
1+
{"extmarks":[[1,1,0,{"ns_id":3,"priority":10,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287269001C5gRusYfX7A1w1]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false}],[2,2,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[3,3,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[4,3,39,{"ns_id":3,"hl_group":"OpencodeMention","end_row":3,"end_col":53,"end_right_gravity":false,"priority":1000,"right_gravity":true,"hl_eol":false}],[5,4,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[6,5,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[7,8,0,{"ns_id":3,"priority":10,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287287001HVwpPaH7WkRVdN]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false}],[8,10,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[9,11,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[10,12,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[11,13,0,{"ns_id":3,"end_col":0,"virt_text_pos":"overlay","virt_text":[["-","OpencodeDiffDelete"]],"priority":5000,"hl_group":"OpencodeDiffDelete","end_row":14,"hl_eol":true,"end_right_gravity":false,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false}],[12,13,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[13,14,0,{"ns_id":3,"end_col":0,"virt_text_pos":"overlay","virt_text":[["+","OpencodeDiffAdd"]],"priority":5000,"hl_group":"OpencodeDiffAdd","end_row":15,"hl_eol":true,"end_right_gravity":false,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false}],[14,14,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[15,15,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[16,16,0,{"ns_id":3,"priority":4096,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true}],[17,21,0,{"ns_id":3,"priority":10,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:43:03)","OpencodeHint"],[" [msg_9d7288f2f001hW6NqqhtBc72UU]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false}]],"timestamp":1769172144,"actions":[{"key":"R","args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"display_line":18,"range":{"from":18,"to":18},"type":"diff_revert_selected_file","text":"[R]evert file"},{"key":"A","args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"display_line":18,"range":{"from":18,"to":18},"type":"diff_revert_all","text":"Revert [A]ll"},{"key":"D","args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"display_line":18,"range":{"from":18,"to":18},"type":"diff_open","text":"[D]iff"}],"lines":["----","","","can you add \"great\" before \"string\" in @diff-test.txt?","","[diff-test.txt](diff-test.txt)","","----","","","** edit** `/Users/cam/tmp/a/diff-test.txt`","","`````txt"," this is a string"," this is a great string","","`````","","**󰻛 Created Snapshot** `1f593f7e`","","----","","",""]}

0 commit comments

Comments
 (0)