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/modules/vim/map.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { CursorPosition } from "@vimee/core"
|
|
2
|
-
import { createGraphemeCodec } from "./graphemes"
|
|
3
|
-
|
|
4
|
-
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
5
|
-
|
|
6
|
-
function graphemeWidth(value: string) {
|
|
7
|
-
// Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.
|
|
8
|
-
// OpenTUI's edit buffer gives tabs a fixed two-column width by default.
|
|
9
|
-
if (value === "\t") return 2
|
|
10
|
-
return value === "\n" ? 1 : Bun.stringWidth(value)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function charToDisplay(text: string, charIndex: number): number {
|
|
14
|
-
let width = 0
|
|
15
|
-
const limit = Math.max(0, Math.min(charIndex, text.length))
|
|
16
|
-
for (const part of graphemes.segment(text)) {
|
|
17
|
-
if (part.index + part.segment.length > limit) break
|
|
18
|
-
width += graphemeWidth(part.segment)
|
|
19
|
-
}
|
|
20
|
-
return width
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function displayToChar(text: string, displayOffset: number): number {
|
|
24
|
-
let width = 0
|
|
25
|
-
for (const part of graphemes.segment(text)) {
|
|
26
|
-
const next = width + graphemeWidth(part.segment)
|
|
27
|
-
if (next > displayOffset) return part.index
|
|
28
|
-
width = next
|
|
29
|
-
}
|
|
30
|
-
return text.length
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function displayWidth(text: string): number {
|
|
34
|
-
return charToDisplay(text, text.length)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type PromptMap = {
|
|
38
|
-
hostText: string
|
|
39
|
-
vimText: string
|
|
40
|
-
hostToVim: number[]
|
|
41
|
-
vimToHost: number[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function createPromptMap(hostText: string, codec = createGraphemeCodec()): PromptMap {
|
|
45
|
-
const hostToVim: number[] = []
|
|
46
|
-
const vimToHost: number[] = []
|
|
47
|
-
let vimText = ""
|
|
48
|
-
let vimOffset = 0
|
|
49
|
-
|
|
50
|
-
for (const { index: hostOffset, segment } of graphemes.segment(hostText)) {
|
|
51
|
-
for (let index = hostOffset; index < hostOffset + segment.length; index++) hostToVim[index] = vimOffset
|
|
52
|
-
vimText += codec.encode(segment)
|
|
53
|
-
vimToHost[vimOffset] = hostOffset
|
|
54
|
-
vimOffset++
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
hostToVim[hostText.length] = vimOffset
|
|
58
|
-
vimToHost[vimOffset] = hostText.length
|
|
59
|
-
return { hostText, vimText, hostToVim, vimToHost }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function hostPosition(map: PromptMap, hostDisplayOffset: number): CursorPosition {
|
|
63
|
-
const charIdx = displayToChar(map.hostText, hostDisplayOffset)
|
|
64
|
-
return positionFromOffset(map.vimText, map.hostToVim[clamp(charIdx, 0, map.hostText.length)])
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function hostOffset(map: PromptMap, position: CursorPosition) {
|
|
68
|
-
return hostFromVimOffset(map, offsetFromPosition(map.vimText, position))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function hostFromVimOffset(map: PromptMap, offset: number) {
|
|
72
|
-
return charToDisplay(map.hostText, map.vimToHost[clamp(offset, 0, map.vimText.length)])
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function positionFromOffset(text: string, offset: number): CursorPosition {
|
|
76
|
-
const lines = text.slice(0, offset).split("\n")
|
|
77
|
-
return { line: lines.length - 1, col: lines[lines.length - 1]?.length ?? 0 }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function offsetFromPosition(text: string, position: CursorPosition) {
|
|
81
|
-
const lines = text.split("\n")
|
|
82
|
-
const line = clamp(position.line, 0, Math.max(0, lines.length - 1))
|
|
83
|
-
let offset = 0
|
|
84
|
-
for (let index = 0; index < line; index++) offset += lines[index].length + 1
|
|
85
|
-
return offset + clamp(position.col, 0, lines[line]?.length ?? 0)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function clamp(value: number, min: number, max: number) {
|
|
89
|
-
return Math.max(min, Math.min(max, value))
|
|
90
|
-
}
|
|
@@ -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
|
-
}
|
package/src/modules/vim/view.tsx
DELETED
|
@@ -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
|
-
}
|