Implement searching the diff
This commit is contained in:
parent
88bd3ee9ca
commit
f7f6df675f
@ -151,6 +151,7 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
|
||||
}
|
||||
lp.OnResize = h.on_resize
|
||||
lp.OnKeyEvent = h.on_key_event
|
||||
lp.OnText = h.on_text
|
||||
err = lp.Run()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
|
||||
@ -67,7 +67,7 @@ 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, 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() {
|
||||
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()))
|
||||
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()))
|
||||
message_format = ctx.SprintFunc("bold")
|
||||
make_bracketer := func(start, end string) func(...any) string {
|
||||
s, e := ctx.SprintFunc(start), ctx.SprintFunc(end)
|
||||
end = e(" ")
|
||||
@ -133,7 +134,7 @@ func title_lines(left_path, right_path string, columns, margin_size int, ans []*
|
||||
l1 := ll
|
||||
l1.screen_lines = []string{title_format(name)}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
138
tools/cmd/diff/search.go
Normal file
138
tools/cmd/diff/search.go
Normal 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
|
||||
}
|
||||
@ -4,12 +4,14 @@ package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"kitty/tools/config"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/tui/readline"
|
||||
"kitty/tools/utils"
|
||||
"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)
|
||||
}
|
||||
|
||||
func (self ScrollPos) Add(other ScrollPos) ScrollPos {
|
||||
return ScrollPos{self.logical_line + other.logical_line, self.screen_line + other.screen_line}
|
||||
}
|
||||
|
||||
type AsyncResult struct {
|
||||
err error
|
||||
rtype ResultType
|
||||
@ -40,21 +46,24 @@ type AsyncResult struct {
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
async_results chan AsyncResult
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
pending_keys []string
|
||||
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, max_scroll_pos ScrollPos
|
||||
restore_position *ScrollPos
|
||||
inputting_command bool
|
||||
statusline_message string
|
||||
async_results chan AsyncResult
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
pending_keys []string
|
||||
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, max_scroll_pos ScrollPos
|
||||
restore_position *ScrollPos
|
||||
inputting_command bool
|
||||
statusline_message string
|
||||
rl *readline.Readline
|
||||
current_search *Search
|
||||
current_search_is_regex, current_search_is_backward bool
|
||||
}
|
||||
|
||||
func (self *Handler) calculate_statistics() {
|
||||
@ -70,6 +79,7 @@ var DebugPrintln func(...any)
|
||||
func (self *Handler) initialize() {
|
||||
DebugPrintln = self.lp.DebugPrintln
|
||||
self.pending_keys = make([]string, 0, 4)
|
||||
self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
|
||||
self.current_context_count = opts.Context
|
||||
if self.current_context_count < 0 {
|
||||
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.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
|
||||
// TODO: current search see python implementation
|
||||
}
|
||||
|
||||
func (self *Handler) draw_screen() {
|
||||
@ -238,7 +250,10 @@ func (self *Handler) draw_screen() {
|
||||
if i == 0 {
|
||||
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.MoveCursorVertically(1)
|
||||
lp.QueueWriteString("\r")
|
||||
@ -257,10 +272,11 @@ func (self *Handler) draw_status_line() {
|
||||
}
|
||||
self.lp.MoveCursorTo(1, self.screen_size.rows)
|
||||
self.lp.ClearToEndOfLine()
|
||||
self.lp.SetCursorVisible(self.inputting_command)
|
||||
if self.inputting_command {
|
||||
// TODO: implement this
|
||||
self.rl.RedrawNonAtomic()
|
||||
} else if self.statusline_message != "" {
|
||||
// TODO: implement this
|
||||
self.lp.QueueWriteString(message_format(wcswidth.TruncateToVisualLength(sanitize(self.statusline_message), self.screen_size.columns)))
|
||||
} else {
|
||||
num := self.logical_lines.NumScreenLinesTo(self.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))
|
||||
}
|
||||
sp := statusline_format(fmt.Sprintf("%d%%", frac))
|
||||
// TODO: output num of search matches
|
||||
counts := added_count_format(strconv.Itoa(self.added_count)) + statusline_format(`,`) + removed_count_format(strconv.Itoa(self.removed_count))
|
||||
var counts string
|
||||
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
|
||||
prefix := statusline_format(":")
|
||||
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 {
|
||||
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)
|
||||
if ac != nil {
|
||||
return self.dispatch_action(ac.Name, ac.Args)
|
||||
@ -317,8 +407,35 @@ func (self *Handler) scroll_to_next_change(backwards bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Handler) scroll_to_next_match(backwards bool) bool {
|
||||
// TODO: Implement me
|
||||
func (self *Handler) scroll_to_next_match(backwards, include_current_match bool) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@ -335,6 +452,18 @@ func (self *Handler) change_context_count(val int) bool {
|
||||
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 {
|
||||
switch name {
|
||||
case `quit`:
|
||||
@ -359,7 +488,7 @@ func (self *Handler) dispatch_action(name, args string) error {
|
||||
case strings.Contains(args, `change`):
|
||||
done = self.scroll_to_next_change(strings.Contains(args, `prev`))
|
||||
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`):
|
||||
amt := self.screen_size.num_lines
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user