Skip to content

Commit 3efecc1

Browse files
committed
fix: refine search highlighting and status line indicators
Use background-only search highlight to preserve syntax colors, add reverse video fallback for no-colors mode, show mode indicators always (muted inactive / active fg) on right side with ≋ search icon, darken default muted and search background colors, fix stale godoc, update screenshot.
1 parent 44d50f9 commit 3efecc1

10 files changed

Lines changed: 113 additions & 87 deletions

File tree

.claude-plugin/skills/revdiff/references/config.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
3939
| `--color-accent` | Active pane borders and directory names | `#D5895F` |
4040
| `--color-border` | Inactive pane borders | `#585858` |
4141
| `--color-normal` | File entries and context lines | `#d0d0d0` |
42-
| `--color-muted` | Divider lines and status bar | `#6c6c6c` |
42+
| `--color-muted` | Divider lines and status bar | `#585858` |
4343
| `--color-selected-fg` | Selected file text | `#ffffaf` |
4444
| `--color-selected-bg` | Selected file background | `#D5895F` |
4545
| `--color-annotation` | Annotation text and markers | `#ffd700` |
@@ -53,10 +53,10 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
5353
| `--color-modify-bg` | Modified line background (collapsed mode) | `#3D2E00` |
5454
| `--color-tree-bg` | File tree pane background | terminal default |
5555
| `--color-diff-bg` | Diff pane background | terminal default |
56-
| `--color-status-fg` | Status bar foreground | `#2D2D2D` |
56+
| `--color-status-fg` | Status bar foreground | `#202020` |
5757
| `--color-status-bg` | Status bar background | `#C5794F` |
5858
| `--color-search-fg` | Search match text | `#1a1a1a` |
59-
| `--color-search-bg` | Search match background | `#d7d700` |
59+
| `--color-search-bg` | Search match background | `#4a4a00` |
6060

6161
## Chroma Syntax Highlighting Styles
6262

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ git diff → diff.ParseUnifiedDiff() → []DiffLine
3636
continuation lines get `↪` gutter marker, cursorViewportY() sums wrapped line counts
3737
when search is active (`/` to search, `n`/`N` to navigate, `esc` to clear):
3838
buildSearchMatchSet() converts match indices to O(1) map per render,
39-
highlightSearchMatches() inserts raw ANSI fg+bg sequences around matched
40-
substrings (character-level, ANSI-aware via ansi.Strip for position mapping)
39+
highlightSearchMatches() inserts ANSI bg-only sequence around matched substrings
40+
(preserves syntax foreground; falls back to reverse video in --no-colors mode)
4141
→ viewport.SetContent() → terminal
4242
```
4343

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
159159
| `--color-accent` | Active pane borders and directory names | `#D5895F` |
160160
| `--color-border` | Inactive pane borders | `#585858` |
161161
| `--color-normal` | File entries and context lines | `#d0d0d0` |
162-
| `--color-muted` | Divider lines and status bar | `#6c6c6c` |
162+
| `--color-muted` | Divider lines and status bar | `#585858` |
163163
| `--color-selected-fg` | Selected file text | `#ffffaf` |
164164
| `--color-selected-bg` | Selected file background | `#D5895F` |
165165
| `--color-annotation` | Annotation text and markers | `#ffd700` |
@@ -173,10 +173,10 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
173173
| `--color-modify-bg` | Modified line background (collapsed mode) | `#3D2E00` |
174174
| `--color-tree-bg` | File tree pane background | terminal default |
175175
| `--color-diff-bg` | Diff pane background | terminal default |
176-
| `--color-status-fg` | Status bar foreground | `#2D2D2D` |
176+
| `--color-status-fg` | Status bar foreground | `#202020` |
177177
| `--color-status-bg` | Status bar background | `#C5794F` |
178178
| `--color-search-fg` | Search match text | `#1a1a1a` |
179-
| `--color-search-bg` | Search match background | `#d7d700` |
179+
| `--color-search-bg` | Search match background | `#4a4a00` |
180180

