// License: GPLv3 Copyright: 2023, Kovid Goyal, package diff import ( "fmt" "kitty/tools/utils" "kitty/tools/utils/style" "kitty/tools/wcswidth" "strconv" "strings" ) var _ = fmt.Print type LineType int const ( TITLE_LINE LineType = iota CHANGE_LINE HUNK_TITLE_LINE IMAGE_LINE EMPTY_LINE ) type Reference struct { path string linenum int } type LogicalLine struct { src Reference line_type LineType screen_lines []string } func fit_in(text string, count int) string { truncated := wcswidth.TruncateToVisualLength(text, count) if len(truncated) >= len(text) { return text } if count > 1 { truncated = wcswidth.TruncateToVisualLength(text, count-1) } return truncated + `…` } func fill_in(text string, sz int) string { w := wcswidth.Stringwidth(text) if w < sz { text += strings.Repeat(` `, (sz - w)) } return text } func place_in(text string, sz int) string { return fill_in(fit_in(text, sz), sz) } var title_format, text_format, margin_format, added_format, removed_format, added_margin_format, removed_margin_format, filler_format, margin_filler_format, hunk_margin_format, hunk_format, added_center, removed_center func(...any) string func create_formatters() { ctx := style.Context{AllowEscapeCodes: true} text_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Background.AsRGBSharp())) filler_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Filler_bg.AsRGBSharp())) if conf.Margin_filler_bg.IsSet { margin_filler_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Margin_filler_bg.Color.AsRGBSharp())) } else { margin_filler_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Filler_bg.AsRGBSharp())) } added_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Added_bg.AsRGBSharp())) added_margin_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Added_margin_bg.AsRGBSharp())) removed_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Removed_bg.AsRGBSharp())) removed_margin_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Removed_margin_bg.AsRGBSharp())) title_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", conf.Title_fg.AsRGBSharp(), conf.Title_bg.AsRGBSharp())) margin_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Margin_bg.AsRGBSharp())) hunk_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_bg.AsRGBSharp())) hunk_margin_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_margin_bg.AsRGBSharp())) make_bracketer := func(start, end string) func(...any) string { s, e := ctx.SprintFunc(start), ctx.SprintFunc(end) end = e(" ") idx := strings.LastIndexByte(end, ' ') end = end[:idx] start = s(" ") idx = strings.LastIndexByte(start, ' ') start = start[:idx] return func(args ...any) string { return start + fmt.Sprint(args) + end } } added_center = make_bracketer("bg="+conf.Highlight_added_bg.AsRGBSharp(), "bg="+conf.Added_bg.AsRGBSharp()) removed_center = make_bracketer("bg="+conf.Highlight_removed_bg.AsRGBSharp(), "bg="+conf.Removed_bg.AsRGBSharp()) } func highlight_boundaries(ltype, text string) string { switch ltype { case "add": return added_center(text) case "remove": return removed_center(text) } return text } func title_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) []*LogicalLine { left_name, right_name := path_name_map[left_path], path_name_map[right_path] name := "" m := strings.Repeat(` `, margin_size) if right_name != "" && right_name != left_name { n1 := fit_in(m+sanitize(left_name), columns/2-margin_size) n1 = place_in(n1, columns/2) n2 := fit_in(m+sanitize(right_name), columns/2-margin_size) n2 = place_in(n2, columns/2) name = n1 + n2 } else { name = place_in(m+sanitize(left_name), columns) } ll := LogicalLine{line_type: TITLE_LINE, src: Reference{path: left_path, linenum: 0}} l1 := ll l1.screen_lines = []string{title_format(name)} l2 := ll l2.screen_lines = []string{title_format(strings.Repeat("━", columns+1))} return append(ans, &l1, &l2) } type LogicalLines struct { lines []*LogicalLine margin_size, columns int } func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) { // TODO: Implement this return ans, nil } func human_readable(size int) string { divisor, suffix := 1, "B" for i, candidate := range []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} { if size < (1 << ((i + 1) * 10)) { divisor, suffix = (1 << (i * 10)), candidate break } } fs := float64(size) / float64(divisor) s := strconv.FormatFloat(fs, 'f', 2, 64) if idx := strings.Index(s, "."); idx > -1 { s = s[:idx+2] } if strings.HasSuffix(s, ".0") || strings.HasSuffix(s, ".00") { idx := strings.IndexByte(s, '.') s = s[:idx] } return s + " " + suffix } func render_diff_line(number, text, ltype string, margin_size int, available_cols int) string { m, c := margin_format, text_format switch ltype { case `filler`: m = margin_filler_format c = filler_format case `remove`: m = removed_margin_format c = removed_format case `add`: m = added_margin_format c = added_format } margin := m(place_in(number, margin_size)) content := c(fill_in(text, available_cols)) return margin + content } func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) { available_cols := columns/2 - margin_size fl := func(path string, formatter func(...any) string) string { if err == nil { var data string data, err = data_for_path(path) text := fmt.Sprintf("Binary file: %s", human_readable(len(data))) text = place_in(text, available_cols) return margin_format(strings.Repeat(` `, margin_size)) + formatter(text) } return "" } line := "" if left_path == "" { filler := render_diff_line(``, ``, `filler`, margin_size, available_cols) line = filler + fl(right_path, added_format) } else if right_path == "" { filler := render_diff_line(``, ``, `filler`, margin_size, available_cols) line = fl(left_path, removed_format) + filler } else { line = fl(left_path, removed_format) + fl(right_path, added_format) } ll := LogicalLine{line_type: CHANGE_LINE, src: Reference{path: left_path, linenum: 0}, screen_lines: []string{line}} return append(ans, &ll), err } type DiffData struct { left_path, right_path string available_cols, margin_size int left_lines, right_lines []string filler_line, left_filler_line, right_filler_line string } func hunk_title(hunk_num int, hunk *Hunk, margin_size, available_cols int) string { m := hunk_margin_format(strings.Repeat(" ", margin_size)) t := fmt.Sprintf("@@ %d,%d +%d,%d @@ %s", hunk.left_start+1, hunk.left_count, hunk.right_start+1, hunk.right_count, hunk.title) return m + hunk_format(place_in(t, available_cols)) } func lines_for_context_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine { for i := 0; i < chunk.left_count; i++ { left_line_number := chunk.left_start + i right_line_number := chunk.right_start + i ll := LogicalLine{line_type: CHANGE_LINE, src: Reference{path: data.left_path, linenum: left_line_number}} left_line_number_s := strconv.Itoa(left_line_number + 1) right_line_number_s := strconv.Itoa(right_line_number + 1) for _, text := range splitlines(data.left_lines[left_line_number], data.available_cols) { line := render_diff_line(left_line_number_s, text, `context`, data.margin_size, data.available_cols) if right_line_number_s == left_line_number_s { line += line } else { line += render_diff_line(right_line_number_s, text, `context`, data.margin_size, data.available_cols) } ll.screen_lines = append(ll.screen_lines, line) left_line_number_s, right_line_number_s = "", "" } ans = append(ans, &ll) } return ans } func splitlines(text string, width int) []string { lines := style.WrapTextAsLines(text, "", width) if len(lines) > 1 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } return lines } func render_half_line(line_number int, line, ltype string, margin_size, available_cols int, center Center, ans []string) []string { if center.prefix_count > 0 { line_sz := len(line) if center.prefix_count+center.suffix_count < line_sz { end := len(line) - center.suffix_count line = line[:center.prefix_count] + highlight_boundaries(ltype, line[center.prefix_count:end]) + line[end:] } } lnum := strconv.Itoa(line_number + 1) for _, sc := range splitlines(line, available_cols) { ans = append(ans, render_diff_line(lnum, sc, ltype, margin_size, available_cols)) lnum = "" } return ans } func lines_for_diff_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine { common := utils.Min(chunk.left_count, chunk.right_count) ll, rl := make([]string, 0, 32), make([]string, 0, 32) for i := 0; i < utils.Max(chunk.left_count, chunk.right_count); i++ { ll, rl = ll[:0], rl[:0] ref_ln, ref_path := 0, "" var center Center if i < len(chunk.centers) { center = chunk.centers[i] } if i < chunk.left_count { ref_path = data.left_path ref_ln = chunk.left_start + i ll = render_half_line(ref_ln, data.left_lines[ref_ln], "remove", data.margin_size, data.available_cols, center, ll) } if i < chunk.right_count { ref_path = data.right_path ref_ln = chunk.right_start + i rl = render_half_line(ref_ln, data.right_lines[ref_ln], "add", data.margin_size, data.available_cols, center, rl) } if i < common { extra := len(ll) - len(rl) if extra < 0 { ll = append(ll, utils.Repeat(data.left_filler_line, -extra)...) } else if extra > 0 { rl = append(rl, utils.Repeat(data.right_filler_line, extra)...) } } else { if len(ll) > 0 { rl = append(rl, utils.Repeat(data.filler_line, len(ll))...) } else if len(rl) > 0 { ll = append(ll, utils.Repeat(data.filler_line, len(rl))...) } } logline := LogicalLine{line_type: CHANGE_LINE, src: Reference{path: ref_path, linenum: ref_ln}} for l := 0; l < len(ll); l++ { logline.screen_lines = append(logline.screen_lines, ll[l]+rl[l]) } ans = append(ans, &logline) } return ans } func lines_for_diff(left_path string, right_path string, patch *Patch, columns, margin_size int, ans []*LogicalLine) (result []*LogicalLine, err error) { available_cols := columns/2 - margin_size data := DiffData{left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size} if left_path != "" { data.left_lines, err = lines_for_path(left_path) if err != nil { return } } if right_path != "" { data.right_lines, err = lines_for_path(right_path) if err != nil { return } } data.filler_line = render_diff_line("", "", "filler", margin_size, available_cols) data.left_filler_line = render_diff_line("", "", "remove", margin_size, available_cols) data.right_filler_line = render_diff_line("", "", "add", margin_size, available_cols) ht := LogicalLine{line_type: HUNK_TITLE_LINE, src: Reference{path: left_path}} for hunk_num, hunk := range patch.all_hunks { htl := ht htl.src.linenum = hunk.left_start htl.screen_lines = []string{hunk_title(hunk_num, hunk, margin_size, columns-margin_size)} ans = append(ans, &htl) for cnum, chunk := range hunk.chunks { if chunk.is_context { ans = lines_for_context_chunk(&data, hunk_num, chunk, cnum, ans) } else { ans = lines_for_diff_chunk(&data, hunk_num, chunk, cnum, ans) } } } return ans, nil } func render(collection *Collection, diff_map map[string]*Patch, columns int) (result *LogicalLines, err error) { largest_line_number := 0 collection.Apply(func(path, typ, changed_path string) error { if typ == "diff" { patch := diff_map[path] if patch != nil { largest_line_number = utils.Max(largest_line_number, patch.largest_line_number) } } return nil }) margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1) ans := make([]*LogicalLine, 0, 1024) empty_line := LogicalLine{line_type: EMPTY_LINE} err = collection.Apply(func(path, item_type, changed_path string) error { ans = title_lines(path, changed_path, columns, margin_size, ans) defer func() { el := empty_line ans = append(ans, &el) }() is_binary := !is_path_text(path) if !is_binary && item_type == `diff` && !is_path_text(changed_path) { is_binary = true } is_img := is_binary && is_image(path) || (item_type == `diff` && is_image(changed_path)) _ = is_img switch item_type { case "diff": if is_binary { if is_img { ans, err = image_lines(path, changed_path, columns, margin_size, ans) } else { ans, err = binary_lines(path, changed_path, columns, margin_size, ans) } } else { ans, err = lines_for_diff(path, changed_path, diff_map[path], columns, margin_size, ans) } if err != nil { return err } case "add": case "removal": case "rename": } return nil }) return &LogicalLines{lines: ans[:len(ans)-1], margin_size: margin_size, columns: columns}, err } func (self *LogicalLines) num_of_screen_lines() (ans int) { for _, l := range self.lines { ans += len(l.screen_lines) } return }