Implement searching the diff

This commit is contained in:
Kovid Goyal 2023-03-22 20:58:07 +05:30
parent 88bd3ee9ca
commit f7f6df675f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 300 additions and 26 deletions

View File

@ -151,6 +151,7 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
} }
lp.OnResize = h.on_resize lp.OnResize = h.on_resize
lp.OnKeyEvent = h.on_key_event lp.OnKeyEvent = h.on_key_event
lp.OnText = h.on_text
err = lp.Run() err = lp.Run()
if err != nil { if err != nil {
return 1, err return 1, err

View File

@ -67,7 +67,7 @@ func place_in(text string, sz int) string {
return fill_in(fit_in(text, sz), sz) 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, statusline_format, added_count_format, removed_count_format func(...any) string 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, statusline_format, added_count_format, removed_count_format, message_format func(...any) string
func create_formatters() { func create_formatters() {
ctx := style.Context{AllowEscapeCodes: true} ctx := style.Context{AllowEscapeCodes: true}
@ -89,6 +89,7 @@ func create_formatters() {
removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_removed_bg.AsRGBSharp())) removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_removed_bg.AsRGBSharp()))
hunk_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_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())) hunk_margin_format = ctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_margin_bg.AsRGBSharp()))
message_format = ctx.SprintFunc("bold")
make_bracketer := func(start, end string) func(...any) string { make_bracketer := func(start, end string) func(...any) string {
s, e := ctx.SprintFunc(start), ctx.SprintFunc(end) s, e := ctx.SprintFunc(start), ctx.SprintFunc(end)
end = e(" ") end = e(" ")
@ -133,7 +134,7 @@ func title_lines(left_path, right_path string, columns, margin_size int, ans []*
l1 := ll l1 := ll
l1.screen_lines = []string{title_format(name)} l1.screen_lines = []string{title_format(name)}
l2 := ll l2 := ll
l2.screen_lines = []string{title_format(strings.Repeat("━", columns+1))} l2.screen_lines = []string{title_format(strings.Repeat("━", columns))}
return append(ans, &l1, &l2) return append(ans, &l1, &l2)
} }

138
tools/cmd/diff/search.go Normal file
View File

@ -0,0 +1,138 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package diff
import (
"fmt"
"regexp"
"strings"
"sync"
"kitty/tools/tui/sgr"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
type Search struct {
pat *regexp.Regexp
matches map[ScrollPos][]*sgr.Span
}
func (self *Search) Len() int { return len(self.matches) }
func (self *Search) find_matches_in_lines(clean_lines []string, origin int, send_result func(screen_line, offset, size int)) {
lengths := utils.Map(func(x string) int { return len(x) }, clean_lines)
offsets := make([]int, len(clean_lines))
for i := range clean_lines {
if i > 1 {
offsets[i] = offsets[i-1] + len(clean_lines[i-1])
}
}
matches := self.pat.FindAllStringIndex(strings.Join(clean_lines, ""), -1)
pos := 0
find_pos := func(start int) int {
for i := pos; i < len(clean_lines); i++ {
if start < offsets[i]+lengths[i] {
pos = i
return pos
}
}
return -1
}
for _, m := range matches {
start, end := m[0], m[1]
total_size := end - start
if total_size < 1 {
continue
}
start_line := find_pos(start)
if start_line > -1 {
end_line := find_pos(end)
if end_line > -1 {
for i := start_line; i <= end_line; i++ {
offset := 0
if i == start_line {
offset = start - offsets[i]
}
size := len(clean_lines[i]) - offset
if i == end_line {
size = (end - offsets[i]) - offset
}
send_result(i, origin+offset, size)
}
}
}
}
}
func (self *Search) find_matches_in_line(line *LogicalLine, margin_size, cols int, send_result func(screen_line, offset, size int)) {
half_width := cols / 2
right_offset := half_width + 1 + margin_size
left_clean_lines, right_clean_lines := make([]string, len(line.screen_lines)), make([]string, len(line.screen_lines))
for i, line := range line.screen_lines {
line = wcswidth.StripEscapeCodes(line)
if len(line) >= half_width+1 {
left_clean_lines[i] = line[margin_size : half_width+1]
}
if len(line) > right_offset {
right_clean_lines[i] = line[right_offset:]
}
}
self.find_matches_in_lines(left_clean_lines, margin_size, send_result)
self.find_matches_in_lines(right_clean_lines, right_offset, send_result)
}
func (self *Search) Has(pos ScrollPos) bool {
return len(self.matches[pos]) > 0
}
func (self *Search) search(logical_lines *LogicalLines) {
margin_size := logical_lines.margin_size
cols := logical_lines.columns
self.matches = make(map[ScrollPos][]*sgr.Span)
ctx := images.Context{}
mutex := sync.Mutex{}
s := sgr.NewSpan(0, 0)
s.SetForeground(conf.Search_fg).SetBackground(conf.Search_bg)
ctx.Parallel(0, logical_lines.Len(), func(nums <-chan int) {
for i := range nums {
line := logical_lines.At(i)
if line.line_type == EMPTY_LINE || line.line_type == IMAGE_LINE {
continue
}
self.find_matches_in_line(line, margin_size, cols, func(screen_line, offset, size int) {
mutex.Lock()
defer mutex.Unlock()
sn := *s
sn.Offset, sn.Size = offset, size
pos := ScrollPos{i, screen_line}
self.matches[pos] = append(self.matches[pos], &sn)
})
}
})
for _, spans := range self.matches {
slices.SortFunc(spans, func(a, b *sgr.Span) bool { return a.Offset < b.Offset })
}
}
func (self *Search) markup_line(line string, pos ScrollPos) string {
spans := self.matches[pos]
if spans == nil {
return line
}
return sgr.InsertFormatting(line, spans...)
}
func do_search(pat *regexp.Regexp, logical_lines *LogicalLines) *Search {
ans := &Search{pat: pat, matches: make(map[ScrollPos][]*sgr.Span)}
ans.search(logical_lines)
return ans
}

View File

@ -4,12 +4,14 @@ package diff
import ( import (
"fmt" "fmt"
"regexp"
"strconv" "strconv"
"strings" "strings"
"kitty/tools/config" "kitty/tools/config"
"kitty/tools/tui/graphics" "kitty/tools/tui/graphics"
"kitty/tools/tui/loop" "kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils" "kitty/tools/utils"
"kitty/tools/wcswidth" "kitty/tools/wcswidth"
) )
@ -32,6 +34,10 @@ func (self ScrollPos) Less(other ScrollPos) bool {
return self.logical_line < other.logical_line || (self.logical_line == other.logical_line && self.screen_line < other.screen_line) return self.logical_line < other.logical_line || (self.logical_line == other.logical_line && self.screen_line < other.screen_line)
} }
func (self ScrollPos) Add(other ScrollPos) ScrollPos {
return ScrollPos{self.logical_line + other.logical_line, self.screen_line + other.screen_line}
}
type AsyncResult struct { type AsyncResult struct {
err error err error
rtype ResultType rtype ResultType
@ -55,6 +61,9 @@ type Handler struct {
restore_position *ScrollPos restore_position *ScrollPos
inputting_command bool inputting_command bool
statusline_message string statusline_message string
rl *readline.Readline
current_search *Search
current_search_is_regex, current_search_is_backward bool
} }
func (self *Handler) calculate_statistics() { func (self *Handler) calculate_statistics() {
@ -70,6 +79,7 @@ var DebugPrintln func(...any)
func (self *Handler) initialize() { func (self *Handler) initialize() {
DebugPrintln = self.lp.DebugPrintln DebugPrintln = self.lp.DebugPrintln
self.pending_keys = make([]string, 0, 4) self.pending_keys = make([]string, 0, 4)
self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
self.current_context_count = opts.Context self.current_context_count = opts.Context
if self.current_context_count < 0 { if self.current_context_count < 0 {
self.current_context_count = int(conf.Num_context_lines) self.current_context_count = int(conf.Num_context_lines)
@ -214,8 +224,10 @@ func (self *Handler) render_diff() (err error) {
self.max_scroll_pos.screen_line = 0 self.max_scroll_pos.screen_line = 0
} }
self.logical_lines.IncrementScrollPosBy(&self.max_scroll_pos, -self.screen_size.num_lines+1) self.logical_lines.IncrementScrollPosBy(&self.max_scroll_pos, -self.screen_size.num_lines+1)
if self.current_search != nil {
self.current_search.search(self.logical_lines)
}
return nil return nil
// TODO: current search see python implementation
} }
func (self *Handler) draw_screen() { func (self *Handler) draw_screen() {
@ -238,7 +250,10 @@ func (self *Handler) draw_screen() {
if i == 0 { if i == 0 {
screen_lines = screen_lines[self.scroll_pos.screen_line:] screen_lines = screen_lines[self.scroll_pos.screen_line:]
} }
for _, sl := range screen_lines { for snum, sl := range screen_lines {
if self.current_search != nil {
sl = self.current_search.markup_line(sl, ScrollPos{i, snum}.Add(self.scroll_pos))
}
lp.QueueWriteString(sl) lp.QueueWriteString(sl)
lp.MoveCursorVertically(1) lp.MoveCursorVertically(1)
lp.QueueWriteString("\r") lp.QueueWriteString("\r")
@ -257,10 +272,11 @@ func (self *Handler) draw_status_line() {
} }
self.lp.MoveCursorTo(1, self.screen_size.rows) self.lp.MoveCursorTo(1, self.screen_size.rows)
self.lp.ClearToEndOfLine() self.lp.ClearToEndOfLine()
self.lp.SetCursorVisible(self.inputting_command)
if self.inputting_command { if self.inputting_command {
// TODO: implement this self.rl.RedrawNonAtomic()
} else if self.statusline_message != "" { } else if self.statusline_message != "" {
// TODO: implement this self.lp.QueueWriteString(message_format(wcswidth.TruncateToVisualLength(sanitize(self.statusline_message), self.screen_size.columns)))
} else { } else {
num := self.logical_lines.NumScreenLinesTo(self.scroll_pos) num := self.logical_lines.NumScreenLinesTo(self.scroll_pos)
den := self.logical_lines.NumScreenLinesTo(self.max_scroll_pos) den := self.logical_lines.NumScreenLinesTo(self.max_scroll_pos)
@ -269,8 +285,12 @@ func (self *Handler) draw_status_line() {
frac = int((float64(num) * 100.0) / float64(den)) frac = int((float64(num) * 100.0) / float64(den))
} }
sp := statusline_format(fmt.Sprintf("%d%%", frac)) sp := statusline_format(fmt.Sprintf("%d%%", frac))
// TODO: output num of search matches var counts string
counts := added_count_format(strconv.Itoa(self.added_count)) + statusline_format(`,`) + removed_count_format(strconv.Itoa(self.removed_count)) if self.current_search == nil {
counts = added_count_format(strconv.Itoa(self.added_count)) + statusline_format(`,`) + removed_count_format(strconv.Itoa(self.removed_count))
} else {
counts = statusline_format(fmt.Sprintf("%d matches", self.current_search.Len()))
}
suffix := counts + " " + sp suffix := counts + " " + sp
prefix := statusline_format(":") prefix := statusline_format(":")
filler := strings.Repeat(" ", utils.Max(0, self.screen_size.columns-wcswidth.Stringwidth(prefix)-wcswidth.Stringwidth(suffix))) filler := strings.Repeat(" ", utils.Max(0, self.screen_size.columns-wcswidth.Stringwidth(prefix)-wcswidth.Stringwidth(suffix)))
@ -278,7 +298,77 @@ func (self *Handler) draw_status_line() {
} }
} }
func (self *Handler) on_text(text string, a, b bool) error {
if self.inputting_command {
defer self.draw_status_line()
return self.rl.OnText(text, a, b)
}
if self.statusline_message != "" {
self.statusline_message = ""
self.draw_status_line()
return nil
}
return nil
}
func (self *Handler) do_search(query string) {
self.current_search = nil
if len(query) < 2 {
return
}
if !self.current_search_is_regex {
query = regexp.QuoteMeta(query)
}
pat, err := regexp.Compile(query)
if err != nil {
self.statusline_message = fmt.Sprintf("Bad regex: %s", err)
self.lp.Beep()
return
}
self.current_search = do_search(pat, self.logical_lines)
if self.current_search.Len() == 0 {
self.current_search = nil
self.statusline_message = fmt.Sprintf("No matches for: %#v", query)
self.lp.Beep()
} else {
if self.scroll_to_next_match(false, true) {
self.draw_screen()
} else {
self.lp.Beep()
}
}
}
func (self *Handler) on_key_event(ev *loop.KeyEvent) error { func (self *Handler) on_key_event(ev *loop.KeyEvent) error {
if self.inputting_command {
defer self.draw_status_line()
if ev.MatchesPressOrRepeat("esc") {
self.inputting_command = false
ev.Handled = true
return nil
}
if ev.MatchesPressOrRepeat("enter") {
self.inputting_command = false
ev.Handled = true
self.do_search(self.rl.AllText())
self.draw_screen()
return nil
}
return self.rl.OnKeyEvent(ev)
}
if self.statusline_message != "" {
if ev.Type != loop.RELEASE {
ev.Handled = true
self.statusline_message = ""
self.draw_status_line()
}
return nil
}
if self.current_search != nil && ev.MatchesPressOrRepeat("esc") {
self.current_search = nil
self.draw_screen()
return nil
}
ac := self.shortcut_tracker.Match(ev, conf.KeyboardShortcuts) ac := self.shortcut_tracker.Match(ev, conf.KeyboardShortcuts)
if ac != nil { if ac != nil {
return self.dispatch_action(ac.Name, ac.Args) return self.dispatch_action(ac.Name, ac.Args)
@ -317,8 +407,35 @@ func (self *Handler) scroll_to_next_change(backwards bool) bool {
return false return false
} }
func (self *Handler) scroll_to_next_match(backwards bool) bool { func (self *Handler) scroll_to_next_match(backwards, include_current_match bool) bool {
// TODO: Implement me if self.current_search == nil {
return false
}
if self.current_search_is_backward {
backwards = !backwards
}
offset, delta := 1, 1
if include_current_match {
offset = 0
}
if backwards {
offset *= -1
delta *= -1
}
pos := self.scroll_pos
if self.logical_lines.IncrementScrollPosBy(&pos, offset) == 0 {
return false
}
for {
if self.current_search.Has(pos) {
self.scroll_pos = pos
self.draw_screen()
return true
}
if self.logical_lines.IncrementScrollPosBy(&pos, delta) == 0 || self.max_scroll_pos.Less(pos) {
break
}
}
return false return false
} }
@ -335,6 +452,18 @@ func (self *Handler) change_context_count(val int) bool {
return true return true
} }
func (self *Handler) start_search(is_regex, is_backward bool) {
if self.inputting_command {
self.lp.Beep()
return
}
self.inputting_command = true
self.current_search_is_regex = is_regex
self.current_search_is_backward = is_backward
self.rl.SetText(``)
self.draw_status_line()
}
func (self *Handler) dispatch_action(name, args string) error { func (self *Handler) dispatch_action(name, args string) error {
switch name { switch name {
case `quit`: case `quit`:
@ -359,7 +488,7 @@ func (self *Handler) dispatch_action(name, args string) error {
case strings.Contains(args, `change`): case strings.Contains(args, `change`):
done = self.scroll_to_next_change(strings.Contains(args, `prev`)) done = self.scroll_to_next_change(strings.Contains(args, `prev`))
case strings.Contains(args, `match`): case strings.Contains(args, `match`):
done = self.scroll_to_next_match(strings.Contains(args, `prev`)) done = self.scroll_to_next_match(strings.Contains(args, `prev`), false)
case strings.Contains(args, `page`): case strings.Contains(args, `page`):
amt := self.screen_size.num_lines amt := self.screen_size.num_lines
if strings.Contains(args, `prev`) { if strings.Contains(args, `prev`) {
@ -395,6 +524,11 @@ func (self *Handler) dispatch_action(name, args string) error {
if !self.change_context_count(new_ctx) { if !self.change_context_count(new_ctx) {
self.lp.Beep() self.lp.Beep()
} }
case `start_search`:
if self.diff_map != nil && self.logical_lines != nil {
a, b, _ := strings.Cut(args, " ")
self.start_search(config.StringToBool(a), config.StringToBool(b))
}
} }
return nil return nil
} }