181181
</details>
182182

cmd/revdiff/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ type options struct {
4141
Accent string `long:"color-accent" ini-name:"color-accent" env:"REVDIFF_COLOR_ACCENT" default:"#D5895F" description:"active pane borders and directory names"`
4242
Border string `long:"color-border" ini-name:"color-border" env:"REVDIFF_COLOR_BORDER" default:"#585858" description:"inactive pane borders"`
4343
Normal string `long:"color-normal" ini-name:"color-normal" env:"REVDIFF_COLOR_NORMAL" default:"#d0d0d0" description:"file entries and context lines"`
44-
Muted string `long:"color-muted" ini-name:"color-muted" env:"REVDIFF_COLOR_MUTED" default:"#6c6c6c" description:"line numbers and status bar"`
44+
Muted string `long:"color-muted" ini-name:"color-muted" env:"REVDIFF_COLOR_MUTED" default:"#585858" description:"line numbers and status bar"`
4545
SelectedFg string `long:"color-selected-fg" ini-name:"color-selected-fg" env:"REVDIFF_COLOR_SELECTED_FG" default:"#ffffaf" description:"selected file text color"`
4646
SelectedBg string `long:"color-selected-bg" ini-name:"color-selected-bg" env:"REVDIFF_COLOR_SELECTED_BG" default:"#D5895F" description:"selected file background color"`
4747
Annotation string `long:"color-annotation" ini-name:"color-annotation" env:"REVDIFF_COLOR_ANNOTATION" default:"#ffd700" description:"annotation text and markers"`
@@ -58,7 +58,7 @@ type options struct {
5858
StatusFg string `long:"color-status-fg" ini-name:"color-status-fg" env:"REVDIFF_COLOR_STATUS_FG" default:"#202020" description:"status bar foreground"`
5959
StatusBg string `long:"color-status-bg" ini-name:"color-status-bg" env:"REVDIFF_COLOR_STATUS_BG" default:"#C5794F" description:"status bar background"`
6060
SearchFg string `long:"color-search-fg" ini-name:"color-search-fg" env:"REVDIFF_COLOR_SEARCH_FG" default:"#1a1a1a" description:"search match foreground"`
61-
SearchBg string `long:"color-search-bg" ini-name:"color-search-bg" env:"REVDIFF_COLOR_SEARCH_BG" default:"#d7d700" description:"search match background"`
61+
SearchBg string `long:"color-search-bg" ini-name:"color-search-bg" env:"REVDIFF_COLOR_SEARCH_BG" default:"#4a4a00" description:"search match background"`
6262
} `group:"color options"`
6363
}
6464

