From 5d8b5ab7201967790d4e40b786cebe71b37898cd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 19 Mar 2023 17:17:41 +0530 Subject: [PATCH] More work on porting diff kitten --- tools/cmd/diff/collect.go | 6 +- tools/cmd/diff/main.go | 2 + tools/cmd/diff/render.go | 143 ++++++++++++++++++++++++++++++++++++++ tools/cmd/diff/ui.go | 65 +++++++++++++++-- tools/utils/misc.go | 2 +- 5 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 tools/cmd/diff/render.go diff --git a/tools/cmd/diff/collect.go b/tools/cmd/diff/collect.go index a736b894f..9f3e43376 100644 --- a/tools/cmd/diff/collect.go +++ b/tools/cmd/diff/collect.go @@ -64,9 +64,13 @@ func data_for_path(path string) (string, error) { }) } +func is_image(path string) bool { + return strings.HasPrefix(mimetype_for_path(path), "image/") +} + func is_path_text(path string) bool { return is_text_cache.MustGetOrCreate(path, func(path string) bool { - if strings.HasPrefix(mimetype_for_path(path), "image/") { + if is_image(path) { return false } s1, err := os.Stat(path) diff --git a/tools/cmd/diff/main.go b/tools/cmd/diff/main.go index 0927ebce5..02f004616 100644 --- a/tools/cmd/diff/main.go +++ b/tools/cmd/diff/main.go @@ -109,6 +109,7 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) { return 1, err } init_caches() + create_formatters() defer func() { for tdir := range remote_dirs { os.RemoveAll(tdir) @@ -148,6 +149,7 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) { lp.SetCursorVisible(true) return "" } + lp.OnResize = h.on_resize err = lp.Run() if err != nil { return 1, err diff --git a/tools/cmd/diff/render.go b/tools/cmd/diff/render.go new file mode 100644 index 000000000..b4036746d --- /dev/null +++ b/tools/cmd/diff/render.go @@ -0,0 +1,143 @@ +// 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 + IMAGE_LINE +) + +type Reference struct { + path string + linenum int +} + +type LogicalLine struct { + src Reference + line_type LineType + margin_size, columns int + 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 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.IsNull { + margin_filler_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Filler_bg.AsRGBSharp())) + } else { + margin_filler_format = ctx.SprintFunc(fmt.Sprintf("bg=%s", conf.Margin_filler_bg.Color.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", 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())) +} + +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{columns: columns, margin_size: margin_size, 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(name)} + return append(ans, &l1, &l2) +} + +type LogicalLines struct { + lines []*LogicalLine + margin_size, columns int +} + +func render(collection *Collection, diff_map map[string]*Patch, columns int) (*LogicalLines, 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) + err := collection.Apply(func(path, item_type, changed_path string) error { + ans = title_lines(path, changed_path, columns, margin_size, ans) + + 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 + + return nil + }) + if err != nil { + return nil, err + } + + return &LogicalLines{lines: ans[:len(ans)-1], margin_size: margin_size, columns: columns}, nil +} + +func (self *LogicalLines) num_of_screen_lines() (ans int) { + for _, l := range self.lines { + ans += len(l.screen_lines) + } + return +} diff --git a/tools/cmd/diff/ui.go b/tools/cmd/diff/ui.go index 4d16087ed..2a3803fcb 100644 --- a/tools/cmd/diff/ui.go +++ b/tools/cmd/diff/ui.go @@ -4,6 +4,7 @@ package diff import ( "fmt" + "kitty/tools/tui/graphics" "kitty/tools/tui/loop" ) @@ -17,7 +18,8 @@ const ( HIGHLIGHT ) -type Reference struct { +type ScrollPos struct { + logical_line, screen_line int } type AsyncResult struct { @@ -32,9 +34,12 @@ type Handler struct { left, right string collection *Collection diff_map map[string]*Patch + logical_lines *LogicalLines lp *loop.Loop current_context_count, original_context_count int added_count, removed_count int + screen_size struct{ rows, columns, num_lines int } + scroll_pos ScrollPos } func (self *Handler) calculate_statistics() { @@ -45,11 +50,18 @@ func (self *Handler) calculate_statistics() { } } +var DebugPrintln func(...any) + func (self *Handler) initialize() { + DebugPrintln = self.lp.DebugPrintln self.current_context_count = opts.Context if self.current_context_count < 0 { self.current_context_count = int(conf.Num_context_lines) } + sz, _ := self.lp.ScreenSize() + self.screen_size.rows = int(sz.HeightCells) + self.screen_size.columns = int(sz.WidthCells) + self.screen_size.num_lines = self.screen_size.rows - 1 self.original_context_count = self.current_context_count self.lp.SetDefaultColor(loop.FOREGROUND, conf.Foreground) self.lp.SetDefaultColor(loop.CURSOR, conf.Foreground) @@ -65,6 +77,7 @@ func (self *Handler) initialize() { self.async_results <- r self.lp.WakeupMainThread() }() + self.draw_screen() } func (self *Handler) generate_diff() { @@ -112,14 +125,54 @@ func (self *Handler) handle_async_result(r AsyncResult) error { case DIFF: self.diff_map = r.diff_map self.calculate_statistics() - self.render_diff() - self.scroll_pos = 0 - if self.restore_position != nil { - self.set_current_position(self.restore_position) - self.restore_position = nil + err := self.render_diff() + if err != nil { + return err } + self.scroll_pos = ScrollPos{} + // TODO: restore_position uncomment and implement below + // if self.restore_position != nil { + // self.set_current_position(self.restore_position) + // self.restore_position = nil + // } self.draw_screen() case HIGHLIGHT: } return nil } + +func (self *Handler) on_resize(old_size, new_size loop.ScreenSize) error { + self.screen_size.rows = int(new_size.HeightCells) + self.screen_size.num_lines = self.screen_size.rows - 1 + self.screen_size.columns = int(new_size.WidthCells) + if self.diff_map != nil && self.collection != nil { + err := self.render_diff() + if err != nil { + return err + } + } + self.draw_screen() + return nil +} + +func (self *Handler) render_diff() (err error) { + self.logical_lines, err = render(self.collection, self.diff_map, self.screen_size.columns) + if err != nil { + return err + } + return nil + // TODO: current search see python implementation +} + +func (self *Handler) draw_screen() { + self.lp.StartAtomicUpdate() + defer self.lp.EndAtomicUpdate() + g := (&graphics.GraphicsCommand{}).SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_delete_visible) + g.WriteWithPayloadToLoop(self.lp, nil) + lp.MoveCursorTo(1, 1) + if self.logical_lines == nil || self.diff_map == nil || self.collection == nil { + lp.Println(`Calculating diff, please wait...`) + return + } + +} diff --git a/tools/utils/misc.go b/tools/utils/misc.go index f6e63b607..d067f4915 100644 --- a/tools/utils/misc.go +++ b/tools/utils/misc.go @@ -146,7 +146,7 @@ func Memset[T any](dest []T, pattern ...T) []T { } var ControlCodesPat = (&Once[*regexp.Regexp]{Run: func() *regexp.Regexp { - return regexp.MustCompile("[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]") + return regexp.MustCompile("[\x00-\x09\x0b-\x1f\x7f\u0080-\u009f]") }}).Get func SanitizeControlCodes(raw string, replace_with ...string) string {