saeeol 1.2.2 → 1.2.4

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.
Files changed (52) hide show
  1. package/bin/saeeol.cjs +203 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +2 -2
  4. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +3 -3
  5. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +1 -1
  6. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +5 -5
  7. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +4 -4
  8. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +5 -5
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +3 -3
  10. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +2 -2
  11. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +3 -3
  12. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +4 -4
  13. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +4 -4
  14. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +4 -4
  15. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  16. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  17. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  18. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  19. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  20. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  21. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  22. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  23. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  24. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  25. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  26. package/src/cli/cmd/tui/context/args.tsx +1 -15
  27. package/src/cli/cmd/tui/context/directory.ts +1 -15
  28. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  29. package/src/cli/cmd/tui/context/editor.ts +1 -425
  30. package/src/cli/cmd/tui/context/event.ts +1 -45
  31. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  32. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  33. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  34. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  35. package/src/cli/cmd/tui/context/local.tsx +1 -478
  36. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  37. package/src/cli/cmd/tui/context/project.tsx +1 -109
  38. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  39. package/src/cli/cmd/tui/context/route.tsx +1 -67
  40. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  41. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  42. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  43. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  44. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  45. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  46. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  47. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  48. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  49. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  50. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
  51. package/src/ltm/pipeline.ts +103 -1
  52. package/test/server/contract.test.ts +249 -0
@@ -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
- import { createSimpleContext } from "./helper"
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
- import { createMemo } from "solid-js"
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
- import { clamp } from "@saeeol/boxes/clamp"
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"