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/pane-current-line-address.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Give opted-in extension panes the current line's `{ side, line }` source address on `currentLine`, matching command selection, so a pane can follow the cursor without waiting for a keypress.
5 changes: 3 additions & 2 deletions docs/extension-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ the planner resolves it to an integer target before applying bounds and lets a
session-local divider drag override that automatic size.

`src/ui/components/panes/ExtensionPane.tsx` mounts panes with guarded actions and
failure containment. `DiffPane` exposes optional current-line paint without
publishing Pierre rows, plans, cursor keys, or caches. Deprecated sidebar APIs
failure containment. `DiffPane` exposes optional current-line paint — the row
painter plus the public `{ side, line }` address — without publishing Pierre
rows, plans, cursor keys, or caches. Deprecated sidebar APIs
normalize into this same registry and layout path.

## File-view system
Expand Down
20 changes: 12 additions & 8 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,10 @@ new instances and run that shutdown/startup pair around the replacement.

### `hunk.apiVersion`

The API generation this Hunk speaks (currently `14`). Generation 14 adds structured `rangeEndpoints`
to two-revision VCS diff requests. Branch on it if you want
one file to support several Hunk versions. Version 13 adds saved-note parent identities and
The API generation this Hunk speaks (currently `15`). Branch on it if you want
one file to support several Hunk versions. Version 15 adds `{ side, line }` to
opted-in pane `currentLine` paint; version 14 added structured `rangeEndpoints`
to two-revision VCS diff requests; version 13 added saved-note parent identities and
committed note-edit events; version 12 adds responsive fractional pane sizing; version 11 added
the `"dim"` line-highlight tone; version 10 added generic top-level CLI commands; version 9
added exact-filename and glob selectors to `registerFileLanguage`; version 8
Expand Down Expand Up @@ -753,10 +754,13 @@ panes keep their own open state. User remaps and unbindings of
`hunk.view.toggleFilesPane` apply to the resolved slot in the usual way. The
former `hunk.view.toggleSidebar` id remains a compatibility alias.

