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
5 changes: 5 additions & 0 deletions .changeset/reveal-line-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Extensions can jump the review to one exact source line with `ctx.navigation.revealLine(fileId, side, line)` (API v5), so a target deep inside a tall hunk lands near the top of the viewport instead of pages below its anchor.
11 changes: 11 additions & 0 deletions docs/extension-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ chord at a time and detected by probing matchers with a synthesized event
`src/ui/lib/extensionSelection.ts`, derived from the same frozen file views the
panes render. App reads it through a ref so the dispatch table stays stable.

`src/ui/lib/extensionNavigation.ts` mints the guarded navigation behind both
`ctx.navigation` and a pane's `actions`, so a jump from either surface is
validated, attributed, and reported the same way. It owns argument policy only
— visible-file validation, hunk clamping, `revealLine`'s side and line-number
checks — and delegates the move itself to the review controller. Where a jump
puts a line on screen stays host policy: `useReviewController` tags each
current-line reveal with a placement, and `DiffPane` reads it to choose between
stepping's minimum-distance scroll and the top-padded position hunk, note, and
`revealLine` reveals share. Extensions name a target; they never name a scroll
position.

`ctx.dialogs` is the one place extension code can interrupt the user, so its
ordering and settlement live outside React in
`src/ui/lib/extensionDialogs.ts` — one FIFO queue per App instance, minting a
Expand Down
74 changes: 56 additions & 18 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,9 @@ new instances and run that shutdown/startup pair around the replacement.
### `hunk.apiVersion`

The API generation this Hunk speaks (currently `5`). Version 5 adds line
highlighters; version 4 added keyboard modes and docked panes, with API-v3
sidebar names remaining as deprecated aliases.
highlighters and line-granular navigation (`revealLine`); version 4 added
keyboard modes and docked panes, with API-v3 sidebar names remaining as
deprecated aliases.

### `hunk.registerTheme(theme)`

Expand Down Expand Up @@ -648,13 +649,16 @@ The component receives fresh props as the app changes:
API-v3 sidebar names remain as deprecated aliases: use `registerPane`,
`ExtensionPane*`, `ctx.panes`, and `replaces: "hunk:files"` in new code.

`actions.selectFile(fileId)` and `actions.selectHunk(fileId, hunkIndex)` route
through the same review controller as the built-in files pane and the keyboard
shortcuts, so the review stream scrolls, selection updates, and the
`selection_changed` event fires exactly as if the user had clicked a built-in
row. `actions.notify(message, type?)` shows a toast attributed to your
extension. An action given a file id that is not currently visible is refused
with a warning rather than corrupting the selection.
`actions.selectFile(fileId)`, `actions.selectHunk(fileId, hunkIndex)`, and
`actions.revealLine(fileId, side, line)` route through the same review
controller as the built-in files pane and the keyboard shortcuts, so the review
stream scrolls, selection updates, and the `selection_changed` event fires
exactly as if the user had clicked a built-in row. `actions.notify(message,
type?)` shows a toast attributed to your extension. An action given a file id
that is not currently visible is refused with a warning rather than corrupting
the selection. A pane's `actions` carry the same navigation methods a command
handler's [`ctx.navigation`](#navigating-the-review) does, with the same
guarantees.

The three hunk surfaces line up by design: each file's `hunks` lists public
`ExtensionDiffHunk` summaries (`index`, the `@@` header, inclusive old/new
Expand Down Expand Up @@ -1358,15 +1362,49 @@ session keyboard modes. See [Session keyboard modes](#session-keyboard-modes).
`{ fileId }`-scoped. See
[`hunk.registerLineHighlighter`](#hunkregisterlinehighlighterhighlighter).

`ctx.navigation` moves the review stream: `selectFile(fileId)` and
`selectHunk(fileId, hunkIndex)`, the same guarded navigation a pane's
`actions` carry, routed through the same review controller — the stream
scrolls, selection updates, and `selection_changed` fires exactly as if the
user had clicked a pane row. Unlike `selection` it is live, not a snapshot:
a call acts on the review as it is at that moment, so a handler that awaits a
dialog and then navigates still works. A file id the stream cannot currently
show is refused with a warning rather than corrupting the selection, and a
hunk index is clamped into the file's real range.
#### Navigating the review

`ctx.navigation` moves the review stream: `selectFile(fileId)`,
`selectHunk(fileId, hunkIndex)`, and `revealLine(fileId, side, line)`, the same
guarded navigation a pane's `actions` carry, routed through the same review
controller — the stream scrolls, selection updates, and `selection_changed`
fires exactly as if the user had clicked a pane row. Unlike `selection` it is
live, not a snapshot: a call acts on the review as it is at that moment, so a
handler that awaits a dialog and then navigates still works. A file id the
stream cannot currently show is refused with a warning rather than corrupting
the selection, and a hunk index is clamped into the file's real range.

`revealLine` is the finest target there is, and the one to reach for when your
extension knows exactly which line it means — a search hit, a lint finding, the
line a mark from [`registerLineHighlighter`](#hunkregisterlinehighlighterhighlighter)
sits on. A hunk hundreds of lines tall has one anchor, so `selectHunk` can leave
the line you meant pages below the viewport; `revealLine` scrolls to the line
itself, lands it a little below the viewport top like every other Hunk reveal,
and makes it the current line so the reverse-video marker sits on it.

`line` is 1-based on `side` as the patch numbers it, so a context line answers
to either side's number. Two things soften the target rather than failing it:
when no rendered row carries that line — it is inside a collapsed gap, absent
from a partial patch, or the reviewer turned the current-line marker off
(`view.cursor_line = "off"`) — the jump lands on the hunk containing the line
instead. Only a line no hunk of the file covers is refused, with a warning
naming your extension, and so are a side outside `"old"`/`"new"` and a line
number that is not a positive whole number.

```ts
hunk.registerCommand({ id: "first-todo", title: "Jump to the first TODO" }, async (ctx) => {
const file = ctx.selection.file;
if (!file) {
return;
}

const document = await ctx.workspace.readDocument(file.id, "new");
const index = (document ?? "").split("\n").findIndex((line) => line.includes("TODO"));
if (index >= 0) {
ctx.navigation.revealLine(file.id, "new", index + 1);
}
});
```

A handler may be async; a failure (sync or rejected) becomes a warning naming
your extension.
Expand Down
Loading
Loading