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
3 changes: 2 additions & 1 deletion .claude-plugin/skills/revdiff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ which revdiff
```

If not found, guide installation:
- `go install github.com/umputun/revdiff/cmd/revdiff@latest`
- `brew install umputun/apps/revdiff`
- Binary releases: https://github.com/umputun/revdiff/releases

### Step 1: Determine Review Mode

Expand Down
5 changes: 0 additions & 5 deletions .claude-plugin/skills/revdiff/references/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
brew install umputun/apps/revdiff
```

**Go install:**
```bash
go install github.com/umputun/revdiff/cmd/revdiff@latest
```

**Binary releases:** download from [GitHub Releases](https://github.com/umputun/revdiff/releases) (deb, rpm, archives for linux/darwin amd64/arm64).

## Claude Code Plugin
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/skills/revdiff/scripts/launch-revdiff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ set -euo pipefail
REVDIFF_BIN=$(command -v revdiff 2>/dev/null || true)
if [ -z "$REVDIFF_BIN" ]; then
echo "error: revdiff not found in PATH" >&2
echo "install: go install github.com/umputun/revdiff/cmd/revdiff@latest" >&2
echo "install: brew install umputun/apps/revdiff (or download from https://github.com/umputun/revdiff/releases)" >&2
exit 1
fi

Expand Down
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2

builds:
- id: revdiff
main: ./cmd/revdiff
main: ./app
binary: revdiff
env:
- CGO_ENABLED=0
Expand Down
4 changes: 2 additions & 2 deletions .zed/tasks.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[
{
"label": "revdiff: run",
"command": "go run ./cmd/revdiff --dbg",
"command": "go run ./app --dbg",
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": true,
"allow_concurrent_runs": false
},
{
"label": "revdiff: run staged",
"command": "go run ./cmd/revdiff --staged --dbg",
"command": "go run ./app --staged --dbg",
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": true,
"allow_concurrent_runs": false
Expand Down
26 changes: 13 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ TUI for reviewing diffs, files, and documents with inline annotations, built wit
- Vendor after adding deps: `go mod vendor`

## Project Structure
- `cmd/revdiff/` - entry point, CLI flags, wiring
- `diff/` - git interaction, unified diff parsing (`ParseUnifiedDiff`, `DiffLine`)
- `ui/` - bubbletea TUI model, views, styles, file tree, annotations
- `highlight/` - chroma-based syntax highlighting, foreground-only ANSI output
- `keymap/` - user-configurable keybindings (`Action` constants, `Keymap` type, parser, defaults, dump)
- `theme/` - color theme system: Parse (with hex validation), Load, List, Dump, InitBundled, BundledNames, ColorKeys (bundled: dracula, nord, solarized-dark)
- `annotation/` - in-memory annotation store, structured output formatting; `Annotation.EndLine` enables hunk range headers when comment contains "hunk" keyword
- `ui/mocks/` - moq-generated mocks (never edit manually)
- `app/` - entry point (`main.go`), CLI flags, wiring
- `app/diff/` - git interaction, unified diff parsing (`ParseUnifiedDiff`, `DiffLine`)
- `app/ui/` - bubbletea TUI model, views, styles, file tree, annotations
- `app/highlight/` - chroma-based syntax highlighting, foreground-only ANSI output
- `app/keymap/` - user-configurable keybindings (`Action` constants, `Keymap` type, parser, defaults, dump)
- `app/theme/` - color theme system: Parse (with hex validation), Load, List, Dump, InitBundled, BundledNames, ColorKeys (bundled: dracula, nord, solarized-dark)
- `app/annotation/` - in-memory annotation store, structured output formatting; `Annotation.EndLine` enables hunk range headers when comment contains "hunk" keyword
- `app/ui/mocks/` - moq-generated mocks (never edit manually)

## Key Interfaces (consumer-side, in `ui/`)
## Key Interfaces (consumer-side, in `app/ui/`)
- `Renderer` - `ChangedFiles()`, `FileDiff()` - implemented by `diff.Git`, `diff.FallbackRenderer`, `diff.FileReader`, `diff.DirectoryReader`, `diff.StdinReader`, `diff.ExcludeFilter`
- `SyntaxHighlighter` - `HighlightLines()` - implemented by `highlight.Highlighter`

Expand Down Expand Up @@ -73,7 +73,7 @@ git diff → diff.ParseUnifiedDiff() → []DiffLine
- Keybindings file: `~/.config/revdiff/keybindings` (`map <key> <action>` / `unmap <key>` format)
- `--keys` overrides keybindings path, `--dump-keys` prints effective bindings
- `keymap.Keymap` passed to `Model` via `ModelConfig.Keymap`; handlers switch on `m.keymap.Resolve(msg.String())` instead of raw key strings
- ~30 `Action` constants in `keymap/keymap.go` (e.g., `ActionDown`, `ActionQuit`); modal text-entry keys (annotation input, search input, confirm discard) stay hardcoded; modal overlay navigation (annotation list, help) uses keymap for j/k/up/down but keeps `enter` and `esc` hardcoded
- ~30 `Action` constants in `app/keymap/keymap.go` (e.g., `ActionDown`, `ActionQuit`); modal text-entry keys (annotation input, search input, confirm discard) stay hardcoded; modal overlay navigation (annotation list, help) uses keymap for j/k/up/down but keeps `enter` and `esc` hardcoded
- Help overlay is dynamically rendered from `m.keymap.HelpSections()`

## Website
Expand Down Expand Up @@ -107,7 +107,7 @@ git diff → diff.ParseUnifiedDiff() → []DiffLine
- **Background fill for themed panes**: lipgloss pane `Render()` and viewport internal padding emit plain spaces after `\033[0m` reset, causing pane background to show terminal default. Three workarounds: (1) `extendLineBg()` pads individual add/remove/modify lines to full content width with their specific bg color; (2) `padContentBg()` strips viewport trailing spaces and re-pads every line of pane content with DiffBg/TreeBg; (3) `BorderBackground()` is set on pane border styles to match pane bg. Context and line-number styles also set DiffBg explicitly via `contextStyle()`/`lineNumberStyle()`/`contextHighlightStyle()`.
- Status bar mode icons (`▼ ◉ ↩ ≋ ⊟ # @`) are always rendered on the right side via `statusModeIcons()`. `@` indicates blame gutter active via `B` toggle. `⊟` indicates tree/TOC pane hidden via `t` toggle. Active modes use `StatusFg`, inactive use `Muted` — both via raw ANSI fg sequences. Graceful degradation on narrow terminals drops left segments: search position first (`statusSegmentsNoSearch`), then line number and hunk info (`statusSegmentsMinimal`), then truncates filename.
- Search and hunk navigation both use `centerViewportOnCursor()` to center the target in the middle of the viewport. Use `syncViewportToCursor()` only for cursor movements that should keep the cursor barely visible (j/k scrolling).
- Single-file mode (`m.singleFile`): when diff has exactly one file, tree pane is hidden, `treeWidth = 0`, diff gets full width (`m.width - 2` for borders, content width `m.width - 4` including right padding). Pane-switching keys (tab, h, l) and file navigation (n/p, f) become no-ops. Search nav (n/N) still works. Detection happens in `handleFilesLoaded`. Exception: when the file is markdown and full-context (all `ChangeContext` lines), an `mdTOC` pane replaces the tree pane with header navigation — see `ui/mdtoc.go`.
- Single-file mode (`m.singleFile`): when diff has exactly one file, tree pane is hidden, `treeWidth = 0`, diff gets full width (`m.width - 2` for borders, content width `m.width - 4` including right padding). Pane-switching keys (tab, h, l) and file navigation (n/p, f) become no-ops. Search nav (n/N) still works. Detection happens in `handleFilesLoaded`. Exception: when the file is markdown and full-context (all `ChangeContext` lines), an `mdTOC` pane replaces the tree pane with header navigation — see `app/ui/mdtoc.go`.
- Tree pane toggle (`t` key): `m.treeHidden` hides the tree/TOC pane and gives diff full width. Orthogonal to `singleFile` — sets `treeWidth = 0`, forces `focus = paneDiff`, blocks `togglePane()`/`handleSwitchToTree()`. `handleViewToggle()` dispatches `v`, `w`, `t`, and `L` keys. `handleFileLoaded` respects `treeHidden` when setting up mdTOC layout.
- Markdown TOC (`ui/mdtoc.go`): `mdTOC` component mirrors `fileTree` pattern (entries/cursor/offset/render). Activated in `handleFileLoaded` when `singleFile && isMarkdownFile && isFullContext`. Uses `paneTree` slot so `togglePane()` and key dispatch work unchanged. `handleTOCNav` routes j/k/pgdn/pgup/home/end to TOC cursor; Enter jumps to header line via `centerViewportOnCursor()`. `n/p` keys in diff pane jump to next/prev TOC entry via `jumpTOCEntry()`. `syncTOCActiveSection()` called on diff cursor movement to track current section. `syncTOCCursorToActive()` syncs cursor when switching back to TOC pane. `syncDiffToTOCCursor()` jumps diff viewport to current TOC cursor.
- Annotation list popup (`@` key): `ui/annotlist.go` — overlay listing all annotations across files. Navigation keys (j/k/up/down) routed through `m.keymap.Resolve()`, `enter` and `esc` hardcoded (modal overlay convention). Cross-file jumps use `pendingAnnotJump` field: stores target annotation, triggers file load via `selectByPath`, then `handleFileLoaded` checks and positions cursor. Guard: `pendingAnnotJump.File == msg.file` prevents stale jumps.
- Markdown TOC (`app/ui/mdtoc.go`): `mdTOC` component mirrors `fileTree` pattern (entries/cursor/offset/render). Activated in `handleFileLoaded` when `singleFile && isMarkdownFile && isFullContext`. Uses `paneTree` slot so `togglePane()` and key dispatch work unchanged. `handleTOCNav` routes j/k/pgdn/pgup/home/end to TOC cursor; Enter jumps to header line via `centerViewportOnCursor()`. `n/p` keys in diff pane jump to next/prev TOC entry via `jumpTOCEntry()`. `syncTOCActiveSection()` called on diff cursor movement to track current section. `syncTOCCursorToActive()` syncs cursor when switching back to TOC pane. `syncDiffToTOCCursor()` jumps diff viewport to current TOC cursor.
- Annotation list popup (`@` key): `app/ui/annotlist.go` — overlay listing all annotations across files. Navigation keys (j/k/up/down) routed through `m.keymap.Resolve()`, `enter` and `esc` hardcoded (modal overlay convention). Cross-file jumps use `pendingAnnotJump` field: stores target annotation, triggers file load via `selectByPath`, then `handleFileLoaded` checks and positions cursor. Guard: `pendingAnnotJump.File == msg.file` prevents stale jumps.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ REV=$(if $(filter --,$(GIT_REV)),latest,$(GIT_REV))
all: test build

build:

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make build writes the output binary to .bin/revdiff.$(BRANCH) but the .bin/ directory is not created anywhere in the Makefile. In a clean checkout this target will fail with “no such file or directory”. Consider adding a mkdir -p .bin step (or using $(shell mkdir -p .bin) via a dedicated prerequisite) before go build/cp.

Suggested change
build:
build:
mkdir -p .bin

Copilot uses AI. Check for mistakes.
cd cmd/revdiff && go build -ldflags "-X main.revision=$(REV) -s -w" -o ../../.bin/revdiff.$(BRANCH)
go build -ldflags "-X main.revision=$(REV) -s -w" -o .bin/revdiff.$(BRANCH) ./app
cp .bin/revdiff.$(BRANCH) .bin/revdiff

test:
Expand All @@ -33,6 +33,6 @@ version:
@echo "revision: $(REV)"

site:
cp llms.txt site/
@echo "site assets are in site/ directory"

.PHONY: build test lint fmt race version site
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ Built for a specific use case: reviewing code changes, plans, and documents with
brew install umputun/apps/revdiff
```

**Go install:**

```bash
go install github.com/umputun/revdiff/cmd/revdiff@latest
```

**Binary releases:** download from [GitHub Releases](https://github.com/umputun/revdiff/releases) (deb, rpm, archives for linux/darwin amd64/arm64).

## Claude Code Plugin
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion highlight/highlight.go → app/highlight/highlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

// Highlighter applies syntax highlighting to source code lines using Chroma.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

func TestHighlighter_HighlightLines(t *testing.T) {
Expand Down
File renamed without changes.
File renamed without changes.
12 changes: 6 additions & 6 deletions cmd/revdiff/main.go → app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/jessevdk/go-flags"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/highlight"
"github.com/umputun/revdiff/keymap"
"github.com/umputun/revdiff/theme"
"github.com/umputun/revdiff/ui"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/diff"
"github.com/umputun/revdiff/app/highlight"
"github.com/umputun/revdiff/app/keymap"
"github.com/umputun/revdiff/app/theme"
"github.com/umputun/revdiff/app/ui"
)

type options struct {
Expand Down
4 changes: 2 additions & 2 deletions cmd/revdiff/main_test.go → app/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/theme"
"github.com/umputun/revdiff/app/diff"
"github.com/umputun/revdiff/app/theme"
)

// noConfigArgs returns args that point to a nonexistent config file,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions ui/annotate.go → app/ui/annotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/diff"
)

// hunkKeywordRe matches whole-word "hunk" (case-insensitive).
Expand Down
4 changes: 2 additions & 2 deletions ui/annotlist.go → app/ui/annotlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/keymap"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/keymap"
)

// buildAnnotListItems builds a flat list of all annotations across all files.
Expand Down
6 changes: 3 additions & 3 deletions ui/annotlist_test.go → app/ui/annotlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/keymap"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/diff"
"github.com/umputun/revdiff/app/keymap"
)

func TestModel_BuildAnnotListItems(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion ui/collapsed.go → app/ui/collapsed.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/charmbracelet/lipgloss"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

// collapsedState holds the state for collapsed diff view mode.
Expand Down
4 changes: 2 additions & 2 deletions ui/collapsed_test.go → app/ui/collapsed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/diff"
)

func TestModel_VKeyTogglesCollapsedMode(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion ui/diffview.go → app/ui/diffview.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/charmbracelet/x/ansi"
"github.com/mattn/go-runewidth"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

// matchRange represents a range of visible character positions for search match highlighting.
Expand Down
2 changes: 1 addition & 1 deletion ui/diffview_test.go → app/ui/diffview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/charmbracelet/x/ansi"
"github.com/stretchr/testify/assert"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

func TestModel_LineNumGutter(t *testing.T) {
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion ui/mdtoc.go → app/ui/mdtoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"path/filepath"
"strings"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

// tocEntry represents a single markdown header in the table of contents.
Expand Down
2 changes: 1 addition & 1 deletion ui/mdtoc_test.go → app/ui/mdtoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/app/diff"
)

func TestParseTOC(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion ui/mocks/blamer.go → app/ui/mocks/blamer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/mocks/renderer.go → app/ui/mocks/renderer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions ui/model.go → app/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import (
"github.com/charmbracelet/x/ansi"
"github.com/mattn/go-runewidth"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/keymap"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/diff"
"github.com/umputun/revdiff/app/keymap"
)

// Renderer provides methods to extract changed files and build full-file diff views.
Expand Down
8 changes: 4 additions & 4 deletions ui/model_test.go → app/ui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/umputun/revdiff/annotation"
"github.com/umputun/revdiff/diff"
"github.com/umputun/revdiff/keymap"
"github.com/umputun/revdiff/ui/mocks"
"github.com/umputun/revdiff/app/annotation"
"github.com/umputun/revdiff/app/diff"
"github.com/umputun/revdiff/app/keymap"
"github.com/umputun/revdiff/app/ui/mocks"
)

func noopHighlighter() *mocks.SyntaxHighlighterMock {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading