@neurocode-ai/tui 1.18.8
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/bunfig.toml +4 -0
- package/package.json +72 -0
- package/src/app.tsx +1134 -0
- package/src/attention.ts +260 -0
- package/src/audio.d.ts +9 -0
- package/src/audio.ts +53 -0
- package/src/clipboard.ts +124 -0
- package/src/component/bg-pulse-render.ts +436 -0
- package/src/component/bg-pulse.tsx +99 -0
- package/src/component/command-palette.tsx +79 -0
- package/src/component/dialog-agent.tsx +31 -0
- package/src/component/dialog-console-org.tsx +135 -0
- package/src/component/dialog-debug.tsx +90 -0
- package/src/component/dialog-mcp.tsx +85 -0
- package/src/component/dialog-model.tsx +197 -0
- package/src/component/dialog-move-session.tsx +353 -0
- package/src/component/dialog-provider.tsx +469 -0
- package/src/component/dialog-retry-action.tsx +160 -0
- package/src/component/dialog-session-delete-failed.tsx +99 -0
- package/src/component/dialog-session-list.tsx +364 -0
- package/src/component/dialog-session-rename.tsx +31 -0
- package/src/component/dialog-skill.tsx +70 -0
- package/src/component/dialog-stash.tsx +87 -0
- package/src/component/dialog-status.tsx +168 -0
- package/src/component/dialog-tag.tsx +47 -0
- package/src/component/dialog-theme-list.tsx +50 -0
- package/src/component/dialog-variant.tsx +39 -0
- package/src/component/dialog-workspace-create.tsx +308 -0
- package/src/component/dialog-workspace-file-changes.tsx +144 -0
- package/src/component/dialog-workspace-list.tsx +112 -0
- package/src/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/component/error-component.tsx +240 -0
- package/src/component/logo.tsx +61 -0
- package/src/component/plugin-route-missing.tsx +14 -0
- package/src/component/prompt/autocomplete.tsx +781 -0
- package/src/component/prompt/cwd.ts +0 -0
- package/src/component/prompt/frecency.tsx +1 -0
- package/src/component/prompt/history.tsx +1 -0
- package/src/component/prompt/index.tsx +1713 -0
- package/src/component/prompt/local-attachment.ts +48 -0
- package/src/component/prompt/move.tsx +205 -0
- package/src/component/prompt/stash.tsx +1 -0
- package/src/component/prompt/workspace.tsx +137 -0
- package/src/component/register-spinner.ts +6 -0
- package/src/component/spinner.tsx +26 -0
- package/src/component/startup-loading.tsx +63 -0
- package/src/component/todo-item.tsx +32 -0
- package/src/component/use-connected.tsx +12 -0
- package/src/component/workspace-label.tsx +19 -0
- package/src/config/index.tsx +129 -0
- package/src/config/keybind.ts +471 -0
- package/src/context/args.tsx +16 -0
- package/src/context/clipboard.tsx +18 -0
- package/src/context/data.tsx +569 -0
- package/src/context/directory.ts +17 -0
- package/src/context/editor.ts +408 -0
- package/src/context/epilogue.tsx +6 -0
- package/src/context/event.ts +36 -0
- package/src/context/exit.tsx +8 -0
- package/src/context/helper.tsx +26 -0
- package/src/context/kv.tsx +66 -0
- package/src/context/local.tsx +542 -0
- package/src/context/location.tsx +14 -0
- package/src/context/path-format.tsx +24 -0
- package/src/context/permission.tsx +26 -0
- package/src/context/project.tsx +115 -0
- package/src/context/prompt.tsx +18 -0
- package/src/context/route.tsx +60 -0
- package/src/context/runtime.tsx +62 -0
- package/src/context/sdk.tsx +151 -0
- package/src/context/sync.tsx +666 -0
- package/src/context/theme.tsx +332 -0
- package/src/context/thinking.ts +67 -0
- package/src/editor-zed.ts +286 -0
- package/src/editor.ts +101 -0
- package/src/feature-plugins/builtins.ts +36 -0
- package/src/feature-plugins/home/footer.tsx +100 -0
- package/src/feature-plugins/home/tips-view.tsx +287 -0
- package/src/feature-plugins/home/tips.tsx +59 -0
- package/src/feature-plugins/sidebar/context.tsx +65 -0
- package/src/feature-plugins/sidebar/files.tsx +70 -0
- package/src/feature-plugins/sidebar/footer.tsx +98 -0
- package/src/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
- package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
- package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
- package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
- package/src/feature-plugins/system/notifications.ts +94 -0
- package/src/feature-plugins/system/plugins.tsx +269 -0
- package/src/feature-plugins/system/which-key.tsx +608 -0
- package/src/index.tsx +1 -0
- package/src/keymap.tsx +290 -0
- package/src/logo.ts +11 -0
- package/src/parsers-config.ts +386 -0
- package/src/plugin/adapters.tsx +355 -0
- package/src/plugin/api.ts +52 -0
- package/src/plugin/command-shim.ts +109 -0
- package/src/plugin/runtime.tsx +81 -0
- package/src/plugin/slots.tsx +65 -0
- package/src/prompt/display.ts +48 -0
- package/src/prompt/frecency.tsx +80 -0
- package/src/prompt/history.tsx +111 -0
- package/src/prompt/part.ts +29 -0
- package/src/prompt/stash.tsx +89 -0
- package/src/prompt/traits.ts +29 -0
- package/src/routes/home/session-destination.tsx +41 -0
- package/src/routes/home.tsx +95 -0
- package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/routes/session/dialog-message.tsx +109 -0
- package/src/routes/session/dialog-subagent.tsx +26 -0
- package/src/routes/session/dialog-timeline.tsx +47 -0
- package/src/routes/session/footer.tsx +91 -0
- package/src/routes/session/index.tsx +2710 -0
- package/src/routes/session/permission.tsx +718 -0
- package/src/routes/session/question.tsx +514 -0
- package/src/routes/session/sidebar.tsx +103 -0
- package/src/routes/session/subagent-footer.tsx +132 -0
- package/src/runtime.tsx +9 -0
- package/src/terminal-win32.ts +130 -0
- package/src/theme/assets/aura.json +69 -0
- package/src/theme/assets/ayu.json +80 -0
- package/src/theme/assets/carbonfox.json +248 -0
- package/src/theme/assets/catppuccin-frappe.json +230 -0
- package/src/theme/assets/catppuccin-macchiato.json +230 -0
- package/src/theme/assets/catppuccin.json +112 -0
- package/src/theme/assets/cobalt2.json +225 -0
- package/src/theme/assets/cursor.json +249 -0
- package/src/theme/assets/dracula.json +219 -0
- package/src/theme/assets/everforest.json +241 -0
- package/src/theme/assets/flexoki.json +237 -0
- package/src/theme/assets/github.json +233 -0
- package/src/theme/assets/gruvbox.json +242 -0
- package/src/theme/assets/kanagawa.json +77 -0
- package/src/theme/assets/lucent-orng.json +234 -0
- package/src/theme/assets/material.json +235 -0
- package/src/theme/assets/matrix.json +77 -0
- package/src/theme/assets/mercury.json +252 -0
- package/src/theme/assets/monokai.json +221 -0
- package/src/theme/assets/nightowl.json +221 -0
- package/src/theme/assets/nord.json +223 -0
- package/src/theme/assets/one-dark.json +84 -0
- package/src/theme/assets/opencode.json +245 -0
- package/src/theme/assets/orng.json +249 -0
- package/src/theme/assets/osaka-jade.json +93 -0
- package/src/theme/assets/palenight.json +222 -0
- package/src/theme/assets/rosepine.json +234 -0
- package/src/theme/assets/solarized.json +223 -0
- package/src/theme/assets/synthwave84.json +226 -0
- package/src/theme/assets/tokyonight.json +243 -0
- package/src/theme/assets/vercel.json +245 -0
- package/src/theme/assets/vesper.json +218 -0
- package/src/theme/assets/zenburn.json +223 -0
- package/src/theme/index.ts +1089 -0
- package/src/ui/border.ts +21 -0
- package/src/ui/dialog-alert.tsx +66 -0
- package/src/ui/dialog-confirm.tsx +108 -0
- package/src/ui/dialog-export-options.tsx +217 -0
- package/src/ui/dialog-help.tsx +40 -0
- package/src/ui/dialog-prompt.tsx +126 -0
- package/src/ui/dialog-select.tsx +790 -0
- package/src/ui/dialog.tsx +231 -0
- package/src/ui/link.tsx +34 -0
- package/src/ui/spinner.ts +368 -0
- package/src/ui/toast.tsx +102 -0
- package/src/util/collapse-tool-output.ts +19 -0
- package/src/util/error.ts +182 -0
- package/src/util/filetype.ts +130 -0
- package/src/util/format.ts +20 -0
- package/src/util/layout.ts +25 -0
- package/src/util/locale.ts +86 -0
- package/src/util/model.ts +28 -0
- package/src/util/path.ts +12 -0
- package/src/util/persistence.ts +33 -0
- package/src/util/presentation.ts +38 -0
- package/src/util/provider-origin.ts +7 -0
- package/src/util/record.ts +3 -0
- package/src/util/renderer.ts +7 -0
- package/src/util/revert-diff.ts +18 -0
- package/src/util/scroll.ts +27 -0
- package/src/util/selection.ts +79 -0
- package/src/util/session.ts +3 -0
- package/src/util/signal.ts +51 -0
- package/src/util/system.ts +20 -0
- package/src/util/tool-display.ts +13 -0
- package/src/util/transcript.ts +112 -0
- package/sst-env.d.ts +10 -0
- package/test/app-lifecycle.test.tsx +128 -0
- package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
- package/test/cli/cmd/tui/model-options.test.ts +32 -0
- package/test/cli/cmd/tui/notifications.test.ts +267 -0
- package/test/cli/cmd/tui/provider-options.test.ts +41 -0
- package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
- package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
- package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
- package/test/cli/cmd/tui/sync.test.tsx +65 -0
- package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
- package/test/cli/tui/data.test.tsx +486 -0
- package/test/cli/tui/dialog-prompt.test.tsx +147 -0
- package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
- package/test/cli/tui/diff-viewer.test.tsx +268 -0
- package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
- package/test/cli/tui/prompt-submit-race.test.ts +98 -0
- package/test/cli/tui/thinking.test.ts +36 -0
- package/test/cli/tui/use-event.test.tsx +148 -0
- package/test/clipboard.test.ts +19 -0
- package/test/component/dialog-session-list.test.ts +46 -0
- package/test/config.test.tsx +127 -0
- package/test/context/local.test.ts +22 -0
- package/test/editor.test.ts +32 -0
- package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
- package/test/fixture/fixture.ts +13 -0
- package/test/fixture/tui-environment.tsx +32 -0
- package/test/fixture/tui-plugin.ts +36 -0
- package/test/fixture/tui-runtime.ts +12 -0
- package/test/fixture/tui-sdk.ts +109 -0
- package/test/index.test.tsx +6 -0
- package/test/keymap.test.tsx +141 -0
- package/test/plugin/runtime.test.ts +50 -0
- package/test/plugin/slots.test.tsx +38 -0
- package/test/prompt/display.test.ts +33 -0
- package/test/prompt/history.test.ts +39 -0
- package/test/prompt/jsonl.test.ts +24 -0
- package/test/prompt/local-attachment.test.ts +43 -0
- package/test/prompt/part.test.ts +53 -0
- package/test/prompt/persistence.test.ts +23 -0
- package/test/prompt/traits.test.ts +25 -0
- package/test/runtime.test.tsx +37 -0
- package/test/theme.test.ts +81 -0
- package/test/util/error.test.ts +49 -0
- package/test/util/filetype.test.ts +16 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/model.test.ts +9 -0
- package/test/util/presentation.test.ts +8 -0
- package/test/util/renderer.test.ts +30 -0
- package/test/util/revert-diff.test.ts +35 -0
- package/test/util/session.test.ts +10 -0
- package/test/util/tool-display.test.ts +40 -0
- package/test/util/transcript.test.ts +421 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { CliRenderEvents, SyntaxStyle, type TerminalColors } from "@opentui/core"
|
|
2
|
+
import { useRenderer } from "@opentui/solid"
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_THEMES,
|
|
5
|
+
addTheme,
|
|
6
|
+
allThemes,
|
|
7
|
+
generateSubtleSyntax,
|
|
8
|
+
generateSyntax,
|
|
9
|
+
generateSystem,
|
|
10
|
+
hasTheme,
|
|
11
|
+
isTheme,
|
|
12
|
+
resolveTheme,
|
|
13
|
+
selectedForeground,
|
|
14
|
+
setCustomThemes,
|
|
15
|
+
setSystemTheme,
|
|
16
|
+
subscribeThemes,
|
|
17
|
+
terminalMode,
|
|
18
|
+
tint,
|
|
19
|
+
upsertTheme,
|
|
20
|
+
type ThemeJson,
|
|
21
|
+
} from "../theme"
|
|
22
|
+
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
|
23
|
+
import { createStore, produce } from "solid-js/store"
|
|
24
|
+
import { createSimpleContext } from "./helper"
|
|
25
|
+
import { useKV } from "./kv"
|
|
26
|
+
import { useTuiConfig } from "../config"
|
|
27
|
+
import { Global } from "@neurocode-ai/core/global"
|
|
28
|
+
import { Glob } from "@neurocode-ai/core/util/glob"
|
|
29
|
+
import { readFile } from "node:fs/promises"
|
|
30
|
+
import path from "node:path"
|
|
31
|
+
|
|
32
|
+
export type ThemeSource = Readonly<{
|
|
33
|
+
discover(): Promise<Record<string, unknown>>
|
|
34
|
+
subscribeRefresh?(refresh: () => void): () => void
|
|
35
|
+
}>
|
|
36
|
+
|
|
37
|
+
const themeSource: ThemeSource = {
|
|
38
|
+
async discover() {
|
|
39
|
+
const directories = [Global.Path.config]
|
|
40
|
+
for (let current = process.cwd(); ; current = path.dirname(current)) {
|
|
41
|
+
directories.push(path.join(current, ".opencode"))
|
|
42
|
+
if (path.dirname(current) === current) break
|
|
43
|
+
}
|
|
44
|
+
return discoverThemes(directories)
|
|
45
|
+
},
|
|
46
|
+
subscribeRefresh(refresh) {
|
|
47
|
+
process.on("SIGUSR2", refresh)
|
|
48
|
+
return () => process.off("SIGUSR2", refresh)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function discoverThemes(directories: string[]) {
|
|
53
|
+
const result: Record<string, unknown> = {}
|
|
54
|
+
for (const directory of directories) {
|
|
55
|
+
const files = await Glob.scan("themes/*.json", { cwd: directory, absolute: true, dot: true, symlink: true })
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
result[path.basename(file, ".json")] = JSON.parse(await readFile(file, "utf8")) as unknown
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
DEFAULT_THEMES,
|
|
65
|
+
addTheme,
|
|
66
|
+
allThemes,
|
|
67
|
+
generateSubtleSyntax,
|
|
68
|
+
generateSyntax,
|
|
69
|
+
generateSystem,
|
|
70
|
+
hasTheme,
|
|
71
|
+
isTheme,
|
|
72
|
+
resolveTheme,
|
|
73
|
+
selectedForeground,
|
|
74
|
+
terminalMode,
|
|
75
|
+
tint,
|
|
76
|
+
upsertTheme,
|
|
77
|
+
type Theme,
|
|
78
|
+
type ThemeJson,
|
|
79
|
+
type SyntaxStyleOverrides,
|
|
80
|
+
} from "../theme"
|
|
81
|
+
|
|
82
|
+
const THEME_REFRESH_DELAYS = [250, 1000] as const
|
|
83
|
+
|
|
84
|
+
type State = {
|
|
85
|
+
themes: Record<string, ThemeJson>
|
|
86
|
+
mode: "dark" | "light"
|
|
87
|
+
lock: "dark" | "light" | undefined
|
|
88
|
+
active: string
|
|
89
|
+
ready: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [store, setStore] = createStore<State>({
|
|
93
|
+
themes: allThemes(),
|
|
94
|
+
mode: "dark",
|
|
95
|
+
lock: undefined,
|
|
96
|
+
active: "opencode",
|
|
97
|
+
ready: false,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
subscribeThemes((themes) => setStore("themes", themes))
|
|
101
|
+
|
|
102
|
+
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|
103
|
+
name: "Theme",
|
|
104
|
+
init: (props: { mode: "dark" | "light"; source?: ThemeSource }) => {
|
|
105
|
+
const renderer = useRenderer()
|
|
106
|
+
const config = useTuiConfig()
|
|
107
|
+
const kv = useKV()
|
|
108
|
+
const themes = props.source ?? themeSource
|
|
109
|
+
const pick = (value: unknown) => {
|
|
110
|
+
if (value === "dark" || value === "light") return value
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setStore(
|
|
115
|
+
produce((draft) => {
|
|
116
|
+
const lock = pick(kv.get("theme_mode_lock"))
|
|
117
|
+
const mode = lock ?? pick(renderer.themeMode) ?? props.mode
|
|
118
|
+
if (!lock && pick(kv.get("theme_mode")) !== undefined) kv.set("theme_mode", undefined)
|
|
119
|
+
draft.mode = mode
|
|
120
|
+
draft.lock = lock
|
|
121
|
+
const active = config.theme ?? kv.get("theme", "opencode")
|
|
122
|
+
draft.active = typeof active === "string" ? active : "opencode"
|
|
123
|
+
draft.ready = false
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
createEffect(() => {
|
|
128
|
+
const theme = config.theme
|
|
129
|
+
if (theme) setStore("active", theme)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
function syncCustomThemes() {
|
|
133
|
+
return themes
|
|
134
|
+
.discover()
|
|
135
|
+
.then((themes) => {
|
|
136
|
+
setCustomThemes(
|
|
137
|
+
Object.entries(themes).reduce<Record<string, ThemeJson>>((result, [name, theme]) => {
|
|
138
|
+
if (isTheme(theme)) result[name] = theme
|
|
139
|
+
return result
|
|
140
|
+
}, {}),
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
.catch(() => setStore("active", "opencode"))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onMount(() => {
|
|
147
|
+
void Promise.allSettled([resolveSystemTheme(store.mode), syncCustomThemes()]).finally(() => {
|
|
148
|
+
setStore("ready", true)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
let systemThemeSignature: string | undefined
|
|
153
|
+
let systemThemeMode: "dark" | "light" | undefined
|
|
154
|
+
let hasResolvedSystemTheme = false
|
|
155
|
+
function resolveSystemTheme(mode: "dark" | "light" = store.mode) {
|
|
156
|
+
return renderer
|
|
157
|
+
.getPalette({ size: 16 })
|
|
158
|
+
.then((colors: TerminalColors) => {
|
|
159
|
+
if (!colors.palette[0]) {
|
|
160
|
+
if (hasResolvedSystemTheme) return
|
|
161
|
+
setSystemTheme(undefined)
|
|
162
|
+
if (store.active === "system") setStore("active", "opencode")
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
const next = store.lock ?? terminalMode(colors) ?? mode
|
|
166
|
+
if (store.mode !== next) setStore("mode", next)
|
|
167
|
+
const signature = JSON.stringify(colors)
|
|
168
|
+
hasResolvedSystemTheme = true
|
|
169
|
+
if (store.themes.system && systemThemeSignature === signature && systemThemeMode === next) return
|
|
170
|
+
systemThemeSignature = signature
|
|
171
|
+
systemThemeMode = next
|
|
172
|
+
setSystemTheme(generateSystem(colors, next))
|
|
173
|
+
})
|
|
174
|
+
.catch(() => {
|
|
175
|
+
if (hasResolvedSystemTheme) return
|
|
176
|
+
setSystemTheme(undefined)
|
|
177
|
+
if (store.active === "system") setStore("active", "opencode")
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let systemRefreshRunning = false
|
|
182
|
+
let systemRefreshQueued = false
|
|
183
|
+
let systemRefreshMode = store.mode
|
|
184
|
+
function refreshSystemTheme(mode: "dark" | "light" = store.mode) {
|
|
185
|
+
systemRefreshMode = mode
|
|
186
|
+
if (systemRefreshRunning) {
|
|
187
|
+
systemRefreshQueued = true
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
systemRefreshRunning = true
|
|
192
|
+
const retry = renderer.paletteDetectionStatus === "detecting"
|
|
193
|
+
renderer.clearPaletteCache()
|
|
194
|
+
void resolveSystemTheme(mode).finally(() => {
|
|
195
|
+
systemRefreshRunning = false
|
|
196
|
+
if (!retry && !systemRefreshQueued) return
|
|
197
|
+
systemRefreshQueued = false
|
|
198
|
+
refreshSystemTheme(systemRefreshMode)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function apply(mode: "dark" | "light") {
|
|
203
|
+
if (store.lock !== undefined) kv.set("theme_mode", mode)
|
|
204
|
+
if (store.mode === mode) return
|
|
205
|
+
setStore("mode", mode)
|
|
206
|
+
refreshSystemTheme(mode)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function pin(mode: "dark" | "light" = store.mode) {
|
|
210
|
+
setStore("lock", mode)
|
|
211
|
+
kv.set("theme_mode_lock", mode)
|
|
212
|
+
apply(mode)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function free() {
|
|
216
|
+
setStore("lock", undefined)
|
|
217
|
+
kv.set("theme_mode_lock", undefined)
|
|
218
|
+
kv.set("theme_mode", undefined)
|
|
219
|
+
refreshSystemTheme(renderer.themeMode ?? store.mode)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const handle = (mode: "dark" | "light") => {
|
|
223
|
+
if (store.lock) return
|
|
224
|
+
apply(mode)
|
|
225
|
+
}
|
|
226
|
+
renderer.on(CliRenderEvents.THEME_MODE, handle)
|
|
227
|
+
|
|
228
|
+
const handleThemeNotification = (sequence: string) => {
|
|
229
|
+
if (sequence !== "\x1b[?997;1n" && sequence !== "\x1b[?997;2n") return false
|
|
230
|
+
queueMicrotask(() => refreshSystemTheme())
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
renderer.prependInputHandler(handleThemeNotification)
|
|
234
|
+
|
|
235
|
+
let themeRefreshTimeouts: ReturnType<typeof setTimeout>[] = []
|
|
236
|
+
const refresh = () => {
|
|
237
|
+
for (const timeout of themeRefreshTimeouts) clearTimeout(timeout)
|
|
238
|
+
themeRefreshTimeouts = THEME_REFRESH_DELAYS.map((delay) =>
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
refreshSystemTheme()
|
|
241
|
+
if (delay === THEME_REFRESH_DELAYS[THEME_REFRESH_DELAYS.length - 1]) void syncCustomThemes()
|
|
242
|
+
}, delay),
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
let unsubscribeRefresh: (() => void) | undefined
|
|
246
|
+
unsubscribeRefresh = themes.subscribeRefresh?.(refresh)
|
|
247
|
+
|
|
248
|
+
onCleanup(() => {
|
|
249
|
+
renderer.off(CliRenderEvents.THEME_MODE, handle)
|
|
250
|
+
renderer.removeInputHandler(handleThemeNotification)
|
|
251
|
+
unsubscribeRefresh?.()
|
|
252
|
+
for (const timeout of themeRefreshTimeouts) clearTimeout(timeout)
|
|
253
|
+
themeRefreshTimeouts.length = 0
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const values = createMemo(() => {
|
|
257
|
+
const active = store.themes[store.active]
|
|
258
|
+
if (active) return resolveTheme(active, store.mode)
|
|
259
|
+
|
|
260
|
+
const saved = kv.get("theme")
|
|
261
|
+
if (typeof saved === "string") {
|
|
262
|
+
const theme = store.themes[saved]
|
|
263
|
+
if (theme) return resolveTheme(theme, store.mode)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return resolveTheme(store.themes.opencode, store.mode)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
createEffect(() => renderer.setBackgroundColor(values().background))
|
|
270
|
+
|
|
271
|
+
const syntax = createSyntaxStyleMemo(() => generateSyntax(values()))
|
|
272
|
+
const subtleSyntax = createSyntaxStyleMemo(() => generateSubtleSyntax(values()))
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
theme: new Proxy(values(), {
|
|
276
|
+
get(_target, prop) {
|
|
277
|
+
// @ts-expect-error Properties are forwarded to the current reactive value.
|
|
278
|
+
return values()[prop]
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
get selected() {
|
|
282
|
+
return store.active
|
|
283
|
+
},
|
|
284
|
+
all: allThemes,
|
|
285
|
+
has: hasTheme,
|
|
286
|
+
syntax,
|
|
287
|
+
subtleSyntax,
|
|
288
|
+
mode: () => store.mode,
|
|
289
|
+
locked: () => store.lock !== undefined,
|
|
290
|
+
lock: () => pin(store.mode),
|
|
291
|
+
unlock: free,
|
|
292
|
+
setMode: pin,
|
|
293
|
+
set(theme: string) {
|
|
294
|
+
if (!hasTheme(theme)) return false
|
|
295
|
+
setStore("active", theme)
|
|
296
|
+
kv.set("theme", theme)
|
|
297
|
+
return true
|
|
298
|
+
},
|
|
299
|
+
get ready() {
|
|
300
|
+
return store.ready
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
export function createSyntaxStyleMemo(factory: () => SyntaxStyle) {
|
|
307
|
+
const renderer = useRenderer()
|
|
308
|
+
const retained = new Set<SyntaxStyle>()
|
|
309
|
+
let current: SyntaxStyle | undefined
|
|
310
|
+
|
|
311
|
+
const release = (style: SyntaxStyle) => {
|
|
312
|
+
retained.add(style)
|
|
313
|
+
void renderer
|
|
314
|
+
.idle()
|
|
315
|
+
.catch(() => {})
|
|
316
|
+
.finally(() => {
|
|
317
|
+
if (!retained.delete(style)) return
|
|
318
|
+
style.destroy()
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
onCleanup(() => {
|
|
323
|
+
if (current) release(current)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
return createMemo(() => {
|
|
327
|
+
const previous = current
|
|
328
|
+
current = factory()
|
|
329
|
+
if (previous) release(previous)
|
|
330
|
+
return current
|
|
331
|
+
})
|
|
332
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createMemo, type Setter } from "solid-js"
|
|
2
|
+
import { useKV } from "./kv"
|
|
3
|
+
|
|
4
|
+
export type ThinkingMode = "show" | "hide"
|
|
5
|
+
|
|
6
|
+
const MODES: readonly ThinkingMode[] = ["show", "hide"] as const
|
|
7
|
+
|
|
8
|
+
// OpenAI's Responses API surfaces reasoning summaries that start with a bolded
|
|
9
|
+
// title block: "**Inspecting PR workflow**\n\n<body>". Treat that first block,
|
|
10
|
+
// or a complete title still awaiting its body while streaming, as disclosure
|
|
11
|
+
// metadata so the TUI can style its header independently from the markdown body.
|
|
12
|
+
export function reasoningSummary(text: string) {
|
|
13
|
+
const content = text.trim()
|
|
14
|
+
const match = content.match(/^\*\*([^*\n]+)\*\*(?:\r?\n\r?\n|$)/)
|
|
15
|
+
if (!match) return { title: null, body: content }
|
|
16
|
+
return { title: match[1].trim(), body: content.slice(match[0].length).trimEnd() }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isThinkingMode(value: unknown): value is ThinkingMode {
|
|
20
|
+
return typeof value === "string" && (MODES as readonly string[]).includes(value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cycle order matches the slash command: show → hide → show.
|
|
24
|
+
export function nextThinkingMode(current: ThinkingMode): ThinkingMode {
|
|
25
|
+
const idx = MODES.indexOf(current)
|
|
26
|
+
return MODES[(idx + 1) % MODES.length] ?? "show"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useThinkingMode() {
|
|
30
|
+
const kv = useKV()
|
|
31
|
+
// Capture pre-state before `kv.signal` seeds a default, so we can detect
|
|
32
|
+
// first-time users with a legacy `thinking_visibility` boolean and migrate.
|
|
33
|
+
// The KVProvider only renders children once kv.ready, so reads here are safe.
|
|
34
|
+
const hadStored = kv.get("thinking_mode") !== undefined
|
|
35
|
+
const legacy = kv.get("thinking_visibility")
|
|
36
|
+
const [stored, setStored] = kv.signal<ThinkingMode>("thinking_mode", "hide")
|
|
37
|
+
|
|
38
|
+
// The kv signal exposes its setter typed as `Setter<T>` which carries Solid's
|
|
39
|
+
// overload set; passing an updater fn through a property access loses the
|
|
40
|
+
// bivariance trick the existing `setX((prev) => ...)` callsites rely on.
|
|
41
|
+
// Wrap it in a sane shape so consumers can just call `set(next)` or pass
|
|
42
|
+
// an updater.
|
|
43
|
+
const set = (next: ThinkingMode | ((prev: ThinkingMode) => ThinkingMode)) => {
|
|
44
|
+
if (typeof next === "function") setStored(next as Setter<ThinkingMode>)
|
|
45
|
+
else setStored(() => next)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Preserve previous experience for users who had explicitly toggled the
|
|
49
|
+
// legacy `thinking_visibility` boolean. First-time users (no legacy key)
|
|
50
|
+
// get the new "hide" default (collapsed thinking).
|
|
51
|
+
if (!hadStored) {
|
|
52
|
+
if (legacy === true) set("show")
|
|
53
|
+
else if (legacy === false) set("hide")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if ((stored() as string) === "minimal") set("hide")
|
|
57
|
+
|
|
58
|
+
const mode = createMemo<ThinkingMode>(() => {
|
|
59
|
+
const value = stored()
|
|
60
|
+
return isThinkingMode(value) ? value : "hide"
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
mode,
|
|
65
|
+
set,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { statSync } from "node:fs"
|
|
3
|
+
import { readFile as readFileAsync } from "node:fs/promises"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { Option, Schema } from "effect"
|
|
7
|
+
import type { EditorSelection } from "./context/editor"
|
|
8
|
+
|
|
9
|
+
const ZedEditorRowSchema = Schema.Struct({
|
|
10
|
+
item_kind: Schema.String,
|
|
11
|
+
editor_id: Schema.NullOr(Schema.Number),
|
|
12
|
+
workspace_id: Schema.Number,
|
|
13
|
+
workspace_paths: Schema.NullOr(Schema.String),
|
|
14
|
+
timestamp: Schema.String,
|
|
15
|
+
buffer_path: Schema.NullOr(Schema.String),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const ZedSelectionRowSchema = Schema.Struct({
|
|
19
|
+
selection_start: Schema.NullOr(Schema.Number),
|
|
20
|
+
selection_end: Schema.NullOr(Schema.Number),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const ZedEditorContentsSchema = Schema.Struct({
|
|
24
|
+
contents: Schema.NullOr(Schema.String),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const decodeZedEditorRow = Schema.decodeUnknownOption(ZedEditorRowSchema)
|
|
28
|
+
const decodeZedSelectionRow = Schema.decodeUnknownOption(ZedSelectionRowSchema)
|
|
29
|
+
const decodeZedEditorContents = Schema.decodeUnknownOption(ZedEditorContentsSchema)
|
|
30
|
+
|
|
31
|
+
const utf8 = new TextEncoder()
|
|
32
|
+
|
|
33
|
+
type ZedEditorRow = Schema.Schema.Type<typeof ZedEditorRowSchema>
|
|
34
|
+
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
|
35
|
+
|
|
36
|
+
export type ZedSelectionResult =
|
|
37
|
+
| { type: "selection"; selection: EditorSelection }
|
|
38
|
+
| { type: "empty" }
|
|
39
|
+
| { type: "unavailable" }
|
|
40
|
+
|
|
41
|
+
export async function resolveZedSelection(dbPath: string, cwd = process.cwd()): Promise<ZedSelectionResult> {
|
|
42
|
+
const active = queryZedActiveEditor(dbPath, cwd)
|
|
43
|
+
if (active.type !== "row") return active
|
|
44
|
+
|
|
45
|
+
const row = active.row
|
|
46
|
+
if (!row.buffer_path) return { type: "empty" }
|
|
47
|
+
|
|
48
|
+
const selections = queryZedEditorSelections(dbPath, row)
|
|
49
|
+
if (selections.type !== "selections") return selections
|
|
50
|
+
const byteRanges = selections.selections
|
|
51
|
+
.flatMap((selection) => {
|
|
52
|
+
if (selection.selection_start == null || selection.selection_end == null) return []
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
start: Math.min(selection.selection_start, selection.selection_end),
|
|
56
|
+
end: Math.max(selection.selection_start, selection.selection_end),
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
.sort((left, right) => left.start - right.start || left.end - right.end)
|
|
61
|
+
if (byteRanges.length === 0) return { type: "unavailable" }
|
|
62
|
+
|
|
63
|
+
const contents = queryZedEditorContents(dbPath, row)
|
|
64
|
+
const text =
|
|
65
|
+
contents.type === "contents" && contents.contents != null
|
|
66
|
+
? contents.contents
|
|
67
|
+
: await readFileAsync(row.buffer_path, "utf8").catch(() => undefined)
|
|
68
|
+
if (text == null) return { type: "unavailable" }
|
|
69
|
+
|
|
70
|
+
const ranges = byteRanges.map((range) => {
|
|
71
|
+
const startOffset = utf8ByteOffsetToStringIndex(text, range.start)
|
|
72
|
+
const endOffset = utf8ByteOffsetToStringIndex(text, range.end)
|
|
73
|
+
return {
|
|
74
|
+
text: text.slice(startOffset, endOffset),
|
|
75
|
+
selection: offsetsToSelection(text, startOffset, endOffset),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
type: "selection",
|
|
81
|
+
selection: {
|
|
82
|
+
filePath: row.buffer_path,
|
|
83
|
+
source: "zed",
|
|
84
|
+
ranges,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function queryZedActiveEditor(dbPath: string, cwd: string) {
|
|
90
|
+
let db: Database | undefined
|
|
91
|
+
try {
|
|
92
|
+
db = new Database(dbPath, { readonly: true })
|
|
93
|
+
const raw = db
|
|
94
|
+
.query(
|
|
95
|
+
`select
|
|
96
|
+
i.kind as item_kind,
|
|
97
|
+
e.item_id as editor_id,
|
|
98
|
+
i.workspace_id as workspace_id,
|
|
99
|
+
w.paths as workspace_paths,
|
|
100
|
+
w.timestamp as timestamp,
|
|
101
|
+
e.buffer_path as buffer_path
|
|
102
|
+
from items i
|
|
103
|
+
join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
|
|
104
|
+
join workspaces w on w.workspace_id = i.workspace_id
|
|
105
|
+
left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
|
|
106
|
+
where i.active = 1 and p.active = 1
|
|
107
|
+
order by w.timestamp desc`,
|
|
108
|
+
)
|
|
109
|
+
.all()
|
|
110
|
+
|
|
111
|
+
const rows = raw.flatMap((row) => {
|
|
112
|
+
const parsed = decodeZedEditorRow(row)
|
|
113
|
+
return Option.isSome(parsed) ? [parsed.value] : []
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
|
|
117
|
+
|
|
118
|
+
const row = rows
|
|
119
|
+
.map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) }))
|
|
120
|
+
.filter((entry) => entry.score > 0)
|
|
121
|
+
.sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row
|
|
122
|
+
if (!row) return { type: "empty" as const }
|
|
123
|
+
if (row.item_kind !== "Editor") return { type: "unavailable" as const }
|
|
124
|
+
if (!isZedActiveEditorRow(row)) return { type: "empty" as const }
|
|
125
|
+
return { type: "row" as const, row }
|
|
126
|
+
} catch {
|
|
127
|
+
return { type: "unavailable" as const }
|
|
128
|
+
} finally {
|
|
129
|
+
db?.close()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
|
|
134
|
+
let db: Database | undefined
|
|
135
|
+
try {
|
|
136
|
+
db = new Database(dbPath, { readonly: true })
|
|
137
|
+
const raw = db
|
|
138
|
+
.query(
|
|
139
|
+
`select
|
|
140
|
+
start as selection_start,
|
|
141
|
+
end as selection_end
|
|
142
|
+
from editor_selections
|
|
143
|
+
where editor_id = $editorID and workspace_id = $workspaceID`,
|
|
144
|
+
)
|
|
145
|
+
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
|
|
146
|
+
|
|
147
|
+
const selections = raw.flatMap((selection) => {
|
|
148
|
+
const parsed = decodeZedSelectionRow(selection)
|
|
149
|
+
return Option.isSome(parsed) ? [parsed.value] : []
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
|
|
153
|
+
return { type: "selections" as const, selections }
|
|
154
|
+
} catch {
|
|
155
|
+
return { type: "unavailable" as const }
|
|
156
|
+
} finally {
|
|
157
|
+
db?.close()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
|
|
162
|
+
let db: Database | undefined
|
|
163
|
+
try {
|
|
164
|
+
db = new Database(dbPath, { readonly: true })
|
|
165
|
+
const parsed = decodeZedEditorContents(
|
|
166
|
+
db
|
|
167
|
+
.query(
|
|
168
|
+
`select contents
|
|
169
|
+
from editors
|
|
170
|
+
where item_id = $editorID and workspace_id = $workspaceID`,
|
|
171
|
+
)
|
|
172
|
+
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
|
|
173
|
+
)
|
|
174
|
+
if (Option.isNone(parsed)) return { type: "unavailable" as const }
|
|
175
|
+
return { type: "contents" as const, contents: parsed.value.contents }
|
|
176
|
+
} catch {
|
|
177
|
+
return { type: "unavailable" as const }
|
|
178
|
+
} finally {
|
|
179
|
+
db?.close()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
|
|
184
|
+
return row.item_kind === "Editor" && row.editor_id != null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resolveZedDbPath() {
|
|
188
|
+
const candidates = [
|
|
189
|
+
process.env.NEUROCODE_ZED_DB,
|
|
190
|
+
path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"),
|
|
191
|
+
path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
|
|
192
|
+
].filter((item): item is string => Boolean(item))
|
|
193
|
+
|
|
194
|
+
return candidates.find((item) => isFile(item))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function isZedTerminal() {
|
|
198
|
+
return process.env.ZED_TERM === "true" || process.env.TERM_PROGRAM?.toLowerCase() === "zed"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isFile(item: string) {
|
|
202
|
+
try {
|
|
203
|
+
return statSync(item).isFile()
|
|
204
|
+
} catch {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function scoreZedWorkspace(workspacePaths: string | null, cwd: string) {
|
|
210
|
+
return zedWorkspacePaths(workspacePaths).reduce((score, item) => {
|
|
211
|
+
if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length)
|
|
212
|
+
return score
|
|
213
|
+
}, 0)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function zedWorkspacePaths(value: string | null) {
|
|
217
|
+
if (!value) return []
|
|
218
|
+
const parsed = parseJson(value)
|
|
219
|
+
if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string")
|
|
220
|
+
return value.split(/\r?\n/).filter(Boolean)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function offsetToPosition(text: string, offset: number) {
|
|
224
|
+
const stringOffset = utf8ByteOffsetToStringIndex(text, offset)
|
|
225
|
+
return offsetsToSelection(text, stringOffset, stringOffset).start
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function utf8ByteOffsetToStringIndex(text: string, byteOffset: number) {
|
|
229
|
+
if (byteOffset <= 0) return 0
|
|
230
|
+
|
|
231
|
+
let bytes = 0
|
|
232
|
+
for (let index = 0; index < text.length; ) {
|
|
233
|
+
const codePoint = text.codePointAt(index)
|
|
234
|
+
if (codePoint === undefined) return text.length
|
|
235
|
+
|
|
236
|
+
const nextIndex = index + (codePoint > 0xffff ? 2 : 1)
|
|
237
|
+
bytes += utf8.encode(text.slice(index, nextIndex)).length
|
|
238
|
+
if (bytes >= byteOffset) return nextIndex
|
|
239
|
+
index = nextIndex
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return text.length
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function offsetsToSelection(text: string, startOffset: number, endOffset: number) {
|
|
246
|
+
const start = Math.max(0, Math.min(startOffset, text.length))
|
|
247
|
+
const end = Math.max(0, Math.min(endOffset, text.length))
|
|
248
|
+
let line = 1
|
|
249
|
+
let lineStart = 0
|
|
250
|
+
let startPosition = position(line, lineStart, start)
|
|
251
|
+
let endPosition = position(line, lineStart, end)
|
|
252
|
+
|
|
253
|
+
for (let index = 0; index <= end; index++) {
|
|
254
|
+
if (index === start) startPosition = position(line, lineStart, index)
|
|
255
|
+
if (index === end) {
|
|
256
|
+
endPosition = position(line, lineStart, index)
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
if (text[index] === "\n") {
|
|
260
|
+
line += 1
|
|
261
|
+
lineStart = index + 1
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { start: startPosition, end: endPosition }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function position(line: number, lineStart: number, offset: number) {
|
|
269
|
+
return {
|
|
270
|
+
line,
|
|
271
|
+
character: offset - lineStart + 1,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function pathContains(parent: string, child: string) {
|
|
276
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child))
|
|
277
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseJson(value: string) {
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(value) as unknown
|
|
283
|
+
} catch {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
}
|