Skip to content

Commit adfd3ad

Browse files
committed
refactor(ui): track rendered blocks in RenderState
Add a RenderedBlock type and maintain block metadata maps on the RenderState (instance-level tables: _blocks, _message_blocks, _part_blocks). Introduce APIs to set/get/remove blocks and to associate blocks with messages and parts: set_block, set_message_block, set_part_block, get_block, get_message_block, get_part_block, and remove_block. Ensure block line ranges stay in sync when messages/parts are updated, shifted, or removed. Update reset, shift and removal logic accordingly and add unit tests for block metadata handling.
1 parent 7c22c24 commit adfd3ad

2 files changed

Lines changed: 369 additions & 0 deletions

File tree

lua/opencode/ui/render_state.lua

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,22 @@
1111
---@field actions table[] Actions associated with this part
1212
---@field has_extmarks boolean? Whether the part currently has extmarks applied
1313

14+
---@class RenderedBlock
15+
---@field block RenderBlock Direct reference to a cached/rendered block
16+
---@field key string Stable render block key
17+
---@field kind string Block kind (message/part/overlay)
18+
---@field message_id string? Parent message ID when applicable
19+
---@field part_id string? Parent part ID when applicable
20+
---@field line_start integer? Line where the block starts when mounted
21+
---@field line_end integer? Line where the block ends when mounted
22+
---@field line_count integer? Cached block line count
23+
1424
---@class RenderState
1525
---@field _messages table<string, RenderedMessage> Message ID -> rendered message
1626
---@field _parts table<string, RenderedPart> Part ID -> rendered part
27+
---@field _blocks table<string, RenderedBlock> Block key -> rendered block
28+
---@field _message_blocks table<string, string> Message ID -> primary block key
29+
---@field _part_blocks table<string, string> Part ID -> primary block key
1730
---@field _part_ranges {[1]: integer, [2]: integer, [3]: string}[] Sorted [line_start, line_end, part_id] for binary search
1831
---@field _message_ranges {[1]: integer, [2]: integer, [3]: string}[] Sorted [line_start, line_end, message_id] for binary search
1932
---@field _ranges_valid boolean Whether range arrays are sorted and up-to-date
@@ -32,6 +45,9 @@ end
3245
function RenderState:reset()
3346
self._messages = {}
3447
self._parts = {}
48+
self._blocks = {}
49+
self._message_blocks = {}
50+
self._part_blocks = {}
3551
self._part_ranges = {}
3652
self._message_ranges = {}
3753
self._ranges_valid = false
@@ -44,6 +60,92 @@ function RenderState:reset()
4460
self._snapshot_id_index = {} -- snapshot_id -> OpencodeMessagePart
4561
end
4662

63+
---@param mapping table<string, string>
64+
---@param id string?
65+
---@param key string?
66+
local function clear_block_reference(mapping, id, key)
67+
if not id or not key then
68+
return
69+
end
70+
71+
if mapping[id] == key then
72+
mapping[id] = nil
73+
end
74+
end
75+
76+
---@param block RenderBlock?
77+
---@return string?
78+
local function get_block_key(block)
79+
if not block or not block.key or block.key == '' then
80+
return nil
81+
end
82+
return block.key
83+
end
84+
85+
---@param message string|OpencodeMessage|nil
86+
---@return string?
87+
local function resolve_message_id(message)
88+
if type(message) == 'string' then
89+
return message ~= '' and message or nil
90+
end
91+
92+
return message and message.info and message.info.id or nil
93+
end
94+
95+
---@param part string|OpencodeMessagePart|nil
96+
---@return string?
97+
local function resolve_part_id(part)
98+
if type(part) == 'string' then
99+
return part ~= '' and part or nil
100+
end
101+
102+
return part and part.id or nil
103+
end
104+
105+
---@param block_key string
106+
---@param line_start integer?
107+
---@param line_end integer?
108+
---@param message_id string?
109+
---@param part_id string?
110+
---@return RenderedBlock?
111+
function RenderState:_upsert_block_entry(block_key, line_start, line_end, message_id, part_id)
112+
local entry = self._blocks[block_key]
113+
if not entry then
114+
return nil
115+
end
116+
117+
local previous_message_id = entry.message_id
118+
local previous_part_id = entry.part_id
119+
120+
entry.message_id = message_id or entry.message_id
121+
entry.part_id = part_id or entry.part_id
122+
entry.line_count = entry.block and entry.block.line_count or entry.line_count
123+
entry.kind = entry.block and entry.block.kind or entry.kind
124+
125+
if line_start ~= nil then
126+
entry.line_start = line_start
127+
end
128+
if line_end ~= nil then
129+
entry.line_end = line_end
130+
end
131+
132+
if previous_message_id ~= entry.message_id then
133+
clear_block_reference(self._message_blocks, previous_message_id, block_key)
134+
end
135+
if previous_part_id ~= entry.part_id then
136+
clear_block_reference(self._part_blocks, previous_part_id, block_key)
137+
end
138+
139+
if entry.kind == 'message' and entry.message_id then
140+
self._message_blocks[entry.message_id] = block_key
141+
end
142+
if entry.kind == 'part' and entry.part_id then
143+
self._part_blocks[entry.part_id] = block_key
144+
end
145+
146+
return entry
147+
end
148+
47149
function RenderState:_recompute_max_line_end()
48150
local max_line_end = 0
49151

