Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .claude-plugin/skills/revdiff/references/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
| `--color-accent` | Active pane borders and directory names | `#D5895F` |
| `--color-border` | Inactive pane borders | `#585858` |
| `--color-normal` | File entries and context lines | `#d0d0d0` |
| `--color-muted` | Divider lines and status bar | `#6c6c6c` |
| `--color-muted` | Divider lines and status bar | `#585858` |
| `--color-selected-fg` | Selected file text | `#ffffaf` |
| `--color-selected-bg` | Selected file background | `#D5895F` |
| `--color-annotation` | Annotation text and markers | `#ffd700` |
Expand All @@ -53,8 +53,10 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
| `--color-modify-bg` | Modified line background (collapsed mode) | `#3D2E00` |
| `--color-tree-bg` | File tree pane background | terminal default |
| `--color-diff-bg` | Diff pane background | terminal default |
| `--color-status-fg` | Status bar foreground | `#2D2D2D` |
| `--color-status-fg` | Status bar foreground | `#202020` |
| `--color-status-bg` | Status bar background | `#C5794F` |
| `--color-search-fg` | Search match text | `#1a1a1a` |
| `--color-search-bg` | Search match background | `#4a4a00` |

## Chroma Syntax Highlighting Styles

Expand Down
11 changes: 10 additions & 1 deletion .claude-plugin/skills/revdiff/references/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,18 @@ revdiff HEAD~1 # review last commit
| `Ctrl+d/Ctrl+u` | Page scroll in file tree and diff pane |
| `Home/End` | Jump to first/last item |
| `Enter` | Switch to diff pane (tree) / start annotation (diff pane) |
| `n/p` | Next/previous changed file |
| `n/p` | Next/previous changed file (n = next match when search active) |
| `[` / `]` | Jump to previous/next change hunk in diff |

**Search:**

| Key | Action |
|-----|--------|
| `/` | Start search in diff pane |
| `n` | Next search match (overrides next file when search active) |
| `N` | Previous search match |
| `Esc` | Cancel search input / clear search results |

**Annotations:**

| Key | Action |
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ git diff → diff.ParseUnifiedDiff() → []DiffLine
when wrap mode is on (`w` toggle, orthogonal to above):
wrapContent() splits long lines via ansi.Wrap,
continuation lines get `↪` gutter marker, cursorViewportY() sums wrapped line counts
when search is active (`/` to search, `n`/`N` to navigate, `esc` to clear):
buildSearchMatchSet() converts match indices to O(1) map per render,
highlightSearchMatches() inserts ANSI bg-only sequence around matched substrings
(preserves syntax foreground; falls back to reverse video in --no-colors mode)
→ viewport.SetContent() → terminal
```

Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Built for a specific use case: reviewing code changes without leaving a terminal
- Word wrap mode: wraps long lines at viewport boundary with `↪` continuation markers, toggle with `w`
- Annotate any line in the diff (added, removed, or context) plus file-level notes
- Two-pane TUI: file tree (left) + colorized diff viewport (right)
- Vim-style `/` search within diff with `n`/`N` match navigation
- Hunk navigation to jump between change groups
- Filter file tree to show only annotated files
- Status line with filename, diff stats, hunk position, and mode indicators
Expand Down Expand Up @@ -158,7 +159,7 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
| `--color-accent` | Active pane borders and directory names | `#D5895F` |
| `--color-border` | Inactive pane borders | `#585858` |
| `--color-normal` | File entries and context lines | `#d0d0d0` |
| `--color-muted` | Divider lines and status bar | `#6c6c6c` |
| `--color-muted` | Divider lines and status bar | `#585858` |
| `--color-selected-fg` | Selected file text | `#ffffaf` |
| `--color-selected-bg` | Selected file background | `#D5895F` |
| `--color-annotation` | Annotation text and markers | `#ffd700` |
Expand All @@ -172,8 +173,10 @@ All color options accept hex values (`#rrggbb`) and have corresponding `REVDIFF_
| `--color-modify-bg` | Modified line background (collapsed mode) | `#3D2E00` |
| `--color-tree-bg` | File tree pane background | terminal default |
| `--color-diff-bg` | Diff pane background | terminal default |
| `--color-status-fg` | Status bar foreground | `#2D2D2D` |
| `--color-status-fg` | Status bar foreground | `#202020` |
| `--color-status-bg` | Status bar background | `#C5794F` |
| `--color-search-fg` | Search match text | `#1a1a1a` |
| `--color-search-bg` | Search match background | `#4a4a00` |