cmd/revdiff/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func TestParseArgs_ColorDefaults(t *testing.T) {
108108
assert.Equal(t, "#D5895F", opts.Colors.Accent)
109109
assert.Equal(t, "#585858", opts.Colors.Border)
110110
assert.Equal(t, "#d0d0d0", opts.Colors.Normal)
111-
assert.Equal(t, "#6c6c6c", opts.Colors.Muted)
111+
assert.Equal(t, "#585858", opts.Colors.Muted)
112112
assert.Equal(t, "#87d787", opts.Colors.AddFg)
113113
assert.Equal(t, "#123800", opts.Colors.AddBg)
114114
assert.Equal(t, "#ff8787", opts.Colors.RemoveFg)

screenshot.png

50.3 KB
Loading

ui/collapsed_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,10 +1088,10 @@ func TestModel_StatusBarCollapsedIndicator(t *testing.T) {
10881088
m.focus = paneDiff
10891089
m.width = 200
10901090

1091-
t.Run("expanded mode has no collapsed indicator", func(t *testing.T) {
1091+
t.Run("collapsed indicator always present", func(t *testing.T) {
10921092
m.collapsed.enabled = false
10931093
status := m.statusBarText()
1094-
assert.NotContains(t, status, "▼")
1094+
assert.Contains(t, status, "▼", "indicator always shown, muted when inactive")
10951095
})
10961096

10971097
t.Run("collapsed mode shows indicator", func(t *testing.T) {

ui/diffview.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"github.com/umputun/revdiff/diff"
99
)
1010

11+
// matchRange represents a range of visible character positions for search match highlighting.
12+
type matchRange struct{ start, end int }
13+
1114
// renderDiff renders the current file's diff lines with styling, cursor highlight,
1215
// and injected annotation lines.
1316
func (m Model) renderDiff() string {
@@ -172,8 +175,8 @@ func (m Model) linePrefix(changeType diff.ChangeType) string {
172175
}
173176

174177
// highlightSearchMatches wraps each occurrence of the search term in the visible text
175-
// with ANSI fg+bg color sequences. works with both plain text and ANSI-coded content
176-
// by stripping ANSI to find match positions, then inserting highlights at correct visible-character positions.
178+
// with ANSI background color sequence (preserving syntax foreground within matches).
179+
// works with both plain text and ANSI-coded content by stripping ANSI to find match positions.
177180
func (m Model) highlightSearchMatches(s string) string {
178181
if m.searchTerm == "" {
179182
return s
@@ -188,7 +191,6 @@ func (m Model) highlightSearchMatches(s string) string {
188191
}
189192

190193
// collect all match ranges in visible-character positions
191-
type matchRange struct{ start, end int }
192194
var matches []matchRange
193195
offset := 0
194196
for {
@@ -204,10 +206,21 @@ func (m Model) highlightSearchMatches(s string) string {
204206
return s
205207
}
206208

207-
hlOn := m.ansiFg(m.styles.colors.SearchFg) + m.ansiBg(m.styles.colors.SearchBg)
208-
hlOff := "\033[39m\033[49m"
209+
// background-only highlight preserves syntax foreground colors within matches
210+
hlOn := m.ansiBg(m.styles.colors.SearchBg)
211+
hlOff := "\033[49m"
212+
if hlOn == "" {
213+
// no-colors mode: fall back to reverse video so matches remain visible
214+
hlOn = "\033[7m"
215+
hlOff = "\033[27m"
216+
}
217+
218+
return m.insertHighlightMarkers(s, matches, hlOn, hlOff)
219+
}
209220

210-
// walk original string, track visible char position, insert highlights
221+
// insertHighlightMarkers walks the string inserting hlOn/hlOff ANSI sequences at match positions,
222+
// skipping over existing ANSI escape sequences to preserve them.
223+
func (m Model) insertHighlightMarkers(s string, matches []matchRange, hlOn, hlOff string) string {
211224
var b strings.Builder
212225
visPos := 0 // current position in visible text
213226
matchIdx := 0 // current match we're processing
@@ -228,14 +241,13 @@ func (m Model) highlightSearchMatches(s string) string {
228241
continue
229242
}
230243

231-
// check if we need to insert highlight start/end at this visible position
244+
// insert highlight start/end at match boundaries
232245
if matchIdx < len(matches) && visPos == matches[matchIdx].start {
233246
b.WriteString(hlOn)
234247
}
235248
if matchIdx < len(matches) && visPos == matches[matchIdx].end {
236249
b.WriteString(hlOff)
237250
matchIdx++
238-
// check next match start at same position
239251
if matchIdx < len(matches) && visPos == matches[matchIdx].start {
240252
b.WriteString(hlOn)
241253
}

ui/model.go

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -575,11 +575,6 @@ func (m Model) statusBarText() string {
575575
segments = append(segments, ss)
576576
}
577577

578-
// mode indicators (combined into one segment)
579-
if modeIcons := m.statusModeIcons(); modeIcons != "" {
580-
segments = append(segments, modeIcons)
581-
}
582-
583578
// build right-side segments
584579
var rightParts []string
585580
if cnt := m.store.Count(); cnt > 0 {
@@ -589,7 +584,7 @@ func (m Model) statusBarText() string {
589584
}
590585
rightParts = append(rightParts, fmt.Sprintf("%d %s", cnt, suffix))
591586
}
592-
rightParts = append(rightParts, "? help")
587+
rightParts = append(rightParts, m.statusModeIcons(), "? help")
593588

594589
// build separator with muted foreground using raw ANSI (not lipgloss.Render)
595590
// to avoid full reset that would break the status bar background
@@ -605,10 +600,10 @@ func (m Model) statusBarText() string {
605600
minRight := lipgloss.Width(right) + 5 // 2 for status bar padding + 3 for separator
606601
available := max(m.width-minRight, 0)
607602

608-
// graceful degradation: drop segments from right to left when too narrow
603+
// graceful degradation: drop left segments when too narrow
609604
if lipgloss.Width(left) > available {
610-
// rebuild without mode icons first
611-
segments = m.statusSegmentsNoIcons()
605+
// rebuild without search position
606+
segments = m.statusSegmentsNoSearch()
612607
left = strings.Join(segments, sep)
613608
}
614609
if lipgloss.Width(left) > available {
@@ -719,33 +714,47 @@ func (m Model) ansiFg(hex string) string { return m.ansiColor(hex, 38) }
719714
// ansiBg returns an ANSI 24-bit background escape sequence for a hex color.
720715
func (m Model) ansiBg(hex string) string { return m.ansiColor(hex, 48) }
721716

722-
// statusModeIcons returns combined mode indicator icons (▼ for collapsed, ◉ for filter, ↩ for wrap).
717+
// statusModeIcons returns combined mode indicator icons (▼ collapsed, ◉ filter, ↩ wrap, ≋ search).
718+
// all icons are always shown; active modes use status foreground, inactive use muted color.
723719
func (m Model) statusModeIcons() string {
724-
var icons []string
725-
if m.collapsed.enabled {
726-
icons = append(icons, "▼")
720+
type indicator struct {
721+
icon string
722+
active bool
727723
}
728-
if m.tree.filter {
729-
icons = append(icons, "◉")
724+
indicators := []indicator{
725+
{"▼", m.collapsed.enabled},
726+
{"◉", m.tree.filter},
727+
{"↩", m.wrapMode},
728+
{"≋", len(m.searchMatches) > 0},
730729
}
731-
if m.wrapMode {
732-
icons = append(icons, "↩")
730+
731+
statusFg := m.styles.colors.Muted
732+
if m.styles.colors.StatusFg != "" {
733+
statusFg = m.styles.colors.StatusFg
733734
}
734-
return strings.Join(icons, " ")
735+
mutedSeq := m.ansiFg(m.styles.colors.Muted)
736+
activeSeq := m.ansiFg(statusFg)
737+
738+
var icons []string
739+
for _, ind := range indicators {
740+
if ind.active {
741+
icons = append(icons, activeSeq+ind.icon)
742+
} else {
743+
icons = append(icons, mutedSeq+ind.icon)
744+
}
745+
}
746+
return strings.Join(icons, " ") + activeSeq
735747
}
736748

737-
// statusSegmentsNoIcons returns left segments without mode indicators (▼ ◉ ↩).
738-
func (m Model) statusSegmentsNoIcons() []string {
749+
// statusSegmentsNoSearch returns left segments without search position (for narrow terminals).
750+
func (m Model) statusSegmentsNoSearch() []string {
739751
var segments []string
740752
if m.currFile != "" {
741753
segments = append(segments, m.currFile, fmt.Sprintf("+%d/-%d", m.fileAdds, m.fileRemoves))
742754
}
743755
if hs := m.hunkSegment(); hs != "" {
744756
segments = append(segments, hs)
745757
}
746-
if ss := m.searchSegment(); ss != "" {
747-
segments = append(segments, ss)
748-
}
749758
return segments
750759
}
751760

0 commit comments

Comments
 (0)