@@ -59,6 +161,12 @@ function RenderState:_recompute_max_line_end()
59161
end
60162
end
61163

164+
for _, block_data in pairs(self._blocks) do
165+
if block_data.line_end and block_data.line_end > max_line_end then
166+
max_line_end = block_data.line_end
167+
end
168+
end
169+
62170
self._max_line_end = max_line_end
63171
self._max_line_end_valid = true
64172
return max_line_end
@@ -205,6 +313,44 @@ function RenderState:get_message(message_id)
205313
return self._messages[message_id]
206314
end
207315

316+
---@param block_key string
317+
---@return RenderedBlock?
318+
function RenderState:get_block(block_key)
319+
return self._blocks[block_key]
320+
end
321+
322+
---@param message string|OpencodeMessage
323+
---@return RenderedBlock?
324+
function RenderState:get_message_block(message)
325+
local message_id = resolve_message_id(message)
326+
if not message_id then
327+
return nil
328+
end
329+
330+
local block_key = self._message_blocks[message_id]
331+
if not block_key then
332+
block_key = 'message:' .. message_id
333+
end
334+
335+
return self._blocks[block_key]
336+
end
337+
338+
---@param part string|OpencodeMessagePart
339+
---@return RenderedBlock?
340+
function RenderState:get_part_block(part)
341+
local part_id = resolve_part_id(part)
342+
if not part_id then
343+
return nil
344+
end
345+
346+
local block_key = self._part_blocks[part_id]
347+
if not block_key then
348+
block_key = 'part:' .. part_id
349+
end
350+
351+
return self._blocks[block_key]
352+
end
353+
208354
---@param line integer 1-indexed
209355
---@return RenderedMessage?
210356
function RenderState:get_message_at_line(line)
@@ -347,6 +493,58 @@ function RenderState:set_message(message, line_start, line_end)
347493
self._max_line_end = line_end
348494
end
349495
end
496+
497+
local block_key = self._message_blocks[message_id] or ('message:' .. message_id)
498+
if self._blocks[block_key] then
499+
self:_upsert_block_entry(block_key, line_start, line_end, message_id, nil)
500+
end
501+
end
502+
503+
---@param block RenderBlock
504+
---@param line_start integer?
505+
---@param line_end integer?
506+
---@return RenderedBlock?
507+
function RenderState:set_block(block, line_start, line_end)
508+
local block_key = get_block_key(block)
509+
if not block_key then
510+
return nil
511+
end
512+
513+
local existing = self._blocks[block_key]
514+
if not existing then
515+
self._blocks[block_key] = {
516+
block = block,
517+
key = block_key,
518+
kind = block.kind,
519+
message_id = block.message_id,
520+
part_id = block.part_id,
521+
line_start = line_start,
522+
line_end = line_end,
523+
line_count = block.line_count,
524+
}
525+
else
526+
existing.block = block
527+
existing.key = block_key
528+
existing.kind = block.kind
529+
existing.line_count = block.line_count
530+
end
531+
532+
return self:_upsert_block_entry(block_key, line_start, line_end, block.message_id, block.part_id)
533+
end
534+
535+
---@param message string|OpencodeMessage
536+
---@param block RenderBlock
537+
---@param line_start integer?
538+
---@param line_end integer?
539+
---@return RenderedBlock?
540+
function RenderState:set_message_block(message, block, line_start, line_end)
541+
local message_id = resolve_message_id(message)
542+
local entry = self:set_block(block, line_start, line_end)
543+
if not entry then
544+
return nil
545+
end
546+
547+
return self:_upsert_block_entry(entry.key, line_start, line_end, message_id, entry.part_id)
350548
end
351549

