Skip to content

Commit 3f6ccff

Browse files
committed
refactor(render_state): build line index lazily
1 parent a8d396f commit 3f6ccff

File tree

2 files changed

+45
-53
lines changed

2 files changed

+45
-53
lines changed

lua/opencode/ui/render_state.lua

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ local state = require('opencode.state')
2020
---@field _messages table<string, RenderedMessage> Message ID to render data
2121
---@field _parts table<string, RenderedPart> Part ID to render data
2222
---@field _line_index LineIndex Line number to ID mappings
23+
---@field _line_index_valid boolean Whether line index is up to date
2324
local RenderState = {}
2425
RenderState.__index = RenderState
2526

@@ -37,6 +38,7 @@ function RenderState:reset()
3738
line_to_part = {},
3839
line_to_message = {},
3940
}
41+
self._line_index_valid = false
4042
end
4143

4244
---Get message render data by ID
@@ -71,10 +73,18 @@ function RenderState:get_part_by_call_id(call_id, message_id)
7173
return nil
7274
end
7375

76+
---Ensure line index is up to date
77+
function RenderState:_ensure_line_index()
78+
if not self._line_index_valid then
79+
self:_rebuild_line_index()
80+
end
81+
end
82+
7483
---Get part at specific line
7584
---@param line integer Line number (1-indexed)
7685
---@return RenderedPart?
7786
function RenderState:get_part_at_line(line)
87+
self:_ensure_line_index()
7888
local part_id = self._line_index.line_to_part[line]
7989
if not part_id then
8090
return nil
@@ -86,6 +96,7 @@ end
8696
---@param line integer Line number (1-indexed)
8797
---@return RenderedMessage?
8898
function RenderState:get_message_at_line(line)
99+
self:_ensure_line_index()
89100
local message_id = self._line_index.line_to_message[line]
90101
if not message_id then
91102
return nil
@@ -97,6 +108,7 @@ end
97108
---@param line integer Line number (1-indexed)
98109
---@return table[] List of actions at that line
99110
function RenderState:get_actions_at_line(line)
111+
self:_ensure_line_index()
100112
local part_id = self._line_index.line_to_part[line]
101113
if not part_id then
102114
return {}
@@ -140,9 +152,7 @@ function RenderState:set_message(message_id, message_ref, line_start, line_end)
140152
end
141153

142154
if line_start and line_end then
143-
for line = line_start, line_end do
144-
self._line_index.line_to_message[line] = message_id
145-
end
155+
self._line_index_valid = false
146156
end
147157
end
148158

@@ -174,9 +184,7 @@ function RenderState:set_part(part_id, part, message_id, line_start, line_end)
174184
end
175185

176186
if line_start and line_end then
177-
for line = line_start, line_end do
178-
self._line_index.line_to_part[line] = part_id
179-
end
187+
self._line_index_valid = false
180188
end
181189
end
182190

@@ -197,16 +205,10 @@ function RenderState:update_part_lines(part_id, new_line_start, new_line_end)
197205
local new_line_count = new_line_end - new_line_start + 1
198206
local delta = new_line_count - old_line_count
199207

200-
for line = old_line_start, old_line_end do
201-
self._line_index.line_to_part[line] = nil
202-
end
203-
204208
part_data.line_start = new_line_start
205209
part_data.line_end = new_line_end
206210

207-
for line = new_line_start, new_line_end do
208-
self._line_index.line_to_part[line] = part_id
209-
end
211+
self._line_index_valid = false
210212

211213
if delta ~= 0 then
212214
self:shift_all(old_line_end + 1, delta)
@@ -297,11 +299,8 @@ function RenderState:remove_part(part_id)
297299
local line_count = part_data.line_end - part_data.line_start + 1
298300
local shift_from = part_data.line_end + 1
299301

300-
for line = part_data.line_start, part_data.line_end do
301-
self._line_index.line_to_part[line] = nil
302-
end
303-
304302
self._parts[part_id] = nil
303+
self._line_index_valid = false
305304

306305
self:shift_all(shift_from, -line_count)
307306

@@ -320,11 +319,8 @@ function RenderState:remove_message(message_id)
320319
local line_count = msg_data.line_end - msg_data.line_start + 1
321320
local shift_from = msg_data.line_end + 1
322321

323-
for line = msg_data.line_start, msg_data.line_end do
324-
self._line_index.line_to_message[line] = nil
325-
end
326-
327322
self._messages[message_id] = nil
323+
self._line_index_valid = false
328324

329325
self:shift_all(shift_from, -line_count)
330326

@@ -341,7 +337,7 @@ function RenderState:shift_all(from_line, delta)
341337
end
342338

343339
local found_content_before_from_line = false
344-
local shifted = false
340+
local anything_shifted = false
345341

346342
for i = #state.messages, 1, -1 do
347343
local msg_wrapper = state.messages[i]
@@ -353,7 +349,7 @@ function RenderState:shift_all(from_line, delta)
353349
if msg_data.line_start >= from_line then
354350
msg_data.line_start = msg_data.line_start + delta
355351
msg_data.line_end = msg_data.line_end + delta
356-
shifted = true
352+
anything_shifted = true
357353
elseif msg_data.line_end < from_line then
358354
found_content_before_from_line = true
359355
end
@@ -369,7 +365,7 @@ function RenderState:shift_all(from_line, delta)
369365
if part_data.line_start >= from_line then
370366
part_data.line_start = part_data.line_start + delta
371367
part_data.line_end = part_data.line_end + delta
372-
shifted = true
368+
anything_shifted = true
373369

