opencode-vim 0.0.9 → 0.0.11

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Adds Vim-style insert and normal mode editing to the OpenCode prompt.
4
4
 
5
- ![Demo](./assets/demo.gif)
5
+ ![Demo](./assets/demo2.gif)
6
6
 
7
7
  ## Installation
8
8
 
@@ -12,4 +12,17 @@ Install from the CLI:
12
12
  opencode plugin opencode-vim@latest --global
13
13
  ```
14
14
 
15
+ ## Supported Keys
16
+
17
+ | Key | Behavior |
18
+ | --- | --- |
19
+ | `<Esc>`, `<C-[>` | Enter normal mode |
20
+ | `i`, `a`, `A`, `o`, `O` | Return to insert mode |
21
+ | `h`, `j`, `k`, `l`, `w`, `b`, `e`, `$`, `0` | Move through the prompt |
22
+ | `x`, `d`, `c`, `y`, `p`, `u`, `<C-r>` | Edit, yank, paste, undo, redo |
23
+ | `v`, `V` | Visual and visual-line selection |
24
+ | `3w`, `diw`, `ci"`, `yiq`, `dip`, `yib` | Counts and text objects |
25
+ | `<CR>` in normal mode | Submit the prompt |
26
+ | `/vim` | Toggle Vim mode on or off |
27
+
15
28
  See [docs/configuration.md](./docs/configuration.md) for configuration options and keymap examples.
package/assets/demo.gif CHANGED
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-vim",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "exports": {
5
5
  "./tui": {
6
6
  "import": "./tui.tsx"
@@ -2,6 +2,7 @@
2
2
  import type { KeyEvent, ParsedKey } from "@opentui/core"
3
3
  import { useKeyboard } from "@opentui/solid"
4
4
  import { onCleanup } from "solid-js"
5
+ import type { Accessor } from "solid-js"
5
6
  import type { PromptContext, PromptModule } from "../../prompt/types"
6
7
  import { applyVimCursorStyle, focusedInput } from "./actions"
7
8
  import type { VimConfig } from "./config"
@@ -13,7 +14,7 @@ import { createVimState } from "./state"
13
14
  import { createVimeeAdapter } from "./vimee"
14
15
  import { VimStatus } from "./view"
15
16
 
16
- export function createVimModule(options?: unknown): PromptModule {
17
+ export function createVimModule(options?: unknown, enabled: Accessor<boolean> = () => true): PromptModule {
17
18
  const config = createVimConfig(options)
18
19
  const log = createVimLog(config)
19
20
  const state = createVimState(config.defaultMode, log)
@@ -26,15 +27,15 @@ export function createVimModule(options?: unknown): PromptModule {
26
27
  log("module.setup", { kind: ctx.kind, sessionID: ctx.sessionID, workspaceID: ctx.workspaceID })
27
28
  },
28
29
  renderAbove(ctx) {
29
- return <VimKeyboard ctx={ctx} config={config} state={state} log={log} />
30
+ return <VimKeyboard ctx={ctx} config={config} state={state} enabled={enabled} log={log} />
30
31
  },
31
32
  renderRight(ctx) {
32
- return <VimStatus mode={state.mode} pending={() => readablePending(state.pending())} theme={ctx.api.theme.current} pendingDisplayDelay={config.pendingDisplayDelay} disabled={ctx.disabled} log={log} requestRender={ctx.requestRender} />
33
+ return <VimStatus mode={state.mode} pending={() => readablePending(state.pending())} enabled={enabled} theme={ctx.api.theme.current} pendingDisplayDelay={config.pendingDisplayDelay} disabled={ctx.disabled} log={log} requestRender={ctx.requestRender} />
33
34
  },
34
35
  }
35
36
  }
36
37
 
37
- function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: ReturnType<typeof createVimState>; log: VimLog }) {
38
+ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: ReturnType<typeof createVimState>; enabled: Accessor<boolean>; log: VimLog }) {
38
39
  let cursorStyleMode = ""
39
40
  const vimee = createVimeeAdapter(props.state, props.config, props.log)
40
41
  props.log("keyboard.mount", { kind: props.ctx.kind })
@@ -53,8 +54,8 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
53
54
  pending: props.state.pending(),
54
55
  })