352550
---@param part OpencodeMessagePart
@@ -394,6 +592,26 @@ function RenderState:set_part(part, line_start, line_end)
394592
end
395593

396594
self:_index_task_part_child_session(part_id, part)
595+
596+
local block_key = self._part_blocks[part_id] or ('part:' .. part_id)
597+
if self._blocks[block_key] then
598+
self:_upsert_block_entry(block_key, line_start, line_end, message_id, part_id)
599+
end
600+
end
601+
602+
---@param part string|OpencodeMessagePart
603+
---@param block RenderBlock
604+
---@param line_start integer?
605+
---@param line_end integer?
606+
---@return RenderedBlock?
607+
function RenderState:set_part_block(part, block, line_start, line_end)
608+
local part_id = resolve_part_id(part)
609+
local entry = self:set_block(block, line_start, line_end)
610+
if not entry then
611+
return nil
612+
end
613+
614+
return self:_upsert_block_entry(entry.key, line_start, line_end, entry.message_id, part_id)
397615
end
398616

399617
---@param part_id string
@@ -419,6 +637,11 @@ function RenderState:update_part_lines(part_id, new_line_start, new_line_end)
419637
part_data.line_end = new_line_end
420638
self._ranges_valid = false
421639

640+
local block_key = self._part_blocks[part_id] or ('part:' .. part_id)
641+
if self._blocks[block_key] then
642+
self:_upsert_block_entry(block_key, new_line_start, new_line_end, part_data.message_id, part_id)
643+
end
644+
422645
if self._max_line_end_valid then
423646
if old_line_end == self._max_line_end and new_line_end < old_line_end then
424647
self._max_line_end_valid = false
@@ -462,6 +685,8 @@ function RenderState:remove_part(part_id)
462685
return false
463686
end
464687

688+
self:remove_block(self._part_blocks[part_id] or ('part:' .. part_id))
689+
465690
if part_data.part and part_data.part.type == 'patch' and part_data.part.hash then
466691
self._snapshot_id_index[part_data.part.hash] = nil
467692
end
@@ -486,6 +711,20 @@ function RenderState:remove_part(part_id)
486711
return true
487712
end
488713

714+
---@param block_key string
715+
---@return boolean
716+
function RenderState:remove_block(block_key)
717+
local entry = self._blocks[block_key]
718+
if not entry then
719+
return false
720+
end
721+
722+
clear_block_reference(self._message_blocks, entry.message_id, block_key)
723+
clear_block_reference(self._part_blocks, entry.part_id, block_key)
724+
self._blocks[block_key] = nil
725+
return true
726+
end
727+
489728
---@param message_id string
490729
---@return boolean
491730
function RenderState:remove_message(message_id)
@@ -494,6 +733,8 @@ function RenderState:remove_message(message_id)
494733
return false
495734
end
496735

736+
self:remove_block(self._message_blocks[message_id] or ('message:' .. message_id))
737+
497738
local line_count = msg_data.line_end - msg_data.line_start + 1
498739
local shift_from = msg_data.line_end + 1
499740

@@ -577,6 +818,14 @@ function RenderState:shift_all(from_line, delta)
577818
end
578819
end
579820

821+
for _, block_data in pairs(self._blocks) do
822+
if block_data.line_start and block_data.line_end and block_data.line_start >= from_line then
823+
block_data.line_start = block_data.line_start + delta
824+
block_data.line_end = block_data.line_end + delta
825+
shifted = true
826+
end
827+
end
828+
580829
if shifted then
581830
self._ranges_valid = false
582831
if self._max_line_end_valid then

0 commit comments

Comments
 (0)