Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278import { Database } from "#zed-sqlite"import { statSync } from "node:fs"import { readFile as readFileAsync } from "node:fs/promises"import os from "node:os"import path from "node:path"import { Option, Schema } from "effect"import type { EditorSelection } from "./context/editor"
const ZedEditorRowSchema = Schema.Struct({ item_kind: Schema.String, editor_id: Schema.NullOr(Schema.Number), workspace_id: Schema.Number, workspace_paths: Schema.NullOr(Schema.String), timestamp: Schema.String, buffer_path: Schema.NullOr(Schema.String),})
const ZedSelectionRowSchema = Schema.Struct({ selection_start: Schema.NullOr(Schema.Number), selection_end: Schema.NullOr(Schema.Number),})
const ZedEditorContentsSchema = Schema.Struct({ contents: Schema.NullOr(Schema.String),})
const decodeZedEditorRow = Schema.decodeUnknownOption(ZedEditorRowSchema)const decodeZedSelectionRow = Schema.decodeUnknownOption(ZedSelectionRowSchema)const decodeZedEditorContents = Schema.decodeUnknownOption(ZedEditorContentsSchema)
const utf8 = new TextEncoder()
type ZedEditorRow = Schema.Schema.Type<typeof ZedEditorRowSchema>type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
export type ZedSelectionResult = | { type: "selection"; selection: EditorSelection } | { type: "empty" } | { type: "unavailable" }
export async function resolveZedSelection(dbPath: string, cwd = process.cwd()): Promise<ZedSelectionResult> { const active = queryZedActiveEditor(dbPath, cwd) if (active.type !== "row") return active
const row = active.row if (!row.buffer_path) return { type: "empty" }
const selections = queryZedEditorSelections(dbPath, row) if (selections.type !== "selections") return selections const byteRanges = selections.selections .flatMap((selection) => { if (selection.selection_start == null || selection.selection_end == null) return [] return [ { start: Math.min(selection.selection_start, selection.selection_end), end: Math.max(selection.selection_start, selection.selection_end), }, ] }) .sort((left, right) => left.start - right.start || left.end - right.end) if (byteRanges.length === 0) return { type: "unavailable" }
const contents = queryZedEditorContents(dbPath, row) const text = contents.type === "contents" && contents.contents != null ? contents.contents : await readFileAsync(row.buffer_path, "utf8").catch(() => undefined) if (text == null) return { type: "unavailable" }
const ranges = byteRanges.map((range) => { const startOffset = utf8ByteOffsetToStringIndex(text, range.start) const endOffset = utf8ByteOffsetToStringIndex(text, range.end) return { text: text.slice(startOffset, endOffset), selection: offsetsToSelection(text, startOffset, endOffset), } })
return { type: "selection", selection: { filePath: row.buffer_path, source: "zed", ranges, }, }}
function queryZedActiveEditor(dbPath: string, cwd: string) { let db: Database | undefined try { db = new Database(dbPath, { readonly: true }) const raw = db .query( `select i.kind as item_kind, e.item_id as editor_id, i.workspace_id as workspace_id, w.paths as workspace_paths, w.timestamp as timestamp, e.buffer_path as buffer_path from items i join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id join workspaces w on w.workspace_id = i.workspace_id left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id where i.active = 1 and p.active = 1 order by w.timestamp desc`, ) .all()
const rows = raw.flatMap((row) => { const parsed = decodeZedEditorRow(row) return Option.isSome(parsed) ? [parsed.value] : [] })
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
const row = rows .map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) })) .filter((entry) => entry.score > 0) .sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row if (!row) return { type: "empty" as const } if (row.item_kind !== "Editor") return { type: "unavailable" as const } if (!isZedActiveEditorRow(row)) return { type: "empty" as const } return { type: "row" as const, row } } catch { return { type: "unavailable" as const } } finally { db?.close() }}
function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) { let db: Database | undefined try { db = new Database(dbPath, { readonly: true }) const raw = db .query( `select start as selection_start, end as selection_end from editor_selections where editor_id = $editorID and workspace_id = $workspaceID`, ) .all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
const selections = raw.flatMap((selection) => { const parsed = decodeZedSelectionRow(selection) return Option.isSome(parsed) ? [parsed.value] : [] })
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const } return { type: "selections" as const, selections } } catch { return { type: "unavailable" as const } } finally { db?.close() }}
function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) { let db: Database | undefined try { db = new Database(dbPath, { readonly: true }) const parsed = decodeZedEditorContents( db .query( `select contents from editors where item_id = $editorID and workspace_id = $workspaceID`, ) .get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }), ) if (Option.isNone(parsed)) return { type: "unavailable" as const } return { type: "contents" as const, contents: parsed.value.contents } } catch { return { type: "unavailable" as const } } finally { db?.close() }}
function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow { return row.item_kind === "Editor" && row.editor_id != null}
export function resolveZedDbPath() { const candidates = [ process.env.OPENCODE_ZED_DB, path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"), path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"), ].filter((item): item is string => Boolean(item))
return candidates.find((item) => isFile(item))}
function isFile(item: string) { try { return statSync(item).isFile() } catch { return false }}
function scoreZedWorkspace(workspacePaths: string | null, cwd: string) { return zedWorkspacePaths(workspacePaths).reduce((score, item) => { if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length) return score }, 0)}
function zedWorkspacePaths(value: string | null) { if (!value) return [] const parsed = parseJson(value) if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string") return value.split(/\r?\n/).filter(Boolean)}
function utf8ByteOffsetToStringIndex(text: string, byteOffset: number) { if (byteOffset <= 0) return 0
let bytes = 0 for (let index = 0; index < text.length; ) { const codePoint = text.codePointAt(index) if (codePoint === undefined) return text.length
const nextIndex = index + (codePoint > 0xffff ? 2 : 1) bytes += utf8.encode(text.slice(index, nextIndex)).length if (bytes >= byteOffset) return nextIndex index = nextIndex }
return text.length}
function offsetsToSelection(text: string, startOffset: number, endOffset: number) { const start = Math.max(0, Math.min(startOffset, text.length)) const end = Math.max(0, Math.min(endOffset, text.length)) let line = 1 let lineStart = 0 let startPosition = position(line, lineStart, start) let endPosition = position(line, lineStart, end)
for (let index = 0; index <= end; index++) { if (index === start) startPosition = position(line, lineStart, index) if (index === end) { endPosition = position(line, lineStart, index) break } if (text[index] === "\n") { line += 1 lineStart = index + 1 } }
return { start: startPosition, end: endPosition }}
function position(line: number, lineStart: number, offset: number) { return { line, character: offset - lineStart + 1, }}
function pathContains(parent: string, child: string) { const relative = path.relative(path.resolve(parent), path.resolve(child)) return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))}
function parseJson(value: string) { try { return JSON.parse(value) as unknown } catch { return }}