opencode-vim 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -63
- package/assets/vim-in-motion-dialogs.gif +0 -0
- package/dist/tui.js +3326 -0
- package/docs/configuration.md +50 -400
- package/docs/keymap-actions.md +60 -188
- package/docs/vim-behavior.md +93 -0
- package/package.json +6 -6
- package/src/modules/snippets/index.tsx +0 -17
- package/src/modules/snippets/loader.ts +0 -147
- package/src/modules/snippets/search.ts +0 -118
- package/src/modules/snippets/skill-loader.ts +0 -81
- package/src/modules/snippets/state.ts +0 -40
- package/src/modules/snippets/trigger.ts +0 -68
- package/src/modules/snippets/types.ts +0 -50
- package/src/modules/snippets/view.tsx +0 -674
- package/src/modules/vim/actions.ts +0 -85
- package/src/modules/vim/config.ts +0 -125
- package/src/modules/vim/edit.test.ts +0 -51
- package/src/modules/vim/edit.ts +0 -33
- package/src/modules/vim/graphemes.ts +0 -38
- package/src/modules/vim/index.tsx +0 -200
- package/src/modules/vim/keys.ts +0 -38
- package/src/modules/vim/log.ts +0 -37
- package/src/modules/vim/map.test.ts +0 -45
- package/src/modules/vim/map.ts +0 -198
- package/src/modules/vim/state.test.ts +0 -18
- package/src/modules/vim/state.tsx +0 -46
- package/src/modules/vim/view.tsx +0 -130
- package/src/modules/vim/vimee.test.ts +0 -211
- package/src/modules/vim/vimee.ts +0 -1146
- package/src/plugin.tsx +0 -116
- package/src/prompt/host.tsx +0 -38
- package/src/prompt/modules.ts +0 -16
- package/src/prompt/root.tsx +0 -69
- package/src/prompt/types.ts +0 -42
- package/src/update.ts +0 -120
- package/tui.tsx +0 -275
- 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
|
+
};
|