374370
if part_data.actions then
375371
for _, action in ipairs(part_data.actions) do
@@ -389,8 +385,8 @@ function RenderState:shift_all(from_line, delta)
389385
end
390386
end
391387

392-
if shifted then
393-
self:_rebuild_line_index()
388+
if anything_shifted then
389+
self._line_index_valid = false
394390
end
395391
end
396392

@@ -414,6 +410,7 @@ function RenderState:_rebuild_line_index()
414410
end
415411
end
416412
end
413+
self._line_index_valid = true
417414
end
418415

419416
return RenderState

tests/unit/render_state_spec.lua

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,19 @@ describe('RenderState', function()
1919
assert.is_table(render_state._messages)
2020
assert.is_table(render_state._parts)
2121
assert.is_table(render_state._line_index)
22+
assert.is_false(render_state._line_index_valid)
2223
end)
2324

2425
it('resets to empty state', function()
2526
render_state._messages = { test = true }
2627
render_state._parts = { test = true }
28+
render_state._line_index_valid = true
2729
render_state:reset()
2830
assert.is_true(vim.tbl_isempty(render_state._messages))
2931
assert.is_true(vim.tbl_isempty(render_state._parts))
3032
assert.is_true(vim.tbl_isempty(render_state._line_index.line_to_part))
3133
assert.is_true(vim.tbl_isempty(render_state._line_index.line_to_message))
34+
assert.is_false(render_state._line_index_valid)
3235
end)
3336
end)
3437

@@ -48,9 +51,11 @@ describe('RenderState', function()
4851
local msg = { id = 'msg1' }
4952
render_state:set_message('msg1', msg, 5, 7)
5053

51-
assert.equals('msg1', render_state._line_index.line_to_message[5])
52-
assert.equals('msg1', render_state._line_index.line_to_message[6])
53-
assert.equals('msg1', render_state._line_index.line_to_message[7])
54+
assert.is_false(render_state._line_index_valid)
55+
56+
local result = render_state:get_message_at_line(6)
57+
assert.is_not_nil(result)
58+
assert.equals('msg1', result.message.id)
5459
end)
5560

5661
it('updates existing message', function()
@@ -83,9 +88,11 @@ describe('RenderState', function()
8388
local part = { id = 'part1' }
8489
render_state:set_part('part1', part, 'msg1', 20, 22)
8590

86-
assert.equals('part1', render_state._line_index.line_to_part[20])
87-
assert.equals('part1', render_state._line_index.line_to_part[21])
88-
assert.equals('part1', render_state._line_index.line_to_part[22])
91+
assert.is_false(render_state._line_index_valid)
92+
93+
local result = render_state:get_part_at_line(21)
94+
assert.is_not_nil(result)
95+
assert.equals('part1', result.part.id)
8996
end)
9097

9198
it('initializes actions array', function()
@@ -317,8 +324,8 @@ describe('RenderState', function()
317324

318325
render_state:remove_part('part1')
319326

320-
assert.is_nil(render_state._line_index.line_to_part[10])
321-
assert.is_nil(render_state._line_index.line_to_part[15])
327+
assert.is_nil(render_state:get_part_at_line(10))
328+
assert.is_nil(render_state:get_part_at_line(15))
322329
end)
323330

324331
it('returns false for non-existent part', function()
@@ -361,8 +368,8 @@ describe('RenderState', function()
361368

362369
render_state:remove_message('msg1')
363370

364-
assert.is_nil(render_state._line_index.line_to_message[1])
365-
assert.is_nil(render_state._line_index.line_to_message[5])
371+
assert.is_nil(render_state:get_message_at_line(1))
372+
assert.is_nil(render_state:get_message_at_line(5))
366373
end)
367374

368375
it('returns false for non-existent message', function()
@@ -431,34 +438,22 @@ describe('RenderState', function()
431438
local part = { id = 'part1' }
432439
render_state:set_part('part1', part, 'msg1', 10, 15)
433440

434-
local rebuild_called = false
435-
local original_rebuild = render_state._rebuild_line_index
436-
render_state._rebuild_line_index = function(self)
437-
rebuild_called = true
438-
original_rebuild(self)
439-
end
441+
render_state._line_index_valid = true
440442

441443
render_state:shift_all(100, 5)
442444

443-
assert.is_false(rebuild_called)
444-
render_state._rebuild_line_index = original_rebuild
445+
assert.is_true(render_state._line_index_valid)
445446
end)
446447

447-
it('rebuilds index when content shifted', function()
448+
it('invalidates index when content shifted', function()
448449
local part = { id = 'part1' }
449450
render_state:set_part('part1', part, 'msg1', 10, 15)
450451

451-
local rebuild_called = false
452-
local original_rebuild = render_state._rebuild_line_index
453-
render_state._rebuild_line_index = function(self)
454-
rebuild_called = true
455-
original_rebuild(self)
456-
end
452+
render_state._line_index_valid = true
457453

458454
render_state:shift_all(10, 5)
459455

460-
assert.is_true(rebuild_called)
461-
render_state._rebuild_line_index = original_rebuild
456+
assert.is_false(render_state._line_index_valid)
462457
end)
463458

464459
it('exits early when content found before from_line', function()

0 commit comments

Comments
 (0)