@@ -5,6 +5,11 @@ local output_window = require('opencode.ui.output_window')
55
66local M = {}
77
8+ local function resolve_extmark (extmark )
9+ local actual_extmark = type (extmark ) == ' function' and extmark () or extmark
10+ return vim .deepcopy (actual_extmark )
11+ end
12+
813local function merge_output (target , source )
914 if not source then
1015 return
@@ -60,6 +65,181 @@ local function add_trailing_padding(output)
6065 return output
6166end
6267
68+ local function build_output_with_padding (visible_render )
69+ return add_trailing_padding (M .flatten_visible_render (visible_render ))
70+ end
71+
72+ local function resolve_line_extmarks (output , line_index )
73+ local resolved_extmarks = {}
74+
75+ for _ , extmark in ipairs ((output .extmarks or {})[line_index ] or {}) do
76+ resolved_extmarks [# resolved_extmarks + 1 ] = resolve_extmark (extmark )
77+ end
78+
79+ return resolved_extmarks
80+ end
81+
82+ local function line_matches (previous_output , previous_line_index , next_output , next_line_index )
83+ return (previous_output .lines or {})[previous_line_index + 1 ] == (next_output .lines or {})[next_line_index + 1 ]
84+ and vim .deep_equal (
85+ resolve_line_extmarks (previous_output , previous_line_index ),
86+ resolve_line_extmarks (next_output , next_line_index )
87+ )
88+ end
89+
90+ local function build_line_patches (previous_output , next_output )
91+ local previous_line_count = # (previous_output .lines or {})
92+ local next_line_count = # (next_output .lines or {})
93+ local shared_prefix = 0
94+
95+ while shared_prefix < previous_line_count and shared_prefix < next_line_count do
96+ if not line_matches (previous_output , shared_prefix , next_output , shared_prefix ) then
97+ break
98+ end
99+ shared_prefix = shared_prefix + 1
100+ end
101+
102+ if shared_prefix == previous_line_count and shared_prefix == next_line_count then
103+ return {}
104+ end
105+
106+ local shared_suffix = 0
107+ while shared_suffix < (previous_line_count - shared_prefix ) and shared_suffix < (next_line_count - shared_prefix ) do
108+ if not line_matches (
109+ previous_output ,
110+ previous_line_count - shared_suffix - 1 ,
111+ next_output ,
112+ next_line_count - shared_suffix - 1
113+ ) then
114+ break
115+ end
116+ shared_suffix = shared_suffix + 1
117+ end
118+
119+ return {
120+ {
121+ old_start_line = shared_prefix ,
122+ old_end_line_exclusive = previous_line_count - shared_suffix ,
123+ new_start_line = shared_prefix ,
124+ new_end_line_exclusive = next_line_count - shared_suffix ,
125+ },
126+ }
127+ end
128+
129+ local function slice_output (output , start_line , end_line_exclusive )
130+ local lines = {}
131+ for line_index = start_line + 1 , end_line_exclusive do
132+ lines [# lines + 1 ] = (output .lines or {})[line_index ] or ' '
133+ end
134+
135+ local extmarks = {}
136+ for line_index , marks in pairs (output .extmarks or {}) do
137+ if line_index >= start_line and line_index < end_line_exclusive then
138+ local target_index = line_index - start_line
139+ extmarks [target_index ] = {}
140+ for _ , extmark in ipairs (marks ) do
141+ local next_extmark = resolve_extmark (extmark )
142+ if next_extmark .end_row then
143+ next_extmark .end_row = next_extmark .end_row - start_line
144+ end
145+ extmarks [target_index ][# extmarks [target_index ] + 1 ] = next_extmark
146+ end
147+ end
148+ end
149+
150+ return {
151+ lines = lines ,
152+ extmarks = extmarks ,
153+ }
154+ end
155+
156+ local function apply_full_output (output )
157+ local updated_ranges = {
158+ {
159+ start_line = 0 ,
160+ end_line_exclusive = # (output .lines or {}),
161+ },
162+ }
163+
164+ if output_window .buffer_valid () then
165+ output_window .begin_update ()
166+ output_window .clear_extmarks (0 , - 1 , true )
167+ output_window .set_lines (output .lines or {})
168+ output_window .set_extmarks (output .extmarks )
169+ output_window .end_update ()
170+ end
171+
172+ return updated_ranges
173+ end
174+
175+ local function apply_changed_line_patches (previous_output , next_output )
176+ local updated_ranges = {}
177+ local patches = build_line_patches (previous_output , next_output )
178+ local line_delta = 0
179+
180+ if output_window .buffer_valid () then
181+ output_window .begin_update ()
182+ output_window .clear_extmarks (0 , - 1 , true )
183+ for _ , patch in ipairs (patches ) do
184+ local start_line = patch .old_start_line + line_delta
185+ local old_end_line_exclusive = patch .old_end_line_exclusive + line_delta
186+ local replacement = slice_output (next_output , patch .new_start_line , patch .new_end_line_exclusive )
187+
188+ output_window .set_lines (replacement .lines or {}, start_line , old_end_line_exclusive )
189+
190+ updated_ranges [# updated_ranges + 1 ] = {
191+ start_line = start_line ,
192+ end_line_exclusive = start_line + # (replacement .lines or {}),
193+ }
194+
195+ line_delta = line_delta + (# (replacement .lines or {}) - (patch .old_end_line_exclusive - patch .old_start_line ))
196+ end
197+ output_window .set_extmarks (next_output .extmarks )
198+ output_window .end_update ()
199+ end
200+
201+ return updated_ranges
202+ end
203+
204+ local function apply_block_patch (visible_render , patch )
205+ local replacement = flatten_block_range (visible_render .blocks or {}, patch .new_start_index , patch .new_end_index )
206+ if patch .include_trailing_padding then
207+ replacement = add_trailing_padding (replacement )
208+ end
209+
210+ local updated_ranges = {
211+ {
212+ start_line = patch .start_line ,
213+ end_line_exclusive = patch .start_line + # (replacement .lines or {}),
214+ },
215+ }
216+
217+ if output_window .buffer_valid () then
218+ output_window .begin_update ()
219+ output_window .clear_extmarks (patch .start_line , patch .old_end_line_exclusive , true )
220+ output_window .set_lines (replacement .lines or {}, patch .start_line , patch .old_end_line_exclusive )
221+ output_window .set_extmarks (replacement .extmarks , patch .start_line )
222+ output_window .end_update ()
223+ end
224+
225+ return updated_ranges
226+ end
227+
228+ local function maybe_flash_updated_ranges (ranges )
229+ if not require (' opencode.config' ).debug .highlight_updated_lines then
230+ return
231+ end
232+
233+ local flashable_ranges = {}
234+ for _ , range in ipairs (ranges or {}) do
235+ if (range .end_line_exclusive or 0 ) > (range .start_line or 0 ) then
236+ flashable_ranges [# flashable_ranges + 1 ] = range
237+ end
238+ end
239+
240+ output_window .flash_updated_line_ranges (flashable_ranges )
241+ end
242+
63243local function get_message_by_id (message_id )
64244 for _ , message in ipairs (state .messages or {}) do
65245 if message and message .info and message .info .id == message_id then
@@ -113,48 +293,52 @@ end
113293--- @param visible_render { blocks ?: RenderBlock[] }
114294--- @return Output
115295function M .apply (visible_render )
116- local output = add_trailing_padding (M .flatten_visible_render (visible_render ))
117-
118- if output_window .buffer_valid () then
119- output_window .begin_update ()
120- output_window .set_lines (output .lines or {})
121- output_window .clear_extmarks (0 , - 1 , true )
122- output_window .set_extmarks (output .extmarks )
123- output_window .end_update ()
124- end
296+ local output = build_output_with_padding (visible_render )
297+ local updated_ranges = apply_full_output (output )
125298
126299 rebuild_render_state_from_blocks (visible_render and visible_render .blocks or {})
300+ maybe_flash_updated_ranges (updated_ranges )
127301 return output
128302end
129303
130304--- @param plan { strategy : ' noop' | ' full' | ' patch' , visible_render : table , patch ?: table }
131305--- @return Output
132306function M .apply_plan (plan )
133307 if plan and plan .strategy == ' noop' then
134- return add_trailing_padding ( M . flatten_visible_render ( plan .visible_render ) )
308+ return build_output_with_padding ( plan .visible_render )
135309 end
136310
137311 if not plan or plan .strategy ~= ' patch' or not plan .patch then
138- return M .apply (plan and plan .visible_render or nil )
312+ local visible_render = plan and plan .visible_render or nil
313+ local next_output = build_output_with_padding (visible_render )
314+ local previous_visible_render = ctx .prev_visible_render
315+ local updated_ranges
316+
317+ if previous_visible_render then
318+ updated_ranges = apply_changed_line_patches (build_output_with_padding (previous_visible_render ), next_output )
319+ else
320+ updated_ranges = apply_full_output (next_output )
321+ end
322+
323+ rebuild_render_state_from_blocks (visible_render and visible_render .blocks or {})
324+ maybe_flash_updated_ranges (updated_ranges )
325+ return next_output
139326 end
140327
141328 local visible_render = plan .visible_render or {}
142- local patch = plan .patch
143- local replacement = flatten_block_range (visible_render .blocks or {}, patch .new_start_index , patch .new_end_index )
144- if patch .include_trailing_padding then
145- replacement = add_trailing_padding (replacement )
146- end
329+ local next_output = build_output_with_padding (visible_render )
330+ local previous_visible_render = ctx .prev_visible_render
331+ local updated_ranges
147332
148- if output_window .buffer_valid () then
149- output_window .begin_update ()
150- output_window .set_lines (replacement .lines or {}, patch .start_line , patch .old_end_line_exclusive )
151- output_window .clear_extmarks (patch .start_line , patch .old_end_line_exclusive , false )
152- output_window .set_extmarks (replacement .extmarks , patch .start_line )
153- output_window .end_update ()
333+ if previous_visible_render then
334+ updated_ranges = apply_changed_line_patches (build_output_with_padding (previous_visible_render ), next_output )
335+ else
336+ updated_ranges = apply_block_patch (visible_render , plan .patch )
154337 end
155338
156339 rebuild_render_state_from_blocks (visible_render .blocks or {})
157- return add_trailing_padding (M .flatten_visible_render (visible_render ))
340+ maybe_flash_updated_ranges (updated_ranges )
341+ return next_output
158342end
159343
160344--- @param prev_visible_render { blocks ?: RenderBlock[] }?
0 commit comments