opencode-vim 0.0.22 → 0.0.24

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 (38) hide show
  1. package/README.md +25 -63
  2. package/assets/vim-in-motion-dialogs.gif +0 -0
  3. package/dist/tui.js +3326 -0
  4. package/docs/configuration.md +50 -400
  5. package/docs/keymap-actions.md +60 -188
  6. package/docs/vim-behavior.md +93 -0
  7. package/package.json +6 -6
  8. package/src/modules/snippets/index.tsx +0 -17
  9. package/src/modules/snippets/loader.ts +0 -147
  10. package/src/modules/snippets/search.ts +0 -118
  11. package/src/modules/snippets/skill-loader.ts +0 -81
  12. package/src/modules/snippets/state.ts +0 -40
  13. package/src/modules/snippets/trigger.ts +0 -68
  14. package/src/modules/snippets/types.ts +0 -50
  15. package/src/modules/snippets/view.tsx +0 -674
  16. package/src/modules/vim/actions.ts +0 -85
  17. package/src/modules/vim/config.ts +0 -125
  18. package/src/modules/vim/edit.test.ts +0 -51
  19. package/src/modules/vim/edit.ts +0 -33
  20. package/src/modules/vim/graphemes.ts +0 -38
  21. package/src/modules/vim/index.tsx +0 -200
  22. package/src/modules/vim/keys.ts +0 -38
  23. package/src/modules/vim/log.ts +0 -37
  24. package/src/modules/vim/map.test.ts +0 -45
  25. package/src/modules/vim/map.ts +0 -198
  26. package/src/modules/vim/state.test.ts +0 -18
  27. package/src/modules/vim/state.tsx +0 -46
  28. package/src/modules/vim/view.tsx +0 -130
  29. package/src/modules/vim/vimee.test.ts +0 -211
  30. package/src/modules/vim/vimee.ts +0 -1146
  31. package/src/plugin.tsx +0 -116
  32. package/src/prompt/host.tsx +0 -38
  33. package/src/prompt/modules.ts +0 -16
  34. package/src/prompt/root.tsx +0 -69
  35. package/src/prompt/types.ts +0 -42
  36. package/src/update.ts +0 -120
  37. package/tui.tsx +0 -275
  38. package/view.tsx +0 -29
