@@ -13,13 +13,14 @@ use std::path::Path;
13
13
use std:: path:: PathBuf ;
14
14
15
15
use crate :: exec_command:: relativize_to_home;
16
+ use crate :: render:: Insets ;
17
+ use crate :: render:: line_utils:: prefix_lines;
16
18
use crate :: render:: renderable:: ColumnRenderable ;
19
+ use crate :: render:: renderable:: InsetRenderable ;
17
20
use crate :: render:: renderable:: Renderable ;
18
21
use codex_core:: git_info:: get_git_repo_root;
19
22
use codex_core:: protocol:: FileChange ;
20
23
21
- const SPACES_AFTER_LINE_NUMBER : usize = 6 ;
22
-
23
24
// Internal representation for diff line rendering
24
25
enum DiffLineType {
25
26
Insert ,
@@ -65,7 +66,10 @@ impl From<DiffSummary> for Box<dyn Renderable> {
65
66
path. extend ( render_line_count_summary ( row. added , row. removed ) ) ;
66
67
rows. push ( Box :: new ( path) ) ;
67
68
rows. push ( Box :: new ( RtLine :: from ( "" ) ) ) ;
68
- rows. push ( Box :: new ( row. change ) ) ;
69
+ rows. push ( Box :: new ( InsetRenderable :: new (
70
+ Box :: new ( row. change ) ,
71
+ Insets :: tlbr ( 0 , 2 , 0 , 0 ) ,
72
+ ) ) ) ;
69
73
}
70
74
71
75
Box :: new ( ColumnRenderable :: new ( rows) )
@@ -181,7 +185,9 @@ fn render_changes_block(rows: Vec<Row>, wrap_cols: usize, cwd: &Path) -> Vec<RtL
181
185
out. push ( RtLine :: from ( header) ) ;
182
186
}
183
187
184
- render_change ( & r. change , & mut out, wrap_cols) ;
188
+ let mut lines = vec ! [ ] ;
189
+ render_change ( & r. change , & mut lines, wrap_cols - 4 ) ;
190
+ out. extend ( prefix_lines ( lines, " " . into ( ) , " " . into ( ) ) ) ;
185
191
}
186
192
187
193
out
@@ -190,31 +196,60 @@ fn render_changes_block(rows: Vec<Row>, wrap_cols: usize, cwd: &Path) -> Vec<RtL
190
196
fn render_change ( change : & FileChange , out : & mut Vec < RtLine < ' static > > , width : usize ) {
191
197
match change {
192
198
FileChange :: Add { content } => {
199
+ let line_number_width = line_number_width ( content. lines ( ) . count ( ) ) ;
193
200
for ( i, raw) in content. lines ( ) . enumerate ( ) {
194
201
out. extend ( push_wrapped_diff_line (
195
202
i + 1 ,
196
203
DiffLineType :: Insert ,
197
204
raw,
198
205
width,
206
+ line_number_width,
199
207
) ) ;
200
208
}
201
209
}
202
210
FileChange :: Delete { content } => {
211
+ let line_number_width = line_number_width ( content. lines ( ) . count ( ) ) ;
203
212
for ( i, raw) in content. lines ( ) . enumerate ( ) {
204
213
out. extend ( push_wrapped_diff_line (
205
214
i + 1 ,
206
215
DiffLineType :: Delete ,
207
216
raw,
208
217
width,
218
+ line_number_width,
209
219
) ) ;
210
220
}
211
221
}
212
222
FileChange :: Update { unified_diff, .. } => {
213
223
if let Ok ( patch) = diffy:: Patch :: from_str ( unified_diff) {
224
+ let mut max_line_number = 0 ;
225
+ for h in patch. hunks ( ) {
226
+ let mut old_ln = h. old_range ( ) . start ( ) ;
227
+ let mut new_ln = h. new_range ( ) . start ( ) ;
228
+ for l in h. lines ( ) {
229
+ match l {
230
+ diffy:: Line :: Insert ( _) => {
231
+ max_line_number = max_line_number. max ( new_ln) ;
232
+ new_ln += 1 ;
233
+ }
234
+ diffy:: Line :: Delete ( _) => {
235
+ max_line_number = max_line_number. max ( old_ln) ;
236
+ old_ln += 1 ;
237
+ }
238
+ diffy:: Line :: Context ( _) => {
239
+ max_line_number = max_line_number. max ( new_ln) ;
240
+ old_ln += 1 ;
241
+ new_ln += 1 ;
242
+ }
243
+ }
244
+ }
245
+ }
246
+ let line_number_width = line_number_width ( max_line_number) ;
214
247
let mut is_first_hunk = true ;
215
248
for h in patch. hunks ( ) {
216
249
if !is_first_hunk {
217
- out. push ( RtLine :: from ( vec ! [ " " . into( ) , "⋮" . dim( ) ] ) ) ;
250
+ let spacer = format ! ( "{:width$} " , "" , width = line_number_width. max( 1 ) ) ;
251
+ let spacer_span = RtSpan :: styled ( spacer, style_gutter ( ) ) ;
252
+ out. push ( RtLine :: from ( vec ! [ spacer_span, "⋮" . dim( ) ] ) ) ;
218
253
}
219
254
is_first_hunk = false ;
220
255
@@ -229,6 +264,7 @@ fn render_change(change: &FileChange, out: &mut Vec<RtLine<'static>>, width: usi
229
264
DiffLineType :: Insert ,
230
265
s,
231
266
width,
267
+ line_number_width,
232
268
) ) ;
233
269
new_ln += 1 ;
234
270
}
@@ -239,6 +275,7 @@ fn render_change(change: &FileChange, out: &mut Vec<RtLine<'static>>, width: usi
239
275
DiffLineType :: Delete ,
240
276
s,
241
277
width,
278
+ line_number_width,
242
279
) ) ;
243
280
old_ln += 1 ;
244
281
}
@@ -249,6 +286,7 @@ fn render_change(change: &FileChange, out: &mut Vec<RtLine<'static>>, width: usi
249
286
DiffLineType :: Context ,
250
287
s,
251
288
width,
289
+ line_number_width,
252
290
) ) ;
253
291
old_ln += 1 ;
254
292
new_ln += 1 ;
@@ -298,17 +336,15 @@ fn push_wrapped_diff_line(
298
336
kind : DiffLineType ,
299
337
text : & str ,
300
338
width : usize ,
339
+ line_number_width : usize ,
301
340
) -> Vec < RtLine < ' static > > {
302
- let indent = " " ;
303
341
let ln_str = line_number. to_string ( ) ;
304
342
let mut remaining_text: & str = text;
305
343
306
- // Reserve a fixed number of spaces after the line number so that content starts
307
- // at a consistent column. Content includes a 1-character diff sign prefix
308
- // ("+"/"-" for inserts/deletes, or a space for context lines) so alignment
309
- // stays consistent across all diff lines.
310
- let gap_after_ln = SPACES_AFTER_LINE_NUMBER . saturating_sub ( ln_str. len ( ) ) ;
311
- let prefix_cols = indent. len ( ) + ln_str. len ( ) + gap_after_ln;
344
+ // Reserve a fixed number of spaces (equal to the widest line number plus a
345
+ // trailing spacer) so the sign column stays aligned across the diff block.
346
+ let gutter_width = line_number_width. max ( 1 ) ;
347
+ let prefix_cols = gutter_width + 1 ;
312
348
313
349
let mut first = true ;
314
350
let ( sign_char, line_style) = match kind {
@@ -332,8 +368,8 @@ fn push_wrapped_diff_line(
332
368
remaining_text = rest;
333
369
334
370
if first {
335
- // Build gutter (indent + line number + spacing ) as a dimmed span
336
- let gutter = format ! ( "{indent}{ ln_str}{}" , " " . repeat ( gap_after_ln ) ) ;
371
+ // Build gutter (right-aligned line number plus spacer ) as a dimmed span
372
+ let gutter = format ! ( "{ln_str:>gutter_width$} " ) ;
337
373
// Content with a sign ('+'/'-'/' ') styled per diff kind
338
374
let content = format ! ( "{sign_char}{chunk}" ) ;
339
375
lines. push ( RtLine :: from ( vec ! [
@@ -343,7 +379,7 @@ fn push_wrapped_diff_line(
343
379
first = false ;
344
380
} else {
345
381
// Continuation lines keep a space for the sign column so content aligns
346
- let gutter = format ! ( "{indent}{} " , " " . repeat ( ln_str . len ( ) + gap_after_ln ) ) ;
382
+ let gutter = format ! ( "{:gutter_width$} " , "" ) ;
347
383
lines. push ( RtLine :: from ( vec ! [
348
384
RtSpan :: styled( gutter, style_gutter( ) ) ,
349
385
RtSpan :: styled( chunk. to_string( ) , line_style) ,
@@ -356,6 +392,14 @@ fn push_wrapped_diff_line(
356
392
lines
357
393
}
358
394
395
+ fn line_number_width ( max_line_number : usize ) -> usize {
396
+ if max_line_number == 0 {
397
+ 1
398
+ } else {
399
+ max_line_number. to_string ( ) . len ( )
400
+ }
401
+ }
402
+
359
403
fn style_gutter ( ) -> Style {
360
404
Style :: default ( ) . add_modifier ( Modifier :: DIM )
361
405
}
@@ -421,7 +465,8 @@ mod tests {
421
465
let long_line = "this is a very long line that should wrap across multiple terminal columns and continue" ;
422
466
423
467
// Call the wrapping function directly so we can precisely control the width
424
- let lines = push_wrapped_diff_line ( 1 , DiffLineType :: Insert , long_line, 80 ) ;
468
+ let lines =
469
+ push_wrapped_diff_line ( 1 , DiffLineType :: Insert , long_line, 80 , line_number_width ( 1 ) ) ;
425
470
426
471
// Render into a small terminal to capture the visual layout
427
472
snapshot_lines ( "wrap_behavior_insert" , lines, 90 , 8 ) ;
@@ -442,11 +487,9 @@ mod tests {
442
487
} ,
443
488
) ;
444
489
445
- for name in [ "apply_update_block" , "apply_update_block_manual" ] {
446
- let lines = diff_summary_for_tests ( & changes) ;
490
+ let lines = diff_summary_for_tests ( & changes) ;
447
491
448
- snapshot_lines ( name, lines, 80 , 12 ) ;
449
- }
492
+ snapshot_lines ( "apply_update_block" , lines, 80 , 12 ) ;
450
493
}
451
494
452
495
#[ test]
@@ -573,14 +616,37 @@ mod tests {
573
616
} ,
574
617
) ;
575
618
576
- let mut lines = create_diff_summary ( & changes, & PathBuf :: from ( "/" ) , 28 ) ;
577
- // Drop the combined header for this text-only snapshot
578
- if !lines. is_empty ( ) {
579
- lines. remove ( 0 ) ;
580
- }
619
+ let lines = create_diff_summary ( & changes, & PathBuf :: from ( "/" ) , 28 ) ;
581
620
snapshot_lines_text ( "apply_update_block_wraps_long_lines_text" , & lines) ;
582
621
}
583
622
623
+ #[ test]
624
+ fn ui_snapshot_apply_update_block_line_numbers_three_digits_text ( ) {
625
+ let original = ( 1 ..=110 ) . map ( |i| format ! ( "line {i}\n " ) ) . collect :: < String > ( ) ;
626
+ let modified = ( 1 ..=110 )
627
+ . map ( |i| {
628
+ if i == 100 {
629
+ format ! ( "line {i} changed\n " )
630
+ } else {
631
+ format ! ( "line {i}\n " )
632
+ }
633
+ } )
634
+ . collect :: < String > ( ) ;
635
+ let patch = diffy:: create_patch ( & original, & modified) . to_string ( ) ;
636
+
637
+ let mut changes: HashMap < PathBuf , FileChange > = HashMap :: new ( ) ;
638
+ changes. insert (
639
+ PathBuf :: from ( "hundreds.txt" ) ,
640
+ FileChange :: Update {
641
+ unified_diff : patch,
642
+ move_path : None ,
643
+ } ,
644
+ ) ;
645
+
646
+ let lines = create_diff_summary ( & changes, & PathBuf :: from ( "/" ) , 80 ) ;
647
+ snapshot_lines_text ( "apply_update_block_line_numbers_three_digits_text" , & lines) ;
648
+ }
649
+
584
650
#[ test]
585
651
fn ui_snapshot_apply_update_block_relativizes_path ( ) {
586
652
let cwd = std:: env:: current_dir ( ) . unwrap_or_else ( |_| PathBuf :: from ( "/" ) ) ;
0 commit comments