Skip to content

Commit 4e1b968

Browse files
committed
feat: add support for multi-range diagnostics with selection-aware filtering
- Enhance diagnostics to handle multiple selection ranges simultaneously - Add visual filter indicator in context bar for scoped diagnostic display - Improve chat context to automatically use stored selections for diagnostic filtering
1 parent 16b5017 commit 4e1b968

File tree

6 files changed

+378
-29
lines changed

6 files changed

+378
-29
lines changed

lua/opencode/context/base_context.lua

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-- Base context utilities
2-
-- Static methods for gathering context data from the editor
3-
41
local util = require('opencode.util')
52
local config = require('opencode.config')
63
local state = require('opencode.state')
@@ -41,7 +38,7 @@ end
4138

4239
---@param buf integer
4340
---@param context_config? OpencodeContextConfig
44-
---@param range? { start_line: integer, end_line: integer }
41+
---@param range? { start_line: integer, end_line: integer } | { start_line: integer, end_line: integer }[]
4542
---@return OpencodeDiagnostic[]|nil
4643
function M.get_diagnostics(buf, context_config, range)
4744
if not M.is_context_enabled('diagnostics', context_config) then
@@ -69,16 +66,27 @@ function M.get_diagnostics(buf, context_config, range)
6966
end
7067

7168
local diagnostics = {}
69+
70+
local ranges = nil
71+
if range then
72+
if range[1] and type(range[1]) == 'table' then
73+
ranges = range
74+
else
75+
ranges = { range }
76+
end
77+
end
78+
7279
if diagnostic_conf.only_closest then
73-
if range then
74-
-- Get diagnostics for the specified range
75-
for line_num = range.start_line, range.end_line do
76-
local line_diagnostics = vim.diagnostic.get(buf, {
77-
lnum = line_num,
78-
severity = severity_levels,
79-
})
80-
for _, diag in ipairs(line_diagnostics) do
81-
table.insert(diagnostics, diag)
80+
if ranges then
81+
for _, r in ipairs(ranges) do
82+
for line_num = r.start_line, r.end_line do
83+
local line_diagnostics = vim.diagnostic.get(buf, {
84+
lnum = line_num,
85+
severity = severity_levels,
86+
})
87+
for _, diag in ipairs(line_diagnostics) do
88+
table.insert(diagnostics, diag)
89+
end
8290
end
8391
end
8492
else
@@ -92,17 +100,7 @@ function M.get_diagnostics(buf, context_config, range)
92100
diagnostics = line_diagnostics
93101
end
94102
else
95-
-- Get all diagnostics, optionally filtered by range
96103
diagnostics = vim.diagnostic.get(buf, { severity = severity_levels })
97-
if range then
98-
local filtered_diagnostics = {}
99-
for _, diag in ipairs(diagnostics) do
100-
if diag.lnum >= range.start_line and diag.lnum <= range.end_line then
101-
table.insert(filtered_diagnostics, diag)
102-
end
103-
end
104-
diagnostics = filtered_diagnostics
105-
end
106104
end
107105

108106
if #diagnostics == 0 then
@@ -124,7 +122,7 @@ function M.get_diagnostics(buf, context_config, range)
124122
})
125123
end
126124

127-
return opencode_diagnostics
125+
return opencode_diagnostics, ranges
128126
end
129127

130128
---@param buf integer

lua/opencode/context/chat_context.lua

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,54 @@ function M.get_mentioned_subagents()
283283
return M.context.mentioned_subagents or {}
284284
end
285285

286+
-- Chat-context-aware get_diagnostics that considers stored selections
287+
---@param buf integer
288+
---@param context_config? OpencodeContextConfig
289+
---@param range? { start_line: integer, end_line: integer }
290+
---@return OpencodeDiagnostic[]|nil
291+
function M.get_diagnostics(buf, context_config, range)
292+
-- Use explicit range if provided
293+
if range then
294+
return base_context.get_diagnostics(buf, context_config, range)
295+
end
296+
297+
if M.context.selections and #M.context.selections > 0 then
298+
local selection_ranges = {}
299+
300+
for _, sel in ipairs(M.context.selections) do
301+
if sel.lines then
302+
-- Handle both formats: "1, 5" and "1-5"
303+
local start_line, end_line = sel.lines:match('(%d+)[,%-]%s*(%d+)')
304+
if not start_line then
305+
-- Single line case like "5, 5" or just "5"
306+
start_line = sel.lines:match('(%d+)')
307+
end_line = start_line
308+
end
309+
310+
if start_line then
311+
local start_num = tonumber(start_line)
312+
local end_num = tonumber(end_line)
313+
314+
if start_num and end_num then
315+
-- Convert to 0-based
316+
local selection_range = {
317+
start_line = start_num - 1,
318+
end_line = end_num - 1,
319+
}
320+
table.insert(selection_ranges, selection_range)
321+
end
322+
end
323+
end
324+
end
325+
326+
if #selection_ranges > 0 then
327+
return base_context.get_diagnostics(buf, context_config, selection_ranges)
328+
end
329+
end
330+
331+
return base_context.get_diagnostics(buf, context_config, nil)
332+
end
333+
286334
---@param current_file table|nil
287335
---@return boolean, boolean -- should_update, is_different_file
288336
function M.should_update_current_file(current_file)
@@ -299,6 +347,7 @@ function M.should_update_current_file(current_file)
299347
return true, true
300348
end
301349

