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
@@ -1,198 +0,0 @@
1
- import type { CursorPosition } from "@vimee/core"
2
- import type { EditBufferLike } from "./actions"
3
- import { createGraphemeCodec, type GraphemeCodec } from "./graphemes"
4
-
5
- const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
6
-
7
- function graphemeWidth(value: string) {
8
- // Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.
9
- // OpenTUI's edit buffer gives tabs a fixed two-column width by default.
10
- if (value === "\t") return 2
11
- return value === "\n" ? 1 : Bun.stringWidth(value)
12
- }
13
-
14
- export function charToDisplay(text: string, charIndex: number): number {
15
- let width = 0
16
- const limit = Math.max(0, Math.min(charIndex, text.length))
17
- for (const part of graphemes.segment(text)) {
18
- if (part.index + part.segment.length > limit) break
19
- width += graphemeWidth(part.segment)
20
- }
21
- return width
22
- }
23
-
24
- export function displayToChar(text: string, displayOffset: number): number {
25
- let width = 0
26
- for (const part of graphemes.segment(text)) {
27
- const next = width + graphemeWidth(part.segment)
28
- if (next > displayOffset) return part.index
29
- width = next
30
- }
31
- return text.length
32
- }
33
-
34
- export function displayWidth(text: string): number {
35
- return charToDisplay(text, text.length)
36
- }
37
-
38
- export type PromptMap = {
39
- codec: GraphemeCodec
40
- hostText: string
41
- vimText: string
42
- hostToVim: number[]
43
- vimToHost: Array<number | undefined>
44
- }
45
-
46
- export function createPromptMap(hostText: string, input?: EditBufferLike, codec = createGraphemeCodec()): PromptMap {
47
- return buildPromptMap(hostText, input ? visualWrapOffsets(input, hostText) : [], codec)
48
- }
49
-
50
- export function derivePromptMap(map: PromptMap, vimText: string): PromptMap {
51
- const prefix = commonPrefix(map.vimText, vimText)
52
- const suffix = commonSuffix(map.vimText, vimText, prefix)
53
- const inserted = vimText.slice(prefix, vimText.length - suffix)
54
- const synthetic = new Set<number>()
55
- let hostText = ""
56
-
57
- for (let vimOffset = 0; vimOffset < vimText.length; vimOffset++) {
58
- const oldOffset = previousOffset(map, vimOffset, vimText.length, prefix, suffix)
59
- if (oldOffset !== undefined && map.vimToHost[oldOffset] === undefined && preserveSynthetic(vimOffset, prefix, suffix, vimText.length, inserted)) {
60
- synthetic.add(vimOffset)
61
- continue
62
- }
63
-
64
- hostText += map.codec.decode(vimText[vimOffset])
65
- }
66
-
67
- return buildPromptMapFromSynthetic(hostText, vimText, synthetic, map.codec)
68
- }
69
-
70
- function preserveSynthetic(vimOffset: number, prefix: number, suffix: number, vimLength: number, inserted: string) {
71
- if (!inserted.includes("\n")) return true
72
- return vimOffset !== prefix - 1 && vimOffset !== vimLength - suffix
73
- }
74
-
75
- export function hostPosition(map: PromptMap, hostDisplayOffset: number): CursorPosition {
76
- const charIdx = displayToChar(map.hostText, hostDisplayOffset)
77
- return positionFromOffset(map.vimText, map.hostToVim[clamp(charIdx, 0, map.hostText.length)] ?? 0)
78
- }
79
-
80
- export function hostOffset(map: PromptMap, position: CursorPosition, bias: "previous" | "next" = "next") {
81
- const charIdx = hostOffsetFromVimOffset(map, offsetFromPosition(map.vimText, position), bias)
82
- return charToDisplay(map.hostText, charIdx)
83
- }
84
-
85
- function buildPromptMap(hostText: string, wraps: number[], codec: GraphemeCodec): PromptMap {
86
- const hostToVim: number[] = []
87
- const vimToHost: Array<number | undefined> = []
88
- const wrapOffsets = new Set(wraps.filter((offset) => offset > 0 && offset < hostText.length && hostText[offset - 1] !== "\n"))
89
- let vimText = ""
90
- let vimOffset = 0
91
-
92
- for (const { index: hostOffset, segment } of graphemes.segment(hostText)) {
93
- if (wrapOffsets.has(hostOffset)) {
94
- vimText += "\n"
95
- vimToHost[vimOffset] = undefined
96
- vimOffset++
97
- }
98
- for (let index = hostOffset; index < hostOffset + segment.length; index++) hostToVim[index] = vimOffset
99
- vimText += codec.encode(segment)
100
- vimToHost[vimOffset] = hostOffset
101
- vimOffset++
102
- }
103
-
104
- hostToVim[hostText.length] = vimOffset
105
- return { hostText, vimText, hostToVim, vimToHost, codec }
106
- }
107
-
108
- function buildPromptMapFromSynthetic(hostText: string, vimText: string, synthetic: Set<number>, codec: GraphemeCodec): PromptMap {
109
- const hostToVim: number[] = []
110
- const vimToHost: Array<number | undefined> = []
111
- let hostOffset = 0
112
-
113
- for (let vimOffset = 0; vimOffset < vimText.length; vimOffset++) {
114
- if (synthetic.has(vimOffset)) {
115
- vimToHost[vimOffset] = undefined
116
- continue
117
- }
118
-
119
- vimToHost[vimOffset] = hostOffset
120
- const length = codec.decode(vimText[vimOffset]).length
121
- for (let index = 0; index < length; index++) hostToVim[hostOffset++] = vimOffset
122
- }
123
-
124
- hostToVim[hostText.length] = vimText.length
125
- return { hostText, vimText, hostToVim, vimToHost, codec }
126
- }
127
-
128
- function previousOffset(map: PromptMap, vimOffset: number, vimLength: number, prefix: number, suffix: number) {
129
- if (vimOffset < prefix) return vimOffset
130
- if (vimOffset >= vimLength - suffix) return map.vimText.length - (vimLength - vimOffset)
131
- return undefined
132
- }
133
-
134
- function visualWrapOffsets(input: EditBufferLike, text: string) {
135
- const wraps: number[] = []
136
- const lines = input.editorView?.getLogicalLineInfo?.()
137
- if (!lines) return wraps
138
- let line = 1
139
- let offset = 0
140
- for (const part of graphemes.segment(text)) {
141
- while (line < lines.lineStartCols.length && lines.lineStartCols[line] <= offset) {
142
- if (lines.lineWraps[line] > 0) wraps.push(part.index)
143
- line++
144
- }
145
- offset += graphemeWidth(part.segment)
146
- }
147
- return wraps
148
- }
149
-
150
- function hostOffsetFromVimOffset(map: PromptMap, vimOffset: number, bias: "previous" | "next") {
151
- const offset = clamp(vimOffset, 0, map.vimText.length)
152
- if (offset === map.vimText.length) return map.hostText.length
153
-
154
- const host = map.vimToHost[offset]
155
- if (host !== undefined) return host
156
-
157
- if (bias === "previous") {
158
- for (let previous = offset - 1; previous >= 0; previous--) {
159
- const previousHost = map.vimToHost[previous]
160
- if (previousHost !== undefined) return previousHost
161
- }
162
- }
163
-
164
- for (let next = offset + 1; next < map.vimToHost.length; next++) {
165
- const nextHost = map.vimToHost[next]
166
- if (nextHost !== undefined) return nextHost
167
- }
168
- return map.hostText.length
169
- }
170
-
171
- function commonPrefix(left: string, right: string) {
172
- let index = 0
173
- while (index < left.length && index < right.length && left[index] === right[index]) index++
174
- return index
175
- }
176
-
177
- function commonSuffix(left: string, right: string, prefix: number) {
178
- let length = 0
179
- while (length + prefix < left.length && length + prefix < right.length && left[left.length - length - 1] === right[right.length - length - 1]) length++
180
- return length
181
- }
182
-
183
- function positionFromOffset(text: string, offset: number): CursorPosition {
184
- const lines = text.slice(0, offset).split("\n")
185
- return { line: lines.length - 1, col: lines[lines.length - 1]?.length ?? 0 }
186
- }
187
-
188
- function offsetFromPosition(text: string, position: CursorPosition) {
189
- const lines = text.split("\n")
190
- const line = clamp(position.line, 0, Math.max(0, lines.length - 1))
191
- let offset = 0
192
- for (let index = 0; index < line; index++) offset += lines[index].length + 1
193
- return offset + clamp(position.col, 0, lines[line]?.length ?? 0)
194
- }
195
-
196
- function clamp(value: number, min: number, max: number) {
197
- return Math.max(min, Math.min(max, value))
198
- }
@@ -1,18 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { createVimState } from "./state"
3
-
4
- describe("vim state", () => {
5
- test("isolates listener errors", () => {
6
- const events: string[] = []
7
- const state = createVimState("normal", (event) => events.push(event))
8
- state.subscribe(() => {
9
- throw new Error("stale renderable")
10
- })
11
- state.subscribe(() => events.push("listener.complete"))
12
-
13
- expect(() => state.setMode("insert")).not.toThrow()
14
- expect(state.mode()).toBe("insert")
15
- expect(events).toContain("state.listener.error")
16
- expect(events).toContain("listener.complete")
17
- })
18
- })
@@ -1,46 +0,0 @@
1
- // Keep this module as TSX so OpenCode's plugin loader rewrites its Solid import
2
- // to the host runtime shared by the footer and cursor effects.
3
- import { createSignal } from "solid-js"
4
- import type { VimLog } from "./log"
5
-
6
- export type VimMode = "normal" | "insert" | "visual" | "visual-line"
7
- export type VimStateListener = () => void
8
-
9
- export function createVimState(defaultMode: VimMode, log: VimLog = () => {}) {
10
- const [mode, setMode] = createSignal<VimMode>(defaultMode)
11
- const [pending, setPending] = createSignal("")
12
- const listeners = new Set<VimStateListener>()
13
-
14
- log("state.init", { mode: defaultMode })
15
-
16
- return {
17
- mode,
18
- setMode(next: VimMode) {
19
- const changed = mode() !== next
20
- if (changed) log("state.mode", { from: mode(), to: next })
21
- setMode(next)
22
- if (changed) notify()
23
- },
24
- pending,
25
- setPending(next: string) {
26
- const changed = pending() !== next
27
- if (changed) log("state.pending", { from: pending(), to: next })
28
- setPending(next)
29
- if (changed) notify()
30
- },
31
- subscribe(listener: VimStateListener) {
32
- listeners.add(listener)
33
- return () => listeners.delete(listener)
34
- },
35
- }
36
-
37
- function notify() {
38
- for (const listener of listeners) {
39
- try {
40
- listener()
41
- } catch (error) {
42
- log("state.listener.error", { error: error instanceof Error ? error.message : String(error) })
43
- }
44
- }
45
- }
46
- }
@@ -1,130 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TextRenderable } from "@opentui/core"
3
- import { onCleanup } from "solid-js"
4
- import type { Accessor } from "solid-js"
5
- import type { PromptContext } from "../../prompt/types"
6
- import type { VimLog } from "./log"
7
- import type { VimMode, VimStateListener } from "./state"
8
-
9
- const STATUS_SYNC_MS = 50
10
-
11
- type VimStatusProps = {
12
- mode: Accessor<VimMode>
13
- pending: Accessor<string | undefined>
14
- subscribe: (listener: VimStateListener) => () => void
15
- enabled: Accessor<boolean>
16
- theme: PromptContext["api"]["theme"]["current"]
17
- pendingDisplayDelay?: number
18
- disabled?: boolean
19
- log?: VimLog
20
- requestRender?: () => void
21
- }
22
-
23
- export function VimStatus(props: VimStatusProps) {
24
- let modeText: TextRenderable | undefined
25
- let pendingText: TextRenderable | undefined
26
- let displayedMode = props.mode()
27
- let displayedPending: string | undefined
28
- let displayedEnabled = props.enabled()
29
- let pendingTimer: ReturnType<typeof setTimeout> | undefined
30
- let scheduledPending: string | undefined
31
-
32
- const sync = () => {
33
- const nextMode = props.mode()
34
- const nextPending = props.pending()
35
- let changed = false
36
-
37
- if (displayedMode !== nextMode) {
38
- props.log?.("status.sync", { fromMode: displayedMode, toMode: nextMode, fromPending: displayedPending, toPending: displayedPending })
39
- displayedMode = nextMode
40
- changed = true
41
- }
42
-
43
- const nextEnabled = props.enabled()
44
- if (displayedEnabled !== nextEnabled) {
45
- props.log?.("status.enabled", { from: displayedEnabled, to: nextEnabled })
46
- displayedEnabled = nextEnabled
47
- changed = true
48
- }
49
-
50
- if (changed) updateModeText()
51
- syncPending(nextPending)
52
- if (changed) props.requestRender?.()
53
- }
54
-
55
- const unsubscribe = props.subscribe(sync)
56
- const timer = setInterval(sync, STATUS_SYNC_MS)
57
- onCleanup(() => {
58
- unsubscribe()
59
- clearInterval(timer)
60
- if (pendingTimer) clearTimeout(pendingTimer)
61
- modeText = undefined
62
- pendingText = undefined
63
- })
64
-
65
- return (
66
- <box paddingLeft={1} paddingRight={1} flexDirection="row">
67
- <text ref={(ref: TextRenderable) => { pendingText = ref; updatePendingText() }} fg={props.theme.info}>{displayedPending ? `${displayedPending} ` : ""}</text>
68
- <text ref={(ref: TextRenderable) => { modeText = ref; updateModeText() }} fg={modeColor(displayedMode, displayedEnabled, props.theme, props.disabled)}>{displayedEnabled ? modeLabel(displayedMode) : ""}</text>
69
- </box>
70
- )
71
-
72
- function updateModeText() {
73
- if (!modeText) return
74
- modeText.content = displayedEnabled ? modeLabel(displayedMode) : ""
75
- modeText.fg = modeColor(displayedMode, displayedEnabled, props.theme, props.disabled)
76
- }
77
-
78
- function updatePendingText() {
79
- if (!pendingText) return
80
- pendingText.content = displayedEnabled && displayedPending ? `${displayedPending} ` : ""
81
- }
82
-
83
- function syncPending(nextPending: string | undefined) {
84
- if (!nextPending) {
85
- if (pendingTimer) clearTimeout(pendingTimer)
86
- pendingTimer = undefined
87
- scheduledPending = undefined
88
- setDisplayedPending(undefined)
89
- return
90
- }
91
-
92
- if (displayedPending === nextPending) return
93
- if (scheduledPending === nextPending) return
94
-
95
- if (pendingTimer) clearTimeout(pendingTimer)
96
- scheduledPending = nextPending
97
- const delay = props.pendingDisplayDelay ?? 120
98
- if (delay <= 0) {
99
- scheduledPending = undefined
100
- setDisplayedPending(nextPending)
101
- return
102
- }
103
-
104
- pendingTimer = setTimeout(() => {
105
- pendingTimer = undefined
106
- scheduledPending = undefined
107
- if (props.pending() === nextPending) setDisplayedPending(nextPending)
108
- }, delay)
109
- }
110
-
111
- function setDisplayedPending(nextPending: string | undefined) {
112
- if (displayedPending === nextPending) return
113
- props.log?.("status.sync", { fromMode: displayedMode, toMode: displayedMode, fromPending: displayedPending, toPending: nextPending })
114
- displayedPending = nextPending
115
- updatePendingText()
116
- props.requestRender?.()
117
- }
118
- }
119
-
120
- function modeColor(mode: VimMode, enabled: boolean, theme: PromptContext["api"]["theme"]["current"], disabled?: boolean) {
121
- if (!enabled) return undefined
122
- if (disabled) return theme.textMuted
123
- return mode === "insert" ? theme.success : theme.warning
124
- }
125
-
126
- function modeLabel(mode: VimMode) {
127
- if (mode === "visual") return "VISUAL"
128
- if (mode === "visual-line") return "VISUAL LINE"
129
- return mode === "normal" ? "NORMAL" : "INSERT"
130
- }
@@ -1,211 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import type { KeyEvent } from "@opentui/core"
3
- import type { PromptContext } from "./actions"
4
- import { createVimConfig } from "./config"
5
- import type { VimLog } from "./log"
6
- import { createVimState, type VimMode } from "./state"
7
- import { createVimeeAdapter } from "./vimee"
8
-
9
- describe("vim command keymaps", () => {
10
- test("does not restore stale cursor state after a normal command", () => {
11
- const fixture = createFixture("normal", "command:test.run")
12
- fixture.dispatch = () => {
13
- fixture.input.plainText = "updated"
14
- fixture.input.cursorOffset = 7
15
- }
16
-
17
- expect(fixture.handle()).toBe(true)
18
- expect(fixture.commands).toEqual(["test.run"])
19
- expect(fixture.input.cursorOffset).toBe(7)
20
- })
21
-
22
- test("uses the host history boundary and restores display width", () => {
23
- const fixture = createFixture("normal", "command:prompt.history.next", "中")
24
- let boundary = -1
25
- fixture.dispatch = () => {
26
- boundary = fixture.input.cursorOffset
27
- fixture.input.plainText = "中中"
28
- fixture.input.cursorOffset = 2
29
- }
30
-
31
- expect(fixture.handle()).toBe(true)
32
- expect(boundary).toBe(1)
33
- expect(fixture.input.cursorOffset).toBe(4)
34
- })
35
-
36
- test("dispatches commands in insert mode", () => {
37
- const fixture = createFixture("insert", "command:test.run")
38
-
39
- expect(fixture.handle()).toBe(true)
40
- expect(fixture.commands).toEqual(["test.run"])
41
- })
42
-
43
- test("rejects an empty command name", () => {
44
- const logs: Array<[string, unknown]> = []
45
- createFixture("normal", "command: ", "", (event, data) => logs.push([event, data]))
46
-
47
- expect(logs.some(([event]) => event === "vimee.keymap.invalid")).toBe(true)
48
- })
49
- })
50
-
51
- describe("vim enter keymaps", () => {
52
- test("submits unmapped Enter in normal mode without moving the cursor", () => {
53
- const fixture = createFixture("normal", undefined, "中")
54
- fixture.input.cursorOffset = 2
55
-
56
- expect(fixture.handle("<CR>")).toBe(true)
57
- expect(fixture.submissions).toHaveLength(1)
58
- expect(fixture.input.cursorOffset).toBe(2)
59
- })
60
-
61
- test("passes unmapped Enter through in insert mode", () => {
62
- const fixture = createFixture("insert", undefined)
63
-
64
- expect(fixture.handle("<CR>")).toBe(false)
65
- expect(fixture.submissions).toHaveLength(0)
66
- })
67
-
68
- test("does not submit a matched mapping with no immediate actions", () => {
69
- const fixture = createFixture("normal", "d", "text", () => {}, "<CR>")
70
-
71
- expect(fixture.handle("<CR>")).toBe(true)
72
- expect(fixture.submissions).toHaveLength(0)
73
- })
74
-
75
- test("runs an Enter command mapping", () => {
76
- const fixture = createFixture("normal", "command:test.run", "", () => {}, "<CR>")
77
-
78
- expect(fixture.handle("<CR>")).toBe(true)
79
- expect(fixture.commands).toEqual(["test.run"])
80
- expect(fixture.submissions).toHaveLength(0)
81
- })
82
-
83
- test("applies mode and submit mappings", () => {
84
- const normal = createFixture("normal", "insert", "", () => {}, "<CR>")
85
- const insert = createFixture("insert", "submit", "", () => {}, "<CR>")
86
-
87
- expect(normal.handle("<CR>")).toBe(true)
88
- expect(normal.handle("a")).toBe(false)
89
- expect(insert.handle("<CR>")).toBe(true)
90
- expect(insert.submissions).toHaveLength(1)
91
- })
92
-
93
- test("allows Enter to finish a multi-key mapping", () => {
94
- const fixture = createFixture("normal", "command:test.run", "", () => {}, "g<CR>")
95
-
96
- expect(fixture.handle("g")).toBe(true)
97
- expect(fixture.handle("<CR>")).toBe(true)
98
- expect(fixture.commands).toEqual(["test.run"])
99
- expect(fixture.submissions).toHaveLength(0)
100
- })
101
-
102
- test("rejects mappings that defer Enter", () => {
103
- const logs: Array<[string, unknown]> = []
104
- const fixture = createFixture("normal", "command:test.run", "", (event, data) => logs.push([event, data]), "<CR>x")
105
-
106
- expect(fixture.handle("<CR>")).toBe(true)
107
- expect(fixture.submissions).toHaveLength(1)
108
- expect(logs.some(([event]) => event === "vimee.keymap.invalid")).toBe(true)
109
- })
110
- })
111
-
112
- describe("vim prompt history", () => {
113
- test("starts from an empty prompt and continues through history", () => {
114
- const fixture = createFixture("normal", undefined, "")
115
- const entries = ["newest", "older", ""]
116
- fixture.dispatch = (command) => {
117
- fixture.input.plainText = entries.shift() ?? ""
118
- fixture.input.cursorOffset = command === "prompt.history.previous" ? 0 : fixture.input.plainText.length
119
- }
120
-
121
- expect(fixture.handle("k")).toBe(true)
122
- expect(fixture.handle("k")).toBe(true)
123
- expect(fixture.handle("j")).toBe(true)
124
- expect(fixture.commands).toEqual([
125
- "prompt.history.previous",
126
- "prompt.history.previous",
127
- "prompt.history.next",
128
- ])
129
- })
130
-
131
- test("keeps normal movement for a nonempty prompt", () => {
132
- const fixture = createFixture("normal", undefined, "text")
133
-
134
- expect(fixture.handle("k")).toBe(true)
135
- expect(fixture.handle("j")).toBe(true)
136
- expect(fixture.commands).toEqual([])
137
- })
138
-
139
- test("stops history navigation after another normal key", () => {
140
- const fixture = createFixture("normal", undefined, "")
141
- fixture.dispatch = () => {
142
- fixture.input.plainText = "recalled"
143
- }
144
-
145
- expect(fixture.handle("k")).toBe(true)
146
- expect(fixture.handle("h")).toBe(true)
147
- expect(fixture.handle("k")).toBe(true)
148
- expect(fixture.commands).toEqual(["prompt.history.previous"])
149
- })
150
-
151
- test("prefers configured keymaps", () => {
152
- const fixture = createFixture("normal", "command:test.run", "", () => {}, "k")
153
-
154
- expect(fixture.handle()).toBe(true)
155
- expect(fixture.commands).toEqual(["test.run"])
156
- })
157
-
158
- test("completes a pending keymap before history navigation", () => {
159
- const fixture = createFixture("normal", "command:test.run", "", () => {}, "gk")
160
-
161
- expect(fixture.handle("g")).toBe(true)
162
- expect(fixture.handle("k")).toBe(true)
163
- expect(fixture.commands).toEqual(["test.run"])
164
- })
165
- })
166
-
167
- function createFixture(mode: VimMode, action: string | undefined, text = "text", log: VimLog = () => {}, mappedKey = "Q") {
168
- const input = {
169
- plainText: text,
170
- cursorOffset: 0,
171
- visualCursor: { visualRow: 0, visualCol: 0, offset: 0 },
172
- moveCursorLeft: () => false,
173
- }
174
- const commands: string[] = []
175
- const submissions: true[] = []
176
- const prompt = {
177
- current: { input: text, mode: "normal", parts: [] },
178
- set() {},
179
- submit() {
180
- submissions.push(true)
181
- },
182
- blur() {},
183
- }
184
- const fixture = {
185
- input,
186
- commands,
187
- submissions,
188
- dispatch: (_command: string) => {},
189
- handle: (key = mappedKey) => adapter.handle({ name: key } as KeyEvent, key, ctx),
190
- }
191
- const ctx = {
192
- api: {
193
- renderer: { currentFocusedRenderable: input },
194
- keymap: {
195
- dispatchCommand(command: string) {
196
- commands.push(command)
197
- fixture.dispatch(command)
198
- return { ok: true as const }
199
- },
200
- },
201
- },
202
- prompt: () => prompt,
203
- requestRender() {},
204
- } as unknown as PromptContext
205
- const config = createVimConfig({
206
- defaultMode: mode,
207
- keymaps: action ? { [mode]: { [mappedKey]: action } } : undefined,
208
- })
209
- const adapter = createVimeeAdapter(createVimState(mode), config, log)
210
- return fixture
211
- }