`currentLine: true` opts into the opaque `currentLine.render(side, width)`
painter. The installable [Hunk Lens](https://github.com/modem-dev/hunk-lens)
extension uses this API; it is not bundled Hunk UI. Install it with
`hunk extension install modem-dev/hunk-lens`.
`currentLine: true` opts into the selected-row painter. `currentLine.render(side, width)`
paints one side as a clipped row; `currentLine.side` and `currentLine.line` are
the same public source address command handlers see on `ctx.selection.currentLine`
(context rows use Hunk's canonical new-side). The installable
[Hunk Lens](https://github.com/modem-dev/hunk-lens) extension uses the painter; a
blame or diagnostic pane can use the address without waiting for a keypress.
Install the lens with `hunk extension install modem-dev/hunk-lens`.

Import `react` normally — Hunk serves its own React instance to extension files
at import time, so hooks, context, and JSX all run on the reconciler drawing the
Expand All @@ -775,7 +779,7 @@ The component receives fresh props as the app changes:
| `placement` | the accepted terminal edge |
| `width` | exact terminal columns in the host-owned rectangle |
| `height` | exact terminal rows in the host-owned rectangle |
| `currentLine` | opaque selected-row painter when the registration opts in, otherwise `null` |
| `currentLine` | selected-row painter plus `{ side, line }` when the registration opts in, otherwise `null` |
| `theme` | hex color tokens from the active theme, updated on theme switch |
| `keybindings` | the current command bindings, resolved from defaults and the user's `[keybindings]` table |
| `actions` | navigation and notifications the pane may trigger |
Expand Down
1 change: 1 addition & 0 deletions scripts/check-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default function (hunk: HunkExtensionAPI) {
const pane = (props: ExtensionPaneProps) => {
hunk.log(\`\${props.placement}:\${props.width}x\${props.height}\`);
props.currentLine?.render("new", props.width);
hunk.log(props.currentLine ? props.currentLine.side + ":" + props.currentLine.line : "no line");
return null;
};
const paneSize: ExtensionPaneSize = { preferred: 3, min: 2, max: 4, fraction: 0.25 };
Expand Down
4 changes: 2 additions & 2 deletions skills/hunk-extensions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ bad or duplicate id is skipped with a startup notice.
| Coordinate with another loaded extension | `hunk.events.emit` / `hunk.events.on` |
| Read user-supplied settings | `hunk.config` (`[extension.<id>]` table) |
| Snapshot stable files and every saved review note | `ctx.review.snapshot()` in a command |
| Branch on the API generation (currently `14`) | `hunk.apiVersion` |
| Branch on the API generation (currently `15`) | `hunk.apiVersion` |

Registration is only valid while the factory runs — Hunk seals the API object
afterwards.
Expand Down Expand Up @@ -163,7 +163,7 @@ transform — gets `ctx.cwd` and `ctx.notify(message, type?)`. A file view's
`ctx.dialogs` (`confirm`/`select`/`input`, queued and attributed), and
`ctx.workspace` (`readDocument`, `canWriteDocument`, `writeDocument` with consent).
- **Pane components** get frozen `files`, selection, placement, exact dimensions,
optional `currentLine` paint, semantic `theme`, resolved `keybindings`, and
optional `currentLine` paint (with `{ side, line }` when opted in), semantic `theme`, resolved `keybindings`, and
guarded navigation/notification `actions`.
- **File-view `layout`** gets `file`, `width`, `signal`, `changes`, and a lazy
`readDocument(side)`.
Expand Down
28 changes: 24 additions & 4 deletions src/extension-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* Extensions can branch on `hunk.apiVersion` so a newer Hunk can keep loading
* older extensions without guessing at their expectations.
*/
export const HUNK_EXTENSION_API_VERSION = 14;
export const HUNK_EXTENSION_API_VERSION = 15;
export type HunkExtensionApiVersion = typeof HUNK_EXTENSION_API_VERSION;

export type ExtensionNotifyType = "info" | "warning" | "error";
Expand Down Expand Up @@ -1118,8 +1118,25 @@ export interface ExtensionPaneSize {
fraction?: number;
}

/** Opaque host renderer for the selected split row. */
/**
* Host renderer for the selected split row, plus the source address that row
* occupies.
*
* `render` is still the only way to paint the row: it does not publish Pierre
* rows, plans, or cursor keys. `side` and `line` are the same public address
* command handlers already see on `ctx.selection.currentLine`, so a pane can
* look up blame, diagnostics, or notes without waiting for a keypress.
*/
export interface ExtensionCurrentLinePaint {
/**
* Which side the current-line marker addresses.
*
* Context rows use Hunk's canonical new-side address, matching
* `ctx.selection.currentLine` and `navigation.revealLine`.
*/
readonly side: ExtensionFileSide;
/** 1-based source line number on `side`. */
readonly line: number;
/** Paint one side as a clipped, no-wrap terminal row. */
render(side: "old" | "new", width: number): unknown;
}
Expand All @@ -1145,7 +1162,10 @@ export interface ExtensionPaneProps {
readonly theme: ExtensionPaneTheme;
readonly keybindings: ExtensionPaneKeybindings;
readonly actions: ExtensionPaneActions;
/** Non-null only when the registration explicitly requested current-line paint. */
/**
* Selected-row painter plus `{ side, line }` when the registration opted in
* with `currentLine: true`; otherwise `null`.
*/
readonly currentLine: ExtensionCurrentLinePaint | null;
}

Expand All @@ -1168,7 +1188,7 @@ interface ExtensionPaneBase {
* pane registered for a named target owns its slot; later claims are skipped.
*/
replaces?: string;
/** Opt into live current-line paint; unrelated panes receive stable null. */
/** Opt into live current-line paint and `{ side, line }`; unrelated panes receive stable null. */
currentLine?: boolean;
/** Synchronous frame-availability policy. */
available?(context: ExtensionPaneAvailabilityContext): boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/ui/hooks/useExtensionPaneController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ describe("useExtensionPaneController", () => {
harness.current().paneLayout.panes.some(({ pane }) => pane.key === "meta:line"),
).toBeFalse();

const paint = { render: () => null };
const paint = { side: "new" as const, line: 1, render: () => null };
await act(async () => {
harness.setCurrentLineCursor({ fileId: "file", stableKey: "cursor" });
harness.current().onCurrentLinePaintChange({
Expand Down
10 changes: 6 additions & 4 deletions src/ui/lib/extensionCurrentLine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function splitPlanFixture() {
}

describe("extension current-line paint", () => {
test("exposes only an opaque painter backed by the accepted row plan", () => {
test("exposes a painter and the cursor's public source address", () => {
const fixture = splitPlanFixture();
const paint = createExtensionCurrentLinePaint({
...fixture,
Expand All @@ -50,7 +50,9 @@ describe("extension current-line paint", () => {
});

expect(paint).not.toBeNull();
expect(Object.keys(paint!)).toEqual(["render"]);
expect(Object.keys(paint!)).toEqual(["side", "line", "render"]);
expect(paint!.side).toBe(fixture.cursor.target.side);
expect(paint!.line).toBe(fixture.cursor.target.line);
const oldPaint = paint!.render("old", 60) as {
props: {
row: { cell: Record<string, unknown> };
Expand Down Expand Up @@ -110,7 +112,7 @@ describe("extension current-line paint", () => {
});

test("withholds stale paint while a new plan is pending", () => {
const paint = { render: () => null };
const paint = { side: "new" as const, line: 1, render: () => null };
const ready = applyExtensionCurrentLinePaintUpdate(
{ status: "unavailable", fileId: null, cursorKey: null, paint: null },
{ status: "ready", fileId: "alpha", cursorKey: "row:1", paint },
Expand All @@ -134,7 +136,7 @@ describe("extension current-line paint", () => {
});

test("clears accepted paint when the current-line capability becomes unavailable", () => {
const paint = { render: () => null };
const paint = { side: "new" as const, line: 1, render: () => null };
const unavailable = applyExtensionCurrentLinePaintUpdate(
{ status: "ready", fileId: "alpha", cursorKey: "row:1", paint },
{ status: "unavailable" },
Expand Down
4 changes: 3 additions & 1 deletion src/ui/lib/extensionCurrentLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function stackRow(row: SplitLineRow, cell: SplitLineCell, side: "old" | "new"):
};
}

/** Build an opaque public painter from the exact accepted private row plan. */
/** Build the public current-line painter and source address from the accepted row plan. */
export function createExtensionCurrentLinePaint({
cursor,
rowPlan,
Expand Down Expand Up @@ -100,6 +100,8 @@ export function createExtensionCurrentLinePaint({
new: stackRow(splitRow, splitRow.right, "new"),
};
return Object.freeze({
side: cursor.target.side,
line: cursor.target.line,
render(side: "old" | "new", width: number) {
return (
<DiffRowView
Expand Down
2 changes: 1 addition & 1 deletion src/ui/lib/extensionPanes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe("extension panes", () => {
expect(availabilityCalls).toBe(1);

available = true;
const paint = { render: () => null };
const paint = { side: "new" as const, line: 1, render: () => null };
const restored = probeExtensionPaneAvailability({ panes, context, currentLine: paint });
expect(restored.available.has(registered)).toBeTrue();
expect(planExtensionPanes({ ...geometry, openKeys: ["a:detail"] }).panes).toHaveLength(1);
Expand Down
4 changes: 2 additions & 2 deletions website/src/content/docs/docs/extend/custom-sidebars.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Use `defaultOpen` to open a pane initially, `replaces: "hunk:files"` to replace

`hunk:files` is a named role, not a left-edge location. The `hunk.view.toggleFilesPane` command (`s` by default) and **View → Files pane** follow the resolved owner of that slot on any edge and leave independently registered panes alone. User remaps and unbindings apply to that command normally; the former `hunk.view.toggleSidebar` id remains a compatibility alias. `ctx.panes.toggle("hunk:files")`, by contrast, addresses the literal built-in pane; use `ctx.commands.execute("hunk.view.toggleFilesPane")` when an extension wants the role-aware slot behavior.

Set `currentLine: true` to receive Hunk's opaque selected-row painter. The external [Hunk Lens](https://github.com/modem-dev/hunk-lens) extension uses it and is not bundled with Hunk. Install it with `hunk extension install modem-dev/hunk-lens`.
Set `currentLine: true` to receive Hunk's selected-row painter plus the `{ side, line }` source address of the current-line marker — the same address command handlers see on `ctx.selection.currentLine`. The external [Hunk Lens](https://github.com/modem-dev/hunk-lens) extension uses the painter; a blame or diagnostic pane can use the address. It is not bundled with Hunk. Install the lens with `hunk extension install modem-dev/hunk-lens`.

API-v3 sidebar names remain as deprecated aliases.

Expand All @@ -69,7 +69,7 @@ The component receives fresh props as the app changes:
| `placement` | the accepted terminal edge |
| `width` | exact terminal columns in the host-owned rectangle |
| `height` | exact terminal rows in the host-owned rectangle |
| `currentLine` | opaque selected-row painter when the registration opts in, otherwise `null` |
| `currentLine` | selected-row painter plus `{ side, line }` when the registration opts in, otherwise `null` |
| `theme` | hex color tokens from the active theme, updated on theme switch |
| `keybindings` | the current command bindings, resolved from defaults and the user's `[keybindings]` table |
| `actions` | guarded navigation and notifications the pane may trigger |
Expand Down
11 changes: 7 additions & 4 deletions website/src/content/docs/docs/extend/extension-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ The extension factory receives one API object. Registration calls are only valid

## `hunk.apiVersion`

The API generation this Hunk speaks (currently `14`). Branch on it if you want
one file to support several Hunk versions. Version 14 adds structured two-revision
VCS diff endpoints; version 12 added responsive fractional pane sizing; version 11 added the `dim` line-highlight tone; version 10 added
The API generation this Hunk speaks (currently `15`). Branch on it if you want
one file to support several Hunk versions. Version 15 adds `{ side, line }` to
opted-in pane `currentLine` paint; version 14 added structured two-revision
VCS diff endpoints; version 13 added saved-note parent identities
and committed note-edit events; version 12 added responsive fractional pane
sizing; version 11 added the `dim` line-highlight tone; version 10 added
generic top-level CLI commands; version 9
added exact-filename and glob selectors to `registerFileLanguage`; version 8
added authoritative review snapshots to command handlers; version 7 added the
Expand Down Expand Up @@ -128,7 +131,7 @@ Full contract: [VCS adapters](/docs/extend/vcs-adapters/).

## `hunk.registerPane(pane)`

Render a React component on the `left`, `right`, `top`, or `bottom` of the review. Panes receive their dimensions, review state, actions, keybindings, and optional current-line paint. `registerSidebarView` remains a deprecated alias.
Render a React component on the `left`, `right`, `top`, or `bottom` of the review. Panes receive their dimensions, review state, actions, keybindings, and optional current-line paint (including `{ side, line }` when opted in). `registerSidebarView` remains a deprecated alias.

Full contract: [Custom panes](/docs/extend/custom-sidebars/).

Expand Down
Loading