</details>

Expand Down Expand Up @@ -218,9 +221,18 @@ revdiff HEAD~1
| `Ctrl+d/Ctrl+u` | Page scroll in file tree and diff pane |
| `Home/End` | Jump to first/last item |
| `Enter` | Switch to diff pane (tree) / start annotation (diff pane) |
| `n/p` | Next/previous changed file |
| `n/p` | Next/previous changed file (n = next match when search active) |
| `[` / `]` | Jump to previous/next change hunk in diff |

**Search:**

| Key | Action |
|-----|--------|
| `/` | Start search in diff pane |
| `n` | Next search match (overrides next file when search active) |
| `N` | Previous search match |
| `Esc` | Cancel search input / clear search results |

**Annotations:**

| Key | Action |
Expand Down
6 changes: 5 additions & 1 deletion cmd/revdiff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type options struct {
Accent string `long:"color-accent" ini-name:"color-accent" env:"REVDIFF_COLOR_ACCENT" default:"#D5895F" description:"active pane borders and directory names"`
Border string `long:"color-border" ini-name:"color-border" env:"REVDIFF_COLOR_BORDER" default:"#585858" description:"inactive pane borders"`
Normal string `long:"color-normal" ini-name:"color-normal" env:"REVDIFF_COLOR_NORMAL" default:"#d0d0d0" description:"file entries and context lines"`
Muted string `long:"color-muted" ini-name:"color-muted" env:"REVDIFF_COLOR_MUTED" default:"#6c6c6c" description:"line numbers and status bar"`
Muted string `long:"color-muted" ini-name:"color-muted" env:"REVDIFF_COLOR_MUTED" default:"#585858" description:"line numbers and status bar"`
SelectedFg string `long:"color-selected-fg" ini-name:"color-selected-fg" env:"REVDIFF_COLOR_SELECTED_FG" default:"#ffffaf" description:"selected file text color"`
SelectedBg string `long:"color-selected-bg" ini-name:"color-selected-bg" env:"REVDIFF_COLOR_SELECTED_BG" default:"#D5895F" description:"selected file background color"`
Annotation string `long:"color-annotation" ini-name:"color-annotation" env:"REVDIFF_COLOR_ANNOTATION" default:"#ffd700" description:"annotation text and markers"`
Expand All @@ -57,6 +57,8 @@ type options struct {
DiffBg string `long:"color-diff-bg" ini-name:"color-diff-bg" env:"REVDIFF_COLOR_DIFF_BG" description:"diff pane background"`
StatusFg string `long:"color-status-fg" ini-name:"color-status-fg" env:"REVDIFF_COLOR_STATUS_FG" default:"#202020" description:"status bar foreground"`
StatusBg string `long:"color-status-bg" ini-name:"color-status-bg" env:"REVDIFF_COLOR_STATUS_BG" default:"#C5794F" description:"status bar background"`
SearchFg string `long:"color-search-fg" ini-name:"color-search-fg" env:"REVDIFF_COLOR_SEARCH_FG" default:"#1a1a1a" description:"search match foreground"`
SearchBg string `long:"color-search-bg" ini-name:"color-search-bg" env:"REVDIFF_COLOR_SEARCH_BG" default:"#4a4a00" description:"search match background"`
} `group:"color options"`
}

Expand Down Expand Up @@ -203,6 +205,8 @@ func run(opts options) error {
DiffBg: opts.Colors.DiffBg,
StatusFg: opts.Colors.StatusFg,
StatusBg: opts.Colors.StatusBg,
SearchFg: opts.Colors.SearchFg,
SearchBg: opts.Colors.SearchBg,
},
})

