231 lines
7.1 KiB
TypeScript
231 lines
7.1 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) {
|
|
const commands = new Map<
|
|
string,
|
|
NonNullable<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[0]["commands"]>[number]
|
|
>()
|
|
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"), "{}")
|
|
|
|
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)
|
|
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
|
|
|
|
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
|