feat(tui): add diff hunk navigation (#30935)
This commit is contained in:
parent
edbe2280a5
commit
7a4d18390a
3 changed files with 205 additions and 16 deletions
|
|
@ -3,6 +3,7 @@ import { expect, test } from "bun:test"
|
|||
import path from "path"
|
||||
import { mkdir } from "fs/promises"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import type { DiffRenderable, Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
|
||||
|
|
@ -10,13 +11,98 @@ import type { Session } from "@opencode-ai/sdk/v2"
|
|||
import { KVProvider } from "../../../src/cli/cmd/tui/context/kv"
|
||||
import { ThemeProvider } from "../../../src/cli/cmd/tui/context/theme"
|
||||
import { TuiConfigProvider } from "../../../src/cli/cmd/tui/context/tui-config"
|
||||
import { TuiKeybind } from "../../../src/cli/cmd/tui/config/keybind"
|
||||
import { OpencodeKeymapProvider } from "../../../src/cli/cmd/tui/keymap"
|
||||
import diffViewerPlugin from "../../../src/cli/cmd/tui/feature-plugins/system/diff-viewer"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
|
||||
test("closing the diff viewer returns to the route it opened from", async () => {
|
||||
const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } }
|
||||
const viewer = await renderDiffViewer([])
|
||||
try {
|
||||
expect(viewer.current()).toEqual({
|
||||
name: "diff",
|
||||
params: { mode: "git", sessionID: "session-1", returnRoute: startRoute },
|
||||
})
|
||||
expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "git", context: 12 })
|
||||
|
||||
expect(viewer.commands.has("diff.close")).toBe(true)
|
||||
viewer.commands.get("diff.close")!.run?.({} as never)
|
||||
expect(viewer.current()).toEqual(startRoute)
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("brackets navigate diff hunks", async () => {
|
||||
const viewer = await renderDiffViewer(
|
||||
[
|
||||
{
|
||||
file: "src/file.ts",
|
||||
additions: 3,
|
||||
deletions: 3,
|
||||
status: "modified",
|
||||
patch: `--- a/src/file.ts
|
||||
+++ b/src/file.ts
|
||||
@@ -1,3 +1,3 @@
|
||||
const first = true
|
||||
-const oldFirst = true
|
||||
+const newFirst = true
|
||||
const afterFirst = true
|
||||
@@ -20,3 +20,3 @@
|
||||
const second = true
|
||||
-const oldSecond = true
|
||||
+const newSecond = true
|
||||
const afterSecond = true
|
||||
@@ -40,3 +40,3 @@
|
||||
const third = true
|
||||
-const oldThird = true
|
||||
+const newThird = true
|
||||
const afterThird = true`,
|
||||
},
|
||||
],
|
||||
12,
|
||||
)
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes("const first"))
|
||||
await viewer.app.waitFor(() => Boolean(findRenderable(viewer.app.renderer.root, "diff-viewer-patches")))
|
||||
await viewer.app.flush()
|
||||
const scroll = findRenderable(viewer.app.renderer.root, "diff-viewer-patches") as ScrollBoxRenderable
|
||||
const diff = findRenderable(viewer.app.renderer.root, "diff-viewer-patch-0") as DiffRenderable
|
||||
expect(diff.getHunkRowOffsets()).toEqual([0, 4, 8])
|
||||
const initial = scroll.scrollTop
|
||||
|
||||
expect(TuiKeybind.defaultValue("diff_next_hunk")).toBe("]")
|
||||
expect(TuiKeybind.defaultValue("diff_previous_hunk")).toBe("[")
|
||||
|
||||
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
|
||||
await viewer.app.renderOnce()
|
||||
const first = scroll.scrollTop
|
||||
expect(first).toBeGreaterThan(initial)
|
||||
|
||||
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
|
||||
await viewer.app.renderOnce()
|
||||
const second = scroll.scrollTop
|
||||
expect(second).toBeGreaterThan(first)
|
||||
|
||||
viewer.commands.get("diff.previous_hunk")!.run?.({} as never)
|
||||
await viewer.app.renderOnce()
|
||||
expect(scroll.scrollTop).toBe(first)
|
||||
|
||||
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
|
||||
await viewer.app.renderOnce()
|
||||
expect(scroll.scrollTop).toBe(second)
|
||||
|
||||
scroll.scrollTo(initial)
|
||||
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
|
||||
await viewer.app.renderOnce()
|
||||
expect(scroll.scrollTop).toBe(first)
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
|
||||
const commands = new Map<
|
||||
string,
|
||||
NonNullable<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[0]["commands"]>[number]
|
||||
|
|
@ -24,6 +110,7 @@ test("closing the diff viewer returns to the route it opened from", async () =>
|
|||
let current = startRoute
|
||||
let renderDiff: TuiRouteDefinition["render"] | undefined
|
||||
let vcsDiffInput: unknown
|
||||
const config = createTuiResolvedConfig()
|
||||
await mkdir(Global.Path.state, { recursive: true })
|
||||
await Bun.write(path.join(Global.Path.state, "kv.json"), "{}")
|
||||
|
||||
|
|
@ -41,7 +128,7 @@ test("closing the diff viewer returns to the route it opened from", async () =>
|
|||
vcs: {
|
||||
diff: async (input: unknown) => {
|
||||
vcsDiffInput = input
|
||||
return { data: [] }
|
||||
return { data: vcsDiff }
|
||||
},
|
||||
},
|
||||
session: { diff: async () => ({ data: [] }) },
|
||||
|
|
@ -73,7 +160,7 @@ test("closing the diff viewer returns to the route it opened from", async () =>
|
|||
|
||||
return (
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<TuiConfigProvider config={createTuiResolvedConfig()}>
|
||||
<TuiConfigProvider config={config}>
|
||||
<KVProvider>
|
||||
<ThemeProvider mode="dark">
|
||||
{renderDiff?.({ params: "params" in current ? current.params : undefined })}
|
||||
|
|
@ -84,19 +171,25 @@ test("closing the diff viewer returns to the route it opened from", async () =>
|
|||
)
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { width: 80, height: 20 })
|
||||
try {
|
||||
await waitForCommand(app, commands, "diff.close")
|
||||
expect(current).toEqual({ name: "diff", params: { mode: "git", sessionID: "session-1", returnRoute: startRoute } })
|
||||
expect(vcsDiffInput).toEqual({ directory: "/repo/session", mode: "git", context: 12 })
|
||||
|
||||
expect(commands.has("diff.close")).toBe(true)
|
||||
commands.get("diff.close")!.run?.({} as never)
|
||||
expect(current).toEqual(startRoute)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
const app = await testRender(() => <Harness />, { width: 80, height })
|
||||
await waitForCommand(app, commands, "diff.close")
|
||||
return {
|
||||
app,
|
||||
commands,
|
||||
current: () => current,
|
||||
vcsDiffInput: () => vcsDiffInput,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } }
|
||||
|
||||
function findRenderable(root: Renderable, id: string): Renderable | undefined {
|
||||
if (root.id === id) return root
|
||||
return root
|
||||
.getChildren()
|
||||
.map((child) => findRenderable(child, id))
|
||||
.find(Boolean)
|
||||
}
|
||||
|
||||
const session = {
|
||||
id: "session-1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue