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.
Files changed (38) hide show
  1. package/README.md +11 -0
  2. package/dist/tui.js +3326 -0
  3. package/package.json +6 -6
  4. package/src/clipboard.tsx +0 -50
  5. package/src/modules/snippets/index.tsx +0 -17
  6. package/src/modules/snippets/loader.ts +0 -147
  7. package/src/modules/snippets/search.ts +0 -118
  8. package/src/modules/snippets/skill-loader.ts +0 -81
  9. package/src/modules/snippets/state.ts +0 -40
  10. package/src/modules/snippets/trigger.ts +0 -68
  11. package/src/modules/snippets/types.ts +0 -50
  12. package/src/modules/snippets/view.tsx +0 -674
  13. package/src/modules/vim/actions.ts +0 -82
  14. package/src/modules/vim/config.ts +0 -137
  15. package/src/modules/vim/edit.test.ts +0 -51
  16. package/src/modules/vim/edit.ts +0 -33
  17. package/src/modules/vim/graphemes.ts +0 -38
  18. package/src/modules/vim/index.tsx +0 -200
  19. package/src/modules/vim/keys.ts +0 -38
  20. package/src/modules/vim/log.ts +0 -37
  21. package/src/modules/vim/map.test.ts +0 -45
  22. package/src/modules/vim/map.ts +0 -90
  23. package/src/modules/vim/state.test.ts +0 -18
  24. package/src/modules/vim/state.tsx +0 -46
  25. package/src/modules/vim/view.tsx +0 -130
  26. package/src/modules/vim/vimee.test.ts +0 -211
  27. package/src/modules/vim/vimee.ts +0 -1211
  28. package/src/plugin.tsx +0 -116
  29. package/src/prompt/host.tsx +0 -38
  30. package/src/prompt/modules.ts +0 -16
  31. package/src/prompt/root.tsx +0 -69
  32. package/src/prompt/types.ts +0 -42
  33. package/src/session.tsx +0 -291
  34. package/src/transcript-items.tsx +0 -168
  35. package/src/transcript.tsx +0 -179
  36. package/src/update.ts +0 -120
  37. package/tui.tsx +0 -340
  38. package/view.tsx +0 -29
