From 5dc29563e84ee95eb3986fe289aad0db38f5df21 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 25 May 2025 10:00:56 +0530 Subject: [PATCH] Highlight current result using text styles not sizes Works better when the current result is moved around --- kittens/choose_files/main.go | 21 ++++-- kittens/choose_files/results.go | 116 ++++++++++++++++------------- kittens/choose_files/search-bar.go | 3 +- 3 files changed, 82 insertions(+), 58 deletions(-) diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go index 07cafee0b..b3641b6be 100644 --- a/kittens/choose_files/main.go +++ b/kittens/choose_files/main.go @@ -32,16 +32,25 @@ type State struct { exclude_patterns []*regexp.Regexp score_patterns []ScorePattern search_text string + current_idx int } -func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) } -func (s State) SelectDirs() bool { return s.select_dirs } -func (s State) Multiselect() bool { return s.multiselect } -func (s State) MaxDepth() int { return utils.IfElse(s.max_depth < 1, 4, s.max_depth) } -func (s State) String() string { return utils.Repr(s) } -func (s State) SearchText() string { return s.search_text } +func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) } +func (s State) SelectDirs() bool { return s.select_dirs } +func (s State) Multiselect() bool { return s.multiselect } +func (s State) MaxDepth() int { return utils.IfElse(s.max_depth < 1, 4, s.max_depth) } +func (s State) String() string { return utils.Repr(s) } +func (s State) SearchText() string { return s.search_text } +func (s *State) SetSearchText(val string) { + if s.search_text != val { + s.search_text = val + s.current_idx = 0 + } +} func (s State) ExcludePatterns() []*regexp.Regexp { return s.exclude_patterns } func (s State) ScorePatterns() []ScorePattern { return s.score_patterns } +func (s State) CurrentIndex() int { return s.current_idx } +func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) } func (s State) CurrentDir() string { return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir) } diff --git a/kittens/choose_files/results.go b/kittens/choose_files/results.go index fc84ea9ca..a7e740a7d 100644 --- a/kittens/choose_files/results.go +++ b/kittens/choose_files/results.go @@ -8,7 +8,6 @@ import ( "unicode/utf8" "github.com/kovidgoyal/kitty/tools/icons" - "github.com/kovidgoyal/kitty/tools/tui/loop" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/style" "github.com/kovidgoyal/kitty/tools/wcswidth" @@ -55,26 +54,16 @@ func (h *Handler) draw_no_matches_message(in_progress bool) { } const matching_position_style = "fg=green" +const current_style = "fg=intense-white bold" -func (h *Handler) draw_matching_result(r *ResultItem) { - icon := icon_for(r.abspath, r.dir_entry) - h.lp.MoveCursorHorizontally(1) - p, s, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ") - h.lp.QueueWriteString(p) - h.lp.DrawSizedText(icon, loop.SizedText{Scale: 2}) - h.lp.QueueWriteString(s) - text := r.text - available_width := (h.screen_size.width - 6) / 2 - add_ellipsis := false - if wcswidth.Stringwidth(text) > available_width { - text = wcswidth.TruncateToVisualLength(text, available_width-2) - add_ellipsis = true - } - h.render_match_with_positions(text, add_ellipsis, r.positions, 2) -} - -func (h *Handler) render_match_with_positions(text string, add_ellipsis bool, positions []int, scale int) { +func (h *Handler) render_match_with_positions(text string, add_ellipsis bool, positions []int, is_current bool) { prefix, suffix, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ") + if is_current { + p, s, _ := strings.Cut(h.lp.SprintStyled(current_style, " "), " ") + h.lp.QueueWriteString(p) + defer h.lp.QueueWriteString(s) + suffix += p + } write_chunk := func(text string, emphasize bool) { if text == "" { return @@ -85,11 +74,7 @@ func (h *Handler) render_match_with_positions(text string, add_ellipsis bool, po h.lp.QueueWriteString(suffix) }() } - if scale > 1 { - h.lp.DrawSizedText(text, loop.SizedText{Scale: scale}) - } else { - h.lp.QueueWriteString(text) - } + h.lp.QueueWriteString(text) } at := 0 limit := len(text) @@ -129,29 +114,40 @@ func icon_for(path string, x os.DirEntry) string { return ans } -func (h *Handler) draw_column_of_matches(matches []*ResultItem, x, available_width, num_extra_matches int) { +func (h *Handler) draw_column_of_matches(matches []*ResultItem, current_idx int, x, available_width, num_before, num_after int) { + if num_before > 0 { + h.lp.QueueWriteString("\r") + h.lp.MoveCursorHorizontally(x) + h.lp.QueueWriteString("… ") + text := h.lp.SprintStyled("italic", fmt.Sprintf("%d prev matches", num_before)) + h.render_match_with_positions(text, false, nil, false) + } for i, m := range matches { h.lp.QueueWriteString("\r") h.lp.MoveCursorHorizontally(x) icon := icon_for(m.abspath, m.dir_entry) - text := "" + text := m.text add_ellipsis := false - positions := m.positions - if num_extra_matches > 0 && i == len(matches)-1 { - icon = "… " - text = h.lp.SprintStyled("italic", fmt.Sprintf("%d more matches", num_extra_matches)) - positions = nil - } else { - text = m.text - if wcswidth.Stringwidth(text) > available_width-3 { - text = wcswidth.TruncateToVisualLength(text, available_width-4) - add_ellipsis = true - } + if wcswidth.Stringwidth(text) > available_width-3 { + text = wcswidth.TruncateToVisualLength(text, available_width-4) + add_ellipsis = true } - h.lp.QueueWriteString(icon + " ") - h.render_match_with_positions(text, add_ellipsis, positions, 1) + is_current := i == current_idx + if is_current { + h.lp.QueueWriteString(h.lp.SprintStyled(matching_position_style, icon+" ")) + } else { + h.lp.QueueWriteString(icon + " ") + } + h.render_match_with_positions(text, add_ellipsis, m.positions, is_current) h.lp.MoveCursorVertically(1) } + if num_after > 0 { + h.lp.QueueWriteString("\r") + h.lp.MoveCursorHorizontally(x) + h.lp.QueueWriteString("… ") + text := h.lp.SprintStyled("italic", fmt.Sprintf("%d more matches", num_after)) + h.render_match_with_positions(text, false, nil, false) + } } func (h *Handler) draw_list_of_results(matches []*ResultItem, y, height int) { @@ -169,14 +165,39 @@ func (h *Handler) draw_list_of_results(matches []*ResultItem, y, height int) { } col_width = available_width / num_cols } + num_that_can_be_displayed := num_cols * height + num_after, num_before := 0, 0 + idx := min(h.state.CurrentIndex(), len(matches)-1) + if idx == 0 { + num_after = max(0, len(matches)-num_that_can_be_displayed) + } else { + num_after = max(0, len(matches)-num_that_can_be_displayed) + last_idx := len(matches) - 1 - num_after + if last_idx < idx { + num_before = last_idx - idx + num_after = max(0, num_after-num_before) + } + } + pos := num_before x := 1 for i := range num_cols { - is_last := i == num_cols-1 - chunk := matches[:min(len(matches), height)] - matches = matches[len(chunk):] + is_last, is_first := i == num_cols-1, i == 0 + num := height + if is_first && num_before > 0 { + num-- + } + if is_last && num_after > 0 { + num-- + } h.lp.MoveCursorTo(x, y) - h.draw_column_of_matches(chunk, x, col_width-1, utils.IfElse(is_last, len(matches), 0)) + limit := min(len(matches), pos+num) + h.draw_column_of_matches(matches[pos:limit], idx-pos, x, col_width-1, num_before, utils.IfElse(is_last, len(matches)-limit, 0)) x += col_width + pos += num + num_before = 0 + if pos >= len(matches) { + break + } } } @@ -192,14 +213,7 @@ func (h *Handler) draw_results(y, bottom_margin int, matches []*ResultItem, in_p case 0: h.draw_no_matches_message(in_progress) default: - switch h.state.SearchText() { - case "": - h.draw_list_of_results(matches, y, height-2) - default: - h.draw_matching_result(matches[0]) - y += 2 - h.draw_list_of_results(matches[1:], y, height-4) - } + h.draw_list_of_results(matches, y, height-2) } return } diff --git a/kittens/choose_files/search-bar.go b/kittens/choose_files/search-bar.go index ca25c7c5a..83596a7ed 100644 --- a/kittens/choose_files/search-bar.go +++ b/kittens/choose_files/search-bar.go @@ -64,7 +64,8 @@ func (h *Handler) handle_edit_keys(ev *loop.KeyEvent) bool { if h.state.SearchText() == "" { h.lp.Beep() } else { - h.state.search_text = h.state.search_text[:len(h.state.search_text)-1] + g := wcswidth.SplitIntoGraphemes(h.state.search_text) + h.state.SetSearchText(strings.Join(g[:len(g)-1], "")) return true } }