opencode-vim 0.0.23 → 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.
- package/README.md +11 -0
- package/dist/tui.js +3326 -0
- package/package.json +6 -6
- package/src/clipboard.tsx +0 -50
- package/src/modules/snippets/index.tsx +0 -17
- package/src/modules/snippets/loader.ts +0 -147
- package/src/modules/snippets/search.ts +0 -118
- package/src/modules/snippets/skill-loader.ts +0 -81
- package/src/modules/snippets/state.ts +0 -40
- package/src/modules/snippets/trigger.ts +0 -68
- package/src/modules/snippets/types.ts +0 -50
- package/src/modules/snippets/view.tsx +0 -674
- package/src/modules/vim/actions.ts +0 -82
- package/src/modules/vim/config.ts +0 -137
- package/src/modules/vim/edit.test.ts +0 -51
- package/src/modules/vim/edit.ts +0 -33
- package/src/modules/vim/graphemes.ts +0 -38
- package/src/modules/vim/index.tsx +0 -200
- package/src/modules/vim/keys.ts +0 -38
- package/src/modules/vim/log.ts +0 -37
- package/src/modules/vim/map.test.ts +0 -45
- package/src/modules/vim/map.ts +0 -90
- package/src/modules/vim/state.test.ts +0 -18
- package/src/modules/vim/state.tsx +0 -46
- package/src/modules/vim/view.tsx +0 -130
- package/src/modules/vim/vimee.test.ts +0 -211
- package/src/modules/vim/vimee.ts +0 -1211
- package/src/plugin.tsx +0 -116
- package/src/prompt/host.tsx +0 -38
- package/src/prompt/modules.ts +0 -16
- package/src/prompt/root.tsx +0 -69
- package/src/prompt/types.ts +0 -42
- package/src/session.tsx +0 -291
- package/src/transcript-items.tsx +0 -168
- package/src/transcript.tsx +0 -179
- package/src/update.ts +0 -120
- package/tui.tsx +0 -340
- 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
|
package/src/prompt/host.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/prompt/modules.ts
DELETED
|
@@ -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
|
-
}
|
package/src/prompt/root.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/prompt/types.ts
DELETED
|
@@ -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/session.tsx
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
3
|
-
import { type KeyEvent, type TextareaRenderable } from "@opentui/core"
|
|
4
|
-
import { useTerminalDimensions } from "@opentui/solid"
|
|
5
|
-
import { createEffect, createSignal, onCleanup, onMount, untrack } from "solid-js"
|
|
6
|
-
import type { PromptContext } from "./modules/vim/actions"
|
|
7
|
-
import type { VimConfig } from "./modules/vim/config"
|
|
8
|
-
import { keyNotation } from "./modules/vim/keys"
|
|
9
|
-
import { displayWidth } from "./modules/vim/map"
|
|
10
|
-
import { createVimState } from "./modules/vim/state"
|
|
11
|
-
import { createVimeeAdapter, YANK_FLASH_MS } from "./modules/vim/vimee"
|
|
12
|
-
import { createTranscriptSelection } from "./transcript"
|
|
13
|
-
import type { TranscriptItem } from "./transcript-items"
|
|
14
|
-
import type { VimClipboard } from "./clipboard"
|
|
15
|
-
|
|
16
|
-
export const SESSION_MODE = "opencode-vim.session"
|
|
17
|
-
|
|
18
|
-
export function createSessionMode(context: Context, config: VimConfig, clipboard: VimClipboard) {
|
|
19
|
-
const [active, setActive] = createSignal(false)
|
|
20
|
-
const [reading, setReading] = createSignal<TranscriptItem>()
|
|
21
|
-
const [notice, setNotice] = createSignal("")
|
|
22
|
-
const positions = new Map<string, number>()
|
|
23
|
-
let sessionID = ""
|
|
24
|
-
let selectedID: string | undefined
|
|
25
|
-
let prompt: Context["renderer"]["currentFocusedEditor"] = null
|
|
26
|
-
let transcript: ReturnType<typeof createTranscriptSelection> | undefined
|
|
27
|
-
let popMode: (() => void) | undefined
|
|
28
|
-
let prefix = ""
|
|
29
|
-
let count = ""
|
|
30
|
-
let disposed = false
|
|
31
|
-
let noticeTimer: ReturnType<typeof setTimeout> | undefined
|
|
32
|
-
let yankTimer: ReturnType<typeof setTimeout> | undefined
|
|
33
|
-
let yankID: string | undefined
|
|
34
|
-
|
|
35
|
-
function selected() {
|
|
36
|
-
return transcript?.get(selectedID)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function clearYankFlash() {
|
|
40
|
-
if (yankTimer) clearTimeout(yankTimer)
|
|
41
|
-
yankTimer = undefined
|
|
42
|
-
if (!yankID) return
|
|
43
|
-
yankID = undefined
|
|
44
|
-
context.renderer.requestRender()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function close() {
|
|
48
|
-
if (!active()) return
|
|
49
|
-
clearYankFlash()
|
|
50
|
-
const wasReading = Boolean(reading())
|
|
51
|
-
if (wasReading) context.ui.dialog.clear()
|
|
52
|
-
setActive(false)
|
|
53
|
-
context.renderer.removePostProcessFn(draw)
|
|
54
|
-
popMode?.()
|
|
55
|
-
popMode = undefined
|
|
56
|
-
transcript = undefined
|
|
57
|
-
const savedPrompt = prompt
|
|
58
|
-
const savedSessionID = sessionID
|
|
59
|
-
function restorePrompt() {
|
|
60
|
-
const route = context.ui.router.current()
|
|
61
|
-
if (!active() && context.keymap.mode.current() === "base" && route.type === "session"
|
|
62
|
-
&& route.sessionID === savedSessionID && savedPrompt && !savedPrompt.isDestroyed) savedPrompt.focus()
|
|
63
|
-
}
|
|
64
|
-
restorePrompt()
|
|
65
|
-
// Restore after the dialog's deferred refocus, or it steals focus back.
|
|
66
|
-
if (wasReading) setTimeout(restorePrompt, 1)
|
|
67
|
-
prompt = null
|
|
68
|
-
context.renderer.requestRender()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function enter() {
|
|
72
|
-
const route = context.ui.router.current()
|
|
73
|
-
if (route.type !== "session") return false
|
|
74
|
-
sessionID = route.sessionID
|
|
75
|
-
prompt = context.renderer.currentFocusedEditor
|
|
76
|
-
prefix = count = ""
|
|
77
|
-
positions.clear()
|
|
78
|
-
setNotice("")
|
|
79
|
-
popMode = context.keymap.mode.push(SESSION_MODE)
|
|
80
|
-
transcript = createTranscriptSelection(context, sessionID, (id) => {
|
|
81
|
-
selectedID = id
|
|
82
|
-
})
|
|
83
|
-
setActive(true)
|
|
84
|
-
prompt?.blur()
|
|
85
|
-
transcript.focus()
|
|
86
|
-
transcript.latest()
|
|
87
|
-
context.renderer.addPostProcessFn(draw)
|
|
88
|
-
context.renderer.requestRender()
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const draw: Parameters<Context["renderer"]["addPostProcessFn"]>[0] = (buffer) => {
|
|
93
|
-
if (active() && !reading() && context.keymap.mode.current() === SESSION_MODE) transcript?.draw(buffer, yankID)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function copy(text: string) {
|
|
97
|
-
const copied = await clipboard.write(text)
|
|
98
|
-
if (disposed) return
|
|
99
|
-
if (noticeTimer) clearTimeout(noticeTimer)
|
|
100
|
-
setNotice(copied ? "Copied" : "Yanked · clipboard unavailable")
|
|
101
|
-
noticeTimer = setTimeout(() => setNotice(""), 1500)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function openMessage(message: TranscriptItem) {
|
|
105
|
-
transcript?.focus()
|
|
106
|
-
setReading(message)
|
|
107
|
-
context.ui.dialog.show(() => (
|
|
108
|
-
<MessageReader context={context} config={config} message={message} offset={positions.get(message.id) ?? 0}
|
|
109
|
-
copy={copy} notice={notice} remember={(offset) => positions.set(message.id, offset)}
|
|
110
|
-
back={() => context.ui.dialog.clear()} close={close} />
|
|
111
|
-
), () => {
|
|
112
|
-
setReading(undefined)
|
|
113
|
-
setNotice("")
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const onKey = (event: KeyEvent) => {
|
|
118
|
-
if (!active() || reading()) return
|
|
119
|
-
const key = readerKey(context, event, config.sessionKey)
|
|
120
|
-
if (!key) return
|
|
121
|
-
event.preventDefault()
|
|
122
|
-
event.stopPropagation()
|
|
123
|
-
clearYankFlash()
|
|
124
|
-
setNotice("")
|
|
125
|
-
if (key === config.sessionKey || key === "<Esc>" || key === "<C-[>") { close(); return }
|
|
126
|
-
if (/^[0-9]$/.test(key) && (count || key !== "0")) { count = (count + key).slice(0, 6); return }
|
|
127
|
-
const amount = Number(count) || 1
|
|
128
|
-
count = ""
|
|
129
|
-
const previous = prefix
|
|
130
|
-
prefix = ""
|
|
131
|
-
let command = pageCommand(key)
|
|
132
|
-
if (key === "j" || key === "<Down>") { transcript?.move("next", amount); return }
|
|
133
|
-
else if (key === "k" || key === "<Up>") { transcript?.move("previous", amount); return }
|
|
134
|
-
else if (key === "G") { transcript?.latest(); return }
|
|
135
|
-
else if (key === "g" && previous === "g") command = "session.first"
|
|
136
|
-
else if (key === "y" && previous === "y") {
|
|
137
|
-
const message = selected()
|
|
138
|
-
if (message) {
|
|
139
|
-
yankID = message.id
|
|
140
|
-
yankTimer = setTimeout(clearYankFlash, YANK_FLASH_MS)
|
|
141
|
-
context.renderer.requestRender()
|
|
142
|
-
void copy(message.text)
|
|
143
|
-
}
|
|
144
|
-
} else if (key === "g" || key === "y") prefix = key
|
|
145
|
-
else if (key === "<CR>") {
|
|
146
|
-
transcript?.sync()
|
|
147
|
-
if (transcript?.toggle()) return
|
|
148
|
-
const message = selected()
|
|
149
|
-
if (message) openMessage(message)
|
|
150
|
-
}
|
|
151
|
-
if (command) {
|
|
152
|
-
transcript?.followViewport()
|
|
153
|
-
for (let index = 0; index < amount; index++) context.keymap.dispatch(command)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
createEffect(() => {
|
|
158
|
-
const route = context.ui.router.current()
|
|
159
|
-
if (active() && (route.type !== "session" || route.sessionID !== sessionID)) untrack(close)
|
|
160
|
-
})
|
|
161
|
-
onMount(() => context.renderer.keyInput.prependListener("keypress", onKey))
|
|
162
|
-
onCleanup(() => {
|
|
163
|
-
disposed = true
|
|
164
|
-
close()
|
|
165
|
-
if (noticeTimer) clearTimeout(noticeTimer)
|
|
166
|
-
context.renderer.keyInput.off("keypress", onKey)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
active, enter, close,
|
|
171
|
-
Status() {
|
|
172
|
-
return <text fg={context.theme.text.feedback.info.base}>
|
|
173
|
-
{notice() || `SESSION · Enter open · yy copy · ${config.sessionKey} prompt`}
|
|
174
|
-
</text>
|
|
175
|
-
},
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function MessageReader(props: {
|
|
180
|
-
context: Context
|
|
181
|
-
config: VimConfig
|
|
182
|
-
message: TranscriptItem
|
|
183
|
-
offset: number
|
|
184
|
-
copy: (text: string) => void
|
|
185
|
-
notice: () => string
|
|
186
|
-
remember: (offset: number) => void
|
|
187
|
-
back: () => void
|
|
188
|
-
close: () => void
|
|
189
|
-
}) {
|
|
190
|
-
const context = props.context
|
|
191
|
-
context.ui.dialog.set({ size: "large", centered: true })
|
|
192
|
-
const dimensions = useTerminalDimensions()
|
|
193
|
-
const theme = () => context.theme.surface("dialog")
|
|
194
|
-
const background = () => theme().background.raised.high
|
|
195
|
-
const state = createVimState("normal")
|
|
196
|
-
const [line, setLine] = createSignal(1)
|
|
197
|
-
let input!: TextareaRenderable
|
|
198
|
-
const adapter = createVimeeAdapter(state, props.config, () => {}, { readOnly: true, onYank: props.copy })
|
|
199
|
-
const editorContext: PromptContext = {
|
|
200
|
-
api: {
|
|
201
|
-
renderer: { get currentFocusedRenderable() { return input } },
|
|
202
|
-
keymap: { dispatchCommand: () => ({ ok: false }) },
|
|
203
|
-
theme: { get current() { return {
|
|
204
|
-
warning: theme().text.feedback.warning.base,
|
|
205
|
-
info: theme().text.feedback.info.base,
|
|
206
|
-
background: background(),
|
|
207
|
-
} } },
|
|
208
|
-
},
|
|
209
|
-
prompt: () => ({
|
|
210
|
-
get current() { return { input: input.plainText, mode: "normal", parts: [] } },
|
|
211
|
-
set() {}, submit() {}, blur() {},
|
|
212
|
-
}),
|
|
213
|
-
requestRender: () => context.renderer.requestRender(),
|
|
214
|
-
}
|
|
215
|
-
const onKey = (event: KeyEvent) => {
|
|
216
|
-
if (context.renderer.currentFocusedEditor !== input) return
|
|
217
|
-
const key = readerKey(context, event, props.config.sessionKey, "modal")
|
|
218
|
-
if (!key) return
|
|
219
|
-
event.preventDefault()
|
|
220
|
-
event.stopPropagation()
|
|
221
|
-
if (key === props.config.sessionKey && state.mode() === "normal" && !adapter.isPending()) { props.close(); return }
|
|
222
|
-
// The host's Ctrl+C clears the editor unless we intercept it.
|
|
223
|
-
if (key === "<C-c>") { props.back(); return }
|
|
224
|
-
if ((key === "<Esc>" || key === "<C-[>") && state.mode() === "normal" && !adapter.isPending()) {
|
|
225
|
-
props.back()
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
if (pageCommand(key)) {
|
|
229
|
-
const down = key === "<C-d>" || key === "<C-f>" || key === "<PageDown>"
|
|
230
|
-
const half = key === "<C-d>" || key === "<C-u>"
|
|
231
|
-
const rows = Math.max(1, Math.floor(input.height / (half ? 2 : 1)))
|
|
232
|
-
for (let row = 0; row < rows; row++) adapter.handle({ ...event, ctrl: false } as KeyEvent, down ? "j" : "k", editorContext)
|
|
233
|
-
} else adapter.handle(event, key, editorContext)
|
|
234
|
-
setLine(input.logicalCursor.row + 1)
|
|
235
|
-
}
|
|
236
|
-
onMount(() => {
|
|
237
|
-
input.cursorOffset = Math.min(props.offset, displayWidth(input.plainText))
|
|
238
|
-
adapter.attach(editorContext)
|
|
239
|
-
setLine(input.logicalCursor.row + 1)
|
|
240
|
-
input.focus()
|
|
241
|
-
context.renderer.keyInput.prependListener("keypress", onKey)
|
|
242
|
-
})
|
|
243
|
-
onCleanup(() => {
|
|
244
|
-
props.remember(input.cursorOffset)
|
|
245
|
-
context.renderer.keyInput.off("keypress", onKey)
|
|
246
|
-
adapter.cleanup()
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
function modeLabel() {
|
|
250
|
-
if (state.mode() === "visual") return "VISUAL"
|
|
251
|
-
if (state.mode() === "visual-line") return "VISUAL LINE"
|
|
252
|
-
return "MESSAGE"
|
|
253
|
-
}
|
|
254
|
-
return (
|
|
255
|
-
<box id="vim-message-reader" gap={1}>
|
|
256
|
-
<box paddingLeft={2} paddingRight={2} flexDirection="row" justifyContent="space-between">
|
|
257
|
-
<text fg={theme().text.base}><b>{props.message.author}</b></text>
|
|
258
|
-
<text id="vim-message-close" fg={theme().text.muted} onMouseUp={props.back}>esc</text>
|
|
259
|
-
</box>
|
|
260
|
-
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} backgroundColor={background()}>
|
|
261
|
-
<textarea id="vim-session-message" ref={(value: TextareaRenderable) => {
|
|
262
|
-
input = value
|
|
263
|
-
input.handleKeyPress = () => true
|
|
264
|
-
input.handlePaste = () => {}
|
|
265
|
-
}} initialValue={props.message.text} minHeight={1} maxHeight={Math.max(1, Math.min(20, dimensions().height - 10))}
|
|
266
|
-
wrapMode="word" showCursor cursorStyle={props.config.cursorStyles.normal} textColor={theme().text.base}
|
|
267
|
-
backgroundColor={background()} focusedBackgroundColor={background()} focusedTextColor={theme().text.base} />
|
|
268
|
-
</box>
|
|
269
|
-
<box paddingLeft={2} paddingRight={2} paddingBottom={1} flexDirection="row" flexWrap="wrap" columnGap={3}>
|
|
270
|
-
<text fg={theme().text.muted}>{modeLabel()} · {line()}/{input?.lineCount ?? 1}</text>
|
|
271
|
-
<text fg={theme().text.muted}>
|
|
272
|
-
{props.notice() || (state.mode() !== "normal" ? "y copy · Esc cancel" : `v select · V lines · ${props.config.sessionKey} prompt`)}
|
|
273
|
-
</text>
|
|
274
|
-
</box>
|
|
275
|
-
</box>
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function readerKey(context: Context, event: KeyEvent, sessionKey: string, mode = SESSION_MODE) {
|
|
280
|
-
if (event.defaultPrevented || context.keymap.mode.current() !== mode) return
|
|
281
|
-
if (context.keymap.pending().length || event.super || event.meta) return
|
|
282
|
-
const key = keyNotation(event)
|
|
283
|
-
if (key && (key === sessionKey || !event.ctrl || key === "<C-[>" || pageCommand(key) || (mode === "modal" && key === "<C-c>"))) return key
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function pageCommand(key: string) {
|
|
287
|
-
if (key === "<C-d>") return "session.half.page.down"
|
|
288
|
-
if (key === "<C-u>") return "session.half.page.up"
|
|
289
|
-
if (key === "<C-f>" || key === "<PageDown>") return "session.page.down"
|
|
290
|
-
if (key === "<C-b>" || key === "<PageUp>") return "session.page.up"
|
|
291
|
-
}
|