Skip to content

Commit 40cfe8b

Browse files
update doc
1 parent 86ef653 commit 40cfe8b

4 files changed

Lines changed: 198 additions & 121 deletions

File tree

‎README.md‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ pnpm add @git-diff-view/vue
5050
# Solid / Svelte
5151
pnpm add @git-diff-view/solid
5252
pnpm add @git-diff-view/svelte
53+
54+
# CLI (terminal / Ink)
55+
pnpm add @git-diff-view/cli
5356
```
5457

5558
**[View Examples](https://github.com/MrWangJustToDo/git-diff-view/tree/main/ui)** for React / Vue / Solid / Svelte
@@ -94,14 +97,40 @@ file.buildSplitDiffLines();
9497

9598
**More examples:** [React](https://github.com/MrWangJustToDo/git-diff-view/tree/main/ui/react-example) · [Vue](https://github.com/MrWangJustToDo/git-diff-view/tree/main/ui/vue-example) · [Solid](https://github.com/MrWangJustToDo/git-diff-view/tree/main/ui/solid-example)
9699

100+
### CLI (Terminal)
101+
102+
Terminal-based diff viewer built with [Ink](https://github.com/vadimdemedes/ink). Supports Split/Unified views, syntax highlighting, and fixed-height viewport scrolling.
103+
104+
```bash
105+
pnpm add @git-diff-view/cli
106+
```
107+
108+
```tsx
109+
import { DiffView, DiffModeEnum } from "@git-diff-view/cli";
110+
111+
<DiffView
112+
data={{ oldFile, newFile, hunks }}
113+
diffViewMode={DiffModeEnum.Split}
114+
diffViewTheme="dark"
115+
diffViewHighlight
116+
width={120}
117+
height={20} // viewport height in visual rows (optional)
118+
onScrollChange={console.log}
119+
/>
120+
```
121+
122+
`CodeView` renders a single file; both components expose scroll ref methods (`scrollToTop`, `scrollDown`, `getScrollState`, etc.) when `height` is set.
123+
124+
**Docs:** [`packages/cli/readme.md`](./packages/cli/readme.md) · [Scroll API](./packages/cli/readme.md#scroll-api) · [Implementation notes](./packages/cli/SCROLL.md)
125+
97126
## 📚 Packages
98127

99128
**UI Frameworks**
100129
- [`@git-diff-view/react`](https://www.npmjs.com/package/@git-diff-view/react) - React component
101130
- [`@git-diff-view/vue`](https://www.npmjs.com/package/@git-diff-view/vue) - Vue component
102131
- [`@git-diff-view/solid`](https://www.npmjs.com/package/@git-diff-view/solid) - Solid component
103132
- [`@git-diff-view/svelte`](https://www.npmjs.com/package/@git-diff-view/svelte) - Svelte component
104-
- [`@git-diff-view/cli`](https://www.npmjs.com/package/@git-diff-view/cli) - CLI tool
133+
- [`@git-diff-view/cli`](https://www.npmjs.com/package/@git-diff-view/cli) - Terminal diff viewer (`DiffView`, `CodeView`, scroll API)
105134

106135
**Core Libraries**
107136
- [`@git-diff-view/core`](https://www.npmjs.com/package/@git-diff-view/core) - Core diff engine (git diff)
@@ -123,6 +152,7 @@ file.buildSplitDiffLines();
123152
- ✅ **SSR & RSC** - Full server-side rendering support for React and Vue
124153
- ✅ **Diff Match Patch** - Enhanced line-level diff with FastDiff template
125154
- ✅ **Multiple Platforms** - React, Vue, Solid, Svelte, and CLI
155+
- ✅ **CLI Scroll Viewport** - Fixed-height terminal viewport with programmatic scroll (`@git-diff-view/cli`)
126156

127157
## ⚡ Advanced Features
128158

@@ -230,7 +260,14 @@ git clone https://github.com/MrWangJustToDo/git-diff-view.git
230260
cd git-diff-view
231261
pnpm install
232262
pnpm run build:packages
233-
pnpm run dev:react # or dev:vue, dev:solid, dev:svelte
263+
pnpm run dev:react # or dev:vue, dev:solid, dev:svelte, dev:cli
264+
```
265+
266+
**CLI scroll tests** (after `build:packages`):
267+
268+
```bash
269+
pnpm --filter @git-diff-view/cli test:scroll
270+
pnpm --filter @git-diff-view/cli test:scroll:interactive:diffview
234271
```
235272