package/src/plugin.tsx DELETED
@@ -1,116 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
- import { createSignal } from "solid-js"
4
- import { PromptRoot } from "./prompt/root"
5
- import type { PromptModule } from "./prompt/types"
6
- import { createVimModule } from "./modules/vim"
7
- import { createSnippetsModule } from "./modules/snippets"
8
- import type { SnippetController } from "./modules/snippets/types"
9
- import { checkAutoUpdate } from "./update"
10
-
11
- const tui: TuiPlugin = async (api, options, meta) => {
12
- const vimEnabledKey = `${meta.id}:enabled`
13
- const [vimEnabled, setVimEnabled] = createSignal(api.kv.get(vimEnabledKey, true) === true)
14
-
15
- api.keymap.registerLayer({
16
- commands: [
17
- {
18
- namespace: "palette",
19
- name: "opencode-vim.toggle",
20
- title: "Toggle Vim Mode",
21
- desc: "Enable or disable Vim key handling",
22
- category: "Vim",
23
- slashName: "vim",
24
- run() {
25
- const next = !vimEnabled()
26
- setVimEnabled(next)
27
- api.kv.set(vimEnabledKey, next)
28
- api.ui.toast({ variant: "info", message: `Vim mode ${next ? "enabled" : "disabled"}` })
29
- },
30
- },
31
- ],
32
- })
33
-
34
- if (readAutoUpdate(options)) {
35
- let timer: Timer | undefined
36
- api.lifecycle.onDispose(() => {
37
- if (timer) clearTimeout(timer)
38
- })
39
-
40
- void checkAutoUpdate(meta, api.lifecycle.signal).then((result) => {
41
- if (!result.updated) return
42
-
43
- timer = setTimeout(() => {
44
- api.ui.toast({ variant: "info", message: "Updating opencode-vim. Restart OpenCode to finish." })
45
- }, 5000)
46
- }).catch(() => {})
47
- }
48
-
49
- const moduleCache = new Map<string, PromptModule[]>()
50
- const createModules = (key: string) => {
51
- const cached = moduleCache.get(key)
52
- if (cached) return cached
53
-
54
- const modules: PromptModule[] = []
55
- const snippets: SnippetController = {}
56
- if (hasSnippetsPlugin(api)) modules.push(createSnippetsModule(snippets))
57
- modules.push(createVimModule(options, vimEnabled, snippets))
58
- moduleCache.set(key, modules)
59
- return modules
60
- }
61
-
62
- api.slots.register({
63
- order: 50,
64
- slots: {
65
- home_prompt(ctx, props) {
66
- return <PromptRoot api={api} slot={ctx} kind="home" workspaceID={props.workspace_id} ref={props.ref} modules={createModules(`home:${props.workspace_id ?? ""}`)} />
67
- },
68
- session_prompt(ctx, props) {
69
- return (
70
- <PromptRoot
71
- api={api}
72
- slot={ctx}
73
- kind="session"
74
- sessionID={props.session_id}
75
- visible={props.visible}
76
- disabled={props.disabled}
77
- onSubmit={props.on_submit}
78
- ref={props.ref}
79
- modules={createModules(`session:${props.session_id}`)}
80
- />
81
- )
82
- },
83
- },
84
- })
85
- }
86
-
87
- function readAutoUpdate(options: unknown) {
88
- if (!options || typeof options !== "object") return true
89
- const value = (options as { autoUpdate?: unknown }).autoUpdate
90
- return typeof value === "boolean" ? value : true
91
- }
92
-
93
- function hasSnippetsPlugin(api: TuiPluginApi) {
94
- return configHasSnippets(api.state.config.plugin) || configHasSnippets(api.tuiConfig.plugin) || api.plugins.list().some(pluginHasSnippets)
95
- }
96
-
97
- function configHasSnippets(plugins: unknown) {
98
- if (!Array.isArray(plugins)) return false
99
- return plugins.some((plugin) => specHasSnippets(Array.isArray(plugin) ? plugin[0] : plugin))
100
- }
101
-
102
- function pluginHasSnippets(plugin: ReturnType<TuiPluginApi["plugins"]["list"]>[number]) {
103
- return plugin.enabled && (specHasSnippets(plugin.id) || specHasSnippets(plugin.spec))
104
- }
105
-
106
- function specHasSnippets(spec: unknown) {
107
- if (typeof spec !== "string") return false
108
- return spec === "opencode-snippets" || spec.startsWith("opencode-snippets@") || spec.includes("/opencode-snippets")
109
- }
110
-
111
- const plugin: TuiPluginModule & { id: string } = {
112
- id: "local.opencode-vim",
113
- tui,
114
- }
115
-
116
- export default plugin
@@ -1,38 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TuiPromptRef } from "@opencode-ai/plugin/tui"
3
- import type { JSX } from "@opentui/solid"
4
- import type { PromptContext, PromptModule } from "./types"
5
- import { submitModules } from "./modules"
6
-
7
- type HostPromptProps = {
8
- ctx: PromptContext
9
- modules: PromptModule[]
10
- ref?: (ref: TuiPromptRef | undefined) => void
11
- right?: JSX.Element
12
- }
13
-
14
- export function HostPrompt(props: HostPromptProps) {
15
- const api = props.ctx.api
16
-
17
- const setRef = (ref: TuiPromptRef | undefined) => {
18
- props.ctx.setPromptRef(ref)
19
- props.ref?.(ref)
20
- }
21
-
22
- const onSubmit = () => {
23
- if (submitModules(props.modules, props.ctx)) return
24
- props.ctx.submitHost()
25
- }
26
-
27
- return (
28
- <api.ui.Prompt
29
- sessionID={props.ctx.sessionID}
30
- workspaceID={props.ctx.workspaceID}
31
- visible={props.ctx.visible}
32
- disabled={props.ctx.disabled}
33
- onSubmit={onSubmit}
34
- ref={setRef}
35
- right={props.right}
36
- />
37
- )
38
- }
@@ -1,16 +0,0 @@
1
- import type { PromptContext, PromptModule } from "./types"
2
-
3
- export function sortModules(modules: PromptModule[]) {
4
- return [...modules].sort((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id))
5
- }
6
-
7
- export function submitModules(modules: PromptModule[], ctx: PromptContext) {
8
- for (const module of modules) {
9
- if (module.onSubmit?.(ctx) === true) return true
10
- }
11
- return false
12
- }
13
-
14
- export function notifyPromptRef(modules: PromptModule[], ref: ReturnType<PromptContext["prompt"]>, ctx: PromptContext) {
15
- for (const module of modules) module.onPromptRef?.(ref, ctx)
16
- }
@@ -1,69 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import { onCleanup } from "solid-js"
3
- import type { JSX } from "@opentui/solid"
4
- import { HostPrompt } from "./host"
5
- import { notifyPromptRef, sortModules } from "./modules"
6
- import type { PromptContext, PromptRootProps } from "./types"
7
-
8
- export function PromptRoot(props: PromptRootProps) {
9
- const modules = sortModules(props.modules)
10
- const api = props.api
11
- let current: ReturnType<PromptContext["prompt"]>
12
- const ctx: PromptContext = {
13
- api,
14
- slot: props.slot,
15
- kind: props.kind,
16
- get sessionID() {
17
- return props.sessionID
18
- },
19
- get workspaceID() {
20
- return props.workspaceID
21
- },
22
- get visible() {
23
- return props.visible
24
- },
25
- get disabled() {
26
- return props.disabled
27
- },
28
- prompt: () => current,
29
- setPromptRef(ref) {
30
- current = ref
31
- notifyPromptRef(modules, ref, ctx)
32
- },
33
- submitHost: () => props.onSubmit?.(),
34
- requestRender: () => api.renderer.requestRender(),
35
- }
36
-
37
- const cleanups = modules.flatMap((module) => {
38
- const cleanup = module.setup?.(ctx)
39
- return typeof cleanup === "function" ? [cleanup] : []
40
- })
41
- onCleanup(() => {
42
- for (const cleanup of cleanups) cleanup()
43
- })
44
-
45
- const moduleAbove = modules.map((module) => module.renderAbove?.(ctx)).filter(Boolean) as JSX.Element[]
46
- const moduleBelow = modules.map((module) => module.renderBelow?.(ctx)).filter(Boolean) as JSX.Element[]
47
- const moduleRight = modules.map((module) => module.renderRight?.(ctx)).filter(Boolean) as JSX.Element[]
48
- const hostRight = renderHostRight(props)
49
-
50
- return (
51
- <box>
52
- {moduleAbove}
53
- <HostPrompt ctx={ctx} modules={modules} ref={props.ref} right={<RightItems items={[...moduleRight, hostRight]} />} />
54
- {moduleBelow}
55
- </box>
56
- )
57
- }
58
-
59
- function renderHostRight(props: PromptRootProps) {
60
- const Slot = props.api.ui.Slot
61
- if (props.kind === "home") return <Slot name="home_prompt_right" workspace_id={props.workspaceID} />
62
- return <Slot name="session_prompt_right" session_id={props.sessionID ?? ""} />
63
- }
64
-
65
- function RightItems(props: { items: JSX.Element[] }) {
66
- const items = props.items.filter(Boolean)
67
- if (items.length === 0) return undefined
68
- return <box flexDirection="row">{items}</box>
69
- }
@@ -1,42 +0,0 @@
1
- import type { JSX } from "@opentui/solid"
2
- import type { TuiPluginApi, TuiPromptRef, TuiSlotContext } from "@opencode-ai/plugin/tui"
3
-
4
- export type PromptKind = "home" | "session"
5
-
6
- export type PromptContext = {
7
- api: TuiPluginApi
8
- slot: TuiSlotContext
9
- kind: PromptKind
10
- sessionID?: string
11
- workspaceID?: string
12
- visible?: boolean
13
- disabled?: boolean
14
- prompt: () => TuiPromptRef | undefined
15
- setPromptRef: (ref: TuiPromptRef | undefined) => void
16
- submitHost: () => void
17
- requestRender: () => void
18
- }
19
-
20
- export type PromptModule = {
21
- id: string
22
- order?: number
23
- setup?: (ctx: PromptContext) => void | (() => void)
24
- onPromptRef?: (ref: TuiPromptRef | undefined, ctx: PromptContext) => void
25
- onSubmit?: (ctx: PromptContext) => boolean | void
26
- renderAbove?: (ctx: PromptContext) => JSX.Element
27
- renderBelow?: (ctx: PromptContext) => JSX.Element
28
- renderRight?: (ctx: PromptContext) => JSX.Element
29
- }
30
-
31
- export type PromptRootProps = {
32
- api: TuiPluginApi
33
- slot: TuiSlotContext
34
- kind: PromptKind
35
- sessionID?: string
36
- workspaceID?: string
37
- visible?: boolean
38
- disabled?: boolean
39
- onSubmit?: () => void
40
- ref?: (ref: TuiPromptRef | undefined) => void
41
- modules: PromptModule[]
42
- }
package/src/update.ts DELETED
@@ -1,120 +0,0 @@
1
- import { rm } from "node:fs/promises"
2
- import { basename, dirname, join } from "node:path"
3
- import { fileURLToPath } from "node:url"
4
- import type { TuiPluginMeta } from "@opencode-ai/plugin/tui"
5
-
6
- type PackageJson = {
7
- name?: string
8
- version?: string
9
- dependencies?: Record<string, string>
10
- }
11
-
12
- type UpdateResult =
13
- | { updated: true; name: string; current: string; latest: string }
14
- | { updated: false; error: "remove_failed"; name: string; current: string; latest: string }
15
- | { updated: false }
16
-
17
- export async function checkAutoUpdate(meta: TuiPluginMeta, signal: AbortSignal): Promise<UpdateResult> {
18
- if (meta.source !== "npm") return { updated: false }
19
-
20
- const packageDir = await findPackageDir()
21
- if (!packageDir) return { updated: false }
22
-
23
- const pkg = await readPackageJson(join(packageDir, "package.json"))
24
- if (!pkg.name || !pkg.version) return { updated: false }
25
-
26
- const latest = await fetchLatestVersion(pkg.name, signal)
27
- if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false }
28
-
29
- const removeDir = await updateRemoveDir(packageDir, pkg.name)
30
- try {
31
- await rm(removeDir, { recursive: true, force: true })
32
- } catch {
33
- return { updated: false, error: "remove_failed", name: pkg.name, current: pkg.version, latest }
34
- }
35
-
36
- return { updated: true, name: pkg.name, current: pkg.version, latest }
37
- }
38
-
39
- async function findPackageDir() {
40
- let dir = dirname(fileURLToPath(import.meta.url))
41
- for (;;) {
42
- const packagePath = join(dir, "package.json")
43
- const pkg = await readPackageJson(packagePath)
44
- if (pkg?.name === "opencode-vim") return dir
45
-
46
- const parent = dirname(dir)
47
- if (parent === dir) return undefined
48
- dir = parent
49
- }
50
- }
51
-
52
- async function updateRemoveDir(packageDir: string, name: string) {
53
- const nodeModulesDir = dirname(packageDir)
54
- if (basename(nodeModulesDir) !== "node_modules") return packageDir
55
-
56
- const wrapperDir = dirname(nodeModulesDir)
57
- const wrapperPkg = await readPackageJson(join(wrapperDir, "package.json"))
58
- return wrapperPkg?.dependencies?.[name] ? wrapperDir : packageDir
59
- }
60
-
61
- async function readPackageJson(path: string): Promise<PackageJson | undefined> {
62
- try {
63
- const file = Bun.file(path)
64
- if (!(await file.exists())) return undefined
65
- const data = await file.json()
66
- return data && typeof data === "object" ? (data as PackageJson) : undefined
67
- } catch {
68
- return undefined
69
- }
70
- }
71
-
72
- async function fetchLatestVersion(name: string, signal: AbortSignal) {
73
- try {
74
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { signal })
75
- if (!response.ok) return undefined
76
- const data = await response.json()
77
- return data && typeof data === "object" && typeof data.version === "string" ? data.version : undefined
78
- } catch {
79
- return undefined
80
- }
81
- }
82
-
83
- function isVersionNewer(latest: string, current: string) {
84
- const next = parseVersion(latest)
85
- const prev = parseVersion(current)
86
- if (!next || !prev) return false
87
-
88
- for (let i = 0; i < 3; i++) {
89
- if (next.parts[i] !== prev.parts[i]) return next.parts[i] > prev.parts[i]
90
- }
91
-
92
- if (!next.pre.length && prev.pre.length) return true
93
- if (next.pre.length && !prev.pre.length) return false
94
-
95
- for (let i = 0; i < Math.max(next.pre.length, prev.pre.length); i++) {
96
- const a = next.pre[i]
97
- const b = prev.pre[i]
98
- if (a === undefined) return false
99
- if (b === undefined) return true
100
- if (a === b) continue
101
-
102
- const aNumber = /^\d+$/.test(a) ? Number(a) : undefined
103
- const bNumber = /^\d+$/.test(b) ? Number(b) : undefined
104
- if (aNumber !== undefined && bNumber !== undefined) return aNumber > bNumber
105
- if (aNumber !== undefined) return false
106
- if (bNumber !== undefined) return true
107
- return a > b
108
- }
109
-
110
- return false
111
- }
112
-
113
- function parseVersion(version: string) {
114
- const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.+)?$/)
115
- if (!match) return undefined
116
- return {
117
- parts: [Number(match[1]), Number(match[2]), Number(match[3])],
118
- pre: match[4]?.split(".") ?? [],
119
- }
120
- }
package/tui.tsx DELETED
@@ -1,275 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import { Plugin } from "@opencode/plugin/tui"
3
- import { InputRenderable, type CursorStyleOptions, type KeyEvent } from "@opentui/core"
4
- import { createEffect, onCleanup } from "solid-js"
5
- import { applyVimCursorStyle, focusedInput } from "./src/modules/vim/actions"
6
- import { createVimConfig } from "./src/modules/vim/config"
7
- import { editInput } from "./src/modules/vim/edit"
8
- import { keyNotation } from "./src/modules/vim/keys"
9
- import { createVimLog } from "./src/modules/vim/log"
10
- import { displayToChar } from "./src/modules/vim/map"
11
- import { createVimState } from "./src/modules/vim/state"
12
- import { createVimeeAdapter } from "./src/modules/vim/vimee"
13
- import { VimStatus } from "./view"
14
-
15
- type Context = Parameters<Parameters<typeof Plugin.define>[0]["setup"]>[0]
16
-
17
- export default Plugin.define({
18
- id: "opencode-vim",
19
- setup(context) {
20
- context.ui.slot({ append: "app", render: () => <VimHost context={context} /> })
21
- },
22
- })
23
-
24
- function VimHost(props: { context: Context }) {
25
- const config = createVimConfig(props.context.options)
26
- const normalMappings = Object.keys(config.keymaps.normal ?? {})
27
- const log = createVimLog(config)
28
- const state = createVimState(config.defaultMode, log)
29
- const vimee = createVimeeAdapter(state, config, log)
30
- const dialogState = createVimState(config.defaultMode, log)
31
- const dialogVimee = createVimeeAdapter(dialogState, config, log)
32
- const promptVim = { state, vimee }
33
- const dialogVim = { state: dialogState, vimee: dialogVimee }
34
- let dialogInput: typeof props.context.renderer.currentFocusedEditor = null
35
- const [saved, setSaved] = props.context.storage.store("state", { initial: { enabled: true } })
36
- const enabled = () => saved.enabled
37
- const ctx = createCompatContext(props.context)
38
- let cursorMode = ""
39
- let cursorInput: typeof props.context.renderer.currentFocusedEditor = null
40
- let originalCursorStyle: CursorStyleOptions | undefined
41
-
42
- const removeStatus = props.context.ui.slot({
43
- prepend: "prompt.footer",
44
- render: (footer) =>
45
- footer.mode === "normal" ? (
46
- <VimStatus
47
- mode={state.mode}
48
- enabled={enabled}
49
- theme={compatTheme(props.context)}
50
- />
51
- ) : null,
52
- })
53
-
54
- props.context.keymap.layer(() => ({
55
- mode: "global",
56
- commands: [
57
- {
58
- id: "opencode-vim.toggle",
59
- title: "Toggle Vim Mode",
60
- description: "Enable or disable Vim key handling",
61
- group: "Vim",
62
- palette: true,
63
- slash: { name: "vim" },
64
- run() {
65
- const next = !enabled()
66
- void setSaved((draft) => {
67
- draft.enabled = next
68
- })
69
- if (!next) {
70
- vimee.suspend()
71
- dialogVimee.suspend()
72
- }
73
- props.context.ui.toast.show({ message: `Vim mode ${next ? "enabled" : "disabled"}`, variant: "info" })
74
- props.context.renderer.requestRender()
75
- },
76
- },
77
- ],
78
- }))
79
-
80
- const onKey = (event: KeyEvent) => {
81
- if (!enabled() || event.defaultPrevented) return
82
- const kind = inputKind(props.context)
83
- if (!kind) return
84
- if (props.context.keymap.pending().length) return
85
- const { state, vimee } = kind === "dialog" ? dialogVim : promptVim
86
- const key = keyNotation(event as never)
87
- if (!key) return
88
- const mapped = normalMappings.some((sequence) => sequence.startsWith(key))
89
- if (passThroughKey(event, key, state.mode(), vimee.isPending(), mapped)) return
90
- if (key === "<Esc>" && state.mode() === "normal" && !vimee.isPending()) return
91
-
92
- if (kind === "dialog" && !vimee.isPending() && !mapped && state.mode() === "normal") {
93
- if (key === "<Tab>" || key === "<Home>" || key === "<End>" || key === "<PageUp>" || key === "<PageDown>") return
94
- const command = key === "j" ? "dialog.select.next" : key === "k" ? "dialog.select.prev" : undefined
95
- if (command && props.context.keymap.commands().some((item) => item.id === command)) {
96
- event.preventDefault()
97
- event.stopPropagation()
98
- props.context.keymap.dispatch(command)
99
- return
100
- }
101
- }
102
-
103
- if (kind === "prompt" && !vimee.isPending() && sendCompletionKey(event, props.context, key, state.mode())) {
104
- syncCursor(true)
105
- return
106
- }
107
-
108
- const before = state.mode()
109
- let consumed = false
110
- try {
111
- consumed = vimee.handle(event as never, key, ctx as never)
112
- } finally {
113
- if (consumed || state.mode() !== before) {
114
- event.preventDefault()
115
- event.stopPropagation()
116
- }
117
- }
118
- if (consumed) syncCursor(true)
119
- }
120
-
121
- const onFocus = () => {
122
- vimee.suspend()
123
- dialogVimee.suspend()
124
- syncCursor()
125
- }
126
- props.context.renderer.keyInput.prependListener("keypress", onKey)
127
- props.context.renderer.on("focused_editor", onFocus)
128
- createEffect(() => syncCursor())
129
- onCleanup(() => {
130
- removeStatus()
131
- props.context.renderer.keyInput.off("keypress", onKey)
132
- props.context.renderer.off("focused_editor", onFocus)
133
- vimee.cleanup()
134
- dialogVimee.cleanup()
135
- restoreCursor()
136
- })
137
-
138
- return null
139
-
140
- function syncCursor(force = false) {
141
- const input = props.context.renderer.currentFocusedEditor
142
- const kind = inputKind(props.context)
143
- if (!enabled() || !kind || !input) {
144
- restoreCursor()
145
- return
146
- }
147
- const { state, vimee } = kind === "dialog" ? dialogVim : promptVim
148
- if (kind === "dialog" && dialogInput !== input) {
149
- dialogInput = input
150
- dialogVimee.suspend()
151
- dialogState.setMode(config.defaultMode)
152
- }
153
- const mode = state.mode()
154
- vimee.attach(ctx)
155
- const inputChanged = cursorInput !== input
156
- if (inputChanged) {
157
- restoreCursor()
158
- cursorInput = input
159
- originalCursorStyle = input.cursorStyle
160
- }
161
- if (!force && !inputChanged && cursorMode === mode) return
162
- if (applyVimCursorStyle(ctx as never, config.cursorStyles[mode])) {
163
- cursorMode = mode
164
- props.context.renderer.requestRender()
165
- }
166
- }
167
-
168
- function restoreCursor() {
169
- if (cursorInput && !cursorInput.isDestroyed && originalCursorStyle) cursorInput.cursorStyle = originalCursorStyle
170
- cursorInput = null
171
- cursorMode = ""
172
- }
173
- }
174
-
175
- function createCompatContext(context: Context) {
176
- const prompt = {
177
- get current() {
178
- return { input: focusedInputValue(context), mode: "normal", parts: [] }
179
- },
180
- get focused() {
181
- return inputKind(context) !== undefined
182
- },
183
- set(value: { input: string }) {
184
- const input = context.renderer.currentFocusedEditor
185
- if (input instanceof InputRenderable) input.value = value.input
186
- else if (input) editInput(input, value.input)
187
- },
188
- submit() {
189
- if (inputKind(context) === "dialog") {
190
- const command = context.keymap.commands().some((item) => item.id === "dialog.select.submit")
191
- ? "dialog.select.submit"
192
- : "dialog.prompt.submit"
193
- context.keymap.dispatch(command)
194
- } else {
195
- context.keymap.dispatch("prompt.submit")
196
- }
197
- },
198
- blur() {
199
- context.renderer.currentFocusedEditor?.blur()
200
- },
201
- }
202
-
203
- return {
204
- api: {
205
- renderer: context.renderer,
206
- keymap: {
207
- dispatchCommand(command: string) {
208
- const ok = context.keymap.commands().some((item) => item.id === command)
209
- if (ok) context.keymap.dispatch(command)
210
- return { ok }
211
- },
212
- },
213
- theme: { current: compatTheme(context) },
214
- },
215
- kind: context.ui.router.current().type === "session" ? "session" : "home",
216
- prompt: () => (inputKind(context) ? prompt : undefined),
217
- requestRender: () => context.renderer.requestRender(),
218
- }
219
- }
220
-
221
- function inputKind(context: Context): "prompt" | "dialog" | undefined {
222
- if (!context.renderer.currentFocusedEditor) return
223
- const commands = context.keymap.commands()
224
- if (context.keymap.mode.current() === "modal") {
225
- if (commands.some((item) => item.id === "dialog.select.submit" || item.id === "dialog.prompt.submit")) return "dialog"
226
- return
227
- }
228
- if (commands.some(
229
- (item) =>
230
- item.id === "prompt.submit" || item.id === "prompt.autocomplete.next" || item.id === "prompt.history.previous",
231
- )) return "prompt"
232
- }
233
-
234
- function focusedInputValue(context: Context) {
235
- return context.renderer.currentFocusedEditor?.plainText ?? ""
236
- }
237
-
238
- function compatTheme(context: Context) {
239
- return {
240
- get background() { return context.theme.background.base },
241
- get info() { return context.theme.text.feedback.info.base },
242
- get success() { return context.theme.text.feedback.success.base },
243
- get warning() { return context.theme.text.feedback.warning.base },
244
- get textMuted() { return context.theme.text.muted },
245
- }
246
- }
247
-
248
- function passThroughKey(event: KeyEvent, key: string, mode: string, pending: boolean, mapped: boolean) {
249
- if (mode !== "normal") return false
250
- if (mapped) return false
251
- if (event.ctrl && key !== "<C-r>" && key !== "<C-[>") return true
252
- return event.super === true || event.meta === true || (isArrowKey(key) && !pending)
253
- }
254
-
255
- function sendCompletionKey(event: KeyEvent, context: Context, key: string, mode: string) {
256
- if (mode !== "normal") return false
257
- const command = key === "j" ? "prompt.autocomplete.next" : key === "k" ? "prompt.autocomplete.prev" : undefined
258
- if (!command || !isCompletionToken(context) || !context.keymap.commands().some((item) => item.id === command)) return false
259
- event.preventDefault()
260
- event.stopPropagation()
261
- context.keymap.dispatch(command)
262
- return true
263
- }
264
-
265
- function isCompletionToken(context: Context) {
266
- const input = context.renderer.currentFocusedEditor
267
- const text = input?.plainText ?? ""
268
- const index = displayToChar(text, Math.max(0, input?.cursorOffset ?? 0))
269
- const before = text.slice(0, Math.min(index + 1, text.length))
270
- return /^\/\S*$/.test(before) || /(?:^|\s)@\S*$/.test(before)
271
- }
272
-
273
- function isArrowKey(key: string) {
274
- return key === "<Left>" || key === "<Down>" || key === "<Up>" || key === "<Right>"
275
- }