Skip to content

Commit 4c70ea2

Browse files
authored
fix(tui): scope Zed editor context to containing workspaces (#25211)
1 parent 5ba68a2 commit 4c70ea2

2 files changed

Lines changed: 96 additions & 5 deletions

File tree

packages/opencode/src/cli/cmd/tui/context/editor-zed.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,20 @@ export function resolveZedDbPath() {
189189
path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
190190
].filter((item): item is string => Boolean(item))
191191

192-
return candidates.find((item) => Filesystem.stat(item)?.isFile())
192+
return candidates.find((item) => isFile(item))
193+
}
194+
195+
function isFile(item: string) {
196+
try {
197+
return Filesystem.stat(item)?.isFile() === true
198+
} catch {
199+
return false
200+
}
193201
}
194202

195203
function scoreZedWorkspace(workspacePaths: string | null, cwd: string) {
196204
return zedWorkspacePaths(workspacePaths).reduce((score, item) => {
197-
if (pathContains(item, cwd)) return Math.max(score, 2)
198-
if (pathContains(cwd, item)) return Math.max(score, 1)
205+
if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length)
199206
return score
200207
}, 0)
201208
}

packages/opencode/test/cli/tui/editor-context-zed.test.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Database } from "bun:sqlite"
2+
import { mkdir, symlink } from "node:fs/promises"
3+
import os from "node:os"
24
import path from "node:path"
3-
import { expect, test } from "bun:test"
4-
import { offsetToPosition, resolveZedSelection } from "../../../src/cli/cmd/tui/context/editor-zed"
5+
import { expect, spyOn, test } from "bun:test"
6+
import { offsetToPosition, resolveZedDbPath, resolveZedSelection } from "../../../src/cli/cmd/tui/context/editor-zed"
57
import { tmpdir } from "../../fixture/fixture"
68

79
type ZedFixtureOptions = {
@@ -66,6 +68,23 @@ test("offsetToPosition converts Zed offsets to 1-based editor positions", () =>
6668
})
6769
})
6870

71+
test("resolveZedDbPath skips candidates that cannot be stated", async () => {
72+
await using tmp = await tmpdir()
73+
const loop = path.join(tmp.path, "loop")
74+
await symlink(loop, loop)
75+
const home = spyOn(os, "homedir").mockImplementation(() => tmp.path)
76+
const previous = process.env.OPENCODE_ZED_DB
77+
process.env.OPENCODE_ZED_DB = loop
78+
79+
try {
80+
expect(resolveZedDbPath()).toBeUndefined()
81+
} finally {
82+
if (previous === undefined) delete process.env.OPENCODE_ZED_DB
83+
else process.env.OPENCODE_ZED_DB = previous
84+
home.mockRestore()
85+
}
86+
})
87+
6988
test("resolveZedSelection returns active editor selection", async () => {
7089
await using tmp = await tmpdir()
7190
const fixture = await writeZedFixture(tmp.path)
@@ -251,6 +270,71 @@ test("resolveZedSelection returns empty when no workspace matches", async () =>
251270
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "empty" })
252271
})
253272

273+
test("resolveZedSelection matches a Zed workspace that contains the session directory", async () => {
274+
await using tmp = await tmpdir()
275+
const fixture = await writeZedFixture(tmp.path)
276+
277+
expect(await resolveZedSelection(fixture.dbPath, path.join(tmp.path, "packages", "app"))).toEqual({
278+
type: "selection",
279+
selection: {
280+
filePath: fixture.filePath,
281+
source: "zed",
282+
ranges: [
283+
{
284+
text: "two",
285+
selection: {
286+
start: { line: 2, character: 1 },
287+
end: { line: 2, character: 4 },
288+
},
289+
},
290+
],
291+
},
292+
})
293+
})
294+
295+
test("resolveZedSelection prefers the most specific containing Zed workspace", async () => {
296+
await using tmp = await tmpdir()
297+
const fixture = await writeZedFixture(tmp.path)
298+
const child = path.join(tmp.path, "packages")
299+
const childFile = path.join(child, "child.ts")
300+
await mkdir(child, { recursive: true })
301+
await Bun.write(childFile, "child")
302+
303+
const db = new Database(fixture.dbPath)
304+
db.run("insert into workspaces values (2, ?, ?)", [JSON.stringify([child]), "2026-01-01"])
305+
db.run("insert into panes values (2, 2, 1)")
306+
db.run("insert into items values (2, 2, 2, 1, ?)", ["Editor"])
307+
db.run("insert into editors values (2, 2, ?, ?)", [childFile, "child"])
308+
db.run("insert into editor_selections values (2, 2, 0, 5)")
309+
db.close()
310+
311+
expect(await resolveZedSelection(fixture.dbPath, path.join(child, "app"))).toEqual({
312+
type: "selection",
313+
selection: {
314+
filePath: childFile,
315+
source: "zed",
316+
ranges: [
317+
{
318+
text: "child",
319+
selection: {
320+
start: { line: 1, character: 1 },
321+
end: { line: 1, character: 6 },
322+
},
323+
},
324+
],
325+
},
326+
})
327+
})
328+
329+
test("resolveZedSelection ignores a Zed workspace nested inside the session directory", async () => {
330+
await using tmp = await tmpdir()
331+
const child = path.join(tmp.path, "effect-lab")
332+
await mkdir(child, { recursive: true })
333+
const fixture = await writeZedFixture(child)
334+
335+
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "empty" })
336+
})
337+
254338
test("resolveZedSelection returns unavailable when a Zed terminal is active", async () => {
255339
await using tmp = await tmpdir()
256340
const fixture = await writeZedFixture(tmp.path, { itemKind: "Terminal", editor: false })

0 commit comments

Comments
 (0)