package/dist/tui.js ADDED
@@ -0,0 +1,3326 @@
1
+ // @bun
2
+ // tui.tsx
3
+ import { createComponent as _$createComponent2 } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { Plugin } from "opentui:runtime-module:%40opencode%2Fplugin%2Ftui";
5
+ import { InputRenderable, KeyEvent, PasteEvent } from "opentui:runtime-module:%40opentui%2Fcore";
6
+ import { createEffect as createEffect2, createSignal as createSignal3, onCleanup as onCleanup2 } from "opentui:runtime-module:solid-js";
7
+
8
+ // src/modules/vim/actions.ts
9
+ function applyVimCursorStyle(ctx, style) {
10
+ const input = focusedInput(ctx);
11
+ if (!input)
12
+ return false;
13
+ input.cursorStyle = style;
14
+ return true;
15
+ }
16
+ function focusedInput(ctx) {
17
+ const focused = ctx.api.renderer.currentFocusedRenderable;
18
+ if (!focused || !hasEditBufferMethods(focused))
19
+ return;
20
+ return focused;
21
+ }
22
+ function setInput(ref, input) {
23
+ ref.set(toPromptInfo(ref, input));
24
+ }
25
+ function hasEditBufferMethods(input) {
26
+ return typeof input.moveCursorLeft === "function" || typeof input.moveCursorRight === "function" || typeof input.moveCursorUp === "function" || typeof input.moveCursorDown === "function" || typeof input.gotoLineEnd === "function";
27
+ }
28
+ function toPromptInfo(ref, input) {
29
+ return {
30
+ input,
31
+ mode: ref.current.mode,
32
+ parts: [...ref.current.parts]
33
+ };
34
+ }
35
+
36
+ // src/modules/vim/config.ts
37
+ import { parseKeySequence } from "@vimee/core";
38
+ var DEFAULT_CURSOR_STYLES = {
39
+ insert: {
40
+ style: "line",
41
+ blinking: true
42
+ },
43
+ normal: {
44
+ style: "block",
45
+ blinking: true
46
+ },
47
+ visual: {
48
+ style: "block",
49
+ blinking: true
50
+ },
51
+ "visual-line": {
52
+ style: "block",
53
+ blinking: true
54
+ }
55
+ };
56
+ function createVimConfig(options) {
57
+ const input = readOptions(options);
58
+ return {
59
+ defaultMode: input.defaultMode ?? "insert",
60
+ sessionKey: input.sessionKey ?? "s",
61
+ keymapTimeout: Math.max(0, input.keymapTimeout ?? 500),
62
+ pendingDisplayDelay: Math.max(0, input.pendingDisplayDelay ?? 120),
63
+ cursorStyles: {
64
+ insert: {
65
+ ...DEFAULT_CURSOR_STYLES.insert,
66
+ ...input.cursorStyles?.insert
67
+ },
68
+ normal: {
69
+ ...DEFAULT_CURSOR_STYLES.normal,
70
+ ...input.cursorStyles?.normal
71
+ },
72
+ visual: {
73
+ ...DEFAULT_CURSOR_STYLES.visual,
74
+ ...input.cursorStyles?.visual
75
+ },
76
+ "visual-line": {
77
+ ...DEFAULT_CURSOR_STYLES["visual-line"],
78
+ ...input.cursorStyles?.["visual-line"]
79
+ }
80
+ },
81
+ debug: input.debug ?? process.env.VIM_PROMPT_DEBUG === "1",
82
+ debugPath: input.debugPath,
83
+ keymaps: input.keymaps ?? {}
84
+ };
85
+ }
86
+ function readOptions(options) {
87
+ if (!options || typeof options !== "object")
88
+ return {};
89
+ const raw = "vim" in options ? options.vim : options;
90
+ if (!raw || typeof raw !== "object")
91
+ return {};
92
+ const source = raw;
93
+ return {
94
+ defaultMode: isMode(source.defaultMode) ? source.defaultMode : undefined,
95
+ sessionKey: readSessionKey(source.sessionKey),
96
+ keymapTimeout: readNumber(source.keymapTimeout),
97
+ pendingDisplayDelay: typeof source.pendingDisplayDelay === "number" ? source.pendingDisplayDelay : undefined,
98
+ cursorStyles: readCursorStyles(source.cursorStyles),
99
+ debug: typeof source.debug === "boolean" ? source.debug : undefined,
100
+ debugPath: typeof source.debugPath === "string" ? source.debugPath : undefined,
101
+ keymaps: readKeymaps(source.keymaps)
102
+ };
103
+ }
104
+ function readSessionKey(input) {
105
+ if (typeof input !== "string")
106
+ return;
107
+ try {
108
+ if (parseKeySequence(input).length === 1)
109
+ return input;
110
+ } catch {}
111
+ }
112
+ function readKeymaps(input) {
113
+ if (!input || typeof input !== "object")
114
+ return;
115
+ const source = input;
116
+ const keymaps = {};
117
+ for (const mode of ["insert", "normal", "visual", "visual-line"]) {
118
+ const raw = source[mode];
119
+ if (!raw || typeof raw !== "object")
120
+ continue;
121
+ keymaps[mode] = {};
122
+ for (const [key, action] of Object.entries(raw)) {
123
+ if (isMappedAction(action))
124
+ keymaps[mode][key] = action;
125
+ }
126
+ }
127
+ return keymaps;
128
+ }
129
+ function readCursorStyles(input) {
130
+ if (!input || typeof input !== "object")
131
+ return;
132
+ const source = input;
133
+ return {
134
+ insert: readCursorStyle(source.insert),
135
+ normal: readCursorStyle(source.normal),
136
+ visual: readCursorStyle(source.visual),
137
+ "visual-line": readCursorStyle(source["visual-line"])
138
+ };
139
+ }
140
+ function readCursorStyle(input) {
141
+ if (!input || typeof input !== "object")
142
+ return;
143
+ const source = input;
144
+ if (!isCursorStyle(source.style))
145
+ return;
146
+ return {
147
+ style: source.style,
148
+ blinking: typeof source.blinking === "boolean" ? source.blinking : undefined
149
+ };
150
+ }
151
+ function isCursorStyle(value) {
152
+ return value === "block" || value === "line" || value === "underline" || value === "default";
153
+ }
154
+ function isMode(value) {
155
+ return value === "insert" || value === "normal" || value === "visual" || value === "visual-line";
156
+ }
157
+ function isMappedAction(value) {
158
+ return typeof value === "string" && value.length > 0;
159
+ }
160
+ function readNumber(value) {
161
+ return typeof value === "number" ? value : undefined;
162
+ }
163
+
164
+ // src/modules/vim/graphemes.ts
165
+ var segments = new Intl.Segmenter(undefined, {
166
+ granularity: "grapheme"
167
+ });
168
+ function createGraphemeCodec() {
169
+ const encoded = new Map;
170
+ const decoded = new Map;
171
+ let next = 57344;
172
+ return {
173
+ encode(text) {
174
+ let result = "";
175
+ for (const {
176
+ segment
177
+ } of segments.segment(text)) {
178
+ const code = segment.charCodeAt(0);
179
+ if (segment.length === 1 && (code < 57344 || code > 63743)) {
180
+ result += segment;
181
+ continue;
182
+ }
183
+ let token = encoded.get(segment);
184
+ if (!token) {
185
+ if (next > 63743)
186
+ throw new Error("Vim grapheme dictionary is full");
187
+ token = String.fromCharCode(next++);
188
+ encoded.set(segment, token);
189
+ decoded.set(token, segment);
190
+ }
191
+ result += token;
192
+ }
193
+ return result;
194
+ },
195
+ decode(text) {
196
+ let result = "";
197
+ for (const token of text)
198
+ result += decoded.get(token) ?? token;
199
+ return result;
200
+ }
201
+ };
202
+ }
203
+
204
+ // src/modules/vim/map.ts
205
+ var graphemes = new Intl.Segmenter(undefined, {
206
+ granularity: "grapheme"
207
+ });
208
+ function graphemeWidth(value) {
209
+ if (value === "\t")
210
+ return 2;
211
+ return value === `
212
+ ` ? 1 : Bun.stringWidth(value);
213
+ }
214
+ function charToDisplay(text, charIndex) {
215
+ let width = 0;
216
+ const limit = Math.max(0, Math.min(charIndex, text.length));
217
+ for (const part of graphemes.segment(text)) {
218
+ if (part.index + part.segment.length > limit)
219
+ break;
220
+ width += graphemeWidth(part.segment);
221
+ }
222
+ return width;
223
+ }
224
+ function displayToChar(text, displayOffset) {
225
+ let width = 0;
226
+ for (const part of graphemes.segment(text)) {
227
+ const next = width + graphemeWidth(part.segment);
228
+ if (next > displayOffset)
229
+ return part.index;
230
+ width = next;
231
+ }
232
+ return text.length;
233
+ }
234
+ function displayWidth(text) {
235
+ return charToDisplay(text, text.length);
236
+ }
237
+ function createPromptMap(hostText, codec = createGraphemeCodec()) {
238
+ const hostToVim = [];
239
+ const vimToHost = [];
240
+ let vimText = "";
241
+ let vimOffset = 0;
242
+ for (const {
243
+ index: hostOffset,
244
+ segment
245
+ } of graphemes.segment(hostText)) {
246
+ for (let index = hostOffset;index < hostOffset + segment.length; index++)
247
+ hostToVim[index] = vimOffset;
248
+ vimText += codec.encode(segment);
249
+ vimToHost[vimOffset] = hostOffset;
250
+ vimOffset++;
251
+ }
252
+ hostToVim[hostText.length] = vimOffset;
253
+ vimToHost[vimOffset] = hostText.length;
254
+ return {
255
+ hostText,
256
+ vimText,
257
+ hostToVim,
258
+ vimToHost
259
+ };
260
+ }
261
+ function hostPosition(map, hostDisplayOffset) {
262
+ const charIdx = displayToChar(map.hostText, hostDisplayOffset);
263
+ return positionFromOffset(map.vimText, map.hostToVim[clamp(charIdx, 0, map.hostText.length)]);
264
+ }
265
+ function hostOffset(map, position) {
266
+ return hostFromVimOffset(map, offsetFromPosition(map.vimText, position));
267
+ }
268
+ function hostFromVimOffset(map, offset) {
269
+ return charToDisplay(map.hostText, map.vimToHost[clamp(offset, 0, map.vimText.length)]);
270
+ }
271
+ function positionFromOffset(text, offset) {
272
+ const lines = text.slice(0, offset).split(`
273
+ `);
274
+ return {
275
+ line: lines.length - 1,
276
+ col: lines[lines.length - 1]?.length ?? 0
277
+ };
278
+ }
279
+ function offsetFromPosition(text, position) {
280
+ const lines = text.split(`
281
+ `);
282
+ const line = clamp(position.line, 0, Math.max(0, lines.length - 1));
283
+ let offset = 0;
284
+ for (let index = 0;index < line; index++)
285
+ offset += lines[index].length + 1;
286
+ return offset + clamp(position.col, 0, lines[line]?.length ?? 0);
287
+ }
288
+ function clamp(value, min, max) {
289
+ return Math.max(min, Math.min(max, value));
290
+ }
291
+
292
+ // src/modules/vim/edit.ts
293
+ var graphemes2 = new Intl.Segmenter(undefined, {
294
+ granularity: "grapheme"
295
+ });
296
+ function editInput(input, value) {
297
+ if (input.plainText === value)
298
+ return;
299
+ input.clearSelection();
300
+ const before = [...graphemes2.segment(input.plainText)];
301
+ const after = [...graphemes2.segment(value)];
302
+ let prefix = 0;
303
+ while (prefix < before.length && prefix < after.length && before[prefix].segment === after[prefix].segment)
304
+ prefix++;
305
+ let suffix = 0;
306
+ while (suffix + prefix < before.length && suffix + prefix < after.length && before[before.length - suffix - 1].segment === after[after.length - suffix - 1].segment)
307
+ suffix++;
308
+ const start = before[prefix]?.index ?? input.plainText.length;
309
+ const end = suffix ? before[before.length - suffix].index : input.plainText.length;
310
+ const valueEnd = suffix ? after[after.length - suffix].index : value.length;
311
+ const startOffset = charToDisplay(input.plainText, start);
312
+ if (start === end)
313
+ input.cursorOffset = startOffset;
314
+ else
315
+ input.setSelection(startOffset, charToDisplay(input.plainText, end));
316
+ input.insertText(value.slice(start, valueEnd));
317
+ }
318
+
319
+ // src/modules/vim/keys.ts
320
+ var graphemes3 = new Intl.Segmenter(undefined, {
321
+ granularity: "grapheme"
322
+ });
323
+ var NAMED_KEYS = {
324
+ escape: "<Esc>",
325
+ esc: "<Esc>",
326
+ return: "<CR>",
327
+ enter: "<CR>",
328
+ tab: "<Tab>",
329
+ backspace: "<BS>",
330
+ delete: "<Del>",
331
+ space: "<Space>",
332
+ up: "<Up>",
333
+ down: "<Down>",
334
+ left: "<Left>",
335
+ right: "<Right>",
336
+ home: "<Home>",
337
+ end: "<End>",
338
+ pageup: "<PageUp>",
339
+ pagedown: "<PageDown>"
340
+ };
341
+ function keyNotation(event) {
342
+ const name = event.name?.toLowerCase();
343
+ if (!name)
344
+ return;
345
+ if (event.ctrl)
346
+ return `<C-${ctrlKey(name)}>`;
347
+ if (event.meta)
348
+ return `<M-${name}>`;
349
+ if ([...graphemes3.segment(name)].length === 1)
350
+ return event.shift ? name.toUpperCase() : name;
351
+ return NAMED_KEYS[name] ?? `<${name}>`;
352
+ }
353
+ function ctrlKey(name) {
354
+ if (name === "escape" || name === "esc")
355
+ return "[";
356
+ if (name.length === 1)
357
+ return name;
358
+ return name;
359
+ }
360
+
361
+ // src/modules/vim/log.ts
362
+ import { appendFile, mkdir } from "fs/promises";
363
+ import { dirname, join } from "path";
364
+ function createVimLog(config) {
365
+ if (!config.debug)
366
+ return () => {};
367
+ const path = config.debugPath ?? defaultPath();
368
+ append(path, `
369
+ --- opencode-vim debug ${new Date().toISOString()} ---
370
+ `);
371
+ return (event, data) => {
372
+ const suffix = data ? ` ${safeJson(data)}` : "";
373
+ append(path, `${new Date().toISOString()} ${event}${suffix}
374
+ `);
375
+ };
376
+ }
377
+ function defaultPath() {
378
+ const home = process.env.HOME;
379
+ return home ? join(home, ".cache/opencode/opencode-vim.log") : "/tmp/opencode-vim.log";
380
+ }
381
+ async function append(path, text) {
382
+ try {
383
+ await mkdir(dirname(path), {
384
+ recursive: true
385
+ });
386
+ await appendFile(path, text);
387
+ } catch {}
388
+ }
389
+ function safeJson(data) {
390
+ try {
391
+ return JSON.stringify(data);
392
+ } catch {
393
+ return "{}";
394
+ }
395
+ }
396
+
397
+ // src/modules/vim/state.tsx
398
+ import { createSignal } from "opentui:runtime-module:solid-js";
399
+ function createVimState(defaultMode, log = () => {}) {
400
+ const [mode, setMode] = createSignal(defaultMode);
401
+ const [pending, setPending] = createSignal("");
402
+ const listeners = new Set;
403
+ log("state.init", {
404
+ mode: defaultMode
405
+ });
406
+ return {
407
+ mode,
408
+ setMode(next) {
409
+ const changed = mode() !== next;
410
+ if (changed)
411
+ log("state.mode", {
412
+ from: mode(),
413
+ to: next
414
+ });
415
+ setMode(next);
416
+ if (changed)
417
+ notify();
418
+ },
419
+ pending,
420
+ setPending(next) {
421
+ const changed = pending() !== next;
422
+ if (changed)
423
+ log("state.pending", {
424
+ from: pending(),
425
+ to: next
426
+ });
427
+ setPending(next);
428
+ if (changed)
429
+ notify();
430
+ },
431
+ subscribe(listener) {
432
+ listeners.add(listener);
433
+ return () => listeners.delete(listener);
434
+ }
435
+ };
436
+ function notify() {
437
+ for (const listener of listeners) {
438
+ try {
439
+ listener();
440
+ } catch (error) {
441
+ log("state.listener.error", {
442
+ error: error instanceof Error ? error.message : String(error)
443
+ });
444
+ }
445
+ }
446
+ }
447
+ }
448
+
449
+ // src/modules/vim/vimee.ts
450
+ import { TextBuffer, createInitialContext, createKeybindMap, executeOperatorOnRange, parseKeySequence as parseKeySequence2, processKeystroke, resetContext, resolveMotion } from "@vimee/core";
451
+ var YANK_FLASH_MS = 250;
452
+ function createVimeeAdapter(state, config, log, options = {}) {
453
+ const codec = createGraphemeCodec();
454
+ let buffer = new TextBuffer("");
455
+ let activeMap = createPromptMap("", codec);
456
+ let vim = createInitialContext({
457
+ line: 0,
458
+ col: 0
459
+ });
460
+ const keybinds = options.readOnly ? undefined : createKeybinds(config, log);
461
+ let timer;
462
+ let yankTimer;
463
+ let yankFlashActive = false;
464
+ let pendingInsert = "";
465
+ let pendingTarget;
466
+ let pendingContext;
467
+ let nativeInsertUndoSaved = false;
468
+ let historyText;
469
+ let activeInput;
470
+ let preferredColumn;
471
+ let preferredScreen = false;
472
+ let generation = 0;
473
+ const defaultHistoryKeys = {
474
+ k: !hasNormalKeyPrefix(config, "k"),
475
+ j: !hasNormalKeyPrefix(config, "j")
476
+ };
477
+ return {
478
+ attach,
479
+ suspend,
480
+ setRegister(text) {
481
+ vim = {
482
+ ...vim,
483
+ register: codec.encode(text)
484
+ };
485
+ },
486
+ isPending: () => vim.phase !== "idle" || vim.count > 0 || !!keybinds?.isPending(),
487
+ handle(event, key, ctx) {
488
+ const ref = ctx.prompt();
489
+ if (!ref)
490
+ return false;
491
+ const input = focusedInput(ctx);
492
+ attach(ctx);
493
+ let vimeeKey = keyForVimee(event, key);
494
+ if (!vimeeKey)
495
+ return false;
496
+ if (options.readOnly && !readOnlyKey(vimeeKey, event.ctrl, vim)) {
497
+ vim = resetContext(vim);
498
+ state.setPending("");
499
+ return true;
500
+ }
501
+ vimeeKey = codec.encode(vimeeKey);
502
+ if (state.mode() === "insert") {
503
+ if (vimeeKey === "Escape") {
504
+ cancelPendingInsert(ctx);
505
+ enterNormal(ctx);
506
+ state.setPending("");
507
+ updateTimeout(ctx);
508
+ log("vimee.key", {
509
+ key,
510
+ mode: vim.mode,
511
+ phase: vim.phase,
512
+ cursor: vim.cursor,
513
+ actions: ["mode-change"]
514
+ });
515
+ return true;
516
+ }
517
+ return handleInsertKeybind(event, vimeeKey, ctx);
518
+ }
519
+ const text = input?.plainText ?? ref.current.input;
520
+ const dw = displayWidth(text);
521
+ const displayOff = clamp2(input?.cursorOffset ?? dw, 0, dw);
522
+ const charOff = displayToChar(text, displayOff);
523
+ const canBrowseHistory = !options.readOnly && vim.phase === "idle" && vim.count === 0 && !keybinds?.isPending();
524
+ const historyCommand = canBrowseHistory ? defaultHistoryCommand(vimeeKey, text, historyText, defaultHistoryKeys) : undefined;
525
+ if (historyCommand) {
526
+ const result = dispatchCommand(historyCommand, ctx);
527
+ historyText = result.ok ? focusedInput(ctx)?.plainText ?? ref.current.input : undefined;
528
+ state.setPending("");
529
+ updateTimeout(ctx);
530
+ log("vimee.history", {
531
+ key,
532
+ command: historyCommand,
533
+ handled: result.ok
534
+ });
535
+ return true;
536
+ }
537
+ historyText = undefined;
538
+ const map = mapForHostText(text);
539
+ const cursor = hostPosition(map, displayOff);
540
+ const wasPending = keybinds?.isPending() ?? false;
541
+ const pendingBefore = pendingInsert;
542
+ sync(map, cursor);
543
+ const shouldFlashYank = shouldFlashYankFor(vimeeKey);
544
+ const visualYankRange = visualYankRangeFor(map);
545
+ vimeeKey = textObjectAlias(vimeeKey, vim) ?? vimeeKey;
546
+ const textObjectHandled = handleTextObject(vimeeKey, ctx, map);
547
+ if (textObjectHandled !== undefined)
548
+ return textObjectHandled;
549
+ const resolved = keybinds?.resolve(vimeeKey, vim.mode, event.ctrl);
550
+ if (resolved?.status === "pending") {
551
+ vim = {
552
+ ...vim,
553
+ statusMessage: resolved.display
554
+ };
555
+ state.setPending(resolved.display);
556
+ updateTimeout(ctx);
557
+ log("vimee.key", {
558
+ key,
559
+ vimeeKey,
560
+ mode: vim.mode,
561
+ phase: vim.phase,
562
+ cursor: vim.cursor,
563
+ actions: []
564
+ });
565
+ return true;
566
+ }
567
+ if (vimeeKey === "Enter" && state.mode() === "normal" && vim.phase === "idle" && !wasPending && resolved?.status !== "matched") {
568
+ ref.submit();
569
+ state.setPending("");
570
+ updateTimeout(ctx);
571
+ log("vimee.key", {
572
+ key,
573
+ vimeeKey,
574
+ mode: vim.mode,
575
+ phase: vim.phase,
576
+ cursor: vim.cursor,
577
+ actions: ["submit"]
578
+ });
579
+ return true;
580
+ }
581
+ const commandKey = vimeeKey;
582
+ const definition = resolved?.status === "matched" ? resolved.definition : undefined;
583
+ if (options.readClipboard && needsClipboard(commandKey, event.ctrl, definition)) {
584
+ const before = generation;
585
+ return options.readClipboard().then((text) => {
586
+ if (before !== generation || input?.isDestroyed || input?.plainText !== map.hostText || input?.cursorOffset !== displayOff)
587
+ return false;
588
+ vim = {
589
+ ...vim,
590
+ register: codec.encode(text)
591
+ };
592
+ return finish();
593
+ });
594
+ }
595
+ return finish();
596
+ function finish() {
597
+ let result;
598
+ if (resolved?.status === "matched") {
599
+ const actions = applyKeybind(resolved.definition, ctx, map);
600
+ result = {
601
+ newCtx: vim,
602
+ actions
603
+ };
604
+ } else {
605
+ if (!event.ctrl && vim.phase === "idle" && vim.count === 0 && (commandKey === "j" || commandKey === "k" || commandKey === "ArrowDown" || commandKey === "ArrowUp")) {
606
+ vim = processKey("g").newCtx;
607
+ }
608
+ result = processKey(commandKey, event.ctrl);
609
+ }
610
+ vim = result.newCtx;
611
+ const actions = result.actions;
612
+ if (resolved?.status !== "matched")
613
+ applyActions(actions, ctx, map);
614
+ if (shouldFlashYank)
615
+ flashYank(ctx, activeMap, yankAction(actions), visualYankRange);
616
+ syncMode(state, vim.mode);
617
+ const keybindPending = keybinds?.isPending() ?? false;
618
+ if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert")
619
+ flushPendingInsert(ctx, pendingBefore, charOff);
620
+ pendingInsert = keybindPending && state.mode() === "insert" ? plainPending(vim.statusMessage) : "";
621
+ state.setPending(pendingDisplay(vim, keybindPending));
622
+ updateTimeout(ctx);
623
+ log("vimee.key", {
624
+ key,
625
+ vimeeKey,
626
+ mode: vim.mode,
627
+ phase: vim.phase,
628
+ cursor: vim.cursor,
629
+ actions: actions.map((action) => action.type)
630
+ });
631
+ return consumesKey(commandKey, actions, vim, keybindPending);
632
+ }
633
+ },
634
+ cleanup: suspend
635
+ };
636
+ function needsClipboard(key, ctrl, definition) {
637
+ if (definition) {
638
+ if ("execute" in definition)
639
+ return false;
640
+ return parseKeySequence2(definition.keys).some((token) => ["p", "P", ".", "@"].includes(token));
641
+ }
642
+ if (ctrl)
643
+ return false;
644
+ if (vim.phase === "macro-execute-pending")
645
+ return true;
646
+ if (vim.phase !== "idle" || vim.selectedRegister)
647
+ return false;
648
+ if (key === "p" || key === "P")
649
+ return true;
650
+ return key === "." && vim.lastChange.some((token) => ["p", "P", "@"].includes(token));
651
+ }
652
+ function attach(ctx) {
653
+ const input = focusedInput(ctx);
654
+ if (activeInput === input)
655
+ return;
656
+ suspend();
657
+ activeInput = input;
658
+ activeMap = createPromptMap(input?.plainText ?? ctx.prompt()?.current.input ?? "", codec);
659
+ buffer = new TextBuffer(activeMap.vimText);
660
+ nativeInsertUndoSaved = false;
661
+ vim = {
662
+ ...resetContext(vim),
663
+ cursor: hostPosition(activeMap, input?.cursorOffset ?? 0),
664
+ mode: state.mode()
665
+ };
666
+ }
667
+ function suspend() {
668
+ generation++;
669
+ preferredColumn = undefined;
670
+ if (pendingContext)
671
+ cancelPendingInsert(pendingContext);
672
+ if (timer)
673
+ clearTimeout(timer);
674
+ timer = undefined;
675
+ keybinds?.cancel();
676
+ cancelYankFlash();
677
+ if (activeInput && !activeInput.isDestroyed)
678
+ clearVisualSelection(activeInput);
679
+ if (isVisualMode(state.mode()))
680
+ state.setMode("normal");
681
+ vim = resetContext(vim);
682
+ state.setPending("");
683
+ pendingTarget = undefined;
684
+ pendingContext = undefined;
685
+ }
686
+ function sync(map, cursor) {
687
+ if (cursor.line !== vim.cursor.line || cursor.col !== vim.cursor.col)
688
+ preferredColumn = undefined;
689
+ if (buffer.getContent() !== map.vimText)
690
+ buffer.replaceContent(map.vimText);
691
+ vim = {
692
+ ...vim,
693
+ cursor,
694
+ mode: state.mode()
695
+ };
696
+ }
697
+ function mapForHostText(text) {
698
+ if (activeMap.hostText === text)
699
+ return activeMap;
700
+ if (state.mode() === "insert")
701
+ recordNativeChange(text);
702
+ activeMap = createPromptMap(text, codec);
703
+ if (state.mode() === "insert") {
704
+ if (!nativeInsertUndoSaved) {
705
+ buffer.saveUndoPoint(vim.cursor);
706
+ nativeInsertUndoSaved = true;
707
+ }
708
+ buffer.replaceContent(activeMap.vimText);
709
+ } else {
710
+ buffer = new TextBuffer(activeMap.vimText);
711
+ nativeInsertUndoSaved = false;
712
+ }
713
+ return activeMap;
714
+ }
715
+ function recordNativeChange(text) {
716
+ const before = codec.encode(activeMap.hostText);
717
+ const after = codec.encode(text);
718
+ const start = codec.encode(activeMap.hostText.slice(0, displayToChar(activeMap.hostText, hostOffset(activeMap, vim.cursor)))).length;
719
+ let prefix = 0;
720
+ while (prefix < start && prefix < after.length && before[prefix] === after[prefix])
721
+ prefix++;
722
+ let suffix = 0;
723
+ while (suffix < before.length - start && suffix < after.length - prefix && before[before.length - suffix - 1] === after[after.length - suffix - 1])
724
+ suffix++;
725
+ const keys = [...vim.pendingChange];
726
+ if (!keys.length)
727
+ keys.push("i");
728
+ for (let index = prefix;index < start; index++)
729
+ keys.push("Backspace");
730
+ for (let index = start;index < before.length - suffix; index++)
731
+ keys.push("Delete");
732
+ for (const key of after.slice(prefix, after.length - suffix))
733
+ keys.push(key === `
734
+ ` ? "Enter" : key === "\t" ? "Tab" : key);
735
+ vim = {
736
+ ...vim,
737
+ pendingChange: keys
738
+ };
739
+ }
740
+ function applyActions(actions, ctx, map) {
741
+ const ref = ctx.prompt();
742
+ const input = focusedInput(ctx);
743
+ let currentMap = map;
744
+ if (!ref)
745
+ return;
746
+ for (const action of actions) {
747
+ switch (action.type) {
748
+ case "content-change":
749
+ currentMap = createPromptMap(codec.decode(action.content), codec);
750
+ activeMap = currentMap;
751
+ setInput(ref, currentMap.hostText);
752
+ buffer.replaceContent(action.content);
753
+ break;
754
+ case "cursor-move":
755
+ setCursor(input, currentMap, action.position);
756
+ break;
757
+ case "yank":
758
+ if (!action.register)
759
+ options.onYank?.(codec.decode(action.text));
760
+ break;
761
+ case "mode-change":
762
+ nativeInsertUndoSaved = action.mode === "insert" && actions.some((item) => item.type === "content-change");
763
+ if (action.mode === "insert") {
764
+ cancelYankFlash();
765
+ if (input)
766
+ clearVisualSelection(input);
767
+ }
768
+ syncMode(state, action.mode);
769
+ break;
770
+ case "quit":
771
+ ref.blur();
772
+ break;
773
+ case "submit":
774
+ ref.submit();
775
+ break;
776
+ case "command":
777
+ dispatchCommand(action.command, ctx);
778
+ return;
779
+ }
780
+ }
781
+ setCursor(input, currentMap, vim.cursor);
782
+ syncVisualSelection(input, currentMap, ctx);
783
+ }
784
+ function handleInsertKeybind(event, key, ctx) {
785
+ if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending())
786
+ return false;
787
+ const wasPending = keybinds.isPending();
788
+ const pendingBefore = pendingInsert;
789
+ const resolved = keybinds.resolve(key, "insert", event.ctrl);
790
+ switch (resolved.status) {
791
+ case "pending":
792
+ if (!wasPending) {
793
+ const input = focusedInput(ctx);
794
+ pendingTarget = input ? {
795
+ input,
796
+ offset: input.cursorOffset ?? 0
797
+ } : undefined;
798
+ pendingContext = ctx;
799
+ }
800
+ pendingInsert = codec.decode(plainPending(resolved.display));
801
+ state.setPending(resolved.display);
802
+ updateTimeout(ctx);
803
+ return true;
804
+ case "matched": {
805
+ if (applyInsertKeybind(resolved.definition, ctx)) {
806
+ pendingInsert = "";
807
+ state.setPending("");
808
+ updateTimeout(ctx);
809
+ log("vimee.keybind", {
810
+ key,
811
+ mode: vim.mode,
812
+ phase: vim.phase,
813
+ cursor: vim.cursor,
814
+ actions: ["mode-change"]
815
+ });
816
+ return true;
817
+ }
818
+ if (wasPending && pendingBefore)
819
+ flushPendingInsert(ctx, pendingBefore);
820
+ pendingInsert = "";
821
+ state.setPending("");
822
+ updateTimeout(ctx);
823
+ log("vimee.keybind.unsupported", {
824
+ key
825
+ });
826
+ return false;
827
+ }
828
+ case "none":
829
+ if (wasPending && pendingBefore)
830
+ flushPendingInsert(ctx, pendingBefore);
831
+ pendingInsert = "";
832
+ state.setPending("");
833
+ updateTimeout(ctx);
834
+ return wasPending ? handleInsertKeybind(event, key, ctx) : false;
835
+ }
836
+ }
837
+ function applyInsertKeybind(definition, ctx) {
838
+ switch (insertHostAction(definition)) {
839
+ case "normal":
840
+ enterNormal(ctx);
841
+ return true;
842
+ case "submit":
843
+ ctx.prompt()?.submit();
844
+ return true;
845
+ case "command":
846
+ dispatchCommand(definition.command, ctx);
847
+ return true;
848
+ default:
849
+ return false;
850
+ }
851
+ }
852
+ function enterNormal(ctx) {
853
+ const ref = ctx.prompt();
854
+ const input = focusedInput(ctx);
855
+ const text = input?.plainText ?? ref?.current.input ?? "";
856
+ const map = mapForHostText(text);
857
+ if (input && text.length > 0) {
858
+ const dw = displayWidth(text);
859
+ const offset = clamp2(input?.cursorOffset ?? dw, 0, dw);
860
+ const charOffset = displayToChar(text, offset);
861
+ if (charOffset > 0 && text[charOffset - 1] !== `
862
+ `) {
863
+ input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)));
864
+ }
865
+ clampNormalCursor(input);
866
+ }
867
+ const lastChange = nativeInsertUndoSaved ? [...vim.pendingChange, "Escape"] : vim.lastChange;
868
+ vim = {
869
+ ...resetContext(vim),
870
+ cursor: hostPosition(map, input?.cursorOffset ?? 0),
871
+ mode: "normal",
872
+ statusMessage: "",
873
+ lastChange,
874
+ pendingChange: []
875
+ };
876
+ nativeInsertUndoSaved = false;
877
+ syncMode(state, "normal");
878
+ }
879
+ function insertHostAction(definition) {
880
+ const action = definition.hostAction;
881
+ if (action)
882
+ return action;
883
+ if ("execute" in definition)
884
+ return;
885
+ if (definition.keys === "<Esc>" || definition.keys === "<C-[>" || definition.keys === "Escape")
886
+ return "normal";
887
+ return;
888
+ }
889
+ function applyKeybind(definition, ctx, map) {
890
+ if ("execute" in definition) {
891
+ const actions = definition.execute(vim, buffer);
892
+ vim = {
893
+ ...vim,
894
+ cursor: hostPosition(map, hostOffset(map, vim.cursor))
895
+ };
896
+ applyActions(actions, ctx, map);
897
+ return actions;
898
+ }
899
+ let actions = [];
900
+ for (const token of parseKeySequence2(definition.keys)) {
901
+ const result = processKey(codec.encode(keyToken(token)), tokenCtrl(token));
902
+ vim = result.newCtx;
903
+ applyActions(result.actions, ctx, activeMap);
904
+ actions = [...actions, ...result.actions];
905
+ }
906
+ return actions;
907
+ }
908
+ function processKey(key, ctrl = false) {
909
+ const register = vim.selectedRegister;
910
+ const moving = !ctrl && vim.mode !== "insert" && (vim.phase === "idle" || vim.phase === "operator-pending" || vim.phase === "g-pending");
911
+ const vertical = moving && (key === "j" || key === "k" || key === "ArrowDown" || key === "ArrowUp");
912
+ const lineEnd = moving && key === "$" && vim.phase === "idle";
913
+ const result = processKeyInner(key, ctrl);
914
+ if (register) {
915
+ for (const action of result.actions) {
916
+ if (action.type === "yank")
917
+ action.register = register;
918
+ }
919
+ }
920
+ if (result.actions.some((action) => action.type === "content-change" || action.type === "mode-change"))
921
+ preferredColumn = undefined;
922
+ else if (!vertical && result.actions.some((action) => action.type === "cursor-move"))
923
+ preferredColumn = lineEnd ? Infinity : undefined;
924
+ return result;
925
+ }
926
+ function processKeyInner(key, ctrl) {
927
+ let range = ctrl ? undefined : lineMotion(key);
928
+ if (range && vim.operator && (key === "j" || key === "k" || key === "ArrowDown" || key === "ArrowUp") && range.start.line === range.end.line && range.start.col === range.end.col) {
929
+ return {
930
+ newCtx: {
931
+ ...resetContext(vim),
932
+ pendingChange: []
933
+ },
934
+ actions: []
935
+ };
936
+ }
937
+ if (range && !vim.operator) {
938
+ return {
939
+ newCtx: {
940
+ ...resetContext(vim),
941
+ cursor: range.end
942
+ },
943
+ actions: [{
944
+ type: "cursor-move",
945
+ position: range.end
946
+ }]
947
+ };
948
+ }
949
+ let wordRange;
950
+ if (!ctrl && vim.mode !== "insert" && (key === "w" || key === "W") && (vim.phase === "idle" || vim.phase === "operator-pending")) {
951
+ wordRange = forwardWordRange(key, vim.cursor, buffer, vim.count || 1);
952
+ if (vim.phase === "idle") {
953
+ const cursor = {
954
+ ...wordRange.end,
955
+ col: Math.min(wordRange.end.col, Math.max(0, buffer.getLineLength(wordRange.end.line) - 1))
956
+ };
957
+ return {
958
+ newCtx: {
959
+ ...resetContext(vim),
960
+ cursor
961
+ },
962
+ actions: [{
963
+ type: "cursor-move",
964
+ position: cursor
965
+ }]
966
+ };
967
+ }
968
+ }
969
+ if (key === "." && !ctrl && vim.mode === "normal" && vim.phase === "idle" && vim.lastChange.length) {
970
+ const original = buffer;
971
+ const before = buffer.getContent();
972
+ const cursor = vim.cursor;
973
+ const keys = [...vim.lastChange];
974
+ if (vim.count) {
975
+ while (/^[0-9]$/.test(keys[0] ?? ""))
976
+ keys.shift();
977
+ keys.unshift(...String(vim.count));
978
+ }
979
+ vim = {
980
+ ...vim,
981
+ count: 0
982
+ };
983
+ const actions = [];
984
+ buffer = new TextBuffer(before);
985
+ try {
986
+ for (const token of keys) {
987
+ const result = processKey(token);
988
+ vim = result.newCtx;
989
+ actions.push(...result.actions);
990
+ }
991
+ if (buffer.getContent() !== before) {
992
+ original.saveUndoPoint(cursor);
993
+ original.replaceContent(buffer.getContent());
994
+ }
995
+ } finally {
996
+ buffer = original;
997
+ }
998
+ return {
999
+ newCtx: {
1000
+ ...vim,
1001
+ lastChange: keys,
1002
+ pendingChange: []
1003
+ },
1004
+ actions
1005
+ };
1006
+ }
1007
+ const operator = vim.operator;
1008
+ if (!range && !ctrl && operator && textObjectOperator(operator)) {
1009
+ if (vim.phase === "operator-pending") {
1010
+ const count = vim.count || 1;
1011
+ if (key === "l" || key === "ArrowRight") {
1012
+ range = {
1013
+ start: vim.cursor,
1014
+ end: {
1015
+ line: vim.cursor.line,
1016
+ col: Math.min(buffer.getLineLength(vim.cursor.line), vim.cursor.col + count)
1017
+ },
1018
+ inclusive: false,
1019
+ linewise: false
1020
+ };
1021
+ } else if (key === operator) {
1022
+ range = {
1023
+ start: {
1024
+ line: vim.cursor.line,
1025
+ col: 0
1026
+ },
1027
+ end: {
1028
+ line: Math.min(buffer.getLineCount() - 1, vim.cursor.line + count - 1),
1029
+ col: 0
1030
+ },
1031
+ inclusive: true,
1032
+ linewise: true
1033
+ };
1034
+ } else if (operator === "c" && key === "w" && /\S/.test(buffer.getLine(vim.cursor.line)[vim.cursor.col] ?? "")) {
1035
+ range = resolveMotion("e", {
1036
+ ...vim.cursor,
1037
+ col: vim.cursor.col - 1
1038
+ }, buffer, count, vim.count > 0, vim)?.range;
1039
+ if (range)
1040
+ range.start = vim.cursor;
1041
+ } else if (wordRange) {
1042
+ range = wordRange;
1043
+ if (operator === "d" && range.end.line > range.start.line && /\S/.test(buffer.getLine(range.start.line)[range.start.col] ?? "") && range.end.col === Math.max(0, buffer.getLine(range.end.line).search(/\S/))) {
1044
+ range.end = {
1045
+ line: range.end.line - 1,
1046
+ col: buffer.getLineLength(range.end.line - 1)
1047
+ };
1048
+ }
1049
+ } else if (operator === "c") {
1050
+ const motion = resolveMotion(key, vim.cursor, buffer, count, vim.count > 0, vim);
1051
+ if (motion?.range.linewise)
1052
+ range = motion.range;
1053
+ }
1054
+ } else if (vim.phase === "text-object-pending" && vim.textObjectModifier) {
1055
+ range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer) ?? undefined;
1056
+ }
1057
+ }
1058
+ if (range && operator) {
1059
+ if (vim.phase === "g-pending" && !range.inclusive) {
1060
+ const ordered = orderedRange(range);
1061
+ if (ordered.end.line > ordered.start.line && ordered.end.col === 0) {
1062
+ const linewise = ordered.start.col <= Math.max(0, buffer.getLine(ordered.start.line).search(/\S/));
1063
+ range = {
1064
+ start: ordered.start,
1065
+ end: {
1066
+ line: ordered.end.line - 1,
1067
+ col: buffer.getLineLength(ordered.end.line - 1)
1068
+ },
1069
+ linewise,
1070
+ inclusive: false
1071
+ };
1072
+ }
1073
+ }
1074
+ const keys = [];
1075
+ if (vim.count)
1076
+ keys.push(...String(vim.count));
1077
+ for (const token of vim.pendingChange) {
1078
+ if (!/^[0-9]$/.test(token))
1079
+ keys.push(token);
1080
+ }
1081
+ keys.push(key);
1082
+ const result = executeTextObject(operator, range, buffer, vim);
1083
+ if (operator === "y")
1084
+ result.context.pendingChange = [];
1085
+ else if (result.context.mode === "insert")
1086
+ result.context.pendingChange = keys;
1087
+ else {
1088
+ result.context.lastChange = keys;
1089
+ result.context.pendingChange = [];
1090
+ }
1091
+ return {
1092
+ newCtx: result.context,
1093
+ actions: result.actions
1094
+ };
1095
+ }
1096
+ const yanking = operator === "y" || isVisualMode(vim.mode) && key === "y" || vim.mode === "normal" && key === "Y";
1097
+ const register = vim.selectedRegister ? vim.registers[vim.selectedRegister] ?? "" : vim.register;
1098
+ const emptyLinePaste = !ctrl && vim.mode === "normal" && vim.phase === "idle" && (key === "p" || key === "P") && buffer.getContent() === "" && register.endsWith(`
1099
+ `);
1100
+ const result = processKeystroke(key, vim, yanking ? new TextBuffer(buffer.getContent()) : buffer, ctrl, false);
1101
+ if (emptyLinePaste) {
1102
+ buffer.deleteLines(key === "p" ? 0 : buffer.getLineCount() - 1, 1);
1103
+ result.newCtx.cursor = {
1104
+ line: 0,
1105
+ col: 0
1106
+ };
1107
+ for (const action of result.actions) {
1108
+ if (action.type === "content-change")
1109
+ action.content = buffer.getContent();
1110
+ if (action.type === "cursor-move")
1111
+ action.position = result.newCtx.cursor;
1112
+ }
1113
+ }
1114
+ return result;
1115
+ }
1116
+ function lineMotion(key) {
1117
+ if (vim.mode !== "normal" && !isVisualMode(vim.mode))
1118
+ return;
1119
+ const screen = vim.phase === "g-pending";
1120
+ if (!screen && vim.phase !== "idle" && vim.phase !== "operator-pending")
1121
+ return;
1122
+ const down = key === "j" || key === "ArrowDown";
1123
+ const up = key === "k" || key === "ArrowUp";
1124
+ if (!down && !up && !(screen && (key === "0" || key === "^" || key === "$")))
1125
+ return;
1126
+ const map = buffer.getContent() === activeMap.vimText ? activeMap : createPromptMap(codec.decode(buffer.getContent()), codec);
1127
+ const offset = hostOffset(map, vim.cursor);
1128
+ const count = vim.count || 1;
1129
+ let start;
1130
+ let end;
1131
+ let column;
1132
+ if (screen) {
1133
+ const lines = activeInput?.editorView?.getLogicalLineInfo?.();
1134
+ if (!lines?.lineStartCols.length)
1135
+ return;
1136
+ let row = 0;
1137
+ while (row + 1 < lines.lineStartCols.length && lines.lineStartCols[row + 1] <= offset)
1138
+ row++;
1139
+ column = offset - lines.lineStartCols[row];
1140
+ const delta = down ? count : up ? -count : key === "$" ? count - 1 : 0;
1141
+ const target = clamp2(row + delta, 0, lines.lineStartCols.length - 1);
1142
+ start = lines.lineStartCols[target];
1143
+ end = start + Math.max(0, lines.lineWidthCols[target] - 1);
1144
+ if ((down || up) && target === row)
1145
+ return {
1146
+ start: vim.cursor,
1147
+ end: vim.cursor,
1148
+ linewise: false,
1149
+ inclusive: false
1150
+ };
1151
+ } else {
1152
+ const lineStart = hostOffset(map, {
1153
+ line: vim.cursor.line,
1154
+ col: 0
1155
+ });
1156
+ column = offset - lineStart;
1157
+ const line = clamp2(vim.cursor.line + (down ? count : -count), 0, buffer.getLineCount() - 1);
1158
+ start = hostOffset(map, {
1159
+ line,
1160
+ col: 0
1161
+ });
1162
+ end = Math.max(start, hostOffset(map, {
1163
+ line,
1164
+ col: buffer.getLineLength(line)
1165
+ }) - 1);
1166
+ }
1167
+ let target;
1168
+ if (down || up) {
1169
+ if (preferredColumn === undefined || preferredScreen !== screen && preferredColumn !== Infinity)
1170
+ preferredColumn = column;
1171
+ preferredScreen = screen;
1172
+ target = Math.min(start + preferredColumn, end);
1173
+ } else if (key === "$") {
1174
+ target = end;
1175
+ } else {
1176
+ target = start;
1177
+ if (key === "^") {
1178
+ const from = displayToChar(map.hostText, start);
1179
+ const to = displayToChar(map.hostText, end + 1);
1180
+ const first = map.hostText.slice(from, to).search(/\S/);
1181
+ if (first >= 0)
1182
+ target = charToDisplay(map.hostText, from + first);
1183
+ }
1184
+ }
1185
+ return {
1186
+ start: vim.cursor,
1187
+ end: hostPosition(map, target),
1188
+ linewise: !screen,
1189
+ inclusive: !screen || key === "$"
1190
+ };
1191
+ }
1192
+ function handleTextObject(key, ctx, map) {
1193
+ if (vim.phase !== "text-object-pending" || !vim.textObjectModifier)
1194
+ return;
1195
+ const range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer);
1196
+ if (!range)
1197
+ return;
1198
+ const flashRange = vim.operator === "y" ? motionHostRange(map, range) : undefined;
1199
+ if (!vim.operator) {
1200
+ vim = {
1201
+ ...resetContext(vim),
1202
+ visualAnchor: range.start,
1203
+ cursor: range.end
1204
+ };
1205
+ applyActions([{
1206
+ type: "cursor-move",
1207
+ position: range.end
1208
+ }], ctx, map);
1209
+ state.setPending("");
1210
+ updateTimeout(ctx);
1211
+ log("vimee.textobject", {
1212
+ key,
1213
+ mode: vim.mode,
1214
+ phase: vim.phase,
1215
+ cursor: vim.cursor,
1216
+ actions: ["cursor-move"]
1217
+ });
1218
+ return true;
1219
+ }
1220
+ if (!textObjectOperator(vim.operator))
1221
+ return;
1222
+ const result = processKey(key);
1223
+ vim = result.newCtx;
1224
+ applyActions(result.actions, ctx, map);
1225
+ if (flashRange)
1226
+ flashYank(ctx, activeMap, result.actions.find((action) => action.type === "yank"), flashRange);
1227
+ syncMode(state, vim.mode);
1228
+ state.setPending("");
1229
+ updateTimeout(ctx);
1230
+ log("vimee.textobject", {
1231
+ key,
1232
+ mode: vim.mode,
1233
+ phase: vim.phase,
1234
+ cursor: vim.cursor,
1235
+ actions: result.actions.map((action) => action.type)
1236
+ });
1237
+ return true;
1238
+ }
1239
+ function updateTimeout(ctx) {
1240
+ if (timer)
1241
+ clearTimeout(timer);
1242
+ timer = undefined;
1243
+ if (!keybinds?.isPending())
1244
+ return;
1245
+ timer = setTimeout(() => {
1246
+ keybinds.cancel();
1247
+ flushPendingInsert(ctx, pendingInsert);
1248
+ pendingInsert = "";
1249
+ state.setPending("");
1250
+ ctx.requestRender();
1251
+ }, config.keymapTimeout);
1252
+ }
1253
+ function cancelPendingInsert(ctx, offset, flush = true) {
1254
+ if (!keybinds?.isPending())
1255
+ return;
1256
+ if (timer)
1257
+ clearTimeout(timer);
1258
+ timer = undefined;
1259
+ keybinds.cancel();
1260
+ if (flush)
1261
+ flushPendingInsert(ctx, pendingInsert, offset);
1262
+ pendingInsert = "";
1263
+ pendingTarget = undefined;
1264
+ pendingContext = undefined;
1265
+ state.setPending("");
1266
+ }
1267
+ function flushPendingInsert(ctx, value, charOffset) {
1268
+ if (!value || state.mode() !== "insert")
1269
+ return;
1270
+ if (pendingTarget?.input.isDestroyed)
1271
+ return;
1272
+ if (pendingTarget?.input.insertText && !pendingTarget.input.isDestroyed) {
1273
+ const {
1274
+ input,
1275
+ offset
1276
+ } = pendingTarget;
1277
+ const cursor = input.cursorOffset ?? 0;
1278
+ input.clearSelection?.();
1279
+ input.cursorOffset = offset;
1280
+ input.insertText(value);
1281
+ input.cursorOffset = cursor >= offset ? cursor + displayWidth(value) : cursor;
1282
+ return;
1283
+ }
1284
+ const ref = ctx.prompt();
1285
+ if (!ref)
1286
+ return;
1287
+ const input = focusedInput(ctx);
1288
+ const text = input?.plainText ?? ref.current.input;
1289
+ const currentDisplayOff = input?.cursorOffset ?? 0;
1290
+ const currentCharOff = displayToChar(text, currentDisplayOff);
1291
+ const insertAtChar = charOffset ?? currentCharOff;
1292
+ const next = text.slice(0, insertAtChar) + value + text.slice(insertAtChar);
1293
+ setInput(ref, next);
1294
+ const nextCharOff = currentCharOff >= insertAtChar ? currentCharOff + value.length : currentCharOff;
1295
+ if (input)
1296
+ input.cursorOffset = displayWidth(next.slice(0, nextCharOff));
1297
+ }
1298
+ function setCursor(input, map, position) {
1299
+ if (!input)
1300
+ return;
1301
+ input.cursorOffset = hostOffset(map, position);
1302
+ if (vim.mode !== "insert")
1303
+ clampNormalCursor(input);
1304
+ }
1305
+ function syncVisualSelection(input, map, ctx) {
1306
+ if (!input)
1307
+ return;
1308
+ if (!isVisualMode(vim.mode) || !vim.visualAnchor) {
1309
+ if (!yankFlashActive)
1310
+ clearVisualSelection(input);
1311
+ return;
1312
+ }
1313
+ cancelYankFlash();
1314
+ const range = vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor);
1315
+ if (!range) {
1316
+ clearVisualSelection(input);
1317
+ return;
1318
+ }
1319
+ setSelection(input, range.start, range.end, ctx);
1320
+ }
1321
+ function visualYankRangeFor(map) {
1322
+ if (!isVisualMode(vim.mode) || !vim.visualAnchor)
1323
+ return;
1324
+ return vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor);
1325
+ }
1326
+ function flashYank(ctx, map, action, visualRange) {
1327
+ if (!action)
1328
+ return;
1329
+ const input = focusedInput(ctx);
1330
+ if (!input)
1331
+ return;
1332
+ const range = visualRange ?? yankedTextRange(map, vim.cursor, action.text);
1333
+ if (!range)
1334
+ return;
1335
+ cancelYankFlash();
1336
+ yankFlashActive = true;
1337
+ setYankSelection(input, range.start, range.end, ctx);
1338
+ ctx.requestRender();
1339
+ yankTimer = setTimeout(() => {
1340
+ yankTimer = undefined;
1341
+ if (!yankFlashActive)
1342
+ return;
1343
+ yankFlashActive = false;
1344
+ clearVisualSelection(input);
1345
+ ctx.requestRender();
1346
+ }, YANK_FLASH_MS);
1347
+ }
1348
+ function cancelYankFlash() {
1349
+ if (yankTimer)
1350
+ clearTimeout(yankTimer);
1351
+ yankTimer = undefined;
1352
+ yankFlashActive = false;
1353
+ }
1354
+ function shouldFlashYankFor(key) {
1355
+ if (isVisualMode(vim.mode))
1356
+ return key === "y";
1357
+ return vim.operator === "y";
1358
+ }
1359
+ }
1360
+ function readOnlyKey(key, ctrl, vim) {
1361
+ if (key === "Escape")
1362
+ return true;
1363
+ if (ctrl)
1364
+ return false;
1365
+ if (vim.phase === "char-pending" || vim.phase === "text-object-pending" || vim.phase === "register-pending")
1366
+ return true;
1367
+ if (vim.phase === "g-pending")
1368
+ return ["g", "j", "k", "0", "^", "$", "ArrowDown", "ArrowUp"].includes(key);
1369
+ if ((vim.phase === "operator-pending" || isVisualMode(vim.mode)) && (key === "i" || key === "a"))
1370
+ return true;
1371
+ return /^[0-9hjklwWbBeE$^gGfFtT;,vVy%{}()"]$/.test(key) || ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(key);
1372
+ }
1373
+ function forwardWordRange(key, start, buffer, count) {
1374
+ let {
1375
+ line,
1376
+ col
1377
+ } = start;
1378
+ for (let index = 0;index < count; index++) {
1379
+ const text = buffer.getLine(line);
1380
+ const kind = wordClass(text[col], key === "W");
1381
+ while (col < text.length && wordClass(text[col], key === "W") === kind)
1382
+ col++;
1383
+ while (col < text.length && /\s/.test(text[col]))
1384
+ col++;
1385
+ while (col >= buffer.getLineLength(line) && line < buffer.getLineCount() - 1) {
1386
+ line++;
1387
+ col = 0;
1388
+ const next = buffer.getLine(line);
1389
+ if (!next.length)
1390
+ break;
1391
+ while (col < next.length && /\s/.test(next[col]))
1392
+ col++;
1393
+ if (col < next.length)
1394
+ break;
1395
+ }
1396
+ }
1397
+ return {
1398
+ start,
1399
+ end: {
1400
+ line,
1401
+ col
1402
+ },
1403
+ linewise: false,
1404
+ inclusive: false
1405
+ };
1406
+ }
1407
+ function wordClass(char, big) {
1408
+ if (!char || /\s/.test(char))
1409
+ return "space";
1410
+ return big || /\w/.test(char) ? "word" : "punctuation";
1411
+ }
1412
+ function visualCharRange(map, anchor, cursor) {
1413
+ const anchorOffset = hostOffset(map, anchor);
1414
+ const cursorOffset = hostOffset(map, cursor);
1415
+ return hostRange(map, anchorOffset, cursorOffset);
1416
+ }
1417
+ function visualLineRange(map, anchor, cursor) {
1418
+ const startLine = Math.min(anchor.line, cursor.line);
1419
+ const endLine = Math.max(anchor.line, cursor.line);
1420
+ const start = hostOffset(map, {
1421
+ line: startLine,
1422
+ col: 0
1423
+ });
1424
+ const end = hostOffset(map, {
1425
+ line: endLine,
1426
+ col: vimLineLength(map.vimText, endLine)
1427
+ });
1428
+ return hostRange(map, start, end);
1429
+ }
1430
+ function yankedTextRange(map, cursor, text) {
1431
+ if (!text)
1432
+ return;
1433
+ if (text.endsWith(`
1434
+ `)) {
1435
+ const lineCount = text.split(`
1436
+ `).length - 1;
1437
+ return visualLineRange(map, cursor, {
1438
+ line: cursor.line + Math.max(0, lineCount - 1),
1439
+ col: 0
1440
+ });
1441
+ }
1442
+ const start = vimOffsetFromPosition(map.vimText, cursor);
1443
+ return vimOffsetRange(map, start, start + text.length - 1);
1444
+ }
1445
+ function motionHostRange(map, range) {
1446
+ if (range.linewise)
1447
+ return visualLineRange(map, range.start, range.end);
1448
+ const start = vimOffsetFromPosition(map.vimText, range.start);
1449
+ const end = vimOffsetFromPosition(map.vimText, range.end);
1450
+ return vimOffsetRange(map, start, end);
1451
+ }
1452
+ function vimOffsetRange(map, left, right) {
1453
+ const start = hostFromVimOffset(map, Math.min(left, right));
1454
+ const end = hostFromVimOffset(map, Math.max(left, right));
1455
+ return hostRange(map, start, end);
1456
+ }
1457
+ function hostRange(map, left, right) {
1458
+ if (!map.hostText)
1459
+ return;
1460
+ const dw = displayWidth(map.hostText);
1461
+ const start = clamp2(Math.min(left, right), 0, Math.max(0, dw - 1));
1462
+ const end = clamp2(Math.max(left, right), 0, Math.max(0, dw - 1));
1463
+ return {
1464
+ start,
1465
+ end
1466
+ };
1467
+ }
1468
+ function yankAction(actions) {
1469
+ return actions.find((action) => action.type === "yank");
1470
+ }
1471
+ function setSelection(input, start, end, ctx) {
1472
+ setSelectionColors(input, start, end, ctx.api.theme.current.warning, ctx.api.theme.current.background);
1473
+ }
1474
+ function setYankSelection(input, start, end, ctx) {
1475
+ setSelectionColors(input, start, end, ctx.api.theme.current.info, ctx.api.theme.current.background);
1476
+ }
1477
+ function setSelectionColors(input, start, end, background, foreground) {
1478
+ input.selectionBg = background;
1479
+ input.selectionFg = foreground;
1480
+ if (input.setSelectionInclusive) {
1481
+ input.setSelectionInclusive(start, end);
1482
+ return;
1483
+ }
1484
+ const exclusiveEnd = end + 1;
1485
+ if (input.setSelection) {
1486
+ input.setSelection(start, exclusiveEnd);
1487
+ return;
1488
+ }
1489
+ input.editorView?.setSelection?.(start, exclusiveEnd, background, foreground);
1490
+ }
1491
+ function clearVisualSelection(input) {
1492
+ if (input.clearSelection) {
1493
+ input.clearSelection();
1494
+ return;
1495
+ }
1496
+ input.editorView?.resetSelection?.();
1497
+ }
1498
+ function isVisualMode(mode) {
1499
+ return mode === "visual" || mode === "visual-line";
1500
+ }
1501
+ function vimLineLength(text, line) {
1502
+ return text.split(`
1503
+ `)[line]?.length ?? 0;
1504
+ }
1505
+ function vimOffsetFromPosition(text, position) {
1506
+ const lines = text.split(`
1507
+ `);
1508
+ const line = clamp2(position.line, 0, Math.max(0, lines.length - 1));
1509
+ let offset = 0;
1510
+ for (let index = 0;index < line; index++)
1511
+ offset += lines[index].length + 1;
1512
+ return offset + clamp2(position.col, 0, lines[line]?.length ?? 0);
1513
+ }
1514
+ function clampNormalCursor(input) {
1515
+ const cursor = input.visualCursor;
1516
+ const offset = input.cursorOffset;
1517
+ const text = input.plainText;
1518
+ if (!cursor || offset === undefined || text === undefined)
1519
+ return;
1520
+ if (cursor.visualCol === 0)
1521
+ return;
1522
+ const dw = displayWidth(text);
1523
+ if (offset >= dw) {
1524
+ input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, dw - 1)));
1525
+ return;
1526
+ }
1527
+ const charIdx = displayToChar(text, offset);
1528
+ if (charIdx < text.length && text[charIdx] === `
1529
+ `) {
1530
+ input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)));
1531
+ }
1532
+ }
1533
+ function dispatchCommand(command, ctx) {
1534
+ const input = focusedInput(ctx);
1535
+ if (command === "prompt.history.previous" && input)
1536
+ input.cursorOffset = 0;
1537
+ if (command === "prompt.history.next" && input)
1538
+ input.cursorOffset = input.plainText?.length ?? 0;
1539
+ const result = ctx.api.keymap.dispatchCommand(command);
1540
+ if (command !== "prompt.history.next" || !result.ok)
1541
+ return result;
1542
+ const nextInput = focusedInput(ctx);
1543
+ if (nextInput?.plainText !== undefined)
1544
+ nextInput.cursorOffset = displayWidth(nextInput.plainText);
1545
+ return result;
1546
+ }
1547
+ function defaultHistoryCommand(key, text, historyText, enabled) {
1548
+ if (key !== "j" && key !== "k")
1549
+ return;
1550
+ if (!enabled[key])
1551
+ return;
1552
+ if (text.length > 0 && text !== historyText)
1553
+ return;
1554
+ return key === "k" ? "prompt.history.previous" : "prompt.history.next";
1555
+ }
1556
+ function hasNormalKeyPrefix(config, key) {
1557
+ for (const sequence of Object.keys(config.keymaps.normal ?? {})) {
1558
+ try {
1559
+ if (keyToken(parseKeySequence2(sequence)[0] ?? "") === key)
1560
+ return true;
1561
+ } catch {}
1562
+ }
1563
+ return false;
1564
+ }
1565
+ function createKeybinds(config, log) {
1566
+ const map = createKeybindMap();
1567
+ let count = 0;
1568
+ for (const [mode, keymaps] of Object.entries(config.keymaps)) {
1569
+ if (!keymaps)
1570
+ continue;
1571
+ for (const [keys, action] of Object.entries(keymaps)) {
1572
+ try {
1573
+ const tokens = parseKeySequence2(keys);
1574
+ if (tokens.length > 1 && tokens[0] === "<CR>")
1575
+ throw new Error("<CR> cannot start a multi-key mapping");
1576
+ map.addKeybind(mode, keys, keybindAction(action));
1577
+ count++;
1578
+ } catch (error) {
1579
+ log("vimee.keymap.invalid", {
1580
+ mode,
1581
+ keys,
1582
+ action,
1583
+ error: error instanceof Error ? error.message : String(error)
1584
+ });
1585
+ }
1586
+ }
1587
+ }
1588
+ return count > 0 ? map : undefined;
1589
+ }
1590
+ function keybindAction(action) {
1591
+ if (action.startsWith("command:")) {
1592
+ const command = action.slice(8).trim();
1593
+ if (!command)
1594
+ throw new Error("Command name is required");
1595
+ return {
1596
+ execute: () => [{
1597
+ type: "command",
1598
+ command
1599
+ }],
1600
+ hostAction: "command",
1601
+ command
1602
+ };
1603
+ }
1604
+ switch (action) {
1605
+ case "normal":
1606
+ return {
1607
+ keys: "<Esc>",
1608
+ hostAction: "normal"
1609
+ };
1610
+ case "insert":
1611
+ return {
1612
+ keys: "i"
1613
+ };
1614
+ case "submit":
1615
+ return {
1616
+ execute: () => [{
1617
+ type: "submit"
1618
+ }],
1619
+ hostAction: "submit"
1620
+ };
1621
+ default:
1622
+ return {
1623
+ keys: action
1624
+ };
1625
+ }
1626
+ }
1627
+ function keyForVimee(event, key) {
1628
+ if (key === "<C-[>")
1629
+ return "Escape";
1630
+ if (event.ctrl)
1631
+ return event.name?.toLowerCase();
1632
+ const token = keyToken(key);
1633
+ return token.startsWith("<") ? undefined : token;
1634
+ }
1635
+ function keyToken(token) {
1636
+ if (token === "<Esc>" || token === "<C-[>")
1637
+ return "Escape";
1638
+ if (token === "<CR>")
1639
+ return "Enter";
1640
+ if (token === "<Tab>")
1641
+ return "Tab";
1642
+ if (token === "<BS>")
1643
+ return "Backspace";
1644
+ if (token === "<Del>")
1645
+ return "Delete";
1646
+ if (token === "<Space>")
1647
+ return " ";
1648
+ if (token === "<Up>")
1649
+ return "ArrowUp";
1650
+ if (token === "<Down>")
1651
+ return "ArrowDown";
1652
+ if (token === "<Left>")
1653
+ return "ArrowLeft";
1654
+ if (token === "<Right>")
1655
+ return "ArrowRight";
1656
+ if (token === "<Home>")
1657
+ return "Home";
1658
+ if (token === "<End>")
1659
+ return "End";
1660
+ if (token.startsWith("<C-") && token.endsWith(">"))
1661
+ return token.slice(3, -1).toLowerCase();
1662
+ return token;
1663
+ }
1664
+ function tokenCtrl(token) {
1665
+ return token.startsWith("<C-") && token !== "<C-[>";
1666
+ }
1667
+ function syncMode(state, mode) {
1668
+ state.setMode(mode === "insert" || mode === "visual" || mode === "visual-line" ? mode : "normal");
1669
+ }
1670
+ function pendingDisplay(ctx, keybindPending) {
1671
+ const count = ctx.count > 0 ? String(ctx.count) : "";
1672
+ if (ctx.phase === "operator-pending")
1673
+ return count + (ctx.operator ?? "");
1674
+ if (ctx.phase === "text-object-pending")
1675
+ return count + (ctx.operator ?? "") + (ctx.textObjectModifier ?? "");
1676
+ if (ctx.phase === "char-pending")
1677
+ return count + (ctx.operator ?? "") + (ctx.charCommand ?? "");
1678
+ if (keybindPending)
1679
+ return ctx.statusMessage;
1680
+ return count;
1681
+ }
1682
+ function plainPending(value) {
1683
+ return value.includes("<") || value.includes(">") ? "" : value;
1684
+ }
1685
+ function consumesKey(key, actions, ctx, keybindPending) {
1686
+ if (actions.length > 0)
1687
+ return true;
1688
+ if (keybindPending)
1689
+ return true;
1690
+ if (ctx.phase !== "idle")
1691
+ return true;
1692
+ return ctx.mode !== "insert" || key === "Escape";
1693
+ }
1694
+ function textObjectAlias(key, ctx) {
1695
+ if (ctx.phase !== "text-object-pending")
1696
+ return;
1697
+ if (key === "b")
1698
+ return "(";
1699
+ if (key === "B")
1700
+ return "{";
1701
+ return;
1702
+ }
1703
+ function textObjectOperator(operator) {
1704
+ return operator === "y" || operator === "d" || operator === "c";
1705
+ }
1706
+ function resolvePromptTextObject(modifier, key, cursor, buffer) {
1707
+ if (key === "q")
1708
+ return quoteRange(modifier, cursor, buffer);
1709
+ if (key !== "p")
1710
+ return null;
1711
+ return paragraphRange(modifier, cursor, buffer);
1712
+ }
1713
+ function quoteRange(modifier, cursor, buffer) {
1714
+ const pair = quoteObjectPair(cursor, buffer);
1715
+ if (!pair)
1716
+ return null;
1717
+ const start = modifier === "i" ? pair.open + 1 : pair.open;
1718
+ const end = modifier === "i" ? pair.close - 1 : pair.close;
1719
+ return {
1720
+ start: {
1721
+ line: cursor.line,
1722
+ col: start
1723
+ },
1724
+ end: {
1725
+ line: cursor.line,
1726
+ col: Math.max(start, end)
1727
+ },
1728
+ linewise: false,
1729
+ inclusive: end >= start
1730
+ };
1731
+ }
1732
+ function paragraphRange(modifier, cursor, buffer) {
1733
+ const count = buffer.getLineCount();
1734
+ if (count === 0)
1735
+ return null;
1736
+ let start = clamp2(cursor.line, 0, count - 1);
1737
+ while (start < count && blankLine(buffer.getLine(start)))
1738
+ start++;
1739
+ if (start >= count) {
1740
+ start = clamp2(cursor.line, 0, count - 1);
1741
+ while (start >= 0 && blankLine(buffer.getLine(start)))
1742
+ start--;
1743
+ }
1744
+ if (start < 0 || start >= count)
1745
+ return null;
1746
+ let end = start;
1747
+ while (start > 0 && !blankLine(buffer.getLine(start - 1)))
1748
+ start--;
1749
+ while (end < count - 1 && !blankLine(buffer.getLine(end + 1)))
1750
+ end++;
1751
+ if (modifier === "a") {
1752
+ if (end < count - 1 && blankLine(buffer.getLine(end + 1))) {
1753
+ end++;
1754
+ while (end < count - 1 && blankLine(buffer.getLine(end + 1)))
1755
+ end++;
1756
+ } else {
1757
+ while (start > 0 && blankLine(buffer.getLine(start - 1)))
1758
+ start--;
1759
+ }
1760
+ }
1761
+ return {
1762
+ start: {
1763
+ line: start,
1764
+ col: 0
1765
+ },
1766
+ end: {
1767
+ line: end,
1768
+ col: Math.max(0, buffer.getLineLength(end) - 1)
1769
+ },
1770
+ linewise: true,
1771
+ inclusive: true
1772
+ };
1773
+ }
1774
+ function quoteObjectPair(cursor, buffer) {
1775
+ let best;
1776
+ for (const quote of ['"', "'", "`"]) {
1777
+ const pair = quotePair(cursor, buffer, quote);
1778
+ if (!pair)
1779
+ continue;
1780
+ if (!best || pair.distance < best.distance)
1781
+ best = pair;
1782
+ }
1783
+ return best;
1784
+ }
1785
+ function quotePair(cursor, buffer, quote) {
1786
+ const line = buffer.getLine(cursor.line);
1787
+ let open = -1;
1788
+ let close = -1;
1789
+ let inQuote = false;
1790
+ let quoteStart = -1;
1791
+ for (let index = 0;index < line.length; index++) {
1792
+ if (line[index] !== quote || escaped(line, index))
1793
+ continue;
1794
+ if (!inQuote) {
1795
+ quoteStart = index;
1796
+ inQuote = true;
1797
+ continue;
1798
+ }
1799
+ if (cursor.col >= quoteStart && cursor.col <= index) {
1800
+ return {
1801
+ open: quoteStart,
1802
+ close: index,
1803
+ distance: 0
1804
+ };
1805
+ }
1806
+ inQuote = false;
1807
+ }
1808
+ for (let index = cursor.col;index < line.length; index++) {
1809
+ if (line[index] !== quote || escaped(line, index))
1810
+ continue;
1811
+ if (open === -1)
1812
+ open = index;
1813
+ else {
1814
+ close = index;
1815
+ break;
1816
+ }
1817
+ }
1818
+ if (open !== -1 && close !== -1)
1819
+ return {
1820
+ open,
1821
+ close,
1822
+ distance: open - cursor.col
1823
+ };
1824
+ close = -1;
1825
+ open = -1;
1826
+ for (let index = cursor.col;index >= 0; index--) {
1827
+ if (line[index] !== quote || escaped(line, index))
1828
+ continue;
1829
+ if (close === -1)
1830
+ close = index;
1831
+ else {
1832
+ open = index;
1833
+ break;
1834
+ }
1835
+ }
1836
+ if (open !== -1 && close !== -1)
1837
+ return {
1838
+ open,
1839
+ close,
1840
+ distance: cursor.col - close
1841
+ };
1842
+ return;
1843
+ }
1844
+ function executeTextObject(operator, range, buffer, ctx) {
1845
+ if (operator !== "y")
1846
+ buffer.saveUndoPoint(ctx.cursor);
1847
+ if (operator === ">" || operator === "<") {
1848
+ const result = executeOperatorOnRange(operator, range, buffer, ctx.cursor, {
1849
+ style: ctx.indentStyle,
1850
+ width: ctx.indentWidth
1851
+ });
1852
+ const cursor = {
1853
+ line: result.newCursor.line,
1854
+ col: Math.max(0, buffer.getLine(result.newCursor.line).search(/\S/))
1855
+ };
1856
+ return {
1857
+ actions: [...result.actions, {
1858
+ type: "cursor-move",
1859
+ position: cursor
1860
+ }],
1861
+ context: {
1862
+ ...resetContext(ctx),
1863
+ cursor
1864
+ },
1865
+ yankedText: ""
1866
+ };
1867
+ }
1868
+ const result = range.linewise ? executeLinewiseTextObject(operator, range, buffer, ctx.cursor) : executeCharwiseTextObject(operator, range, buffer);
1869
+ const registers = ctx.selectedRegister ? {
1870
+ ...ctx.registers,
1871
+ [ctx.selectedRegister]: result.yankedText
1872
+ } : ctx.registers;
1873
+ const context = {
1874
+ ...resetContext(ctx),
1875
+ mode: result.mode,
1876
+ cursor: result.cursor,
1877
+ register: result.yankedText,
1878
+ registers,
1879
+ statusMessage: result.statusMessage
1880
+ };
1881
+ const actions = [{
1882
+ type: "yank",
1883
+ text: result.yankedText
1884
+ }, ...result.actions, {
1885
+ type: "mode-change",
1886
+ mode: result.mode
1887
+ }, {
1888
+ type: "cursor-move",
1889
+ position: result.cursor
1890
+ }];
1891
+ return {
1892
+ actions,
1893
+ context,
1894
+ yankedText: result.yankedText
1895
+ };
1896
+ }
1897
+ function executeLinewiseTextObject(operator, range, buffer, cursor) {
1898
+ const startLine = Math.min(range.start.line, range.end.line);
1899
+ const endLine = Math.max(range.start.line, range.end.line);
1900
+ const lineCount = endLine - startLine + 1;
1901
+ const yankedText = buffer.getLines().slice(startLine, endLine + 1).join(`
1902
+ `) + `
1903
+ `;
1904
+ if (operator === "y") {
1905
+ const col = Math.min(cursor.col, Math.max(0, buffer.getLineLength(startLine) - 1));
1906
+ return {
1907
+ actions: [],
1908
+ cursor: {
1909
+ line: startLine,
1910
+ col
1911
+ },
1912
+ mode: "normal",
1913
+ yankedText,
1914
+ statusMessage: lineCount >= 2 ? `${lineCount} lines yanked` : ""
1915
+ };
1916
+ }
1917
+ buffer.deleteLines(startLine, lineCount);
1918
+ if (operator === "c")
1919
+ buffer.insertLine(startLine, "");
1920
+ else if (buffer.getLineCount() === 0)
1921
+ buffer.insertLine(0, "");
1922
+ const line = Math.min(startLine, buffer.getLineCount() - 1);
1923
+ return {
1924
+ actions: [{
1925
+ type: "content-change",
1926
+ content: buffer.getContent()
1927
+ }],
1928
+ cursor: {
1929
+ line,
1930
+ col: 0
1931
+ },
1932
+ mode: operator === "c" ? "insert" : "normal",
1933
+ yankedText,
1934
+ statusMessage: lineCount >= 2 ? `${lineCount} fewer lines` : ""
1935
+ };
1936
+ }
1937
+ function executeCharwiseTextObject(operator, range, buffer) {
1938
+ const ordered = orderedRange(range);
1939
+ const endCol = range.inclusive ? ordered.end.col + 1 : ordered.end.col;
1940
+ const yankedText = textInRange(buffer, ordered.start, {
1941
+ line: ordered.end.line,
1942
+ col: endCol
1943
+ });
1944
+ if (operator === "y") {
1945
+ return {
1946
+ actions: [],
1947
+ cursor: ordered.start,
1948
+ mode: "normal",
1949
+ yankedText,
1950
+ statusMessage: yankedText.split(`
1951
+ `).length >= 2 ? `${yankedText.split(`
1952
+ `).length} lines yanked` : ""
1953
+ };
1954
+ }
1955
+ const linesBefore = buffer.getLineCount();
1956
+ buffer.deleteRange(ordered.start.line, ordered.start.col, ordered.end.line, endCol);
1957
+ const linesRemoved = linesBefore - buffer.getLineCount();
1958
+ return {
1959
+ actions: [{
1960
+ type: "content-change",
1961
+ content: buffer.getContent()
1962
+ }],
1963
+ cursor: {
1964
+ line: ordered.start.line,
1965
+ col: operator === "c" ? ordered.start.col : Math.min(ordered.start.col, Math.max(0, buffer.getLineLength(ordered.start.line) - 1))
1966
+ },
1967
+ mode: operator === "c" ? "insert" : "normal",
1968
+ yankedText,
1969
+ statusMessage: linesRemoved >= 2 ? `${linesRemoved} fewer lines` : ""
1970
+ };
1971
+ }
1972
+ function orderedRange(range) {
1973
+ if (range.start.line > range.end.line || range.start.line === range.end.line && range.start.col > range.end.col) {
1974
+ return {
1975
+ start: range.end,
1976
+ end: range.start
1977
+ };
1978
+ }
1979
+ return {
1980
+ start: range.start,
1981
+ end: range.end
1982
+ };
1983
+ }
1984
+ function textInRange(buffer, start, end) {
1985
+ if (start.line === end.line)
1986
+ return buffer.getLine(start.line).slice(start.col, end.col);
1987
+ const lines = [buffer.getLine(start.line).slice(start.col)];
1988
+ for (let line = start.line + 1;line < end.line; line++)
1989
+ lines.push(buffer.getLine(line));
1990
+ lines.push(buffer.getLine(end.line).slice(0, end.col));
1991
+ return lines.join(`
1992
+ `);
1993
+ }
1994
+ function blankLine(line) {
1995
+ return line.trim().length === 0;
1996
+ }
1997
+ function escaped(line, index) {
1998
+ let slashCount = 0;
1999
+ for (let cursor = index - 1;cursor >= 0 && line[cursor] === "\\"; cursor--)
2000
+ slashCount++;
2001
+ return slashCount % 2 === 1;
2002
+ }
2003
+ function clamp2(value, min, max) {
2004
+ return Math.max(min, Math.min(max, value));
2005
+ }
2006
+
2007
+ // view.tsx
2008
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
2009
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
2010
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
2011
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
2012
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
2013
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
2014
+ function VimStatus(props) {
2015
+ return (() => {
2016
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
2017
+ _$insertNode(_el$, _el$2);
2018
+ _$setProp(_el$, "paddingRight", 1);
2019
+ _$setProp(_el$, "flexDirection", "row");
2020
+ _$setProp(_el$, "flexShrink", 0);
2021
+ _$insert(_el$2, (() => {
2022
+ var _c$ = _$memo(() => !!props.enabled());
2023
+ return () => _c$() ? modeLabel(props.mode()) : "";
2024
+ })());
2025
+ _$effect((_$p) => _$setProp(_el$2, "fg", props.mode() === "insert" ? props.theme.success : props.theme.warning, _$p));
2026
+ return _el$;
2027
+ })();
2028
+ }
2029
+ function modeLabel(mode) {
2030
+ if (mode === "visual")
2031
+ return "VISUAL";
2032
+ if (mode === "visual-line")
2033
+ return "VISUAL LINE";
2034
+ return mode === "normal" ? "NORMAL" : "INSERT";
2035
+ }
2036
+
2037
+ // src/session.tsx
2038
+ import { use as _$use } from "opentui:runtime-module:%40opentui%2Fsolid";
2039
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
2040
+ import { insertNode as _$insertNode2 } from "opentui:runtime-module:%40opentui%2Fsolid";
2041
+ import { setProp as _$setProp2 } from "opentui:runtime-module:%40opentui%2Fsolid";
2042
+ import { effect as _$effect2 } from "opentui:runtime-module:%40opentui%2Fsolid";
2043
+ import { insert as _$insert2 } from "opentui:runtime-module:%40opentui%2Fsolid";
2044
+ import { createElement as _$createElement2 } from "opentui:runtime-module:%40opentui%2Fsolid";
2045
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
2046
+ import { memo as _$memo2 } from "opentui:runtime-module:%40opentui%2Fsolid";
2047
+ import { useTerminalDimensions } from "opentui:runtime-module:%40opentui%2Fsolid";
2048
+ import { createEffect, createSignal as createSignal2, onCleanup, onMount, untrack } from "opentui:runtime-module:solid-js";
2049
+
2050
+ // src/transcript.tsx
2051
+ import { MouseEvent, ScrollBoxRenderable } from "opentui:runtime-module:%40opentui%2Fcore";
2052
+
2053
+ // src/transcript-items.tsx
2054
+ import { MarkdownRenderable, TextBufferRenderable } from "opentui:runtime-module:%40opentui%2Fcore";
2055
+ function parts(messages) {
2056
+ const result = [];
2057
+ for (const message of messages) {
2058
+ if (message.type === "assistant") {
2059
+ const ordinal = {
2060
+ text: 0,
2061
+ reasoning: 0
2062
+ };
2063
+ for (const part of message.content) {
2064
+ const partID = part.type === "tool" ? part.id : `${part.type}:${ordinal[part.type]++}`;
2065
+ let text = part.type === "tool" ? "" : part.text;
2066
+ if (part.type === "reasoning")
2067
+ text = text.replace("[REDACTED]", "");
2068
+ if (part.type !== "tool" && !text.trim())
2069
+ continue;
2070
+ result.push({
2071
+ id: `session-part:${message.id}:${partID}`,
2072
+ messageID: message.id,
2073
+ kind: part.type,
2074
+ author: part.type === "text" ? "Assistant" : part.type === "reasoning" ? "Reasoning" : part.name,
2075
+ text,
2076
+ group: part.type === "reasoning" ? "reasoning" : part.type === "tool" && ["read", "glob", "grep", "webfetch", "websearch"].includes(part.name.toLowerCase()) ? "exploration" : undefined
2077
+ });
2078
+ }
2079
+ if (message.finish && message.finish !== "tool-calls" && message.finish !== "unknown" || message.error || message.retry) {
2080
+ result.push({
2081
+ id: `footer:${message.id}`,
2082
+ messageID: message.id,
2083
+ kind: "footer",
2084
+ author: "",
2085
+ text: ""
2086
+ });
2087
+ }
2088
+ } else {
2089
+ result.push({
2090
+ id: message.id,
2091
+ messageID: message.id,
2092
+ kind: "message",
2093
+ author: message.type === "user" ? "You" : message.type,
2094
+ text: message.type === "user" ? message.text : ""
2095
+ });
2096
+ }
2097
+ }
2098
+ return result;
2099
+ }
2100
+ function textOf(node) {
2101
+ if (!node.visible)
2102
+ return "";
2103
+ if (node instanceof MarkdownRenderable)
2104
+ return node.content;
2105
+ if (node instanceof TextBufferRenderable)
2106
+ return node.plainText;
2107
+ let text = "";
2108
+ let previous;
2109
+ for (const child of node.getChildren()) {
2110
+ const value = textOf(child);
2111
+ if (!value)
2112
+ continue;
2113
+ if (text)
2114
+ text += previous?.y === child.y ? " " : `
2115
+ `;
2116
+ text += value;
2117
+ previous = child;
2118
+ }
2119
+ return text;
2120
+ }
2121
+ function groupAt(node) {
2122
+ const header = node.getChildren()[0];
2123
+ if (!header)
2124
+ return;
2125
+ const body = header.getChildren()[0];
2126
+ let label;
2127
+ if (body instanceof TextBufferRenderable) {
2128
+ if (!body.plainText.startsWith("\u22EF "))
2129
+ return;
2130
+ label = body.plainText.slice(2).trim();
2131
+ } else {
2132
+ const cells = body?.getChildren() ?? [];
2133
+ if (cells.length < 2 || !cells.slice(1).every((cell) => cell instanceof TextBufferRenderable))
2134
+ return;
2135
+ label = cells.slice(1).map((cell) => cell.plainText).join(" ").trim();
2136
+ }
2137
+ let kind;
2138
+ if (/^(Thought|Thinking)(?:$|[: \u00B7\u2026])/.test(label))
2139
+ kind = "reasoning";
2140
+ else if (/^Explor(?:ed|ing):/.test(label))
2141
+ kind = "exploration";
2142
+ else if (/^Instructions:/.test(label))
2143
+ kind = "instructions";
2144
+ else if (/^(?:\d+ (?:commands?|edits?|thoughts?|reads?|tools?|instructions?)(?:, |$))+$/.test(label) || /^(?:Running|Preparing) .+\u2026$/.test(label))
2145
+ kind = "activity";
2146
+ else
2147
+ return;
2148
+ return {
2149
+ header,
2150
+ kind,
2151
+ label,
2152
+ children: node.getChildren().slice(1)
2153
+ };
2154
+ }
2155
+ function transcriptItems(rows, messages) {
2156
+ const sources = parts(messages);
2157
+ const ranges = [];
2158
+ let cursor = 0;
2159
+ let lastSource = -1;
2160
+ function add(node, source, parentID) {
2161
+ const text = source?.text || textOf(node);
2162
+ if (!text.trim())
2163
+ return;
2164
+ ranges.push({
2165
+ id: source?.id ?? node.id,
2166
+ author: source?.author ?? "Tool",
2167
+ text,
2168
+ node,
2169
+ top: node.y,
2170
+ bottom: node.y + node.height,
2171
+ parentID
2172
+ });
2173
+ }
2174
+ function group(node, parentID) {
2175
+ const info = groupAt(node);
2176
+ if (!info)
2177
+ return false;
2178
+ const id = node.id;
2179
+ ranges.push({
2180
+ id,
2181
+ author: info.label,
2182
+ text: info.label,
2183
+ node: info.header,
2184
+ top: info.header.y,
2185
+ bottom: info.header.y + info.header.height,
2186
+ toggle: info.header,
2187
+ parentID
2188
+ });
2189
+ let children = info.children;
2190
+ if (info.kind !== "exploration" && children.length) {
2191
+ children = [...children[0].getChildren(), ...children.slice(1)];
2192
+ }
2193
+ for (const child of children) {
2194
+ if (!child.visible || !child.height)
2195
+ continue;
2196
+ if (!group(child, id))
2197
+ add(child, undefined, id);
2198
+ }
2199
+ return true;
2200
+ }
2201
+ for (const row of rows) {
2202
+ if (row.id === "session-navigation-slack")
2203
+ break;
2204
+ if (!row.visible || !row.height)
2205
+ continue;
2206
+ const anchor = row.getChildren()[0];
2207
+ const info = anchor && groupAt(anchor);
2208
+ let index = sources.findIndex((part) => part.id === row.id);
2209
+ if (index === -1) {
2210
+ index = sources.findIndex((part, position) => position >= cursor && part.messageID === row.id);
2211
+ if (index !== -1 && !info) {
2212
+ const text = textOf(row).trim();
2213
+ const match = sources.findIndex((part, position) => position >= cursor && part.messageID === row.id && part.text.trim() === text);
2214
+ if (text && match !== -1)
2215
+ index = match;
2216
+ }
2217
+ }
2218
+ if (index !== -1)
2219
+ cursor = index;
2220
+ if (!info && index === -1 && sources[cursor]?.kind === "footer") {
2221
+ lastSource = cursor++;
2222
+ continue;
2223
+ }
2224
+ const container = anchor && !(anchor instanceof TextBufferRenderable) && !(anchor instanceof MarkdownRenderable);
2225
+ if (index === -1 && !info && sources[cursor]?.group && container)
2226
+ index = cursor;
2227
+ if (anchor && info) {
2228
+ group(anchor);
2229
+ while (cursor < sources.length) {
2230
+ const part = sources[cursor];
2231
+ if (info.kind === "activity" ? part.kind !== "tool" && part.kind !== "reasoning" : part.group !== info.kind)
2232
+ break;
2233
+ lastSource = cursor++;
2234
+ }
2235
+ } else if (index !== -1) {
2236
+ const source = sources[index];
2237
+ if (source.kind === "footer") {
2238
+ lastSource = cursor++;
2239
+ } else if (source.group && container && anchor.getChildren().length) {
2240
+ for (const child of anchor.getChildren()) {
2241
+ add(child, sources[cursor]);
2242
+ cursor++;
2243
+ }
2244
+ lastSource = cursor - 1;
2245
+ } else {
2246
+ add(row, source);
2247
+ lastSource = cursor++;
2248
+ }
2249
+ }
2250
+ }
2251
+ let last = sources.length - 1;
2252
+ while (last >= 0 && (sources[last].kind === "message" || sources[last].kind === "footer") && !sources[last].text)
2253
+ last--;
2254
+ return {
2255
+ ranges,
2256
+ hasLatest: lastSource >= last
2257
+ };
2258
+ }
2259
+
2260
+ // src/transcript.tsx
2261
+ function createTranscriptSelection(context, sessionID, select) {
2262
+ let scroll;
2263
+ let requestedID;
2264
+ let selectedParents = [];
2265
+ let navigation;
2266
+ function choose(range, view) {
2267
+ requestedID = range.id;
2268
+ select(requestedID);
2269
+ let delta = 0;
2270
+ if (range.bottom <= view.top)
2271
+ delta = range.top - view.top;
2272
+ else if (range.top >= view.bottom) {
2273
+ delta = range.top - view.top - Math.max(0, view.bottom - view.top - (range.bottom - range.top));
2274
+ }
2275
+ if (delta) {
2276
+ const slack = view.scroll.getRenderable("session-navigation-slack")?.height ?? 0;
2277
+ const maximum = Math.max(0, view.scroll.scrollHeight - slack - view.scroll.viewport.height);
2278
+ const position = Math.max(0, Math.min(maximum, view.scroll.scrollTop + delta));
2279
+ if (slack)
2280
+ context.keymap.dispatch(delta < 0 ? "session.line.up" : "session.line.down");
2281
+ view.scroll.scrollTo(position);
2282
+ }
2283
+ context.renderer.requestRender();
2284
+ }
2285
+ function advance() {
2286
+ if (!navigation)
2287
+ return;
2288
+ const view = inspect();
2289
+ const ranges = view?.ranges ?? [];
2290
+ const from = navigation.from ? ranges.findIndex((range) => range.id === navigation.from) : ranges.length;
2291
+ if (navigation.from && from === -1) {
2292
+ navigation = undefined;
2293
+ return;
2294
+ }
2295
+ const target = navigation.latest ? ranges.length - 1 : from + navigation.offset;
2296
+ const loading = navigation.loading;
2297
+ if (loading && context.renderer.frameId < loading.frame) {
2298
+ context.renderer.requestRender();
2299
+ return;
2300
+ }
2301
+ if (view && loading?.up && view.scroll.scrollHeight > loading.height) {
2302
+ const maximum = Math.max(0, view.scroll.scrollHeight - view.scroll.viewport.height);
2303
+ const compensated = Math.max(0, Math.min(maximum, view.scroll.scrollHeight - loading.height - 1));
2304
+ if (view.scroll.scrollTop < compensated)
2305
+ return;
2306
+ }
2307
+ if (view && target >= 0 && target < ranges.length && (!navigation.latest || view.hasLatest)) {
2308
+ navigation = undefined;
2309
+ choose(ranges[target], view);
2310
+ return;
2311
+ }
2312
+ const up = !navigation.latest && target < 0;
2313
+ const messages = context.data.session.message.list(sessionID);
2314
+ const key = `${messages[0]?.id}:${messages.length}:${ranges[0]?.id}:${ranges.at(-1)?.id}:${view?.scroll.scrollHeight}`;
2315
+ if (loading?.key === key)
2316
+ return;
2317
+ navigation.loading = {
2318
+ key,
2319
+ frame: context.renderer.frameId + 2,
2320
+ up,
2321
+ height: view?.scroll.scrollHeight ?? 0
2322
+ };
2323
+ view?.scroll.scrollTo(up ? 0 : view.scroll.scrollHeight);
2324
+ context.keymap.dispatch(up ? "session.line.up" : "session.line.down");
2325
+ context.renderer.requestRender();
2326
+ }
2327
+ function latest() {
2328
+ if (!context.data.session.message.list(sessionID).length) {
2329
+ navigation = undefined;
2330
+ requestedID = undefined;
2331
+ select(undefined);
2332
+ return;
2333
+ }
2334
+ navigation = {
2335
+ from: undefined,
2336
+ offset: 0,
2337
+ latest: true
2338
+ };
2339
+ advance();
2340
+ }
2341
+ function move(direction, count) {
2342
+ if (navigation && (navigation.latest || navigation.offset < 0 && direction === "next" || navigation.offset > 0 && direction === "previous")) {
2343
+ navigation = undefined;
2344
+ }
2345
+ navigation ??= {
2346
+ from: requestedID ?? inspect()?.selected?.id,
2347
+ offset: 0
2348
+ };
2349
+ navigation.offset += direction === "next" ? count : -count;
2350
+ if (navigation.loading)
2351
+ navigation.loading.key = "";
2352
+ advance();
2353
+ }
2354
+ function inspect() {
2355
+ const messages = context.data.session.message.list(sessionID);
2356
+ const ids = new Set(messages.map((message) => message.id));
2357
+ function isRow(node) {
2358
+ if (ids.has(node.id) || node.id?.startsWith("session-part:") && ids.has(node.id.split(":")[1]))
2359
+ return true;
2360
+ const anchor = node.getChildren()[0];
2361
+ return Boolean(anchor && groupAt(anchor));
2362
+ }
2363
+ function findScroll(node) {
2364
+ if (!node.visible)
2365
+ return;
2366
+ if (node instanceof ScrollBoxRenderable && node.getChildren().some(isRow))
2367
+ return node;
2368
+ for (const child of node.getChildren()) {
2369
+ const found = findScroll(child);
2370
+ if (found)
2371
+ return found;
2372
+ }
2373
+ }
2374
+ if (!scroll || scroll.isDestroyed)
2375
+ scroll = findScroll(context.renderer.root);
2376
+ if (!scroll) {
2377
+ select(undefined);
2378
+ return;
2379
+ }
2380
+ const {
2381
+ ranges,
2382
+ hasLatest
2383
+ } = transcriptItems(scroll.getChildren(), messages);
2384
+ const top = scroll.viewport.y;
2385
+ const bottom = top + scroll.viewport.height;
2386
+ let selected = ranges.find((range) => range.id === requestedID);
2387
+ if (!selected && requestedID) {
2388
+ for (const id of selectedParents) {
2389
+ selected = ranges.find((range) => range.id === id);
2390
+ if (selected) {
2391
+ requestedID = id;
2392
+ break;
2393
+ }
2394
+ }
2395
+ if (!selected)
2396
+ requestedID = undefined;
2397
+ }
2398
+ selected ??= ranges.find((range) => range.top <= top + 1 && range.bottom > top + 1) ?? ranges.find((range) => range.bottom > top && range.top < bottom);
2399
+ selectedParents = [];
2400
+ let parent = selected?.parentID;
2401
+ while (parent) {
2402
+ selectedParents.push(parent);
2403
+ parent = ranges.find((range) => range.id === parent)?.parentID;
2404
+ }
2405
+ select(selected?.id);
2406
+ return {
2407
+ scroll,
2408
+ selected,
2409
+ ranges,
2410
+ hasLatest,
2411
+ top,
2412
+ bottom
2413
+ };
2414
+ }
2415
+ return {
2416
+ latest,
2417
+ move,
2418
+ get(id) {
2419
+ if (!scroll || scroll.isDestroyed)
2420
+ return;
2421
+ return transcriptItems(scroll.getChildren(), context.data.session.message.list(sessionID)).ranges.find((range) => range.id === id);
2422
+ },
2423
+ toggle() {
2424
+ const range = inspect()?.selected;
2425
+ if (!range?.toggle)
2426
+ return false;
2427
+ navigation = undefined;
2428
+ requestedID = range.id;
2429
+ const node = range.toggle;
2430
+ node.processMouseEvent(new MouseEvent(node, {
2431
+ type: "up",
2432
+ button: 0,
2433
+ x: node.x,
2434
+ y: node.y,
2435
+ modifiers: {
2436
+ shift: false,
2437
+ alt: false,
2438
+ ctrl: false
2439
+ }
2440
+ }));
2441
+ context.renderer.requestRender();
2442
+ return true;
2443
+ },
2444
+ followViewport() {
2445
+ navigation = undefined;
2446
+ requestedID = undefined;
2447
+ selectedParents = [];
2448
+ },
2449
+ focus() {
2450
+ inspect()?.scroll.focus();
2451
+ },
2452
+ sync() {
2453
+ advance();
2454
+ navigation = undefined;
2455
+ return inspect();
2456
+ },
2457
+ draw(buffer, yankID) {
2458
+ advance();
2459
+ const view = inspect();
2460
+ if (!view?.selected)
2461
+ return;
2462
+ const top = Math.max(0, view.top, view.selected.top);
2463
+ const bottom = Math.min(buffer.height, view.bottom, view.selected.bottom);
2464
+ if (view.selected.id === yankID) {
2465
+ const node = view.selected.node;
2466
+ const left = Math.max(0, view.scroll.viewport.x, node.x);
2467
+ const right = Math.min(buffer.width, view.scroll.viewport.x + view.scroll.viewport.width, node.x + node.width);
2468
+ const foreground = context.theme.background.base.buffer;
2469
+ const background = context.theme.text.feedback.info.base.buffer;
2470
+ const colors = buffer.buffers;
2471
+ for (let y = top;y < bottom; y++) {
2472
+ for (let x = left;x < right; x++) {
2473
+ const offset = (y * buffer.width + x) * 4;
2474
+ colors.fg.set(foreground, offset);
2475
+ colors.bg.set(background, offset);
2476
+ }
2477
+ }
2478
+ }
2479
+ const x = view.scroll.x - 1;
2480
+ if (x < 0)
2481
+ return;
2482
+ for (let y = top;y < bottom; y++) {
2483
+ buffer.drawText("\u258E", x, y, context.theme.text.feedback.info.base, context.theme.background.base);
2484
+ }
2485
+ }
2486
+ };
2487
+ }
2488
+
2489
+ // src/session.tsx
2490
+ var SESSION_MODE = "opencode-vim.session";
2491
+ function createSessionMode(context, config, clipboard) {
2492
+ const [active, setActive] = createSignal2(false);
2493
+ const [reading, setReading] = createSignal2();
2494
+ const [notice, setNotice] = createSignal2("");
2495
+ const positions = new Map;
2496
+ let sessionID = "";
2497
+ let selectedID;
2498
+ let prompt = null;
2499
+ let transcript;
2500
+ let popMode;
2501
+ let prefix = "";
2502
+ let count = "";
2503
+ let disposed = false;
2504
+ let noticeTimer;
2505
+ let yankTimer;
2506
+ let yankID;
2507
+ function selected() {
2508
+ return transcript?.get(selectedID);
2509
+ }
2510
+ function clearYankFlash() {
2511
+ if (yankTimer)
2512
+ clearTimeout(yankTimer);
2513
+ yankTimer = undefined;
2514
+ if (!yankID)
2515
+ return;
2516
+ yankID = undefined;
2517
+ context.renderer.requestRender();
2518
+ }
2519
+ function close() {
2520
+ if (!active())
2521
+ return;
2522
+ clearYankFlash();
2523
+ const wasReading = Boolean(reading());
2524
+ if (wasReading)
2525
+ context.ui.dialog.clear();
2526
+ setActive(false);
2527
+ context.renderer.removePostProcessFn(draw);
2528
+ popMode?.();
2529
+ popMode = undefined;
2530
+ transcript = undefined;
2531
+ const savedPrompt = prompt;
2532
+ const savedSessionID = sessionID;
2533
+ function restorePrompt() {
2534
+ const route = context.ui.router.current();
2535
+ if (!active() && context.keymap.mode.current() === "base" && route.type === "session" && route.sessionID === savedSessionID && savedPrompt && !savedPrompt.isDestroyed)
2536
+ savedPrompt.focus();
2537
+ }
2538
+ restorePrompt();
2539
+ if (wasReading)
2540
+ setTimeout(restorePrompt, 1);
2541
+ prompt = null;
2542
+ context.renderer.requestRender();
2543
+ }
2544
+ function enter() {
2545
+ const route = context.ui.router.current();
2546
+ if (route.type !== "session")
2547
+ return false;
2548
+ sessionID = route.sessionID;
2549
+ prompt = context.renderer.currentFocusedEditor;
2550
+ prefix = count = "";
2551
+ positions.clear();
2552
+ setNotice("");
2553
+ popMode = context.keymap.mode.push(SESSION_MODE);
2554
+ transcript = createTranscriptSelection(context, sessionID, (id) => {
2555
+ selectedID = id;
2556
+ });
2557
+ setActive(true);
2558
+ prompt?.blur();
2559
+ transcript.focus();
2560
+ transcript.latest();
2561
+ context.renderer.addPostProcessFn(draw);
2562
+ context.renderer.requestRender();
2563
+ return true;
2564
+ }
2565
+ const draw = (buffer) => {
2566
+ if (active() && !reading() && context.keymap.mode.current() === SESSION_MODE)
2567
+ transcript?.draw(buffer, yankID);
2568
+ };
2569
+ async function copy(text) {
2570
+ const copied = await clipboard.write(text);
2571
+ if (disposed)
2572
+ return;
2573
+ if (noticeTimer)
2574
+ clearTimeout(noticeTimer);
2575
+ setNotice(copied ? "Copied" : "Yanked \xB7 clipboard unavailable");
2576
+ noticeTimer = setTimeout(() => setNotice(""), 1500);
2577
+ }
2578
+ function openMessage(message) {
2579
+ transcript?.focus();
2580
+ setReading(message);
2581
+ context.ui.dialog.show(() => _$createComponent(MessageReader, {
2582
+ context,
2583
+ config,
2584
+ message,
2585
+ get offset() {
2586
+ return positions.get(message.id) ?? 0;
2587
+ },
2588
+ copy,
2589
+ notice,
2590
+ remember: (offset) => positions.set(message.id, offset),
2591
+ back: () => context.ui.dialog.clear(),
2592
+ close
2593
+ }), () => {
2594
+ setReading(undefined);
2595
+ setNotice("");
2596
+ });
2597
+ }
2598
+ const onKey = (event) => {
2599
+ if (!active() || reading())
2600
+ return;
2601
+ const key = readerKey(context, event, config.sessionKey);
2602
+ if (!key)
2603
+ return;
2604
+ event.preventDefault();
2605
+ event.stopPropagation();
2606
+ clearYankFlash();
2607
+ setNotice("");
2608
+ if (key === config.sessionKey || key === "<Esc>" || key === "<C-[>") {
2609
+ close();
2610
+ return;
2611
+ }
2612
+ if (/^[0-9]$/.test(key) && (count || key !== "0")) {
2613
+ count = (count + key).slice(0, 6);
2614
+ return;
2615
+ }
2616
+ const amount = Number(count) || 1;
2617
+ count = "";
2618
+ const previous = prefix;
2619
+ prefix = "";
2620
+ let command = pageCommand(key);
2621
+ if (key === "j" || key === "<Down>") {
2622
+ transcript?.move("next", amount);
2623
+ return;
2624
+ } else if (key === "k" || key === "<Up>") {
2625
+ transcript?.move("previous", amount);
2626
+ return;
2627
+ } else if (key === "G") {
2628
+ transcript?.latest();
2629
+ return;
2630
+ } else if (key === "g" && previous === "g")
2631
+ command = "session.first";
2632
+ else if (key === "y" && previous === "y") {
2633
+ const message = selected();
2634
+ if (message) {
2635
+ yankID = message.id;
2636
+ yankTimer = setTimeout(clearYankFlash, YANK_FLASH_MS);
2637
+ context.renderer.requestRender();
2638
+ copy(message.text);
2639
+ }
2640
+ } else if (key === "g" || key === "y")
2641
+ prefix = key;
2642
+ else if (key === "<CR>") {
2643
+ transcript?.sync();
2644
+ if (transcript?.toggle())
2645
+ return;
2646
+ const message = selected();
2647
+ if (message)
2648
+ openMessage(message);
2649
+ }
2650
+ if (command) {
2651
+ transcript?.followViewport();
2652
+ for (let index = 0;index < amount; index++)
2653
+ context.keymap.dispatch(command);
2654
+ }
2655
+ };
2656
+ createEffect(() => {
2657
+ const route = context.ui.router.current();
2658
+ if (active() && (route.type !== "session" || route.sessionID !== sessionID))
2659
+ untrack(close);
2660
+ });
2661
+ onMount(() => context.renderer.keyInput.prependListener("keypress", onKey));
2662
+ onCleanup(() => {
2663
+ disposed = true;
2664
+ close();
2665
+ if (noticeTimer)
2666
+ clearTimeout(noticeTimer);
2667
+ context.renderer.keyInput.off("keypress", onKey);
2668
+ });
2669
+ return {
2670
+ active,
2671
+ enter,
2672
+ close,
2673
+ Status() {
2674
+ return (() => {
2675
+ var _el$ = _$createElement2("text");
2676
+ _$insert2(_el$, () => notice() || `SESSION \xB7 Enter open \xB7 yy copy \xB7 ${config.sessionKey} prompt`);
2677
+ _$effect2((_$p) => _$setProp2(_el$, "fg", context.theme.text.feedback.info.base, _$p));
2678
+ return _el$;
2679
+ })();
2680
+ }
2681
+ };
2682
+ }
2683
+ function MessageReader(props) {
2684
+ const context = props.context;
2685
+ context.ui.dialog.set({
2686
+ size: "large",
2687
+ centered: true
2688
+ });
2689
+ const dimensions = useTerminalDimensions();
2690
+ const theme = () => context.theme.surface("dialog");
2691
+ const background = () => theme().background.raised.high;
2692
+ const state = createVimState("normal");
2693
+ const [line, setLine] = createSignal2(1);
2694
+ let input;
2695
+ const adapter = createVimeeAdapter(state, props.config, () => {}, {
2696
+ readOnly: true,
2697
+ onYank: props.copy
2698
+ });
2699
+ const editorContext = {
2700
+ api: {
2701
+ renderer: {
2702
+ get currentFocusedRenderable() {
2703
+ return input;
2704
+ }
2705
+ },
2706
+ keymap: {
2707
+ dispatchCommand: () => ({
2708
+ ok: false
2709
+ })
2710
+ },
2711
+ theme: {
2712
+ get current() {
2713
+ return {
2714
+ warning: theme().text.feedback.warning.base,
2715
+ info: theme().text.feedback.info.base,
2716
+ background: background()
2717
+ };
2718
+ }
2719
+ }
2720
+ },
2721
+ prompt: () => ({
2722
+ get current() {
2723
+ return {
2724
+ input: input.plainText,
2725
+ mode: "normal",
2726
+ parts: []
2727
+ };
2728
+ },
2729
+ set() {},
2730
+ submit() {},
2731
+ blur() {}
2732
+ }),
2733
+ requestRender: () => context.renderer.requestRender()
2734
+ };
2735
+ const onKey = (event) => {
2736
+ if (context.renderer.currentFocusedEditor !== input)
2737
+ return;
2738
+ const key = readerKey(context, event, props.config.sessionKey, "modal");
2739
+ if (!key)
2740
+ return;
2741
+ event.preventDefault();
2742
+ event.stopPropagation();
2743
+ if (key === props.config.sessionKey && state.mode() === "normal" && !adapter.isPending()) {
2744
+ props.close();
2745
+ return;
2746
+ }
2747
+ if (key === "<C-c>") {
2748
+ props.back();
2749
+ return;
2750
+ }
2751
+ if ((key === "<Esc>" || key === "<C-[>") && state.mode() === "normal" && !adapter.isPending()) {
2752
+ props.back();
2753
+ return;
2754
+ }
2755
+ if (pageCommand(key)) {
2756
+ const down = key === "<C-d>" || key === "<C-f>" || key === "<PageDown>";
2757
+ const half = key === "<C-d>" || key === "<C-u>";
2758
+ const rows = Math.max(1, Math.floor(input.height / (half ? 2 : 1)));
2759
+ for (let row = 0;row < rows; row++)
2760
+ adapter.handle({
2761
+ ...event,
2762
+ ctrl: false
2763
+ }, down ? "j" : "k", editorContext);
2764
+ } else
2765
+ adapter.handle(event, key, editorContext);
2766
+ setLine(input.logicalCursor.row + 1);
2767
+ };
2768
+ onMount(() => {
2769
+ input.cursorOffset = Math.min(props.offset, displayWidth(input.plainText));
2770
+ adapter.attach(editorContext);
2771
+ setLine(input.logicalCursor.row + 1);
2772
+ input.focus();
2773
+ context.renderer.keyInput.prependListener("keypress", onKey);
2774
+ });
2775
+ onCleanup(() => {
2776
+ props.remember(input.cursorOffset);
2777
+ context.renderer.keyInput.off("keypress", onKey);
2778
+ adapter.cleanup();
2779
+ });
2780
+ function modeLabel() {
2781
+ if (state.mode() === "visual")
2782
+ return "VISUAL";
2783
+ if (state.mode() === "visual-line")
2784
+ return "VISUAL LINE";
2785
+ return "MESSAGE";
2786
+ }
2787
+ return (() => {
2788
+ var _el$2 = _$createElement2("box"), _el$3 = _$createElement2("box"), _el$4 = _$createElement2("text"), _el$5 = _$createElement2("b"), _el$6 = _$createElement2("text"), _el$8 = _$createElement2("box"), _el$9 = _$createElement2("textarea"), _el$0 = _$createElement2("box"), _el$1 = _$createElement2("text"), _el$10 = _$createTextNode(` \xB7 `), _el$11 = _$createTextNode(`/`), _el$12 = _$createElement2("text");
2789
+ _$insertNode2(_el$2, _el$3);
2790
+ _$insertNode2(_el$2, _el$8);
2791
+ _$insertNode2(_el$2, _el$0);
2792
+ _$setProp2(_el$2, "id", "vim-message-reader");
2793
+ _$setProp2(_el$2, "gap", 1);
2794
+ _$insertNode2(_el$3, _el$4);
2795
+ _$insertNode2(_el$3, _el$6);
2796
+ _$setProp2(_el$3, "paddingLeft", 2);
2797
+ _$setProp2(_el$3, "paddingRight", 2);
2798
+ _$setProp2(_el$3, "flexDirection", "row");
2799
+ _$setProp2(_el$3, "justifyContent", "space-between");
2800
+ _$insertNode2(_el$4, _el$5);
2801
+ _$insert2(_el$5, () => props.message.author);
2802
+ _$insertNode2(_el$6, _$createTextNode(`esc`));
2803
+ _$setProp2(_el$6, "id", "vim-message-close");
2804
+ _$insertNode2(_el$8, _el$9);
2805
+ _$setProp2(_el$8, "paddingLeft", 2);
2806
+ _$setProp2(_el$8, "paddingRight", 2);
2807
+ _$setProp2(_el$8, "paddingTop", 1);
2808
+ _$setProp2(_el$8, "paddingBottom", 1);
2809
+ _$use((value) => {
2810
+ input = value;
2811
+ input.handleKeyPress = () => true;
2812
+ input.handlePaste = () => {};
2813
+ }, _el$9);
2814
+ _$setProp2(_el$9, "id", "vim-session-message");
2815
+ _$setProp2(_el$9, "minHeight", 1);
2816
+ _$setProp2(_el$9, "wrapMode", "word");
2817
+ _$setProp2(_el$9, "showCursor", true);
2818
+ _$insertNode2(_el$0, _el$1);
2819
+ _$insertNode2(_el$0, _el$12);
2820
+ _$setProp2(_el$0, "paddingLeft", 2);
2821
+ _$setProp2(_el$0, "paddingRight", 2);
2822
+ _$setProp2(_el$0, "paddingBottom", 1);
2823
+ _$setProp2(_el$0, "flexDirection", "row");
2824
+ _$setProp2(_el$0, "flexWrap", "wrap");
2825
+ _$setProp2(_el$0, "columnGap", 3);
2826
+ _$insertNode2(_el$1, _el$10);
2827
+ _$insertNode2(_el$1, _el$11);
2828
+ _$insert2(_el$1, modeLabel, _el$10);
2829
+ _$insert2(_el$1, line, _el$11);
2830
+ _$insert2(_el$1, () => input?.lineCount ?? 1, null);
2831
+ _$insert2(_el$12, () => props.notice() || (state.mode() !== "normal" ? "y copy \xB7 Esc cancel" : `v select \xB7 V lines \xB7 ${props.config.sessionKey} prompt`));
2832
+ _$effect2((_p$) => {
2833
+ var _v$ = theme().text.base, _v$2 = theme().text.muted, _v$3 = props.back, _v$4 = background(), _v$5 = props.message.text, _v$6 = Math.max(1, Math.min(20, dimensions().height - 10)), _v$7 = props.config.cursorStyles.normal, _v$8 = theme().text.base, _v$9 = background(), _v$0 = background(), _v$1 = theme().text.base, _v$10 = theme().text.muted, _v$11 = theme().text.muted;
2834
+ _v$ !== _p$.e && (_p$.e = _$setProp2(_el$4, "fg", _v$, _p$.e));
2835
+ _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$6, "fg", _v$2, _p$.t));
2836
+ _v$3 !== _p$.a && (_p$.a = _$setProp2(_el$6, "onMouseUp", _v$3, _p$.a));
2837
+ _v$4 !== _p$.o && (_p$.o = _$setProp2(_el$8, "backgroundColor", _v$4, _p$.o));
2838
+ _v$5 !== _p$.i && (_p$.i = _$setProp2(_el$9, "initialValue", _v$5, _p$.i));
2839
+ _v$6 !== _p$.n && (_p$.n = _$setProp2(_el$9, "maxHeight", _v$6, _p$.n));
2840
+ _v$7 !== _p$.s && (_p$.s = _$setProp2(_el$9, "cursorStyle", _v$7, _p$.s));
2841
+ _v$8 !== _p$.h && (_p$.h = _$setProp2(_el$9, "textColor", _v$8, _p$.h));
2842
+ _v$9 !== _p$.r && (_p$.r = _$setProp2(_el$9, "backgroundColor", _v$9, _p$.r));
2843
+ _v$0 !== _p$.d && (_p$.d = _$setProp2(_el$9, "focusedBackgroundColor", _v$0, _p$.d));
2844
+ _v$1 !== _p$.l && (_p$.l = _$setProp2(_el$9, "focusedTextColor", _v$1, _p$.l));
2845
+ _v$10 !== _p$.u && (_p$.u = _$setProp2(_el$1, "fg", _v$10, _p$.u));
2846
+ _v$11 !== _p$.c && (_p$.c = _$setProp2(_el$12, "fg", _v$11, _p$.c));
2847
+ return _p$;
2848
+ }, {
2849
+ e: undefined,
2850
+ t: undefined,
2851
+ a: undefined,
2852
+ o: undefined,
2853
+ i: undefined,
2854
+ n: undefined,
2855
+ s: undefined,
2856
+ h: undefined,
2857
+ r: undefined,
2858
+ d: undefined,
2859
+ l: undefined,
2860
+ u: undefined,
2861
+ c: undefined
2862
+ });
2863
+ return _el$2;
2864
+ })();
2865
+ }
2866
+ function readerKey(context, event, sessionKey, mode = SESSION_MODE) {
2867
+ if (event.defaultPrevented || context.keymap.mode.current() !== mode)
2868
+ return;
2869
+ if (context.keymap.pending().length || event.super || event.meta)
2870
+ return;
2871
+ const key = keyNotation(event);
2872
+ if (key && (key === sessionKey || !event.ctrl || key === "<C-[>" || pageCommand(key) || mode === "modal" && key === "<C-c>"))
2873
+ return key;
2874
+ }
2875
+ function pageCommand(key) {
2876
+ if (key === "<C-d>")
2877
+ return "session.half.page.down";
2878
+ if (key === "<C-u>")
2879
+ return "session.half.page.up";
2880
+ if (key === "<C-f>" || key === "<PageDown>")
2881
+ return "session.page.down";
2882
+ if (key === "<C-b>" || key === "<PageUp>")
2883
+ return "session.page.up";
2884
+ }
2885
+
2886
+ // src/clipboard.tsx
2887
+ import { createClipboard, createHostClipboard, createRendererClipboardAdapter } from "opentui:runtime-module:%40opentui%2Fcore";
2888
+ function createVimClipboard(renderer) {
2889
+ const terminal = createRendererClipboardAdapter(renderer);
2890
+ const clipboard = createClipboard({
2891
+ host: createHostClipboard(),
2892
+ terminal
2893
+ });
2894
+ let fallback = "";
2895
+ let revision = 0;
2896
+ let writes = Promise.resolve();
2897
+ let disposed = false;
2898
+ return {
2899
+ write(text) {
2900
+ text = text.replaceAll("\x00", "");
2901
+ fallback = text;
2902
+ revision++;
2903
+ const result = writes.then(async () => {
2904
+ if (disposed)
2905
+ return false;
2906
+ try {
2907
+ const result = await clipboard.writeText(text, {
2908
+ destination: "all-available",
2909
+ selection: "clipboard"
2910
+ });
2911
+ return result.host.status === "written" || result.terminal.status === "attempted";
2912
+ } catch {
2913
+ return false;
2914
+ }
2915
+ });
2916
+ writes = result.then(() => {});
2917
+ return result;
2918
+ },
2919
+ async read() {
2920
+ await writes;
2921
+ if (disposed || terminal.remote)
2922
+ return fallback;
2923
+ const before = revision;
2924
+ try {
2925
+ const result = await clipboard.read({
2926
+ preferredTypes: ["text/plain"],
2927
+ selection: "clipboard"
2928
+ });
2929
+ if (before !== revision)
2930
+ return fallback;
2931
+ if (result.status === "empty")
2932
+ fallback = "";
2933
+ else if (result.status === "read" && result.representation.mimeType === "text/plain") {
2934
+ fallback = new TextDecoder().decode(result.representation.bytes).replace(/\r\n?/g, `
2935
+ `).replaceAll("\x00", "");
2936
+ }
2937
+ } catch {}
2938
+ return fallback;
2939
+ },
2940
+ dispose() {
2941
+ disposed = true;
2942
+ return clipboard.dispose();
2943
+ }
2944
+ };
2945
+ }
2946
+
2947
+ // tui.tsx
2948
+ var tui_default = Plugin.define({
2949
+ id: "opencode-vim",
2950
+ setup(context) {
2951
+ context.ui.slot({
2952
+ append: "app",
2953
+ render: () => _$createComponent2(VimHost, {
2954
+ context
2955
+ })
2956
+ });
2957
+ }
2958
+ });
2959
+ function VimHost(props) {
2960
+ const config = createVimConfig(props.context.options);
2961
+ const normalMappings = Object.keys(config.keymaps.normal ?? {});
2962
+ const log = createVimLog(config);
2963
+ const clipboard = createVimClipboard(props.context.renderer);
2964
+ const clipboardOptions = {
2965
+ onYank: (text) => {
2966
+ clipboard.write(text);
2967
+ },
2968
+ readClipboard: clipboard.read
2969
+ };
2970
+ const state = createVimState(config.defaultMode, log);
2971
+ const vimee = createVimeeAdapter(state, config, log, clipboardOptions);
2972
+ const dialogState = createVimState(config.defaultMode, log);
2973
+ const dialogVimee = createVimeeAdapter(dialogState, config, log, clipboardOptions);
2974
+ const [dialogFocused, setDialogFocused] = createSignal3(false);
2975
+ const promptVim = {
2976
+ state,
2977
+ vimee
2978
+ };
2979
+ const dialogVim = {
2980
+ state: dialogState,
2981
+ vimee: dialogVimee
2982
+ };
2983
+ let dialogInput = null;
2984
+ const [saved, setSaved] = props.context.storage.store("state", {
2985
+ initial: {
2986
+ enabled: true
2987
+ }
2988
+ });
2989
+ const enabled = () => saved.enabled;
2990
+ const ctx = createCompatContext(props.context);
2991
+ let cursorMode = "";
2992
+ let cursorInput = null;
2993
+ let originalCursorStyle;
2994
+ const session = createSessionMode(props.context, config, clipboard);
2995
+ let pendingKeys;
2996
+ const removeStatus = props.context.ui.slot({
2997
+ prepend: "prompt.footer",
2998
+ render: (footer) => footer.mode === "normal" ? session.active() && !dialogFocused() ? _$createComponent2(session.Status, {}) : _$createComponent2(VimStatus, {
2999
+ mode: () => dialogFocused() ? dialogState.mode() : state.mode(),
3000
+ enabled,
3001
+ get theme() {
3002
+ return compatTheme(props.context);
3003
+ }
3004
+ }) : null
3005
+ });
3006
+ props.context.keymap.layer(() => ({
3007
+ mode: "global",
3008
+ commands: [{
3009
+ id: "opencode-vim.toggle",
3010
+ title: "Toggle Vim Mode",
3011
+ description: "Enable or disable Vim key handling",
3012
+ group: "Vim",
3013
+ palette: true,
3014
+ slash: {
3015
+ name: "vim"
3016
+ },
3017
+ run() {
3018
+ const next = !enabled();
3019
+ setSaved((draft) => {
3020
+ draft.enabled = next;
3021
+ });
3022
+ if (!next) {
3023
+ pendingKeys = undefined;
3024
+ session.close();
3025
+ vimee.suspend();
3026
+ dialogVimee.suspend();
3027
+ }
3028
+ props.context.ui.toast.show({
3029
+ message: `Vim mode ${next ? "enabled" : "disabled"}`,
3030
+ variant: "info"
3031
+ });
3032
+ props.context.renderer.requestRender();
3033
+ }
3034
+ }]
3035
+ }));
3036
+ const onKey = (event) => {
3037
+ if (!enabled() || event.defaultPrevented)
3038
+ return;
3039
+ if (pendingKeys) {
3040
+ pendingKeys.push(new KeyEvent(event));
3041
+ event.preventDefault();
3042
+ event.stopPropagation();
3043
+ return;
3044
+ }
3045
+ if (props.context.keymap.mode.current() === SESSION_MODE)
3046
+ return;
3047
+ const kind = inputKind(props.context);
3048
+ if (!kind)
3049
+ return;
3050
+ if (props.context.keymap.pending().length)
3051
+ return;
3052
+ const {
3053
+ state,
3054
+ vimee
3055
+ } = kind === "dialog" ? dialogVim : promptVim;
3056
+ const key = keyNotation(event);
3057
+ if (!key)
3058
+ return;
3059
+ const mapped = normalMappings.some((sequence) => sequence.startsWith(key));
3060
+ if (kind === "prompt" && key === config.sessionKey && !mapped && state.mode() === "normal" && !vimee.isPending()) {
3061
+ if (session.enter()) {
3062
+ event.preventDefault();
3063
+ event.stopPropagation();
3064
+ return;
3065
+ }
3066
+ }
3067
+ if (passThroughKey(event, key, state.mode(), vimee.isPending(), mapped))
3068
+ return;
3069
+ if (key === "<Esc>" && state.mode() === "normal" && !vimee.isPending())
3070
+ return;
3071
+ if (kind === "dialog" && !vimee.isPending() && !mapped && state.mode() === "normal") {
3072
+ if (key === "<Tab>" || key === "<Home>" || key === "<End>" || key === "<PageUp>" || key === "<PageDown>")
3073
+ return;
3074
+ const command = key === "j" ? "dialog.select.next" : key === "k" ? "dialog.select.prev" : undefined;
3075
+ if (command && props.context.keymap.commands().some((item) => item.id === command)) {
3076
+ event.preventDefault();
3077
+ event.stopPropagation();
3078
+ props.context.keymap.dispatch(command);
3079
+ return;
3080
+ }
3081
+ }
3082
+ if (kind === "prompt" && !vimee.isPending() && sendCompletionKey(event, props.context, key, state.mode())) {
3083
+ syncCursor(true);
3084
+ return;
3085
+ }
3086
+ const before = state.mode();
3087
+ let consumed = false;
3088
+ try {
3089
+ const result = vimee.handle(event, key, ctx);
3090
+ if (typeof result === "boolean")
3091
+ consumed = result;
3092
+ else {
3093
+ consumed = true;
3094
+ const queued = [];
3095
+ pendingKeys = queued;
3096
+ const input = props.context.renderer.currentFocusedEditor;
3097
+ result.then((handled) => {
3098
+ if (pendingKeys !== queued)
3099
+ return;
3100
+ pendingKeys = undefined;
3101
+ if (!handled)
3102
+ return;
3103
+ syncCursor(true);
3104
+ for (const event of queued) {
3105
+ if (!enabled() || props.context.renderer.currentFocusedEditor !== input)
3106
+ break;
3107
+ if (event instanceof PasteEvent)
3108
+ props.context.renderer.keyInput.emit("paste", event);
3109
+ else
3110
+ props.context.renderer.keyInput.emit("keypress", event);
3111
+ }
3112
+ }).catch((error) => {
3113
+ if (pendingKeys === queued)
3114
+ pendingKeys = undefined;
3115
+ log("vim.clipboard.paste.error", {
3116
+ error: String(error)
3117
+ });
3118
+ });
3119
+ }
3120
+ } finally {
3121
+ if (consumed || state.mode() !== before) {
3122
+ event.preventDefault();
3123
+ event.stopPropagation();
3124
+ }
3125
+ }
3126
+ if (consumed)
3127
+ syncCursor(true);
3128
+ };
3129
+ const onFocus = () => {
3130
+ pendingKeys = undefined;
3131
+ vimee.suspend();
3132
+ dialogVimee.suspend();
3133
+ syncCursor();
3134
+ };
3135
+ const onPaste = (event) => {
3136
+ if (!pendingKeys || event.defaultPrevented)
3137
+ return;
3138
+ pendingKeys.push(new PasteEvent(event.bytes, event.metadata));
3139
+ event.preventDefault();
3140
+ event.stopPropagation();
3141
+ };
3142
+ props.context.renderer.keyInput.prependListener("keypress", onKey);
3143
+ props.context.renderer.keyInput.prependListener("paste", onPaste);
3144
+ props.context.renderer.on("focused_editor", onFocus);
3145
+ createEffect2(() => syncCursor());
3146
+ let route = props.context.ui.router.current();
3147
+ createEffect2(() => {
3148
+ const next = props.context.ui.router.current();
3149
+ if (next !== route && pendingKeys)
3150
+ onFocus();
3151
+ route = next;
3152
+ });
3153
+ onCleanup2(() => {
3154
+ pendingKeys = undefined;
3155
+ removeStatus();
3156
+ session.close();
3157
+ props.context.renderer.keyInput.off("keypress", onKey);
3158
+ props.context.renderer.keyInput.off("paste", onPaste);
3159
+ props.context.renderer.off("focused_editor", onFocus);
3160
+ vimee.cleanup();
3161
+ dialogVimee.cleanup();
3162
+ clipboard.dispose();
3163
+ restoreCursor();
3164
+ });
3165
+ return null;
3166
+ function syncCursor(force = false) {
3167
+ const input = props.context.renderer.currentFocusedEditor;
3168
+ const kind = inputKind(props.context);
3169
+ setDialogFocused(kind === "dialog");
3170
+ if (!enabled() || !kind || !input) {
3171
+ restoreCursor();
3172
+ return;
3173
+ }
3174
+ const {
3175
+ state,
3176
+ vimee
3177
+ } = kind === "dialog" ? dialogVim : promptVim;
3178
+ if (kind === "dialog" && dialogInput !== input) {
3179
+ dialogInput = input;
3180
+ dialogVimee.suspend();
3181
+ dialogState.setMode(promptVim.state.mode());
3182
+ }
3183
+ const mode = state.mode();
3184
+ vimee.attach(ctx);
3185
+ const inputChanged = cursorInput !== input;
3186
+ if (inputChanged) {
3187
+ restoreCursor();
3188
+ cursorInput = input;
3189
+ originalCursorStyle = input.cursorStyle;
3190
+ }
3191
+ if (!force && !inputChanged && cursorMode === mode)
3192
+ return;
3193
+ if (applyVimCursorStyle(ctx, config.cursorStyles[mode])) {
3194
+ cursorMode = mode;
3195
+ props.context.renderer.requestRender();
3196
+ }
3197
+ }
3198
+ function restoreCursor() {
3199
+ if (cursorInput && !cursorInput.isDestroyed && originalCursorStyle)
3200
+ cursorInput.cursorStyle = originalCursorStyle;
3201
+ cursorInput = null;
3202
+ cursorMode = "";
3203
+ }
3204
+ }
3205
+ function createCompatContext(context) {
3206
+ const prompt = {
3207
+ get current() {
3208
+ return {
3209
+ input: focusedInputValue(context),
3210
+ mode: "normal",
3211
+ parts: []
3212
+ };
3213
+ },
3214
+ get focused() {
3215
+ return inputKind(context) !== undefined;
3216
+ },
3217
+ set(value) {
3218
+ const input = context.renderer.currentFocusedEditor;
3219
+ if (input instanceof InputRenderable)
3220
+ input.value = value.input;
3221
+ else if (input)
3222
+ editInput(input, value.input);
3223
+ },
3224
+ submit() {
3225
+ if (inputKind(context) === "dialog") {
3226
+ const command = context.keymap.commands().some((item) => item.id === "dialog.select.submit") ? "dialog.select.submit" : "dialog.prompt.submit";
3227
+ context.keymap.dispatch(command);
3228
+ } else {
3229
+ context.keymap.dispatch("prompt.submit");
3230
+ }
3231
+ },
3232
+ blur() {
3233
+ context.renderer.currentFocusedEditor?.blur();
3234
+ }
3235
+ };
3236
+ return {
3237
+ api: {
3238
+ renderer: context.renderer,
3239
+ keymap: {
3240
+ dispatchCommand(command) {
3241
+ const ok = context.keymap.commands().some((item) => item.id === command);
3242
+ if (ok)
3243
+ context.keymap.dispatch(command);
3244
+ return {
3245
+ ok
3246
+ };
3247
+ }
3248
+ },
3249
+ theme: {
3250
+ current: compatTheme(context)
3251
+ }
3252
+ },
3253
+ kind: context.ui.router.current().type === "session" ? "session" : "home",
3254
+ prompt: () => inputKind(context) ? prompt : undefined,
3255
+ requestRender: () => context.renderer.requestRender()
3256
+ };
3257
+ }
3258
+ function inputKind(context) {
3259
+ if (context.keymap.mode.current() === SESSION_MODE)
3260
+ return;
3261
+ if (!context.renderer.currentFocusedEditor)
3262
+ return;
3263
+ const commands = context.keymap.commands();
3264
+ if (context.keymap.mode.current() === "modal") {
3265
+ if (commands.some((item) => item.id === "dialog.select.submit" || item.id === "dialog.prompt.submit"))
3266
+ return "dialog";
3267
+ return;
3268
+ }
3269
+ if (commands.some((item) => item.id === "prompt.submit" || item.id === "prompt.autocomplete.next" || item.id === "prompt.history.previous"))
3270
+ return "prompt";
3271
+ }
3272
+ function focusedInputValue(context) {
3273
+ return context.renderer.currentFocusedEditor?.plainText ?? "";
3274
+ }
3275
+ function compatTheme(context) {
3276
+ return {
3277
+ get background() {
3278
+ return context.theme.background.base;
3279
+ },
3280
+ get info() {
3281
+ return context.theme.text.feedback.info.base;
3282
+ },
3283
+ get success() {
3284
+ return context.theme.text.feedback.success.base;
3285
+ },
3286
+ get warning() {
3287
+ return context.theme.text.feedback.warning.base;
3288
+ },
3289
+ get textMuted() {
3290
+ return context.theme.text.muted;
3291
+ }
3292
+ };
3293
+ }
3294
+ function passThroughKey(event, key, mode, pending, mapped) {
3295
+ if (mode !== "normal")
3296
+ return false;
3297
+ if (mapped)
3298
+ return false;
3299
+ if (event.ctrl && key !== "<C-r>" && key !== "<C-[>")
3300
+ return true;
3301
+ return event.super === true || event.meta === true || isArrowKey(key) && !pending;
3302
+ }
3303
+ function sendCompletionKey(event, context, key, mode) {
3304
+ if (mode !== "normal")
3305
+ return false;
3306
+ const command = key === "j" ? "prompt.autocomplete.next" : key === "k" ? "prompt.autocomplete.prev" : undefined;
3307
+ if (!command || !isCompletionToken(context) || !context.keymap.commands().some((item) => item.id === command))
3308
+ return false;
3309
+ event.preventDefault();
3310
+ event.stopPropagation();
3311
+ context.keymap.dispatch(command);
3312
+ return true;
3313
+ }
3314
+ function isCompletionToken(context) {
3315
+ const input = context.renderer.currentFocusedEditor;
3316
+ const text = input?.plainText ?? "";
3317
+ const index = displayToChar(text, Math.max(0, input?.cursorOffset ?? 0));
3318
+ const before = text.slice(0, Math.min(index + 1, text.length));
3319
+ return /^\/\S*$/.test(before) || /(?:^|\s)@\S*$/.test(before);
3320
+ }
3321
+ function isArrowKey(key) {
3322
+ return key === "<Left>" || key === "<Down>" || key === "<Up>" || key === "<Right>";
3323
+ }
3324
+ export {
3325
+ tui_default as default
3326
+ };