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
14 changes: 12 additions & 2 deletions lua/opencode/ui/formatter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,12 @@ local function add_symbol_reference_highlights(output, rendered, rendered_mentio
local abs_start = line_start + start_pos - 1
local abs_end = line_start + end_pos - 1

if token and not in_ranges(rendered_mention_ranges, abs_start, abs_end) and symbol_refs[token] then
if
token
and symbol_refs[token]
and symbol_tokens.is_code_like(line, start_pos, end_pos, token)
and not in_ranges(rendered_mention_ranges, abs_start, abs_end)
then
output:add_extmark(first_line_idx + line_idx - 1, {
start_col = start_pos - 1,
end_col = end_pos,
Expand Down Expand Up @@ -818,7 +823,12 @@ local function add_symbol_reference_targets(output, rendered, rendered_mention_r

-- Empty-candidate guard keeps the zero-lookup contract testable here;
-- targets_for_token re-checks internally for other callers.
if token and not in_ranges(rendered_mention_ranges, abs_start, abs_end) and #candidates > 0 then
if
token
and #candidates > 0
and symbol_tokens.is_code_like(line, start_pos, end_pos, token)
and not in_ranges(rendered_mention_ranges, abs_start, abs_end)
then
local targets = symbol_snapshot.targets_for_token(context.symbol_cycle, token, candidates)
if #targets > 0 then
output:add_target({
Expand Down
13 changes: 13 additions & 0 deletions lua/opencode/ui/symbol_tokens.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ function M.find(line, scan_from)
return start_pos, end_pos, token
end

-- Plain prose words collide with locals in candidate files, so only accept
-- identifier-shaped tokens or ones quoted as inline code.
---@param line string
---@param start_pos integer
---@param end_pos integer
---@param token string
---@return boolean
function M.is_code_like(line, start_pos, end_pos, token)
return token:find('[_.:]') ~= nil
or token:find('%l%u') ~= nil
or (line:sub(start_pos - 1, start_pos - 1) == '`' and line:sub(end_pos + 1, end_pos + 1) == '`')
end

function M.at_col(line, col)
local scan_from = 1
while scan_from <= #line do
Expand Down
2 changes: 1 addition & 1 deletion tests/data/symbol-reference-navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"messageID": "msg_symbol_reference_navigation",
"sessionID": "ses_symbol_reference_navigation",
"type": "text",
"text": "collect should be highlighted from the referenced file."
"text": "`collect` should be highlighted from the referenced file."
}
}
}
Expand Down
50 changes: 43 additions & 7 deletions tests/unit/formatter_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ describe('formatter', function()
}

local output = Output.new()
formatter._format_assistant_message(output, 'foo', { id = 'part_symbol_only' }, {
formatter._format_assistant_message(output, '`foo`', { id = 'part_symbol_only' }, {
id = 'msg_symbol_only',
kind = 'assistant',
session_id = 'ses_1',
Expand All @@ -826,12 +826,47 @@ describe('formatter', function()
{
kind = 'symbol',
token = 'foo',
range = { line = 1, start_col = 0, end_col = 3 },
range = { line = 1, start_col = 1, end_col = 4 },
},
}, output.targets)
assert.are.equal('OpencodeSymbolReference', output.extmarks[0][1].hl_group)
end)

it('limits symbol targets to code-like or backticked tokens', function()
local original_symbol_snapshot = package.loaded['opencode.ui.symbol_snapshot']
package.loaded['opencode.ui.symbol_snapshot'] = {
targets_for_token = function(_, token)
return { { token = token, path = '/test/project/src/foo.lua', line = 1, col = 1 } }
end,
}

local output = Output.new()
formatter._format_assistant_message(
output,
'Move to the first line, `first` switch_to_mode M.apply fooBar',
{
id = 'part_prose',
},
nil,
{
interactive = true,
current_files = { '/test/project/src/foo.lua' },
current_refs = {},
symbol_cycle = {},
}
)

package.loaded['opencode.ui.symbol_snapshot'] = original_symbol_snapshot

assert.are.same(
{ 'first', 'switch_to_mode', 'M.apply', 'fooBar' },
vim.tbl_map(function(target)
return target.token
end, output.targets)
)
assert.are.equal(4, #output.extmarks[0])
end)

it('does not create symbol targets without local candidate files', function()
local original_symbol_snapshot = package.loaded['opencode.ui.symbol_snapshot']

Expand Down Expand Up @@ -930,14 +965,15 @@ describe('formatter', function()

it('highlights a symbol before trailing prose colon', function()
local original_symbol_snapshot = package.loaded['opencode.ui.symbol_snapshot']
local text = 'See `src/main.lua` foo: call this'
local text = 'See `src/main.lua` foo_bar: call this'
local ref_start, ref_end = text:find('`src/main.lua`', 1, true)
local part = { id = 'part_colon', kind = 'text', text = text }
local message = assistant({ part }, { id = 'msg_colon' })
package.loaded['opencode.ui.symbol_snapshot'] = {
targets_for_token = function(_, token, candidate_files)
assert.are.same({ vim.fn.getcwd() .. '/src/main.lua' }, candidate_files)
return token == 'foo' and { { token = 'foo', path = vim.fn.getcwd() .. '/src/main.lua', line = 3, col = 1 } }
return token == 'foo_bar'
and { { token = 'foo_bar', path = vim.fn.getcwd() .. '/src/main.lua', line = 3, col = 1 } }
or {}
end,
}
Expand All @@ -961,11 +997,11 @@ describe('formatter', function()
package.loaded['opencode.ui.symbol_snapshot'] = original_symbol_snapshot

local symbol_mark = output.extmarks[0][2]
local foo_start = output.lines[1]:find('foo:', 1, true)
local foo_start = output.lines[1]:find('foo_bar:', 1, true)
assert.are.equal(text:gsub('See ', 'See ' .. require('opencode.ui.icons').get('reference'), 1), output.lines[1])
assert.are.equal(foo_start - 1, symbol_mark.start_col)
assert.are.equal(foo_start + 2, symbol_mark.end_col)
assert.are.equal('foo', output.targets[2].token)
assert.are.equal(foo_start + 6, symbol_mark.end_col)
assert.are.equal('foo_bar', output.targets[2].token)
end)

it('formats grep tools when streamed input contains vim.NIL placeholders', function()
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/symbol_tokens_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,14 @@ describe('opencode.ui.symbol_tokens', function()
assert.is_nil(symbol_tokens.at_col('tests/data/symbol-reference-navigation.json', 8))
assert.is_nil(symbol_tokens.at_col('.cache/', 2))
end)

it('accepts only code-like tokens', function()
for _, token in ipairs({ 'switch_to_mode', 'M.apply', 'A::b', 'fooBar' }) do
assert.is_true(symbol_tokens.is_code_like(token, 1, #token, token), token)
end
for _, token in ipairs({ 'first', 'Update', 'JSON' }) do
assert.is_false(symbol_tokens.is_code_like(token, 1, #token, token), token)
end
assert.is_true(symbol_tokens.is_code_like('see `first`', 6, 10, 'first'))
end)
end)
Loading