saeeol 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/saeeol.cjs +187 -0
- package/npm/bin/saeeol +0 -0
- package/package.json +2 -2
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +1 -1
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +4 -4
- package/src/cli/cmd/tui/context/app/args.tsx +15 -0
- package/src/cli/cmd/tui/context/app/directory.ts +15 -0
- package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
- package/src/cli/cmd/tui/context/app/editor.ts +425 -0
- package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/app/project.tsx +109 -0
- package/src/cli/cmd/tui/context/app/route.tsx +67 -0
- package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
- package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
- package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
- package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/context/args.tsx +1 -15
- package/src/cli/cmd/tui/context/directory.ts +1 -15
- package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
- package/src/cli/cmd/tui/context/editor.ts +1 -425
- package/src/cli/cmd/tui/context/event.ts +1 -45
- package/src/cli/cmd/tui/context/exit.tsx +1 -67
- package/src/cli/cmd/tui/context/helper.tsx +1 -25
- package/src/cli/cmd/tui/context/keybind.tsx +1 -105
- package/src/cli/cmd/tui/context/kv.tsx +1 -76
- package/src/cli/cmd/tui/context/local.tsx +1 -478
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
- package/src/cli/cmd/tui/context/project.tsx +1 -109
- package/src/cli/cmd/tui/context/prompt.tsx +1 -18
- package/src/cli/cmd/tui/context/route.tsx +1 -67
- package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
- package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
- package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
- package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/sdk.tsx +1 -142
- package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/sync.tsx +1 -713
- package/src/cli/cmd/tui/context/theme.tsx +1 -307
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { CliRenderEvents, type TerminalColors } from "@opentui/core"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
|
4
|
+
import { createSimpleContext } from "./helper"
|
|
5
|
+
import { Glob } from "@saeeol/core/util/glob"
|
|
6
|
+
import { useKV } from "../runtime/kv"
|
|
7
|
+
import { useRenderer } from "@opentui/solid"
|
|
8
|
+
import { createStore, produce } from "solid-js/store"
|
|
9
|
+
import { Global } from "@saeeol/core/global"
|
|
10
|
+
import { Filesystem } from "@/util/filesystem"
|
|
11
|
+
import { useTuiConfig } from "./tui-config"
|
|
12
|
+
import { isRecord } from "@/util/record"
|
|
13
|
+
import { selectedForeground, tint } from "../theme/theme-types"
|
|
14
|
+
import type { ThemeJson, Theme } from "../theme/theme-types"
|
|
15
|
+
import { DEFAULT_THEMES, isValidTheme } from "../theme/theme-themes"
|
|
16
|
+
import { resolveTheme } from "../theme/theme-resolve"
|
|
17
|
+
import { generateSystem } from "../theme/theme-system"
|
|
18
|
+
import { generateSyntax, generateSubtleSyntax } from "../theme/theme-syntax"
|
|
19
|
+
|
|
20
|
+
export { selectedForeground, tint }
|
|
21
|
+
export type { ThemeJson }
|
|
22
|
+
export { DEFAULT_THEMES }
|
|
23
|
+
export { resolveTheme }
|
|
24
|
+
|
|
25
|
+
type State = {
|
|
26
|
+
themes: Record<string, ThemeJson>
|
|
27
|
+
mode: "dark" | "light"
|
|
28
|
+
lock: "dark" | "light" | undefined
|
|
29
|
+
active: string
|
|
30
|
+
ready: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pluginThemes: Record<string, ThemeJson> = {}
|
|
34
|
+
let customThemes: Record<string, ThemeJson> = {}
|
|
35
|
+
let systemTheme: ThemeJson | undefined
|
|
36
|
+
|
|
37
|
+
function listThemes() {
|
|
38
|
+
const themes = {
|
|
39
|
+
...DEFAULT_THEMES,
|
|
40
|
+
...pluginThemes,
|
|
41
|
+
...customThemes,
|
|
42
|
+
}
|
|
43
|
+
if (!systemTheme) return themes
|
|
44
|
+
return {
|
|
45
|
+
...themes,
|
|
46
|
+
system: systemTheme,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function syncThemes() {
|
|
51
|
+
setStore("themes", listThemes())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [store, setStore] = createStore<State>({
|
|
55
|
+
themes: listThemes(),
|
|
56
|
+
mode: "dark",
|
|
57
|
+
lock: undefined,
|
|
58
|
+
active: "saeeol",
|
|
59
|
+
ready: false,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export function allThemes() {
|
|
63
|
+
return store.themes
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isTheme(theme: unknown): theme is ThemeJson {
|
|
67
|
+
if (!isRecord(theme)) return false
|
|
68
|
+
if (!isRecord(theme.theme)) return false
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function hasTheme(name: string) {
|
|
73
|
+
if (!name) return false
|
|
74
|
+
return allThemes()[name] !== undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function addTheme(name: string, theme: unknown) {
|
|
78
|
+
if (!name) return false
|
|
79
|
+
if (!isTheme(theme)) return false
|
|
80
|
+
if (hasTheme(name)) return false
|
|
81
|
+
pluginThemes[name] = theme
|
|
82
|
+
syncThemes()
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function upsertTheme(name: string, theme: unknown) {
|
|
87
|
+
if (!name) return false
|
|
88
|
+
if (!isTheme(theme)) return false
|
|
89
|
+
if (customThemes[name] !== undefined) {
|
|
90
|
+
customThemes[name] = theme
|
|
91
|
+
} else {
|
|
92
|
+
pluginThemes[name] = theme
|
|
93
|
+
}
|
|
94
|
+
syncThemes()
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|
99
|
+
name: "Theme",
|
|
100
|
+
init: (props: { mode: "dark" | "light" }) => {
|
|
101
|
+
const renderer = useRenderer()
|
|
102
|
+
const config = useTuiConfig()
|
|
103
|
+
const kv = useKV()
|
|
104
|
+
const pick = (value: unknown) => {
|
|
105
|
+
if (value === "dark" || value === "light") return value
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setStore(
|
|
110
|
+
produce((draft) => {
|
|
111
|
+
const lock = pick(kv.get("theme_mode_lock"))
|
|
112
|
+
const mode = lock ?? pick(renderer.themeMode) ?? props.mode
|
|
113
|
+
if (!lock && pick(kv.get("theme_mode")) !== undefined) {
|
|
114
|
+
kv.set("theme_mode", undefined)
|
|
115
|
+
}
|
|
116
|
+
draft.mode = mode
|
|
117
|
+
draft.lock = lock
|
|
118
|
+
const active = config.theme ?? kv.get("theme", "saeeol")
|
|
119
|
+
draft.active = typeof active === "string" ? active : "saeeol"
|
|
120
|
+
draft.ready = false
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
createEffect(() => {
|
|
125
|
+
const theme = config.theme
|
|
126
|
+
if (theme) setStore("active", theme)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
function init() {
|
|
130
|
+
void Promise.allSettled([
|
|
131
|
+
resolveSystemTheme(store.mode),
|
|
132
|
+
getCustomThemes()
|
|
133
|
+
.then((custom) => {
|
|
134
|
+
customThemes = custom
|
|
135
|
+
syncThemes()
|
|
136
|
+
})
|
|
137
|
+
.catch(() => {
|
|
138
|
+
setStore("active", "saeeol")
|
|
139
|
+
}),
|
|
140
|
+
]).finally(() => {
|
|
141
|
+
setStore("ready", true)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onMount(init)
|
|
146
|
+
|
|
147
|
+
function resolveSystemTheme(mode: "dark" | "light" = store.mode) {
|
|
148
|
+
return renderer
|
|
149
|
+
.getPalette({
|
|
150
|
+
size: 16,
|
|
151
|
+
})
|
|
152
|
+
.then((colors: TerminalColors) => {
|
|
153
|
+
if (!colors.palette[0]) {
|
|
154
|
+
systemTheme = undefined
|
|
155
|
+
syncThemes()
|
|
156
|
+
if (store.active === "system") {
|
|
157
|
+
setStore("active", "saeeol")
|
|
158
|
+
}
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
systemTheme = generateSystem(colors, mode)
|
|
162
|
+
syncThemes()
|
|
163
|
+
})
|
|
164
|
+
.catch(() => {
|
|
165
|
+
systemTheme = undefined
|
|
166
|
+
syncThemes()
|
|
167
|
+
if (store.active === "system") {
|
|
168
|
+
setStore("active", "saeeol")
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function apply(mode: "dark" | "light") {
|
|
174
|
+
if (store.lock !== undefined) kv.set("theme_mode", mode)
|
|
175
|
+
if (store.mode === mode) return
|
|
176
|
+
setStore("mode", mode)
|
|
177
|
+
renderer.clearPaletteCache()
|
|
178
|
+
void resolveSystemTheme(mode)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function pin(mode: "dark" | "light" = store.mode) {
|
|
182
|
+
setStore("lock", mode)
|
|
183
|
+
kv.set("theme_mode_lock", mode)
|
|
184
|
+
apply(mode)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function free() {
|
|
188
|
+
setStore("lock", undefined)
|
|
189
|
+
kv.set("theme_mode_lock", undefined)
|
|
190
|
+
kv.set("theme_mode", undefined)
|
|
191
|
+
const mode = renderer.themeMode
|
|
192
|
+
if (mode) apply(mode)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const handle = (mode: "dark" | "light") => {
|
|
196
|
+
if (store.lock) return
|
|
197
|
+
apply(mode)
|
|
198
|
+
}
|
|
199
|
+
renderer.on(CliRenderEvents.THEME_MODE, handle)
|
|
200
|
+
|
|
201
|
+
const refresh = () => {
|
|
202
|
+
renderer.clearPaletteCache()
|
|
203
|
+
init()
|
|
204
|
+
}
|
|
205
|
+
process.on("SIGUSR2", refresh)
|
|
206
|
+
|
|
207
|
+
onCleanup(() => {
|
|
208
|
+
renderer.off(CliRenderEvents.THEME_MODE, handle)
|
|
209
|
+
process.off("SIGUSR2", refresh)
|
|
210
|
+
})
|
|
211
|
+
const values = createMemo(() => {
|
|
212
|
+
const active = store.themes[store.active]
|
|
213
|
+
if (active) {
|
|
214
|
+
return resolveTheme(active, store.mode)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const saved = kv.get("theme")
|
|
218
|
+
if (typeof saved === "string") {
|
|
219
|
+
const theme = store.themes[saved]
|
|
220
|
+
if (theme) {
|
|
221
|
+
return resolveTheme(theme, store.mode)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return resolveTheme(store.themes.saeeol, store.mode)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
createEffect(() => {
|
|
229
|
+
renderer.setBackgroundColor(values().background)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const syntax = createMemo(() => generateSyntax(values()))
|
|
233
|
+
const subtleSyntax = createMemo(() => generateSubtleSyntax(values()))
|
|
234
|
+
return {
|
|
235
|
+
theme: new Proxy({} as Theme, {
|
|
236
|
+
get(_target, prop) {
|
|
237
|
+
// @ts-expect-error
|
|
238
|
+
return values()[prop]
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
get selected() {
|
|
242
|
+
return store.active
|
|
243
|
+
},
|
|
244
|
+
all() {
|
|
245
|
+
return allThemes()
|
|
246
|
+
},
|
|
247
|
+
has(name: string) {
|
|
248
|
+
return hasTheme(name)
|
|
249
|
+
},
|
|
250
|
+
syntax,
|
|
251
|
+
subtleSyntax,
|
|
252
|
+
mode() {
|
|
253
|
+
return store.mode
|
|
254
|
+
},
|
|
255
|
+
locked() {
|
|
256
|
+
return store.lock !== undefined
|
|
257
|
+
},
|
|
258
|
+
lock() {
|
|
259
|
+
pin(store.mode)
|
|
260
|
+
},
|
|
261
|
+
unlock() {
|
|
262
|
+
free()
|
|
263
|
+
},
|
|
264
|
+
setMode(mode: "dark" | "light") {
|
|
265
|
+
pin(mode)
|
|
266
|
+
},
|
|
267
|
+
set(theme: string) {
|
|
268
|
+
if (!hasTheme(theme)) return false
|
|
269
|
+
setStore("active", theme)
|
|
270
|
+
kv.set("theme", theme)
|
|
271
|
+
return true
|
|
272
|
+
},
|
|
273
|
+
get ready() {
|
|
274
|
+
return store.ready
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
async function getCustomThemes() {
|
|
281
|
+
const directories = [
|
|
282
|
+
Global.Path.config,
|
|
283
|
+
...(await Array.fromAsync(
|
|
284
|
+
Filesystem.up({
|
|
285
|
+
targets: [".saeeol", ".saeeol"],
|
|
286
|
+
start: process.cwd(),
|
|
287
|
+
}),
|
|
288
|
+
)),
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
const result: Record<string, ThemeJson> = {}
|
|
292
|
+
for (const dir of directories) {
|
|
293
|
+
for (const item of await Glob.scan("themes/*.json", {
|
|
294
|
+
cwd: dir,
|
|
295
|
+
absolute: true,
|
|
296
|
+
dot: true,
|
|
297
|
+
symlink: true,
|
|
298
|
+
})) {
|
|
299
|
+
const name = path.basename(item, ".json")
|
|
300
|
+
if (name in DEFAULT_THEMES) continue
|
|
301
|
+
const json = await Filesystem.readJson(item).catch(() => null)
|
|
302
|
+
if (!isValidTheme(json)) continue
|
|
303
|
+
result[name] = json
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result
|
|
307
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
|
2
|
+
import { createSimpleContext } from "./helper"
|
|
3
|
+
|
|
4
|
+
export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({
|
|
5
|
+
name: "TuiConfig",
|
|
6
|
+
init: (props: { config: TuiConfig.Info }) => {
|
|
7
|
+
return props.config
|
|
8
|
+
},
|
|
9
|
+
})
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export interface Args {
|
|
4
|
-
model?: string
|
|
5
|
-
agent?: string
|
|
6
|
-
prompt?: string
|
|
7
|
-
continue?: boolean
|
|
8
|
-
sessionID?: string
|
|
9
|
-
fork?: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
|
|
13
|
-
name: "Args",
|
|
14
|
-
init: (props: Args) => props,
|
|
15
|
-
})
|
|
1
|
+
export * from "./app/args"
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { useProject } from "./project"
|
|
3
|
-
import { useSync } from "./sync"
|
|
4
|
-
import { Global } from "@saeeol/core/global"
|
|
5
|
-
|
|
6
|
-
export function useDirectory() {
|
|
7
|
-
const project = useProject()
|
|
8
|
-
const sync = useSync()
|
|
9
|
-
return createMemo(() => {
|
|
10
|
-
const directory = project.instance.path().directory || process.cwd()
|
|
11
|
-
const result = directory.replace(Global.Path.home, "~")
|
|
12
|
-
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
|
13
|
-
return result
|
|
14
|
-
})
|
|
15
|
-
}
|
|
1
|
+
export * from "./app/directory"
|
|
@@ -1,281 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Database } from "bun:sqlite"
|
|
3
|
-
import os from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import z from "zod"
|
|
6
|
-
import { Filesystem } from "@/util/filesystem"
|
|
7
|
-
import type { EditorSelection } from "./editor"
|
|
8
|
-
|
|
9
|
-
const ZedEditorRowSchema = z.object({
|
|
10
|
-
item_kind: z.string(),
|
|
11
|
-
editor_id: z.number().nullable(),
|
|
12
|
-
workspace_id: z.number(),
|
|
13
|
-
workspace_paths: z.string().nullable(),
|
|
14
|
-
timestamp: z.string(),
|
|
15
|
-
buffer_path: z.string().nullable(),
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const ZedSelectionRowSchema = z.object({
|
|
19
|
-
selection_start: z.number().nullable(),
|
|
20
|
-
selection_end: z.number().nullable(),
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
const ZedEditorContentsSchema = z.object({
|
|
24
|
-
contents: z.string().nullable(),
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const utf8 = new TextEncoder()
|
|
28
|
-
|
|
29
|
-
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
|
|
30
|
-
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
|
31
|
-
type ZedSelectionRow = z.infer<typeof ZedSelectionRowSchema>
|
|
32
|
-
|
|
33
|
-
export type ZedSelectionResult =
|
|
34
|
-
| { type: "selection"; selection: EditorSelection }
|
|
35
|
-
| { type: "empty" }
|
|
36
|
-
| { type: "unavailable" }
|
|
37
|
-
|
|
38
|
-
export async function resolveZedSelection(dbPath: string, cwd = process.cwd()): Promise<ZedSelectionResult> {
|
|
39
|
-
const active = queryZedActiveEditor(dbPath, cwd)
|
|
40
|
-
if (active.type !== "row") return active
|
|
41
|
-
|
|
42
|
-
const row = active.row
|
|
43
|
-
if (!row.buffer_path) return { type: "empty" }
|
|
44
|
-
|
|
45
|
-
const selections = queryZedEditorSelections(dbPath, row)
|
|
46
|
-
if (selections.type !== "selections") return selections
|
|
47
|
-
const byteRanges = selections.selections
|
|
48
|
-
.flatMap((selection) => {
|
|
49
|
-
if (selection.selection_start == null || selection.selection_end == null) return []
|
|
50
|
-
return [
|
|
51
|
-
{
|
|
52
|
-
start: Math.min(selection.selection_start, selection.selection_end),
|
|
53
|
-
end: Math.max(selection.selection_start, selection.selection_end),
|
|
54
|
-
},
|
|
55
|
-
]
|
|
56
|
-
})
|
|
57
|
-
.sort((left, right) => left.start - right.start || left.end - right.end)
|
|
58
|
-
if (byteRanges.length === 0) return { type: "unavailable" }
|
|
59
|
-
|
|
60
|
-
const contents = queryZedEditorContents(dbPath, row)
|
|
61
|
-
const text =
|
|
62
|
-
contents.type === "contents" && contents.contents != null
|
|
63
|
-
? contents.contents
|
|
64
|
-
: await Bun.file(row.buffer_path)
|
|
65
|
-
.text()
|
|
66
|
-
.catch(() => undefined)
|
|
67
|
-
if (text == null) return { type: "unavailable" }
|
|
68
|
-
|
|
69
|
-
const ranges = byteRanges.map((range) => {
|
|
70
|
-
const startOffset = utf8ByteOffsetToStringIndex(text, range.start)
|
|
71
|
-
const endOffset = utf8ByteOffsetToStringIndex(text, range.end)
|
|
72
|
-
return {
|
|
73
|
-
text: text.slice(startOffset, endOffset),
|
|
74
|
-
selection: offsetsToSelection(text, startOffset, endOffset),
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
type: "selection",
|
|
80
|
-
selection: {
|
|
81
|
-
filePath: row.buffer_path,
|
|
82
|
-
source: "zed",
|
|
83
|
-
ranges,
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function queryZedActiveEditor(dbPath: string, cwd: string) {
|
|
89
|
-
let db: Database | undefined
|
|
90
|
-
try {
|
|
91
|
-
db = new Database(dbPath, { readonly: true })
|
|
92
|
-
const raw = db
|
|
93
|
-
.query(
|
|
94
|
-
`select
|
|
95
|
-
i.kind as item_kind,
|
|
96
|
-
e.item_id as editor_id,
|
|
97
|
-
i.workspace_id as workspace_id,
|
|
98
|
-
w.paths as workspace_paths,
|
|
99
|
-
w.timestamp as timestamp,
|
|
100
|
-
e.buffer_path as buffer_path
|
|
101
|
-
from items i
|
|
102
|
-
join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
|
|
103
|
-
join workspaces w on w.workspace_id = i.workspace_id
|
|
104
|
-
left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
|
|
105
|
-
where i.active = 1 and p.active = 1
|
|
106
|
-
order by w.timestamp desc`,
|
|
107
|
-
)
|
|
108
|
-
.all()
|
|
109
|
-
|
|
110
|
-
const rows = raw.flatMap((row) => {
|
|
111
|
-
const parsed = ZedEditorRowSchema.safeParse(row)
|
|
112
|
-
return parsed.success ? [parsed.data] : []
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
|
|
116
|
-
|
|
117
|
-
const row = rows
|
|
118
|
-
.map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) }))
|
|
119
|
-
.filter((entry) => entry.score > 0)
|
|
120
|
-
.sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row
|
|
121
|
-
if (!row) return { type: "empty" as const }
|
|
122
|
-
if (row.item_kind !== "Editor") return { type: "unavailable" as const }
|
|
123
|
-
if (!isZedActiveEditorRow(row)) return { type: "empty" as const }
|
|
124
|
-
return { type: "row" as const, row }
|
|
125
|
-
} catch {
|
|
126
|
-
return { type: "unavailable" as const }
|
|
127
|
-
} finally {
|
|
128
|
-
db?.close()
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
|
|
133
|
-
let db: Database | undefined
|
|
134
|
-
try {
|
|
135
|
-
db = new Database(dbPath, { readonly: true })
|
|
136
|
-
const raw = db
|
|
137
|
-
.query(
|
|
138
|
-
`select
|
|
139
|
-
start as selection_start,
|
|
140
|
-
end as selection_end
|
|
141
|
-
from editor_selections
|
|
142
|
-
where editor_id = $editorID and workspace_id = $workspaceID`,
|
|
143
|
-
)
|
|
144
|
-
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
|
|
145
|
-
|
|
146
|
-
const selections = raw.flatMap((selection) => {
|
|
147
|
-
const parsed = ZedSelectionRowSchema.safeParse(selection)
|
|
148
|
-
return parsed.success ? [parsed.data] : []
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
|
|
152
|
-
return { type: "selections" as const, selections }
|
|
153
|
-
} catch {
|
|
154
|
-
return { type: "unavailable" as const }
|
|
155
|
-
} finally {
|
|
156
|
-
db?.close()
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
|
|
161
|
-
let db: Database | undefined
|
|
162
|
-
try {
|
|
163
|
-
db = new Database(dbPath, { readonly: true })
|
|
164
|
-
const parsed = ZedEditorContentsSchema.safeParse(
|
|
165
|
-
db
|
|
166
|
-
.query(
|
|
167
|
-
`select contents
|
|
168
|
-
from editors
|
|
169
|
-
where item_id = $editorID and workspace_id = $workspaceID`,
|
|
170
|
-
)
|
|
171
|
-
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
|
|
172
|
-
)
|
|
173
|
-
if (!parsed.success) return { type: "unavailable" as const }
|
|
174
|
-
return { type: "contents" as const, contents: parsed.data.contents }
|
|
175
|
-
} catch {
|
|
176
|
-
return { type: "unavailable" as const }
|
|
177
|
-
} finally {
|
|
178
|
-
db?.close()
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
|
|
183
|
-
return row.item_kind === "Editor" && row.editor_id != null
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function resolveZedDbPath() {
|
|
187
|
-
const candidates = [
|
|
188
|
-
process.env.SAEEOL_ZED_DB,
|
|
189
|
-
path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"),
|
|
190
|
-
path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
|
|
191
|
-
].filter((item): item is string => Boolean(item))
|
|
192
|
-
|
|
193
|
-
return candidates.find((item) => isFile(item))
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function isFile(item: string) {
|
|
197
|
-
try {
|
|
198
|
-
return Filesystem.stat(item)?.isFile() === true
|
|
199
|
-
} catch {
|
|
200
|
-
return false
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function scoreZedWorkspace(workspacePaths: string | null, cwd: string) {
|
|
205
|
-
return zedWorkspacePaths(workspacePaths).reduce((score, item) => {
|
|
206
|
-
if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length)
|
|
207
|
-
return score
|
|
208
|
-
}, 0)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function zedWorkspacePaths(value: string | null) {
|
|
212
|
-
if (!value) return []
|
|
213
|
-
const parsed = parseJson(value)
|
|
214
|
-
if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string")
|
|
215
|
-
return value.split(/\r?\n/).filter(Boolean)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function offsetToPosition(text: string, offset: number) {
|
|
219
|
-
const stringOffset = utf8ByteOffsetToStringIndex(text, offset)
|
|
220
|
-
return offsetsToSelection(text, stringOffset, stringOffset).start
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function utf8ByteOffsetToStringIndex(text: string, byteOffset: number) {
|
|
224
|
-
if (byteOffset <= 0) return 0
|
|
225
|
-
|
|
226
|
-
let bytes = 0
|
|
227
|
-
for (let index = 0; index < text.length; ) {
|
|
228
|
-
const codePoint = text.codePointAt(index)
|
|
229
|
-
if (codePoint === undefined) return text.length
|
|
230
|
-
|
|
231
|
-
const nextIndex = index + (codePoint > 0xffff ? 2 : 1)
|
|
232
|
-
bytes += utf8.encode(text.slice(index, nextIndex)).length
|
|
233
|
-
if (bytes >= byteOffset) return nextIndex
|
|
234
|
-
index = nextIndex
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return text.length
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function offsetsToSelection(text: string, startOffset: number, endOffset: number) {
|
|
241
|
-
const start = clamp(startOffset, 0, text.length)
|
|
242
|
-
const end = clamp(endOffset, 0, text.length)
|
|
243
|
-
let line = 1
|
|
244
|
-
let lineStart = 0
|
|
245
|
-
let startPosition = position(line, lineStart, start)
|
|
246
|
-
let endPosition = position(line, lineStart, end)
|
|
247
|
-
|
|
248
|
-
for (let index = 0; index <= end; index++) {
|
|
249
|
-
if (index === start) startPosition = position(line, lineStart, index)
|
|
250
|
-
if (index === end) {
|
|
251
|
-
endPosition = position(line, lineStart, index)
|
|
252
|
-
break
|
|
253
|
-
}
|
|
254
|
-
if (text[index] === "\n") {
|
|
255
|
-
line += 1
|
|
256
|
-
lineStart = index + 1
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return { start: startPosition, end: endPosition }
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function position(line: number, lineStart: number, offset: number) {
|
|
264
|
-
return {
|
|
265
|
-
line,
|
|
266
|
-
character: offset - lineStart + 1,
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function pathContains(parent: string, child: string) {
|
|
271
|
-
const relative = path.relative(path.resolve(parent), path.resolve(child))
|
|
272
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function parseJson(value: string) {
|
|
276
|
-
try {
|
|
277
|
-
return JSON.parse(value) as unknown
|
|
278
|
-
} catch {
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
}
|
|
1
|
+
export * from "./app/editor-zed"
|