23b1ae081b
Users reviewing branch changes need the diff route to stay in branch mode instead of falling back through the session diff opener.
247 lines
7.7 KiB
TypeScript
247 lines
7.7 KiB
TypeScript
/** @jsxImportSource @opentui/solid */
|
|
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"
|
|
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 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, initialRoute?: TuiRouteCurrent) {
|
|
const commands = new Map<
|
|
string,
|
|
NonNullable<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[0]["commands"]>[number]
|
|
>()
|
|
let current = initialRoute ?? 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"), "{}")
|
|
|
|
function Harness() {
|
|
const renderer = useRenderer()
|
|
const keymap = createDefaultOpenTuiKeymap(renderer)
|
|
const registerLayer = keymap.registerLayer.bind(keymap)
|
|
keymap.registerLayer = (layer) => {
|
|
layer.commands?.forEach((command) => commands.set(command.name, command))
|
|
return registerLayer(layer)
|
|
}
|
|
const base = createTuiPluginApi({
|
|
keymap,
|
|
client: {
|
|
vcs: {
|
|
diff: async (input: unknown) => {
|
|
vcsDiffInput = input
|
|
return { data: vcsDiff }
|
|
},
|
|
},
|
|
session: { diff: async () => ({ data: [] }) },
|
|
} as unknown as TuiPluginApi["client"],
|
|
state: {
|
|
session: {
|
|
get: () => session,
|
|
},
|
|
},
|
|
})
|
|
const api = {
|
|
...base,
|
|
route: {
|
|
register(routes) {
|
|
renderDiff = routes.find((route) => route.name === "diff")?.render
|
|
return () => {}
|
|
},
|
|
navigate(name, params) {
|
|
current = params ? { name, params } : { name }
|
|
},
|
|
get current() {
|
|
return current
|
|
},
|
|
},
|
|
} satisfies TuiPluginApi
|
|
|
|
void diffViewerPlugin.tui(api, undefined, pluginMeta)
|
|
if (!initialRoute) commands.get("diff.open")?.run?.({} as never)
|
|
|
|
return (
|
|
<OpencodeKeymapProvider keymap={keymap}>
|
|
<TuiConfigProvider config={config}>
|
|
<KVProvider>
|
|
<ThemeProvider mode="dark">
|
|
{renderDiff?.({ params: "params" in current ? current.params : undefined })}
|
|
</ThemeProvider>
|
|
</KVProvider>
|
|
</TuiConfigProvider>
|
|
</OpencodeKeymapProvider>
|
|
)
|
|
}
|
|
|
|
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",
|
|
slug: "session-1",
|
|
projectID: "project-1",
|
|
directory: "/repo/session",
|
|
title: "Session",
|
|
version: "1",
|
|
time: {
|
|
created: 0,
|
|
updated: 0,
|
|
},
|
|
} satisfies Session
|
|
|
|
test("branch diff source requests branch VCS diff", async () => {
|
|
const viewer = await renderDiffViewer([], 20, {
|
|
name: "diff",
|
|
params: { mode: "branch", sessionID: "session-1", returnRoute: startRoute },
|
|
})
|
|
try {
|
|
expect(viewer.current()).toEqual({
|
|
name: "diff",
|
|
params: { mode: "branch", sessionID: "session-1", returnRoute: startRoute },
|
|
})
|
|
expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "branch", context: 12 })
|
|
} finally {
|
|
viewer.app.renderer.destroy()
|
|
}
|
|
})
|
|
|
|
async function waitForCommand(
|
|
app: Awaited<ReturnType<typeof testRender>>,
|
|
commands: Map<string, unknown>,
|
|
command: string,
|
|
) {
|
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
await app.renderOnce()
|
|
if (commands.has(command)) return
|
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
}
|
|
}
|
|
|
|
const pluginMeta = {
|
|
id: "diff-viewer",
|
|
source: "internal",
|
|
spec: "diff-viewer",
|
|
target: "diff-viewer",
|
|
first_time: 0,
|
|
last_time: 0,
|
|
time_changed: 0,
|
|
load_count: 1,
|
|
fingerprint: "test",
|
|
state: "same",
|
|
} satisfies TuiPluginMeta
|