55
56
 
56
- if (props.ctx.disabled || props.ctx.visible === false) {
57
- props.log("keyboard.skip", { disabled: props.ctx.disabled, visible: props.ctx.visible })
57
+ if (!props.enabled() || props.ctx.disabled || props.ctx.visible === false) {
58
+ props.log("keyboard.skip", { enabled: props.enabled(), disabled: props.ctx.disabled, visible: props.ctx.visible })
58
59
  return
59
60
  }
60
61
 
@@ -10,6 +10,7 @@ const STATUS_SYNC_MS = 50
10
10
  type VimStatusProps = {
11
11
  mode: Accessor<VimMode>
12
12
  pending: Accessor<string | undefined>
13
+ enabled: Accessor<boolean>
13
14
  theme: PromptContext["api"]["theme"]["current"]
14
15
  pendingDisplayDelay?: number
15
16
  disabled?: boolean
@@ -20,6 +21,7 @@ type VimStatusProps = {
20
21
  export function VimStatus(props: VimStatusProps) {
21
22
  const [mode, setMode] = createSignal(props.mode())
22
23
  const [pending, setPending] = createSignal<string | undefined>()
24
+ const [enabled, setEnabled] = createSignal(props.enabled())
23
25
  let pendingTimer: ReturnType<typeof setTimeout> | undefined
24
26
  let scheduledPending: string | undefined
25
27
 
@@ -33,6 +35,13 @@ export function VimStatus(props: VimStatusProps) {
33
35
  props.requestRender?.()
34
36
  }
35
37
 
38
+ const nextEnabled = props.enabled()
39
+ if (enabled() !== nextEnabled) {
40
+ props.log?.("status.enabled", { from: enabled(), to: nextEnabled })
41
+ setEnabled(nextEnabled)
42
+ props.requestRender?.()
43
+ }
44
+
36
45
  syncPending(nextPending)
37
46
  }
38
47
 
@@ -44,8 +53,8 @@ export function VimStatus(props: VimStatusProps) {
44
53
 
45
54
  return (
46
55
  <box paddingLeft={1} paddingRight={1} flexDirection="row">
47
- {pending() ? <text fg={props.theme.info}>{pending()} </text> : undefined}
48
- <text fg={props.disabled ? props.theme.textMuted : mode() === "insert" ? props.theme.success : props.theme.warning}>{modeLabel(mode())}</text>
56
+ {enabled() && pending() ? <text fg={props.theme.info}>{pending()} </text> : undefined}
57
+ {enabled() ? <text fg={props.disabled ? props.theme.textMuted : mode() === "insert" ? props.theme.success : props.theme.warning}>{modeLabel(mode())}</text> : undefined}
49
58
  </box>
50
59
  )
51
60
 
@@ -1,6 +1,6 @@
1
1
  import type { KeyEvent } from "@opentui/core"
2
- import { TextBuffer, createInitialContext, createKeybindMap, parseKeySequence, processKeystroke } from "@vimee/core"
3
- import type { CursorPosition, KeybindDefinition, KeybindMap, ValidKeySequence, VimAction as VimeeAction, VimContext, VimMode as VimeeMode } from "@vimee/core"
2
+ import { TextBuffer, createInitialContext, createKeybindMap, parseKeySequence, processKeystroke, resetContext } from "@vimee/core"
3
+ import type { CursorPosition, KeybindDefinition, KeybindMap, MotionRange, Operator, ValidKeySequence, VimAction as VimeeAction, VimContext, VimMode as VimeeMode } from "@vimee/core"
4
4
  import type { PromptContext } from "../../prompt/types"
5
5
  import { focusedInput, setInput, type EditBufferLike } from "./actions"
6
6
  import type { VimConfig } from "./config"
@@ -38,7 +38,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
38
38
  const offset = clamp(input?.cursorOffset ?? text.length, 0, text.length)
39
39
  const map = mapForHostText(text, input)
40
40
  const cursor = hostPosition(map, offset)
41
- const vimeeKey = keyForVimee(event, key)
41
+ let vimeeKey = keyForVimee(event, key)
42
42
  if (!vimeeKey) return false
43
43
 
44
44
  if (state.mode() === "normal" && key === "<CR>") {
@@ -70,6 +70,9 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
70
70
  const hostEnd = vimeeKey === "e" ? endMotionOffset(map.hostText, offset, vim.count || 1) : undefined
71
71
  const shouldFlashYank = shouldFlashYankFor(vimeeKey)
72
72
  const visualYankRange = visualYankRangeFor(map)
73
+ vimeeKey = textObjectAlias(vimeeKey, vim) ?? vimeeKey
74
+ const textObjectHandled = handleTextObject(vimeeKey, ctx, map)
75
+ if (textObjectHandled !== undefined) return textObjectHandled
73
76
  const result = processKeystroke(vimeeKey, vim, buffer, event.ctrl, false, keybinds)
74
77
  vim = result.newCtx
75
78
  if (hostEnd !== undefined && result.actions.every((action) => action.type === "cursor-move") && hostEnd > hostOffset(map, vim.cursor, "previous")) vim = { ...vim, cursor: hostPosition(map, hostEnd) }
@@ -230,6 +233,39 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
230
233
  return actions
231
234
  }
232
235
 
236
+ function handleTextObject(key: string, ctx: PromptContext, map: PromptMap) {
237
+ if (vim.phase !== "text-object-pending" || !vim.textObjectModifier) return undefined
238
+ const range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer)
239
+ if (!range) return undefined
240
+
241
+ const flashRange = vim.operator === "y" ? motionHostRange(map, range) : undefined
242
+
243
+ if (!vim.operator) {
244
+ vim = { ...resetContext(vim), visualAnchor: range.start, cursor: range.end }
245
+ applyActions([{ type: "cursor-move", position: range.end }], ctx, map)
246
+ state.setPending("")
247
+ updateTimeout(ctx)
248
+ log("vimee.textobject", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["cursor-move"] })
249
+ return true
250
+ }
251
+ if (!textObjectOperator(vim.operator)) return undefined
252
+
253
+ const originalCursor = vim.cursor
254
+ const result = executeTextObject(vim.operator, range, buffer, vim)
255
+ if (vim.operator === "y") {
256
+ result.context = { ...result.context, cursor: originalCursor }
257
+ result.actions = result.actions.map((action) => (action.type === "cursor-move" ? { ...action, position: originalCursor } : action))
258
+ }
259
+ vim = result.context
260
+ applyActions(result.actions, ctx, map)
261
+ if (flashRange) flashYank(ctx, activeMap, { type: "yank", text: result.yankedText }, flashRange)
262
+ syncMode(state, vim.mode)
263
+ state.setPending("")
264
+ updateTimeout(ctx)
265
+ log("vimee.textobject", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: result.actions.map((action) => action.type) })
266
+ return true
267
+ }
268
+
233
269
  function updateTimeout(ctx: PromptContext) {
234
270
  if (timer) clearTimeout(timer)
235
271
  timer = undefined
@@ -370,6 +406,13 @@ function yankedTextRange(map: PromptMap, cursor: CursorPosition, text: string):
370
406
  return vimOffsetRange(map, start, start + text.length - 1)
371
407
  }
372
408
 
409
+ function motionHostRange(map: PromptMap, range: MotionRange): HostRange | undefined {
410
+ if (range.linewise) return visualLineRange(map, range.start, range.end)
411
+ const start = vimOffsetFromPosition(map.vimText, range.start)
412
+ const end = vimOffsetFromPosition(map.vimText, range.end)
413
+ return vimOffsetRange(map, start, end)
414
+ }
415
+
373
416
  function vimOffsetRange(map: PromptMap, left: number, right: number): HostRange | undefined {
374
417
  const start = hostFromVimOffset(map, Math.min(left, right), "next")
375
418
  const end = hostFromVimOffset(map, Math.max(left, right), "previous")
@@ -572,6 +615,206 @@ function consumesKey(key: string, actions: VimeeAction[], ctx: VimContext, keybi
572
615
  return ctx.mode !== "insert" || key === "Escape"
573
616
  }
574
617
 
618
+ function textObjectAlias(key: string, ctx: VimContext) {
619
+ if (ctx.phase !== "text-object-pending") return undefined
620
+ if (key === "b") return "("
621
+ if (key === "B") return "{"
622
+ return undefined
623
+ }
624
+
625
+ function textObjectOperator(operator: Operator): operator is "y" | "d" | "c" {
626
+ return operator === "y" || operator === "d" || operator === "c"
627
+ }
628
+
629
+ function resolvePromptTextObject(modifier: "i" | "a", key: string, cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
630
+ if (key === "q") return quoteRange(modifier, cursor, buffer)
631
+ if (key !== "p") return null
632
+ return paragraphRange(modifier, cursor, buffer)
633
+ }
634
+
635
+ function quoteRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
636
+ const pair = quoteObjectPair(cursor, buffer)
637
+ if (!pair) return null
638
+ const start = modifier === "i" ? pair.open + 1 : pair.open
639
+ const end = modifier === "i" ? pair.close - 1 : pair.close
640
+ return {
641
+ start: { line: cursor.line, col: start },
642
+ end: { line: cursor.line, col: Math.max(start, end) },
643
+ linewise: false,
644
+ inclusive: true,
645
+ }
646
+ }
647
+
648
+ function paragraphRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
649
+ const count = buffer.getLineCount()
650
+ if (count === 0) return null
651
+
652
+ let start = clamp(cursor.line, 0, count - 1)
653
+ while (start < count && blankLine(buffer.getLine(start))) start++
654
+ if (start >= count) {
655
+ start = clamp(cursor.line, 0, count - 1)
656
+ while (start >= 0 && blankLine(buffer.getLine(start))) start--
657
+ }
658
+ if (start < 0 || start >= count) return null
659
+
660
+ let end = start
661
+ while (start > 0 && !blankLine(buffer.getLine(start - 1))) start--
662
+ while (end < count - 1 && !blankLine(buffer.getLine(end + 1))) end++
663
+
664
+ if (modifier === "a") {
665
+ if (end < count - 1 && blankLine(buffer.getLine(end + 1))) {
666
+ end++
667
+ while (end < count - 1 && blankLine(buffer.getLine(end + 1))) end++
668
+ } else {
669
+ while (start > 0 && blankLine(buffer.getLine(start - 1))) start--
670
+ }
671
+ }
672
+
673
+ return {
674
+ start: { line: start, col: 0 },
675
+ end: { line: end, col: Math.max(0, buffer.getLineLength(end) - 1) },
676
+ linewise: true,
677
+ inclusive: true,
678
+ }
679
+ }
680
+
681
+ function quoteObjectPair(cursor: CursorPosition, buffer: TextBuffer) {
682
+ let best: { open: number; close: number; distance: number } | undefined
683
+ for (const quote of ['"', "'", "`"] as const) {
684
+ const pair = quotePair(cursor, buffer, quote)
685
+ if (!pair) continue
686
+ if (!best || pair.distance < best.distance) best = pair
687
+ }
688
+ return best
689
+ }
690
+
691
+ function quotePair(cursor: CursorPosition, buffer: TextBuffer, quote: string) {
692
+ const line = buffer.getLine(cursor.line)
693
+ let open = -1
694
+ let close = -1
695
+ let inQuote = false
696
+ let quoteStart = -1
697
+
698
+ for (let index = 0; index < line.length; index++) {
699
+ if (line[index] !== quote || escaped(line, index)) continue
700
+ if (!inQuote) {
701
+ quoteStart = index
702
+ inQuote = true
703
+ continue
704
+ }
705
+ if (cursor.col >= quoteStart && cursor.col <= index) {
706
+ return { open: quoteStart, close: index, distance: 0 }
707
+ }
708
+ inQuote = false
709
+ }
710
+
711
+ for (let index = cursor.col; index < line.length; index++) {
712
+ if (line[index] !== quote || escaped(line, index)) continue
713
+ if (open === -1) open = index
714
+ else {
715
+ close = index
716
+ break
717
+ }
718
+ }
719
+ if (open !== -1 && close !== -1) return { open, close, distance: open - cursor.col }
720
+
721
+ close = -1
722
+ open = -1
723
+ for (let index = cursor.col; index >= 0; index--) {
724
+ if (line[index] !== quote || escaped(line, index)) continue
725
+ if (close === -1) close = index
726
+ else {
727
+ open = index
728
+ break
729
+ }
730
+ }
731
+ if (open !== -1 && close !== -1) return { open, close, distance: cursor.col - close }
732
+ return undefined
733
+ }
734
+
735
+ function executeTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer, ctx: VimContext) {
736
+ buffer.saveUndoPoint(ctx.cursor)
737
+ const result = range.linewise ? executeLinewiseTextObject(operator, range, buffer) : executeCharwiseTextObject(operator, range, buffer)
738
+ const registers = ctx.selectedRegister ? { ...ctx.registers, [ctx.selectedRegister]: result.yankedText } : ctx.registers
739
+ const context = {
740
+ ...resetContext(ctx),
741
+ mode: result.mode,
742
+ cursor: result.cursor,
743
+ register: result.yankedText,
744
+ registers,
745
+ statusMessage: result.statusMessage,
746
+ }
747
+ const actions: VimeeAction[] = [{ type: "yank", text: result.yankedText }, ...result.actions, { type: "mode-change", mode: result.mode }, { type: "cursor-move", position: result.cursor }]
748
+ return { actions, context, yankedText: result.yankedText }
749
+ }
750
+
751
+ function executeLinewiseTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer) {
752
+ const startLine = Math.min(range.start.line, range.end.line)
753
+ const endLine = Math.max(range.start.line, range.end.line)
754
+ const lineCount = endLine - startLine + 1
755
+ const yankedText = buffer.getLines().slice(startLine, endLine + 1).join("\n") + "\n"
756
+ if (operator === "y") {
757
+ return { actions: [] as VimeeAction[], cursor: { line: startLine, col: 0 }, mode: "normal" as VimeeMode, yankedText, statusMessage: lineCount >= 2 ? `${lineCount} lines yanked` : "" }
758
+ }
759
+
760
+ buffer.deleteLines(startLine, lineCount)
761
+ if (buffer.getLineCount() === 0) buffer.insertLine(0, "")
762
+ const line = Math.min(startLine, buffer.getLineCount() - 1)
763
+ if (operator === "c") buffer.insertLine(line, "")
764
+ return {
765
+ actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
766
+ cursor: { line, col: 0 },
767
+ mode: (operator === "c" ? "insert" : "normal") as VimeeMode,
768
+ yankedText,
769
+ statusMessage: lineCount >= 2 ? `${lineCount} fewer lines` : "",
770
+ }
771
+ }
772
+
773
+ function executeCharwiseTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer) {
774
+ const ordered = orderedRange(range)
775
+ const endCol = range.inclusive ? ordered.end.col + 1 : ordered.end.col
776
+ const yankedText = textInRange(buffer, ordered.start, { line: ordered.end.line, col: endCol })
777
+ if (operator === "y") {
778
+ return { actions: [] as VimeeAction[], cursor: ordered.start, mode: "normal" as VimeeMode, yankedText, statusMessage: yankedText.split("\n").length >= 2 ? `${yankedText.split("\n").length} lines yanked` : "" }
779
+ }
780
+
781
+ const linesBefore = buffer.getLineCount()
782
+ buffer.deleteRange(ordered.start.line, ordered.start.col, ordered.end.line, endCol)
783
+ const linesRemoved = linesBefore - buffer.getLineCount()
784
+ return {
785
+ actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
786
+ cursor: { line: ordered.start.line, col: operator === "c" ? ordered.start.col : Math.min(ordered.start.col, Math.max(0, buffer.getLineLength(ordered.start.line) - 1)) },
787
+ mode: (operator === "c" ? "insert" : "normal") as VimeeMode,
788
+ yankedText,
789
+ statusMessage: linesRemoved >= 2 ? `${linesRemoved} fewer lines` : "",
790
+ }
791
+ }
792
+
793
+ function orderedRange(range: MotionRange) {
794
+ if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.col > range.end.col)) {
795
+ return { start: range.end, end: range.start }
796
+ }
797
+ return { start: range.start, end: range.end }
798
+ }
799
+
800
+ function textInRange(buffer: TextBuffer, start: CursorPosition, end: CursorPosition) {
801
+ if (start.line === end.line) return buffer.getLine(start.line).slice(start.col, end.col)
802
+ const lines = [buffer.getLine(start.line).slice(start.col)]
803
+ for (let line = start.line + 1; line < end.line; line++) lines.push(buffer.getLine(line))
804
+ lines.push(buffer.getLine(end.line).slice(0, end.col))
805
+ return lines.join("\n")
806
+ }
807
+
808
+ function blankLine(line: string) {
809
+ return line.trim().length === 0
810
+ }
811
+
812
+ function escaped(line: string, index: number) {
813
+ let slashCount = 0
814
+ for (let cursor = index - 1; cursor >= 0 && line[cursor] === "\\"; cursor--) slashCount++
815
+ return slashCount % 2 === 1
816
+ }
817
+
575
818
  function clamp(value: number, min: number, max: number) {
576
819
  return Math.max(min, Math.min(max, value))
577
820
  }