350+
-- Same file, check modification time
302351
local file_path = current_file.path
303352
if not file_path or vim.fn.filereadable(file_path) ~= 1 then
304353
return false, false
@@ -341,7 +390,7 @@ function M.load()
341390
end
342391

343392
M.context.cursor_data = cursor_data
344-
M.context.linter_errors = base_context.get_diagnostics(buf, nil, nil)
393+
M.context.linter_errors = M.get_diagnostics(buf, nil, nil)
345394

346395
-- Handle current selection
347396
local current_selection = base_context.get_current_selection()
@@ -483,7 +532,7 @@ M.format_message = Promise.async(function(prompt, opts)
483532
if range then
484533
diag_range = { start_line = math.floor(range.start) - 1, end_line = math.floor(range.stop) - 1 }
485534
end
486-
local diagnostics = base_context.get_diagnostics(buf, context_config, diag_range)
535+
local diagnostics = M.get_diagnostics(buf, context_config, diag_range)
487536
if diagnostics and #diagnostics > 0 then
488537
table.insert(parts, format_diagnostics_part(diagnostics, diag_range))
489538
end

lua/opencode/ui/context_bar.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local M = {}
33
local context = require('opencode.context')
44
local icons = require('opencode.ui.icons')
55
local state = require('opencode.state')
6+
local config = require('opencode.config')
67
local prompt_guard_indicator = require('opencode.ui.prompt_guard_indicator')
78

89
local function get_current_file_info(ctx)
@@ -111,18 +112,18 @@ local function create_winbar_segments()
111112
counts[type_name] = (counts[type_name] or 0) + 1
112113
end
113114

115+
local filter_icon = config.context.diagnostics.only_closest and icons.get('filter') or ''
114116
for _, type_name in pairs(severity_types) do
115117
local count = counts[type_name]
116118
if count and count > 0 then
117119
table.insert(segments, {
118120
icon = icons.get(type_name),
119-
text = '(' .. count .. ')',
121+
text = '(' .. count .. filter_icon .. ')',
120122
highlight = 'OpencodeContext' .. type_name:gsub('^%l', string.upper),
121123
})
122124
end
123125
end
124126
end
125-
126127
return segments
127128
end
128129

lua/opencode/ui/icons.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ local presets = {
3838
error = '',
3939
warning = '',
4040
info = '',
41+
filter = '/',
4142
selection = '󰫙 ',
4243
command = '',
4344
},
@@ -74,6 +75,7 @@ local presets = {
7475
error = '[E]',
7576
warning = '[W]',
7677
info = '[I] ',
78+
filter = '/*',
7779
selection = "'<'> ",
7880
command = '::',
7981
},

tests/unit/context_bar_spec.lua

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local context_bar = require('opencode.ui.context_bar')
22
local context = require('opencode.context')
33
local state = require('opencode.state')
44
local icons = require('opencode.ui.icons')
5+
local config = require('opencode.config')
56
local assert = require('luassert')
67

78
describe('opencode.ui.context_bar', function()
@@ -181,7 +182,9 @@ describe('opencode.ui.context_bar', function()
181182
assert.is_not_nil(winbar_capture.value:find('L:10')) -- Cursor data
182183
end)
183184

184-
it('renders winbar with diagnostics', function()
185+
it('renders winbar with all diagnostics', function()
186+
local original_only_closest = config.context.diagnostics.only_closest
187+
config.context.diagnostics.only_closest = false
185188
mock_context.linter_errors = {
186189
{ severity = 1 }, -- ERROR
187190
{ severity = 1 }, -- ERROR
@@ -199,6 +202,30 @@ describe('opencode.ui.context_bar', function()
199202
assert.is_not_nil(winbar_capture.value:find(icons.get('error') .. '%(2%)')) -- 2 errors
200203
assert.is_not_nil(winbar_capture.value:find(icons.get('warning') .. '%(1%)')) -- Warning icon
201204
assert.is_not_nil(winbar_capture.value:find(icons.get('info') .. '%(1%)')) -- Info icon
205+
config.context.diagnostics.only_closest = original_only_closest
206+
end)
207+
208+
it('renders winbar with filtered diagnostics', function()
209+
local original_only_closest = config.context.diagnostics.only_closest
210+
config.context.diagnostics.only_closest = true
211+
mock_context.linter_errors = {
212+
{ severity = 1 }, -- ERROR
213+
{ severity = 1 }, -- ERROR
214+
{ severity = 2 }, -- WARN
215+
{ severity = 3 }, -- INFO
216+
}
217+
218+
local mock_input_win = 2004
219+
local winbar_capture = create_mock_window(mock_input_win)
220+
221+
state.windows = { input_win = mock_input_win }
222+
context_bar.render()
223+
224+
assert.is_string(winbar_capture.value)
225+
assert.is_not_nil(winbar_capture.value:find(icons.get('error') .. '%(2' .. icons.get('filter') .. '%)')) -- 2 errors
226+
assert.is_not_nil(winbar_capture.value:find(icons.get('warning') .. '%(1' .. icons.get('filter') .. '%)')) -- Warning icon
227+
assert.is_not_nil(winbar_capture.value:find(icons.get('info') .. '%(1' .. icons.get('filter') .. '%)')) -- Info icon
228+
config.context.diagnostics.only_closest = original_only_closest
202229
end)
203230

204231
it('respects context enabled settings', function()

0 commit comments

Comments
 (0)