236273
## 📄 License

‎packages/cli/SCROLL.md‎

Lines changed: 60 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ Shared scroll types and utilities live in `src/components/scroll.ts`.
44

55
## Status
66

7-
| Component | Scroll API | Layout builder | Viewport slice |
8-
|-----------|------------|----------------|----------------|
9-
| **CodeView** | ✅ Implemented | `buildCodeViewLayout` | ✅ |
10-
| **DiffView** | ✅ Implemented | `iterateDiffDisplayEntries` → `buildDiffViewScrollLayout` | ✅ `DiffDisplayList` virtual slice |
7+
| Component | Scroll API | Layout | Rendering |
8+
|-----------|------------|--------|-----------|
9+
| **CodeView** | ✅ | `buildCodeViewLayout` | Single sliced ANSI `<Text>` |
10+
| **DiffView** | ✅ | `iterateDiffDisplayEntries` → `buildDiffViewScrollLayout` | `DiffDisplayList` virtual slice |
1111

1212
---
1313

@@ -26,12 +26,12 @@ A logical line may span multiple visual rows when content exceeds `contentWidth`
2626

2727
```typescript
2828
type ScrollState = {
29-
totalLines: number; // logical line count
30-
totalRows: number; // visual row count after wrap
31-
viewportHeight: number; // fixed height prop, or totalRows when unset
32-
scrollOffset: number; // top visual row (0-based)
33-
startLine: number; // first visible logical line (includes clipped lines)
34-
endLine: number; // last visible logical line (includes clipped lines)
29+
totalLines: number;
30+
totalRows: number;
31+
viewportHeight: number;
32+
scrollOffset: number;
33+
startLine: number;
34+
endLine: number;
3535
canScrollUp: boolean;
3636
canScrollDown: boolean;
3737
};
@@ -41,32 +41,12 @@ type ScrollState = {
4141

4242
---
4343

44-
## CodeView (done)
44+
## CodeView
4545

4646
### Line semantics
4747

4848
- `line` = source file line number (`File.rawFile`, 1..`rawLength`).
49-
- Layout built in `buildCodeViewLayout()` alongside existing ANSI rendering.
50-
51-
### Props
52-
53-
| Prop | Type | Description |
54-
|------|------|-------------|
55-
| `height` | `number` | Viewport height in visual rows |
56-
| `onScrollChange` | `(state: ScrollState) => void` | Fires when scroll state changes |
57-
58-
### Ref (`CodeViewRef`)
59-
60-
Extends `ScrollViewRef`:
61-
62-
| Method | Description |
63-
|--------|-------------|
64-
| `getScrollState()` | Current snapshot |
65-
| `scrollToTop(line)` | Align logical line's first visual row to viewport top |
66-
| `scrollToBottom(line)` | Align logical line's last visual row to viewport bottom |
67-
| `scrollUp({ unit?, step? })` | Scroll up one visual or logical step |
68-
| `scrollDown({ unit?, step? })` | Scroll down one visual or logical step |
69-
| `getFileInstance()` | Existing API |
49+
- Layout built in `buildCodeViewLayout()` alongside ANSI rendering.
7050

7151
### Rendering
7252

@@ -76,96 +56,71 @@ When `height` is set:
7656
2. `useScrollView` maintains `scrollOffset` and slices `layout.rows`.
7757
3. Render sliced ANSI string in `<Box height={height} overflow="hidden">`.
7858

79-
When `height` is unset, all rows render (backward compatible). DiffView skips `buildDiffViewScrollLayout` entirely in that case (uses `EMPTY_SCROLL_LAYOUT`); only the original full render path runs.
80-
81-
### Width / layout changes
82-
83-
When render width (`columns`) changes, wrap counts change and row indices no longer match the previous layout. Both CodeView and DiffView pass a `resetKey` into `useScrollView` so **scroll offset resets to 0** on width change. DiffView also includes `mode` in the reset key (split ↔ unified).
59+
When `height` is unset, all rows render.
8460

8561
---
8662

87-
## DiffView (TODO)
88-
89-
### Line semantics (方案 1 — confirmed)
90-
91-
`line` = **1-based index in the flattened display sequence**, including:
92-
93-
1. **Hunk line** (when `index !== 0`) — always 1 visual row
94-
2. **Content line** — 1+ visual rows (wrap)
95-
3. **Extend line** — height from `renderExtendLine` (0 if not rendered)
96-
97-
Both **Split** and **Unified** modes use the same scroll sequence shape; only the content of each entry differs.
63+
## DiffView
9864

99-
Example unified sequence for 3 diff entries:
65+
### Architecture
10066

10167
```
102-
line 1: ContentLine(index=0)
103-
line 2: HunkLine + ContentLine + ExtendLine(index=1)
104-
line 3: HunkLine + ContentLine + ExtendLine(index=2)
68+
iterateDiffDisplayEntries() ← single source of display sequence
69+
├── buildDiffViewScrollLayout() (row counts for scroll)
70+
└── DiffDisplayList() (render all or visible slice)
71+
└── DiffDisplayEntry() (hunk / content / extend)
10572
```
10673

107-
Each numbered item above is one **scroll logical line** with its own `startRow`/`endRow`.
74+
`DiffUnifiedView` / `DiffSplitView` / `DiffScrollEntryView` were merged into `DiffDisplayList` + `DiffDisplayEntry`.
10875

109-
### Implementation checklist
110-
111-
- [ ] **`buildDiffViewLayout(diffFile, mode, columns, context)`**
112-
- Walk `getUnifiedContentLine` / `getSplitContentLines` same as views.
113-
- For each index, append hunk (if any), content, extend entries to `lines[]`.
114-
- Compute visual row count per entry:
115-
- Hunk: `1`
116-
- Content: reuse `getCurrentLineRow` / `splitCharsIntoRows` (must match `DiffContent` wrap width)
117-
- Extend: measure rendered extend node height (may need ref or pre-render estimate)
118-
- Store pre-rendered ANSI rows OR React node keys for slice rendering.
119-
120-
- [ ] **Rendering strategy**
121-
122-
DiffView currently renders per-line React components, not a single ANSI string. Two options:
76+
### Line semantics
12377

124-
| Option | Pros | Cons |
125-
|--------|------|------|
126-
| **A. Virtual slice** | Keeps component structure | Only mount visible lines; extend/hunk logic stays in components |
127-
| **B. ANSI layout** | Same as CodeView | Hard for extend lines + interactive extend UI |
78+
`line` = **1-based index in the flattened display sequence**:
12879

129-
**Recommended: Option A — virtual slice**
80+
1. **Hunk line** (when `mapIndex !== 0` and hunk should show) — 1 visual row
81+
2. **Content line** — 1+ visual rows (wrap)
82+
3. **Extend line** — `diffViewExtendLineHeight` visual rows (when `renderExtendLine` + `extendData` present)
13083

131-
```
132-
visibleLines = layout.lines.slice(startLine - 1, endLine)
133-
render only those Diff*Line components
134-
```
84+
The **first content block never has a leading hunk line** (matches pre-scroll rendering).
13585

136-
Wrap in `<Box height={height} overflow="hidden">`.
86+
Example sequence for 3 diff entries:
13787

138-
- [ ] **Wire `useScrollView`** in `DiffViewContainerWithRef` or `InternalDiffView`.
88+
```
89+
line 1: Content(index=0)
90+
line 2: Hunk + Content + Extend(index=1)
91+
line 3: Hunk + Content + Extend(index=2)
92+
```
13993

140-
- [ ] **Replace stub** in `createDiffViewScrollStub` with real ref from `useScrollView`.
94+
### Rendering
14195

142-
- [ ] **`onScrollChange`** — same as CodeView via `useScrollView`.
96+
When `height` is set:
14397

144-
- [ ] **Extend line height**
98+
1. `buildDiffViewScrollLayout` walks `iterateDiffDisplayEntries` and records row offsets.
99+
2. `useScrollView` + `getVisibleDiffScrollLines` produce visible entries with optional `clip` (`ScrollSlice`).
100+
3. `DiffDisplayList` renders only visible entries inside `<Box height={height} overflow="hidden">`.
145101

146-
Extend lines are custom React nodes. Options:
147-
- Require `renderExtendLine` to return fixed-height content, or
148-
- Accept `extendLineHeight` prop for scroll calculation, or
149-
- Measure after first render and update layout (complex).
102+
When `height` is unset:
150103

151-
- [ ] **Dynamic updates**
104+
- `buildDiffViewScrollLayout` is **skipped** (`EMPTY_DIFF_VIEW_SCROLL_LAYOUT`).
105+
- `DiffDisplayList` iterates all entries via `iterateDiffDisplayEntries` (full render).
152106

153-
When `diffFile.notifyAll()` fires (syntax highlight, extend update):
154-
- Rebuild layout
155-
- Clamp `scrollOffset`
156-
- Optionally anchor to current `startLine`
107+
### Extend line height
157108

158-
- [ ] **Split vs Unified mode switch**
109+
Extend lines are custom React nodes. Scroll layout uses `diffViewExtendLineHeight` (default `1`). Top clipping for multi-row extend UI is approximate.
159110

160-
Reset scroll offset when mode changes.
111+
### Width / layout changes
161112

162-
### Stub behavior (current)
113+
`resetKey` (`columns:mode`) resets scroll offset to `0` when width or view mode changes.
163114

164-
Until implemented:
115+
### Exported utilities (`diffViewScroll.ts`)
165116

166-
- Ref scroll methods log `__DEV__` warning pointing to this doc.
167-
- `getScrollState()` returns empty layout with `totalLines: diffFile.splitLineLength` (approximate).
168-
- `onScrollChange` fires once on mount with stub state.
117+
| Export | Purpose |
118+
|--------|---------|
119+
| `iterateDiffDisplayEntries` | Walk hunk/content/extend display sequence |
120+
| `buildDiffViewScrollLayout` | Build scroll layout from iterator |
121+
| `getVisibleDiffScrollLines` | Map scroll offset → visible entries + clip |
122+
| `getDiffDisplayEntryRowCount` | Row count for one display entry |
123+
| `getUnifiedContentRowCount` / `getSplitContentRowCount` | Content wrap row counts |
169124

170125
---
171126

@@ -177,29 +132,19 @@ Until implemented:
177132
| `scrollOffsetToTopLine` | Target offset for `scrollToTop` |
178133
| `scrollOffsetToBottomLine` | Target offset for `scrollToBottom` |
179134
| `scrollOffsetUp` / `scrollOffsetDown` | Step scrolling |
180-
| `sliceVisibleRows` | Join visible ANSI rows |
135+
| `sliceVisibleRows` | Join visible ANSI rows (CodeView) |
181136
| `useScrollView` | Hook: state + ref API + `onScrollChange` |
182137

183138
---
184139

185-
## Testing (manual)
140+
## Testing
186141

187-
CodeView:
142+
Build first: `pnpm run build:packages`
188143

189-
```tsx
190-
const ref = useRef<CodeViewRef>(null);
191-
192-
<CodeView ref={ref} height={20} file={file} onScrollChange={console.log} />;
193-
194-
ref.current?.scrollToTop(100);
195-
ref.current?.scrollDown({ unit: "logical" });
196-
console.log(ref.current?.getScrollState());
144+
```bash
145+
pnpm --filter @git-diff-view/cli test:scroll
146+
pnpm --filter @git-diff-view/cli test:scroll:interactive
147+
pnpm --filter @git-diff-view/cli test:scroll:interactive:diffview
197148
```
198149

199-
Verify:
200-
201-
- [ ] Long line wrap: `startLine` stays on logical line when top segment clipped
202-
- [ ] `scrollToBottom` on wrapped line shows last segment at bottom
203-
- [ ] `scrollUp/Down` with `unit: "logical"` vs `"visual"`
204-
- [ ] Width change reclamps offset
205-
- [ ] Without `height`, full content visible, scroll still updates offset
150+
See `test/scroll/README.md` for coverage details.

0 commit comments

Comments
 (0)