Expand Down
2 changes: 1 addition & 1 deletion cmd/revdiff/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func TestParseArgs_ColorDefaults(t *testing.T) {
assert.Equal(t, "#D5895F", opts.Colors.Accent)
assert.Equal(t, "#585858", opts.Colors.Border)
assert.Equal(t, "#d0d0d0", opts.Colors.Normal)
assert.Equal(t, "#6c6c6c", opts.Colors.Muted)
assert.Equal(t, "#585858", opts.Colors.Muted)
assert.Equal(t, "#87d787", opts.Colors.AddFg)
assert.Equal(t, "#123800", opts.Colors.AddBg)
assert.Equal(t, "#ff8787", opts.Colors.RemoveFg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,47 +88,47 @@ Add `/` search in the diff pane following vim/less conventions. Users can search
- Modify: `cmd/revdiff/main.go`
- Modify: `ui/model_test.go`

- [ ] add `SearchFg` and `SearchBg` string fields to `Colors` struct
- [ ] add `SearchMatch lipgloss.Style` to `styles` struct
- [ ] initialize `SearchMatch` style in `newStyles()` with foreground and background from colors
- [ ] update `normalizeColors()` to normalize `SearchFg` and `SearchBg`
- [ ] add CLI flags `--color-search-fg` (default `#1a1a1a`) and `--color-search-bg` (default `#d7d700`) in main.go opts
- [ ] wire new color fields in `run()` function's `ui.Colors{}` literal
- [ ] update `plainStyles()` to include `SearchMatch` for tests
- [ ] run `make test` — must pass before task 2
- [x] add `SearchFg` and `SearchBg` string fields to `Colors` struct
- [x] add `SearchMatch lipgloss.Style` to `styles` struct
- [x] initialize `SearchMatch` style in `newStyles()` with foreground and background from colors
- [x] update `normalizeColors()` to normalize `SearchFg` and `SearchBg`
- [x] add CLI flags `--color-search-fg` (default `#1a1a1a`) and `--color-search-bg` (default `#d7d700`) in main.go opts
- [x] wire new color fields in `run()` function's `ui.Colors{}` literal
- [x] update `plainStyles()` to include `SearchMatch` for tests
- [x] run `make test` — must pass before task 2

### Task 2: Add search model fields and input handling

**Files:**
- Modify: `ui/model.go`
- Modify: `ui/model_test.go`

- [ ] add `searching bool`, `searchTerm string`, `searchMatches []int`, `searchCursor int`, `searchInput textinput.Model` fields to `Model`
- [ ] add `startSearch()` method — creates textinput with `/` placeholder, sets `m.searching = true`
- [ ] add `handleSearchKey(msg)` method — enter calls `submitSearch`, esc calls `cancelSearch`, default forwards to `searchInput.Update(msg)`
- [ ] add `submitSearch()` method — if input empty, call `clearSearch()` and return; otherwise store lowercase search term, scan `diffLines` for case-insensitive matches, populate `searchMatches`, move cursor to first match forward, set `searching = false`
- [ ] add `cancelSearch()` method — clears input, sets `searching = false`
- [ ] forward non-key messages to `searchInput` in `Update()` when `m.searching` (parallel to annotating path)
- [ ] handle `/` key in `handleKey()` — calls `startSearch()` (only from diff pane)
- [ ] handle `searching` priority in `handleKey()` — check before help overlay, after annotation
- [ ] write tests for `startSearch`, `submitSearch`, `cancelSearch` behavior
- [ ] write tests for search input key handling (enter submits, esc cancels)
- [ ] run `make test` — must pass before task 3
- [x] add `searching bool`, `searchTerm string`, `searchMatches []int`, `searchCursor int`, `searchInput textinput.Model` fields to `Model`
- [x] add `startSearch()` method — creates textinput with `/` placeholder, sets `m.searching = true`
- [x] add `handleSearchKey(msg)` method — enter calls `submitSearch`, esc calls `cancelSearch`, default forwards to `searchInput.Update(msg)`
- [x] add `submitSearch()` method — if input empty, call `clearSearch()` and return; otherwise store lowercase search term, scan `diffLines` for case-insensitive matches, populate `searchMatches`, move cursor to first match forward, set `searching = false`
- [x] add `cancelSearch()` method — clears input, sets `searching = false`
- [x] forward non-key messages to `searchInput` in `Update()` when `m.searching` (parallel to annotating path)
- [x] handle `/` key in `handleKey()` — calls `startSearch()` (only from diff pane)
- [x] handle `searching` priority in `handleKey()` — check before help overlay, after annotation
- [x] write tests for `startSearch`, `submitSearch`, `cancelSearch` behavior
- [x] write tests for search input key handling (enter submits, esc cancels)
- [x] run `make test` — must pass before task 3

### Task 3: Add search match navigation (n/N keys)

**Files:**
- Modify: `ui/model.go`
- Modify: `ui/model_test.go`

- [ ] add `nextSearchMatch()` method — advances `searchCursor` with wrap, moves `diffCursor`, syncs viewport
- [ ] add `prevSearchMatch()` method — same but backwards
- [ ] modify `n` key handling: when `len(m.searchMatches) > 0`, call `nextSearchMatch()` instead of `tree.nextFile()`
- [ ] add `N` key handling in `handleKey()`: when `len(m.searchMatches) > 0`, call `prevSearchMatch()`
- [ ] write tests for next/prev match navigation including wrap-around
- [ ] write tests that `n` falls through to next-file when no search active
- [ ] write test that `N` does prev match when search active
- [ ] run `make test` — must pass before task 4
- [x] add `nextSearchMatch()` method — advances `searchCursor` with wrap, moves `diffCursor`, syncs viewport
- [x] add `prevSearchMatch()` method — same but backwards
- [x] modify `n` key handling: when `len(m.searchMatches) > 0`, call `nextSearchMatch()` instead of `tree.nextFile()`
- [x] add `N` key handling in `handleKey()`: when `len(m.searchMatches) > 0`, call `prevSearchMatch()`
- [x] write tests for next/prev match navigation including wrap-around
- [x] write tests that `n` falls through to next-file when no search active
- [x] write test that `N` does prev match when search active
- [x] run `make test` — must pass before task 4

### Task 4: Highlight matching lines in diff rendering

Expand All @@ -137,71 +137,71 @@ Add `/` search in the diff pane following vim/less conventions. Users can search
- Modify: `ui/collapsed.go`
- Modify: `ui/model_test.go`

- [ ] add `searchMatchSet map[int]bool` field to Model, computed in `renderDiff()` and `renderCollapsedDiff()` before line iteration
- [ ] in `renderDiffLine`: when line index is in `m.searchMatchSet`, apply `m.styles.SearchMatch` background to content (after syntax highlight, before horizontal scroll/wrap — highlight propagates to all wrapped continuation rows automatically)
- [ ] in `renderCollapsedAddLine`: same search match highlight check and styling (before wrap if word wrap is active)
- [ ] ensure cursor line styling (`▶`) coexists with search highlight
- [ ] verify search highlight works correctly with word wrap active (all continuation rows highlighted)
- [ ] write tests verifying matched lines contain search highlight styling
- [ ] write tests verifying non-matched lines are unchanged
- [ ] run `make test` — must pass before task 5
- [x] add `searchMatchSet map[int]bool` field to Model, computed in `renderDiff()` and `renderCollapsedDiff()` before line iteration
- [x] in `renderDiffLine`: when line index is in `m.searchMatchSet`, apply `m.styles.SearchMatch` background to content (after syntax highlight, before horizontal scroll/wrap — highlight propagates to all wrapped continuation rows automatically)
- [x] in `renderCollapsedAddLine`: same search match highlight check and styling (before wrap if word wrap is active)
- [x] ensure cursor line styling (`▶`) coexists with search highlight
- [x] verify search highlight works correctly with word wrap active (all continuation rows highlighted)
- [x] write tests verifying matched lines contain search highlight styling
- [x] write tests verifying non-matched lines are unchanged
- [x] run `make test` — must pass before task 5

### Task 5: Status line search indicator and search input display

**Files:**
- Modify: `ui/model.go`
- Modify: `ui/model_test.go`

- [ ] add `if m.searching` branch in `statusBarText()` returning search input view with hints (before `inConfirmDiscard` check)
- [ ] add `[X/Y]` search position segment to status line when `len(m.searchMatches) > 0` (between hunk and mode icons)
- [ ] handle `searchMatches` in `statusSegmentsNoIcons()` and `statusSegmentsMinimal()` for narrow terminal degradation
- [ ] write tests for status bar during active search input
- [ ] write tests for `[X/Y]` display with various match counts
- [ ] run `make test` — must pass before task 6
- [x] add `if m.searching` branch in `statusBarText()` returning search input view with hints (before `inConfirmDiscard` check)
- [x] add `[X/Y]` search position segment to status line when `len(m.searchMatches) > 0` (between hunk and mode icons)
- [x] handle `searchMatches` in `statusSegmentsNoIcons()` and `statusSegmentsMinimal()` for narrow terminal degradation
- [x] write tests for status bar during active search input
- [x] write tests for `[X/Y]` display with various match counts
- [x] run `make test` — must pass before task 6

### Task 6: Clear search on file change

**Files:**
- Modify: `ui/model.go`
- Modify: `ui/model_test.go`

- [ ] add `clearSearch()` method that resets `searchTerm`, `searchMatches`, `searchCursor`, `searchMatchSet`
- [ ] call `clearSearch()` in `handleFileLoaded` after setting `m.diffLines`
- [ ] write tests for search clearing on file load
- [ ] write tests for empty search submit clearing matches (already wired in Task 2's `submitSearch`)
- [ ] run `make test` — must pass before task 7
- [x] add `clearSearch()` method that resets `searchTerm`, `searchMatches`, `searchCursor`, `searchMatchSet`
- [x] call `clearSearch()` in `handleFileLoaded` after setting `m.diffLines`
- [x] write tests for search clearing on file load
- [x] write tests for empty search submit clearing matches (already wired in Task 2's `submitSearch`)
- [x] run `make test` — must pass before task 7

### Task 7: Update help overlay

**Files:**
- Modify: `ui/model.go`
- Modify: `ui/model_test.go`

- [ ] add search entries to help overlay — either new "Search" section or under "Navigation"
- [ ] entries: `/` = search, `n` = next match, `N` = prev match
- [ ] update `n / p` entry to note search-active override
- [ ] write test verifying help overlay contains search key listings
- [ ] run `make test` — must pass before task 8
- [x] add search entries to help overlay — either new "Search" section or under "Navigation"
- [x] entries: `/` = search, `n` = next match, `N` = prev match
- [x] update `n / p` entry to note search-active override
- [x] write test verifying help overlay contains search key listings
- [x] run `make test` — must pass before task 8

### Task 8: Verify acceptance criteria
- [ ] verify `/` opens search input in diff pane
- [ ] verify enter submits and jumps to first match
- [ ] verify esc cancels without searching
- [ ] verify `n`/`N` navigate matches with wrap-around
- [ ] verify `n` reverts to next-file when no search active
- [ ] verify all matches highlighted with search colors
- [ ] verify `[X/Y]` shown in status line
- [ ] verify search clears on file change
- [ ] verify `--color-search-fg`/`--color-search-bg` flags work
- [ ] run full test suite: `make test`
- [ ] run linter: `make lint`
- [x] verify `/` opens search input in diff pane
- [x] verify enter submits and jumps to first match
- [x] verify esc cancels without searching
- [x] verify `n`/`N` navigate matches with wrap-around
- [x] verify `n` reverts to next-file when no search active
- [x] verify all matches highlighted with search colors
- [x] verify `[X/Y]` shown in status line
- [x] verify search clears on file change
- [x] verify `--color-search-fg`/`--color-search-bg` flags work
- [x] run full test suite: `make test`
- [x] run linter: `make lint`

### Task 9: [Final] Update documentation
- [ ] update README.md with `/` search, `n`/`N` navigation keybindings
- [ ] update `.claude-plugin/skills/revdiff/references/usage.md` with search keybindings
- [ ] update `.claude-plugin/skills/revdiff/references/config.md` with search color options
- [ ] update CLAUDE.md if new patterns discovered
- [ ] move this plan to `docs/plans/completed/`
- [x] update README.md with `/` search, `n`/`N` navigation keybindings
- [x] update `.claude-plugin/skills/revdiff/references/usage.md` with search keybindings
- [x] update `.claude-plugin/skills/revdiff/references/config.md` with search color options
- [x] update CLAUDE.md if new patterns discovered
- [x] move this plan to `docs/plans/completed/`

## Post-Completion

Expand Down
Binary file modified screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading