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
1 change: 1 addition & 0 deletions .claude-plugin/skills/revdiff/references/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Then uncomment and edit the values you want to change.
| `--tab-width` | `REVDIFF_TAB_WIDTH` | Spaces per tab character | `4` |
| `--no-colors` | `REVDIFF_NO_COLORS` | Disable all colors including syntax highlighting | `false` |
| `--no-status-bar` | `REVDIFF_NO_STATUS_BAR` | Hide the status bar | `false` |
| `--wrap` | `REVDIFF_WRAP` | Enable line wrapping in diff view | `false` |
| `--no-confirm-discard` | `REVDIFF_NO_CONFIRM_DISCARD` | Skip confirmation when discarding annotations with Q | `false` |
| `--chroma-style` | `REVDIFF_CHROMA_STYLE` | Chroma color theme for syntax highlighting | `catppuccin-macchiato` |
| `-o`, `--output` | `REVDIFF_OUTPUT` | Write annotations to file instead of stdout | |
Expand Down
1 change: 1 addition & 0 deletions .claude-plugin/skills/revdiff/references/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ revdiff HEAD~1 # review last commit
| Key | Action |
|-----|--------|
| `v` | Toggle collapsed diff mode (shows final text with change markers) |
| `w` | Toggle word wrap (long lines wrap with `↪` continuation markers) |
| `.` | Expand/collapse individual hunk under cursor (collapsed mode only) |
| `f` | Toggle filter: all files / annotated only |
| `?` | Toggle help overlay showing all keybindings |
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ git diff → diff.ParseUnifiedDiff() → []DiffLine
collapsed (`v` toggle): renderCollapsedDiff() → skips removed lines,
uses buildModifiedSet() to style adds as modify (amber ~) or pure add (green +)
expanded hunks (`.` toggle) show all lines inline
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
→ viewport.SetContent() → terminal
```

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Built for a specific use case: reviewing code changes without leaving a terminal
- Structured annotation output to stdout - pipe into AI agents, scripts, or other tools
- Full-file diff view with syntax highlighting
- Collapsed diff mode: shows final text with change markers, toggle with `v`
- 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)
- Hunk navigation to jump between change groups
Expand Down Expand Up @@ -126,6 +127,7 @@ revdiff [OPTIONS] [ref]
| `--tab-width` | Number of spaces per tab character, env: `REVDIFF_TAB_WIDTH` | `4` |
| `--no-colors` | Disable all colors including syntax highlighting, env: `REVDIFF_NO_COLORS` | `false` |
| `--no-status-bar` | Hide the status bar, env: `REVDIFF_NO_STATUS_BAR` | `false` |
| `--wrap` | Enable line wrapping in diff view, env: `REVDIFF_WRAP` | `false` |
| `--no-confirm-discard` | Skip confirmation when discarding annotations with Q, env: `REVDIFF_NO_CONFIRM_DISCARD` | `false` |
| `--chroma-style` | Chroma color theme for syntax highlighting, env: `REVDIFF_CHROMA_STYLE` | `catppuccin-macchiato` |
| `-o`, `--output` | Write annotations to file instead of stdout, env: `REVDIFF_OUTPUT` | |
Expand Down Expand Up @@ -233,6 +235,7 @@ revdiff HEAD~1
| Key | Action |
|-----|--------|
| `v` | Toggle collapsed diff mode (shows final text with change markers) |
| `w` | Toggle word wrap (long lines wrap with `↪` continuation markers) |
| `.` | Expand/collapse individual hunk under cursor (collapsed mode only) |
| `f` | Toggle filter: all files / annotated only (shown when annotations exist) |
| `?` | Toggle help overlay showing all keybindings |
Expand Down
2 changes: 2 additions & 0 deletions cmd/revdiff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type options struct {
NoColors bool `long:"no-colors" ini-name:"no-colors" env:"REVDIFF_NO_COLORS" description:"disable all colors including syntax highlighting"`
NoStatusBar bool `long:"no-status-bar" ini-name:"no-status-bar" env:"REVDIFF_NO_STATUS_BAR" description:"hide the status bar"`
NoConfirmDiscard bool `long:"no-confirm-discard" ini-name:"no-confirm-discard" env:"REVDIFF_NO_CONFIRM_DISCARD" description:"skip confirmation prompt when discarding annotations with Q"`
Wrap bool `long:"wrap" ini-name:"wrap" env:"REVDIFF_WRAP" description:"enable line wrapping in diff view"`
ChromaStyle string `long:"chroma-style" ini-name:"chroma-style" env:"REVDIFF_CHROMA_STYLE" default:"catppuccin-macchiato" description:"chroma style for syntax highlighting"`
Output string `long:"output" short:"o" env:"REVDIFF_OUTPUT" no-ini:"true" description:"write annotations to file instead of stdout"`
Config string `long:"config" env:"REVDIFF_CONFIG" no-ini:"true" description:"path to config file"`
Expand Down Expand Up @@ -177,6 +178,7 @@ func run(opts options) error {
NoColors: opts.NoColors,
NoStatusBar: opts.NoStatusBar,
NoConfirmDiscard: opts.NoConfirmDiscard,
Wrap: opts.Wrap,
TabWidth: opts.TabWidth,
Ref: opts.Ref.Ref,
Staged: opts.Staged,
Expand Down
26 changes: 26 additions & 0 deletions cmd/revdiff/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestParseArgs_Defaults(t *testing.T) {
assert.False(t, opts.NoColors)
assert.False(t, opts.NoStatusBar)
assert.False(t, opts.NoConfirmDiscard)
assert.False(t, opts.Wrap)
assert.Empty(t, opts.Output)
assert.Empty(t, opts.Ref.Ref)
}
Expand Down Expand Up @@ -55,6 +56,31 @@ func TestParseArgs_NoConfirmDiscard(t *testing.T) {
})
}

func TestParseArgs_Wrap(t *testing.T) {
t.Run("flag", func(t *testing.T) {
opts, err := parseArgs(append(noConfigArgs(t), "--wrap"))
require.NoError(t, err)
assert.True(t, opts.Wrap)
})

t.Run("env", func(t *testing.T) {
t.Setenv("REVDIFF_WRAP", "true")
opts, err := parseArgs(noConfigArgs(t))
require.NoError(t, err)
assert.True(t, opts.Wrap)
})

t.Run("config file", func(t *testing.T) {
cfgDir := t.TempDir()
cfgPath := filepath.Join(cfgDir, "config")
err := os.WriteFile(cfgPath, []byte("[Application Options]\nwrap = true\n"), 0o600)
require.NoError(t, err)
opts, err := parseArgs([]string{"--config", cfgPath})
require.NoError(t, err)
assert.True(t, opts.Wrap)
})
}

func TestParseArgs_OutputFlag(t *testing.T) {
opts, err := parseArgs([]string{"-o", "/tmp/out.txt"})
require.NoError(t, err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,28 @@ Add a `w` key toggle to enable line wrapping in the diff view. Currently, long l
- Modify: `cmd/revdiff/main.go`
- Modify: `ui/model_test.go`

- [ ] add `wrapMode bool` field to `Model` struct
- [ ] add `Wrap bool` CLI flag to opts struct in `main.go` with long name `wrap`
- [ ] wire the flag value into `Model` initialization
- [ ] add config file support (ini-name tag)
- [ ] add `↩` icon to `statusModeIcons()` when `m.wrapMode` is true
- [ ] write test verifying `wrapMode` is set from options
- [ ] write test that `statusModeIcons()` includes `↩` when wrap active
- [ ] run `make test` — must pass before task 2
- [x] add `wrapMode bool` field to `Model` struct
- [x] add `Wrap bool` CLI flag to opts struct in `main.go` with long name `wrap`
- [x] wire the flag value into `Model` initialization
- [x] add config file support (ini-name tag)
- [x] add `↩` icon to `statusModeIcons()` when `m.wrapMode` is true
- [x] write test verifying `wrapMode` is set from options
- [x] write test that `statusModeIcons()` includes `↩` when wrap active
- [x] run `make test` — must pass before task 2

### Task 2: Implement line wrapping in renderDiffLine

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

- [ ] add `wrapContent(content string, width int) []string` method that uses `ansi.Wrap` and splits on `\n`
- [ ] modify `renderDiffLine()`: when `m.wrapMode` is true, wrap content (excluding gutter), then prepend gutter to first line and `↪` to continuation lines
- [ ] apply same line style (add/remove/context background) to continuation lines
- [ ] skip `ansi.Cut` (horizontal scroll) when `m.wrapMode` is true
- [ ] write tests for `wrapContent()`: short lines, long lines, ANSI content, empty content, multi-byte chars
- [ ] write tests for `renderDiffLine()` output with wrap enabled — verify `↪` markers and line count
- [ ] run `make test` — must pass before task 3
- [x] add `wrapContent(content string, width int) []string` method that uses `ansi.Wrap` and splits on `\n`
- [x] modify `renderDiffLine()`: when `m.wrapMode` is true, wrap content (excluding gutter), then prepend gutter to first line and `↪` to continuation lines
- [x] apply same line style (add/remove/context background) to continuation lines
- [x] skip `ansi.Cut` (horizontal scroll) when `m.wrapMode` is true
- [x] write tests for `wrapContent()`: short lines, long lines, ANSI content, empty content, multi-byte chars
- [x] write tests for `renderDiffLine()` output with wrap enabled — verify `↪` markers and line count
- [x] run `make test` — must pass before task 3

### Task 3: Fix cursor and viewport coordinate math

Expand All @@ -109,60 +109,60 @@ Add a `w` key toggle to enable line wrapping in the diff view. Currently, long l
- Modify: `ui/annotate.go`
- Modify: `ui/model_test.go`

- [ ] add `wrappedLineCount(idx int) int` method that calls `wrapContent()` and returns `len(result)` — stays in sync with rendering
- [ ] update `cursorViewportY()` to use `wrappedLineCount()` instead of counting 1 per line
- [ ] update `renderAnnotationOrInput()` — annotation row follows after all wrapped lines of its diff line
- [ ] write tests for `wrappedLineCount()` with various line lengths and wrap on/off
- [ ] write tests for `cursorViewportY()` with wrapped lines
- [ ] run `make test` — must pass before task 4
- [x] add `wrappedLineCount(idx int) int` method that calls `wrapContent()` and returns `len(result)` — stays in sync with rendering
- [x] update `cursorViewportY()` to use `wrappedLineCount()` instead of counting 1 per line
- [x] update `renderAnnotationOrInput()` — annotation row follows after all wrapped lines of its diff line
- [x] write tests for `wrappedLineCount()` with various line lengths and wrap on/off
- [x] write tests for `cursorViewportY()` with wrapped lines
- [x] run `make test` — must pass before task 4

### Task 4: Apply wrapping to collapsed mode

**Files:**
- Modify: `ui/collapsed.go`
- Modify: `ui/collapsed_test.go`

- [ ] apply wrapping in `renderCollapsedAddLine()` — reuse `wrapContent()` from Task 2
- [ ] apply wrapping in `renderDeletePlaceholder()` for consistency
- [ ] verify `cursorViewportY()` changes from Task 3 work correctly in collapsed mode
- [ ] write tests for collapsed mode rendering with wrap enabled
- [ ] run `make test` — must pass before task 5
- [x] apply wrapping in `renderCollapsedAddLine()` — reuse `wrapContent()` from Task 2
- [x] apply wrapping in `renderDeletePlaceholder()` for consistency
- [x] verify `cursorViewportY()` changes from Task 3 work correctly in collapsed mode
- [x] write tests for collapsed mode rendering with wrap enabled
- [x] run `make test` — must pass before task 5

### Task 5: Wire up `w` key toggle and scroll interaction

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

- [ ] handle `w` key in `handleKey()` to toggle `m.wrapMode`
- [ ] reset `scrollX = 0` when enabling wrap
- [ ] block `left`/`right` scroll keys in `handleDiffNav()` when `m.wrapMode` is true
- [ ] re-render diff content after toggle (`m.viewport.SetContent(m.renderDiff())`)
- [ ] add `w` entry to help overlay under "View" section: `w toggle word wrap`
- [ ] write tests for wrap toggle behavior
- [ ] write tests verifying scroll keys are blocked when wrap is on
- [ ] write test verifying help overlay contains word wrap key listing
- [ ] run `make test` — must pass before task 6
- [x] handle `w` key in `handleKey()` to toggle `m.wrapMode`
- [x] reset `scrollX = 0` when enabling wrap
- [x] block `left`/`right` scroll keys in `handleDiffNav()` when `m.wrapMode` is true
- [x] re-render diff content after toggle (`m.viewport.SetContent(m.renderDiff())`)
- [x] add `w` entry to help overlay under "View" section: `w toggle word wrap`
- [x] write tests for wrap toggle behavior
- [x] write tests verifying scroll keys are blocked when wrap is on
- [x] write test verifying help overlay contains word wrap key listing
- [x] run `make test` — must pass before task 6

### Task 6: Verify acceptance criteria
- [ ] verify `w` toggles wrap mode on/off
- [ ] verify long lines wrap with `↪` continuation markers
- [ ] verify continuation lines have correct background styles (add/remove/context)
- [ ] verify horizontal scroll is disabled in wrap mode
- [ ] verify cursor navigation works correctly with wrapped lines
- [ ] verify annotations appear after last continuation line
- [ ] verify wrap works in collapsed mode
- [ ] verify `--wrap` CLI flag works
- [ ] verify `↩` icon appears in status line mode icons when active
- [ ] run full test suite: `make test`
- [ ] run linter: `make lint`
- [x] verify `w` toggles wrap mode on/off
- [x] verify long lines wrap with `↪` continuation markers
- [x] verify continuation lines have correct background styles (add/remove/context)
- [x] verify horizontal scroll is disabled in wrap mode
- [x] verify cursor navigation works correctly with wrapped lines
- [x] verify annotations appear after last continuation line
- [x] verify wrap works in collapsed mode
- [x] verify `--wrap` CLI flag works
- [x] verify `↩` icon appears in status line mode icons when active
- [x] run full test suite: `make test`
- [x] run linter: `make lint`

### Task 7: [Final] Update documentation
- [ ] update README.md with `w` wrap toggle and `--wrap` flag
- [ ] update `.claude-plugin/skills/revdiff/references/usage.md` with wrap keybinding
- [ ] update `.claude-plugin/skills/revdiff/references/config.md` with wrap config option
- [ ] update CLAUDE.md if any new patterns discovered
- [ ] move this plan to `docs/plans/completed/`
- [x] update README.md with `w` wrap toggle and `--wrap` flag
- [x] update `.claude-plugin/skills/revdiff/references/usage.md` with wrap keybinding
- [x] update `.claude-plugin/skills/revdiff/references/config.md` with wrap config option
- [x] update CLAUDE.md if any new patterns discovered
- [x] move this plan to `docs/plans/completed/`

## Post-Completion

Expand Down
17 changes: 13 additions & 4 deletions ui/annotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,19 @@ func (m Model) cursorViewportY() int {
if m.isCollapsedHidden(i, hunks) {
continue
}
y++ // the diff line itself
// delete-only placeholders don't render annotations, skip counting them
// delete-only placeholders render synthetic text ("⋯ N lines deleted"), not original content.
// use placeholder text for wrapping to stay in sync with renderDeletePlaceholder.
if m.isDeleteOnlyPlaceholder(i, hunks) {
if m.wrapMode {
text := m.deletePlaceholderText(i)
wrapWidth := m.diffContentWidth() - wrapGutterWidth
y += len(m.wrapContent(text, wrapWidth))
} else {
y++ // placeholder is always 1 row when not wrapping
}
continue
}
y += m.wrappedLineCount(i) // the diff line (may occupy multiple visual rows when wrapping)
dl := m.diffLines[i]
if dl.ChangeType != diff.ChangeDivider {
key := m.annotationKey(m.diffLineNum(dl), string(dl.ChangeType))
Expand All @@ -262,9 +270,10 @@ func (m Model) cursorViewportY() int {
}
}
}
// if cursor is on the annotation sub-line, add one more row
// if cursor is on the annotation sub-line, offset by wrapped line count
// (annotation renders after all continuation lines of the diff line)
if m.cursorOnAnnotation {
y++
y += m.wrappedLineCount(m.diffCursor)
}
return y
}
Expand Down
Loading
Loading