Skip to content

Commit f45345c

Browse files
Copilotkovidgoyal
authored andcommitted
Add colorMoved support to kitty diff kitten
Fixes #3241 Fixes #9644
1 parent c785921 commit f45345c

3 files changed

Lines changed: 97 additions & 8 deletions

File tree

kittens/diff/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ def main(args: list[str]) -> None:
152152
opt('added_margin_bg', '#cdffd8', option_type='to_color')
153153
opt('dark_added_margin_bg', '#31503d', option_type='to_color')
154154

155+
opt('moved_bg', '#fffde7', option_type='to_color', long_text='Moved text backgrounds (same text that was removed in one place and added in another)')
156+
opt('dark_moved_bg', '#2c2200', option_type='to_color')
157+
158+
opt('moved_margin_bg', '#fff3b0', option_type='to_color')
159+
opt('dark_moved_margin_bg', '#4a3800', option_type='to_color')
160+
155161
opt('filler_bg', '#fafbfc', option_type='to_color', long_text='Filler (empty) line background')
156162
opt('dark_filler_bg', '#262c36', option_type='to_color')
157163

kittens/diff/patch.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"sync"
1515

1616
parallel "github.com/kovidgoyal/go-parallel"
17+
"github.com/kovidgoyal/kitty/tools/simdstring"
1718
"github.com/kovidgoyal/kitty/tools/utils"
1819
"github.com/kovidgoyal/kitty/tools/utils/images"
1920
"github.com/kovidgoyal/kitty/tools/utils/shlex"
@@ -336,6 +337,7 @@ func (self *Hunk) finalize(left_lines, right_lines []string) error {
336337
type Patch struct {
337338
all_hunks []*Hunk
338339
largest_line_number, added_count, removed_count int
340+
left_moved_lines, right_moved_lines *utils.Set[int]
339341
}
340342

341343
func (self *Patch) Len() int { return len(self.all_hunks) }
@@ -451,6 +453,62 @@ func (self *Patch) compute_centers(left_lines, right_lines []string) error {
451453
return nil
452454
}
453455

456+
func (self *Patch) detect_moved_lines(left_lines, right_lines []string) {
457+
// Build maps from line text to lists of line numbers for removed and added lines.
458+
removed := make(map[string][]int) // text -> left line numbers
459+
added := make(map[string][]int) // text -> right line numbers
460+
// Use SIMD to efficiently find non-blank lines: a line is non-blank if it
461+
// contains at least one character that is not a space or tab.
462+
is_non_blank := func(text string) bool {
463+
for len(text) > 0 {
464+
idx := simdstring.IndexByte2String(text, ' ', '\t')
465+
if idx != 0 {
466+
// idx < 0: no space/tab found, remaining chars are non-blank;
467+
// idx > 0: non-blank chars exist before the first space/tab.
468+
return true
469+
}
470+
text = text[1:]
471+
}
472+
return false
473+
}
474+
for _, hunk := range self.all_hunks {
475+
for _, chunk := range hunk.chunks {
476+
if !chunk.is_context {
477+
for i := 0; i < chunk.left_count; i++ {
478+
lnum := chunk.left_start + i
479+
text := left_lines[lnum]
480+
if is_non_blank(text) {
481+
removed[text] = append(removed[text], lnum)
482+
}
483+
}
484+
for i := 0; i < chunk.right_count; i++ {
485+
rnum := chunk.right_start + i
486+
text := right_lines[rnum]
487+
if is_non_blank(text) {
488+
added[text] = append(added[text], rnum)
489+
}
490+
}
491+
}
492+
}
493+
}
494+
// Lines that appear in both removed and added sets are moved lines. When a
495+
// line appears multiple times on each side, only min(left_count,
496+
// right_count) occurrences are marked as moved.
497+
self.left_moved_lines = utils.NewSet[int]()
498+
self.right_moved_lines = utils.NewSet[int]()
499+
for text, lnums := range removed {
500+
if rnums, ok := added[text]; ok {
501+
count := min(len(lnums), len(rnums))
502+
for _, lnum := range lnums[:count] {
503+
self.left_moved_lines.Add(lnum)
504+
}
505+
for _, rnum := range rnums[:count] {
506+
self.right_moved_lines.Add(rnum)
507+
}
508+
}
509+
}
510+
}
511+
454512
func parse_patch(raw string, left_lines, right_lines []string) (ans *Patch, err error) {
455513
ans = &Patch{all_hunks: make([]*Hunk, 0, 32)}
456514
var current_hunk *Hunk
@@ -486,6 +544,9 @@ func parse_patch(raw string, left_lines, right_lines []string) (ans *Patch, err
486544
ans.largest_line_number = ans.all_hunks[len(ans.all_hunks)-1].largest_line_number
487545
}
488546
err = ans.compute_centers(left_lines, right_lines)
547+
if err == nil {
548+
ans.detect_moved_lines(left_lines, right_lines)
549+
}
489550
return
490551
}
491552

kittens/diff/render.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type HalfScreenLine struct {
4040
marked_up_margin_text string
4141
marked_up_text string
4242
is_filler bool
43+
is_moved bool
4344
cached_wcswidth int
4445
}
4546

@@ -81,6 +82,9 @@ func (self *LogicalLine) render_screen_line(n int, lp *loop.Loop, margin_size, c
8182
if sl.left.is_filler {
8283
left_margin = format_as_sgr.margin_filler + left_margin
8384
left_text = format_as_sgr.filler + left_text
85+
} else if sl.left.is_moved {
86+
left_margin = format_as_sgr.moved_margin + left_margin
87+
left_text = format_as_sgr.moved + left_text
8488
} else {
8589
switch self.line_type {
8690
case CHANGE_LINE, IMAGE_LINE:
@@ -104,6 +108,9 @@ func (self *LogicalLine) render_screen_line(n int, lp *loop.Loop, margin_size, c
104108
if sl.right.is_filler {
105109
right_margin = format_as_sgr.margin_filler + right_margin
106110
right_text = format_as_sgr.filler + right_text
111+
} else if sl.right.is_moved {
112+
right_margin = format_as_sgr.moved_margin + right_margin
113+
right_text = format_as_sgr.moved + right_text
107114
} else {
108115
switch self.line_type {
109116
case CHANGE_LINE, IMAGE_LINE:
@@ -156,7 +163,7 @@ func place_in(text string, sz int) string {
156163
}
157164

158165
var format_as_sgr struct {
159-
title, margin, added, removed, added_margin, removed_margin, filler, margin_filler, hunk_margin, hunk, selection, search string
166+
title, margin, added, removed, added_margin, removed_margin, filler, margin_filler, hunk_margin, hunk, selection, search, moved, moved_margin string
160167
}
161168

162169
var statusline_format, added_count_format, removed_count_format, message_format func(...any) string
@@ -175,6 +182,8 @@ type ResolvedColors struct {
175182
Margin_bg style.RGBA
176183
Margin_fg style.RGBA
177184
Margin_filler_bg style.NullableColor
185+
Moved_bg style.RGBA
186+
Moved_margin_bg style.RGBA
178187
Removed_bg style.RGBA
179188
Removed_margin_bg style.RGBA
180189
Search_bg style.RGBA
@@ -202,6 +211,8 @@ func create_formatters() {
202211
rc.Margin_bg = conf.Dark_margin_bg
203212
rc.Margin_fg = conf.Dark_margin_fg
204213
rc.Margin_filler_bg = conf.Dark_margin_filler_bg
214+
rc.Moved_bg = conf.Dark_moved_bg
215+
rc.Moved_margin_bg = conf.Dark_moved_margin_bg
205216
rc.Removed_bg = conf.Dark_removed_bg
206217
rc.Removed_margin_bg = conf.Dark_removed_margin_bg
207218
rc.Search_bg = conf.Dark_search_bg
@@ -223,6 +234,8 @@ func create_formatters() {
223234
rc.Margin_bg = conf.Margin_bg
224235
rc.Margin_fg = conf.Margin_fg
225236
rc.Margin_filler_bg = conf.Margin_filler_bg
237+
rc.Moved_bg = conf.Moved_bg
238+
rc.Moved_margin_bg = conf.Moved_margin_bg
226239
rc.Removed_bg = conf.Removed_bg
227240
rc.Removed_margin_bg = conf.Removed_margin_bg
228241
rc.Search_bg = conf.Search_bg
@@ -248,6 +261,8 @@ func create_formatters() {
248261
format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Added_margin_bg.AsRGBSharp()))
249262
format_as_sgr.removed = only_open("bg=" + rc.Removed_bg.AsRGBSharp())
250263
format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Removed_margin_bg.AsRGBSharp()))
264+
format_as_sgr.moved = only_open("bg=" + rc.Moved_bg.AsRGBSharp())
265+
format_as_sgr.moved_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Moved_margin_bg.AsRGBSharp()))
251266
format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", rc.Title_fg.AsRGBSharp(), rc.Title_bg.AsRGBSharp()))
252267
format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Margin_bg.AsRGBSharp()))
253268
format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Hunk_bg.AsRGBSharp()))
@@ -532,7 +547,9 @@ type DiffData struct {
532547
left_path, right_path string
533548
available_cols, margin_size int
534549

535-
left_lines, right_lines []string
550+
left_lines, right_lines []string
551+
left_moved_lines *utils.Set[int]
552+
right_moved_lines *utils.Set[int]
536553
}
537554

538555
func hunk_title(hunk *Hunk) string {
@@ -567,7 +584,7 @@ func splitlines(text string, width int) []string {
567584
return style.WrapTextAsLines(text, width, style.WrapOptions{})
568585
}
569586

570-
func render_half_line(line_number int, line, ltype string, available_cols int, center Center, ans []HalfScreenLine) []HalfScreenLine {
587+
func render_half_line(line_number int, line, ltype string, available_cols int, center Center, is_moved bool, ans []HalfScreenLine) []HalfScreenLine {
571588
var regions []Region
572589
if ltype == "remove" {
573590
regions = center.left_regions
@@ -583,7 +600,7 @@ func render_half_line(line_number int, line, ltype string, available_cols int, c
583600
}
584601
lnum := strconv.Itoa(line_number + 1)
585602
for _, sc := range splitlines(line, available_cols) {
586-
ans = append(ans, HalfScreenLine{marked_up_margin_text: lnum, marked_up_text: sc})
603+
ans = append(ans, HalfScreenLine{marked_up_margin_text: lnum, marked_up_text: sc, is_moved: is_moved})
587604
lnum = ""
588605
}
589606
return ans
@@ -601,13 +618,15 @@ func lines_for_diff_chunk(data *DiffData, _ int, chunk *Chunk, _ int, ans []*Log
601618
}
602619
if i < chunk.left_count {
603620
left_lnum = chunk.left_start + i
604-
ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, ll)
621+
left_is_moved := data.left_moved_lines != nil && data.left_moved_lines.Has(left_lnum)
622+
ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, left_is_moved, ll)
605623
left_lnum++
606624
}
607625

608626
if i < chunk.right_count {
609627
right_lnum = chunk.right_start + i
610-
rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, rl)
628+
right_is_moved := data.right_moved_lines != nil && data.right_moved_lines.Has(right_lnum)
629+
rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, right_is_moved, rl)
611630
right_lnum++
612631
}
613632

@@ -663,7 +682,10 @@ func lines_for_diff(left_path string, right_path string, patch *Patch, columns,
663682
return append(ans, &ht), nil
664683
}
665684
available_cols := columns/2 - margin_size
666-
data := DiffData{left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size}
685+
data := DiffData{
686+
left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size,
687+
left_moved_lines: patch.left_moved_lines, right_moved_lines: patch.right_moved_lines,
688+
}
667689
if left_path != "" {
668690
data.left_lines, err = highlighted_lines_for_path(left_path)
669691
if err != nil {
@@ -720,7 +742,7 @@ func all_lines(path string, columns, margin_size int, is_add bool, ans []*Logica
720742
}
721743
for line_number, line := range lines {
722744
hlines := make([]HalfScreenLine, 0, 8)
723-
hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, hlines)
745+
hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, false, hlines)
724746
l := ll
725747
if is_add {
726748
l.right_reference.linenum = line_number + 1

0 commit comments

Comments
 (0)