fix(tui): handle Zed selection byte offsets (#24825)

This commit is contained in:
Kit Langton
2026-04-28 14:09:39 -04:00
committed by GitHub
parent d54ffbda1c
commit 1ff8d289af
2 changed files with 135 additions and 5 deletions

View File

@@ -10,12 +10,14 @@ type ZedFixtureOptions = {
editor?: boolean
selectionStart?: number | null
selectionEnd?: number | null
contents?: string
}
async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
const dbPath = path.join(dir, "zed.sqlite")
const filePath = path.join(dir, "file.ts")
await Bun.write(filePath, "one\ntwo\nthree")
const contents = options.contents ?? "one\ntwo\nthree"
await Bun.write(filePath, contents)
const db = new Database(dbPath)
db.run("create table workspaces (workspace_id integer, paths text, timestamp text)")
@@ -27,7 +29,7 @@ async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
db.run("insert into panes values (1, 1, 1)")
db.run("insert into items values (1, 1, 1, 1, ?)", [options.itemKind ?? "Editor"])
if (options.editor !== false) {
db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"])
db.run("insert into editors values (1, 1, ?, ?)", [filePath, contents])
db.run("insert into editor_selections values (1, 1, ?, ?)", [
options.selectionStart === undefined ? 4 : options.selectionStart,
options.selectionEnd === undefined ? 7 : options.selectionEnd,
@@ -38,11 +40,23 @@ async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
return { dbPath, filePath }
}
function utf8ByteOffset(text: string, offset: number) {
return new TextEncoder().encode(text.slice(0, offset)).length
}
test("offsetToPosition converts Zed offsets to 1-based editor positions", () => {
expect(offsetToPosition("one\ntwo\nthree", 0)).toEqual({ line: 1, character: 1 })
expect(offsetToPosition("one\ntwo\nthree", 4)).toEqual({ line: 2, character: 1 })
expect(offsetToPosition("one\ntwo\nthree", 6)).toEqual({ line: 2, character: 3 })
expect(offsetToPosition("one\ntwo\nthree", 100)).toEqual({ line: 3, character: 6 })
expect(offsetToPosition("Ж\nabc", utf8ByteOffset("Ж\nabc", "Ж\nabc".indexOf("a")))).toEqual({
line: 2,
character: 1,
})
expect(offsetToPosition("😀\nabc", utf8ByteOffset("😀\nabc", "😀\nabc".indexOf("a")))).toEqual({
line: 2,
character: 1,
})
})
test("resolveZedSelection returns active editor selection", async () => {
@@ -63,6 +77,102 @@ test("resolveZedSelection returns active editor selection", async () => {
})
})
test("resolveZedSelection converts Zed UTF-8 byte offsets to string offsets", async () => {
await using tmp = await tmpdir()
const contents = "a\nЖЖЖЖЖЖЖЖЖЖ\nb\nTARGET\nz"
const start = contents.indexOf("TARGET")
const fixture = await writeZedFixture(tmp.path, {
contents,
selectionStart: utf8ByteOffset(contents, start),
selectionEnd: utf8ByteOffset(contents, start + "TARGET".length),
})
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
type: "selection",
selection: {
text: "TARGET",
filePath: fixture.filePath,
source: "zed",
selection: {
start: { line: 4, character: 1 },
end: { line: 4, character: 7 },
},
},
})
})
test("resolveZedSelection handles non-ASCII text inside the selected range", async () => {
await using tmp = await tmpdir()
const contents = "a\npre\nвыбор\nz"
const start = contents.indexOf("выбор")
const fixture = await writeZedFixture(tmp.path, {
contents,
selectionStart: utf8ByteOffset(contents, start),
selectionEnd: utf8ByteOffset(contents, start + "выбор".length),
})
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
type: "selection",
selection: {
text: "выбор",
filePath: fixture.filePath,
source: "zed",
selection: {
start: { line: 3, character: 1 },
end: { line: 3, character: 6 },
},
},
})
})
test("resolveZedSelection handles emoji before the selected range", async () => {
await using tmp = await tmpdir()
const contents = "😀\nTARGET\nz"
const start = contents.indexOf("TARGET")
const fixture = await writeZedFixture(tmp.path, {
contents,
selectionStart: utf8ByteOffset(contents, start),
selectionEnd: utf8ByteOffset(contents, start + "TARGET".length),
})
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
type: "selection",
selection: {
text: "TARGET",
filePath: fixture.filePath,
source: "zed",
selection: {
start: { line: 2, character: 1 },
end: { line: 2, character: 7 },
},
},
})
})
test("resolveZedSelection handles reversed Zed byte offsets", async () => {
await using tmp = await tmpdir()
const contents = "a\nЖЖЖ\nTARGET\nz"
const start = contents.indexOf("TARGET")
const fixture = await writeZedFixture(tmp.path, {
contents,
selectionStart: utf8ByteOffset(contents, start + "TARGET".length),
selectionEnd: utf8ByteOffset(contents, start),
})
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
type: "selection",
selection: {
text: "TARGET",
filePath: fixture.filePath,
source: "zed",
selection: {
start: { line: 3, character: 1 },
end: { line: 3, character: 7 },
},
},
})
})
test("resolveZedSelection returns empty when no workspace matches", async () => {
await using tmp = await tmpdir()
const fixture = await writeZedFixture(tmp.path, {