package/src/plugin.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
+ import { createSignal } from "solid-js"
3
4
  import { PromptRoot } from "./prompt/root"
4
5
  import type { PromptModule } from "./prompt/types"
5
6
  import { createVimModule } from "./modules/vim"
@@ -7,6 +8,23 @@ import { createSnippetsModule } from "./modules/snippets"
7
8
  import { checkAutoUpdate } from "./update"
8
9
 
9
10
  const tui: TuiPlugin = async (api, options, meta) => {
11
+ const vimEnabledKey = `${meta.id}:enabled`
12
+ const [vimEnabled, setVimEnabled] = createSignal(api.kv.get(vimEnabledKey, true) === true)
13
+
14
+ api.command.register(() => [{
15
+ title: vimEnabled() ? "Disable Vim Mode" : "Enable Vim Mode",
16
+ value: "opencode-vim.toggle",
17
+ description: vimEnabled() ? "Turn off Vim key handling" : "Turn on Vim key handling",
18
+ category: "Vim",
19
+ slash: { name: "vim" },
20
+ onSelect() {
21
+ const next = !vimEnabled()
22
+ setVimEnabled(next)
23
+ api.kv.set(vimEnabledKey, next)
24
+ api.ui.toast({ variant: "info", message: `Vim mode ${next ? "enabled" : "disabled"}` })
25
+ },
26
+ }])
27
+
10
28
  if (readAutoUpdate(options)) {
11
29
  let timer: Timer | undefined
12
30
  api.lifecycle.onDispose(() => {
@@ -29,7 +47,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
29
47
 
30
48
  const modules: PromptModule[] = []
31
49
  if (hasSnippetsPlugin(api)) modules.push(createSnippetsModule())
32
- modules.push(createVimModule(options))
50
+ modules.push(createVimModule(options, vimEnabled))
33
51
  moduleCache.set(key, modules)
34
52
  return modules
35
53
  }