mocode-ai 1.2.10 → 1.3.1
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/dist/i18n/index.js +52 -2
- package/dist/llm/index.js +26 -0
- package/dist/permissions/index.js +18 -1
- package/dist/repl/index.js +173 -7
- package/dist/ui/intervention.js +4 -1
- package/dist/ui/layout.js +64 -7
- package/dist/ui/prompt.js +376 -291
- package/package.json +1 -1
package/dist/ui/prompt.js
CHANGED
|
@@ -73,76 +73,24 @@ export async function promptWithSlashMenu(opts) {
|
|
|
73
73
|
}
|
|
74
74
|
const emitter = stdin;
|
|
75
75
|
const promptW = displayWidth(opts.prompt);
|
|
76
|
-
// initialLines(运行中 typeahead 预填)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
:
|
|
80
|
-
|
|
81
|
-
let
|
|
76
|
+
// initialLines(运行中 typeahead 预填):用调用方给的行初始化;光标置文末。
|
|
77
|
+
// 整篇输入是一个 Seg[]:可编辑段与粘贴块交替。光标永远落在某个可编辑段(segs[curSeg].frozen===false)上。
|
|
78
|
+
let segs = opts.initialLines && opts.initialLines.length > 0
|
|
79
|
+
? [{ frozen: false, text: opts.initialLines.join('\n') }]
|
|
80
|
+
: [{ frozen: false, text: '' }];
|
|
81
|
+
let curSeg = 0; // 光标所在段(始终可编辑)
|
|
82
|
+
let curOff = segs[0].text.length; // 段内字符偏移 = 文末
|
|
82
83
|
let justSawCR = false; // \r\n 合一:粘贴的 \r 折行后,紧跟的 \n 吞掉(避免折两行)
|
|
83
|
-
let chip = null; // 原子粘贴块:整段封进 [预览…],不可编辑;提交时拼回全文
|
|
84
|
-
let chipPre = ''; // chip 之前已存在的文本(粘贴发生时光标前的原文,原样多行保留,不截断);随 chip 一起提交/删除
|
|
85
84
|
let menuOpen = false;
|
|
86
85
|
let selected = 0;
|
|
87
86
|
let filtered = [];
|
|
88
87
|
const MENU_MAX_VISIBLE = 7;
|
|
89
88
|
let menuTop = 0; // 窗口首项在 filtered 中的索引,菜单最多显示 MENU_MAX_VISIBLE 条
|
|
89
|
+
/** Ctrl+C 清空前的缓冲快照(Ctrl+Z 还原一次);单次生效,还原后即清。 */
|
|
90
|
+
let undoSnapshot = null;
|
|
90
91
|
let resolved = false;
|
|
91
92
|
let resolve;
|
|
92
93
|
let reject;
|
|
93
|
-
const queryHistory = opts.queryHistory ?? [];
|
|
94
|
-
let historyIndex = queryHistory.length; // length = 草稿哨兵;0..length-1 = 历史项
|
|
95
|
-
let historyDraft = null;
|
|
96
|
-
/** 用历史 query 替换编辑缓冲;历史内容恢复为普通可编辑文本。 */
|
|
97
|
-
function loadHistoryEntry(value) {
|
|
98
|
-
lines = value.split('\n');
|
|
99
|
-
cl = lines.length - 1;
|
|
100
|
-
cc = lines[cl].length;
|
|
101
|
-
chip = null;
|
|
102
|
-
chipPre = '';
|
|
103
|
-
selected = 0;
|
|
104
|
-
menuTop = 0;
|
|
105
|
-
justSawCR = false;
|
|
106
|
-
computeFiltered();
|
|
107
|
-
redraw();
|
|
108
|
-
}
|
|
109
|
-
/** 在首次离开最新位置时保存完整草稿,然后向更旧的 query 移动。 */
|
|
110
|
-
function recallPrevious() {
|
|
111
|
-
if (queryHistory.length === 0 || historyIndex <= 0)
|
|
112
|
-
return;
|
|
113
|
-
if (historyIndex === queryHistory.length) {
|
|
114
|
-
historyDraft = {
|
|
115
|
-
lines: [...lines],
|
|
116
|
-
cl,
|
|
117
|
-
cc,
|
|
118
|
-
chip,
|
|
119
|
-
chipPre,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
historyIndex--;
|
|
123
|
-
loadHistoryEntry(queryHistory[historyIndex]);
|
|
124
|
-
}
|
|
125
|
-
/** 向更新的 query 移动;越过最新历史时恢复进入历史前的草稿。 */
|
|
126
|
-
function recallNext() {
|
|
127
|
-
if (historyIndex >= queryHistory.length)
|
|
128
|
-
return;
|
|
129
|
-
historyIndex++;
|
|
130
|
-
if (historyIndex < queryHistory.length) {
|
|
131
|
-
loadHistoryEntry(queryHistory[historyIndex]);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const draft = historyDraft;
|
|
135
|
-
lines = draft ? [...draft.lines] : [''];
|
|
136
|
-
cl = draft?.cl ?? 0;
|
|
137
|
-
cc = draft?.cc ?? 0;
|
|
138
|
-
chip = draft?.chip ?? null;
|
|
139
|
-
chipPre = draft?.chipPre ?? '';
|
|
140
|
-
selected = 0;
|
|
141
|
-
menuTop = 0;
|
|
142
|
-
justSawCR = false;
|
|
143
|
-
computeFiltered();
|
|
144
|
-
redraw();
|
|
145
|
-
}
|
|
146
94
|
/** 菜单行(预渲染,带色)——向上展开进内容区底,由 layout 贴入。最多显示 MENU_MAX_VISIBLE 条,支持上下滚动。 */
|
|
147
95
|
function menuLines() {
|
|
148
96
|
if (!menuOpen || filtered.length === 0)
|
|
@@ -180,90 +128,163 @@ export async function promptWithSlashMenu(opts) {
|
|
|
180
128
|
return `${marker} ${color}${name}${ui.reset} ${color}${descStr}${ui.reset}${ui.dim}${scrollHint}${ui.reset}`;
|
|
181
129
|
});
|
|
182
130
|
}
|
|
183
|
-
/**
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* ——后者在中文里 20 列只够 10 个汉字就截没,几乎看不到内容;元信息密度更高、长度可预测。 */
|
|
189
|
-
function chipPrefix() {
|
|
190
|
-
if (!chip)
|
|
191
|
-
return '';
|
|
192
|
-
const lines = chip.split('\n').length;
|
|
193
|
-
const chars = chip.length;
|
|
194
|
-
const lineText = t('prompt.lines', { count: lines });
|
|
131
|
+
/** 单个 chip 的展示 token:[📋 行数 · 字符数] (含尾部空格,使相邻 chip 之间留空隙)。 */
|
|
132
|
+
function chipToken(text) {
|
|
133
|
+
const lineCount = text.split('\n').length;
|
|
134
|
+
const charCount = text.length;
|
|
135
|
+
const lineText = t('prompt.lines', { count: lineCount });
|
|
195
136
|
const charText = t('prompt.chars', {
|
|
196
|
-
count:
|
|
137
|
+
count: charCount < 1000 ? charCount : `${(charCount / 1000).toFixed(1)}K`,
|
|
197
138
|
});
|
|
198
139
|
return `[📋 ${lineText} · ${charText}] `;
|
|
199
140
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
141
|
+
/** 构建整段显示:可编辑段按原文(含换行)展开,粘贴块(chip)折叠成一行 token 内联在光标原位。
|
|
142
|
+
* 返回展示行数组,以及每个段在"展示串"(= 展示行以 \n 拼接)中的字符起点,供光标正/反向映射。
|
|
143
|
+
* 关键不变量:展示串与 segs 的"段贡献"长度一致(可编辑段贡献其原文长度,含其中 \n;
|
|
144
|
+
* chip 贡献其 token 长度,无 \n),故 segStart 同时是 dispLines.join('\n') 内的字符偏移。 */
|
|
145
|
+
function buildDisplay() {
|
|
146
|
+
const out = [];
|
|
147
|
+
const segStart = [];
|
|
148
|
+
let gLen = 0;
|
|
149
|
+
for (const s of segs) {
|
|
150
|
+
segStart.push(gLen);
|
|
151
|
+
if (!s.frozen) {
|
|
152
|
+
const ls = s.text.split('\n');
|
|
153
|
+
if (out.length === 0)
|
|
154
|
+
out.push(...ls);
|
|
155
|
+
else {
|
|
156
|
+
out[out.length - 1] += ls[0];
|
|
157
|
+
for (let k = 1; k < ls.length; k++)
|
|
158
|
+
out.push(ls[k]);
|
|
159
|
+
}
|
|
160
|
+
gLen += s.text.length;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const tok = chipToken(s.text);
|
|
164
|
+
if (out.length === 0)
|
|
165
|
+
out.push(tok);
|
|
166
|
+
else
|
|
167
|
+
out[out.length - 1] += tok;
|
|
168
|
+
gLen += tok.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (out.length === 0)
|
|
172
|
+
out.push('');
|
|
173
|
+
return { lines: out, segStart };
|
|
174
|
+
}
|
|
175
|
+
/** 当前光标段内显示列。 */
|
|
176
|
+
function cursorCol() {
|
|
177
|
+
return displayWidth(segs[curSeg].text.slice(0, curOff));
|
|
203
178
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
179
|
+
/** 光标 → 展示 (行, 列)。展示行内的列以显示宽度计。 */
|
|
180
|
+
function cursorDisp() {
|
|
181
|
+
const { lines: dispLines, segStart } = buildDisplay();
|
|
182
|
+
const off = segStart[curSeg] + curOff;
|
|
183
|
+
let remaining = off;
|
|
184
|
+
let dl = 0;
|
|
185
|
+
let dispColChars = 0;
|
|
186
|
+
for (; dl < dispLines.length; dl++) {
|
|
187
|
+
const L = dispLines[dl].length;
|
|
188
|
+
if (remaining <= L) {
|
|
189
|
+
dispColChars = remaining;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
remaining -= L + 1;
|
|
193
|
+
}
|
|
194
|
+
if (dl >= dispLines.length) {
|
|
195
|
+
dl = Math.max(0, dispLines.length - 1);
|
|
196
|
+
dispColChars = dispLines[dl].length;
|
|
197
|
+
}
|
|
198
|
+
return { line: dl, col: displayWidth(dispLines[dl].slice(0, dispColChars)) };
|
|
213
199
|
}
|
|
214
|
-
/**
|
|
215
|
-
function
|
|
216
|
-
|
|
200
|
+
/** 展示 (行, 显示列) → 段内字符偏移。供 ↑/↓ 移动后重新定位,以及鼠标点击的反向映射。 */
|
|
201
|
+
function dispColToCharInLine(dispLine, dispCol) {
|
|
202
|
+
const { lines } = buildDisplay();
|
|
203
|
+
const ln = lines[dispLine] ?? '';
|
|
204
|
+
return visColToCharCol(ln, dispCol);
|
|
217
205
|
}
|
|
218
|
-
/**
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
206
|
+
/** 展示 (行, 字符内偏移) → 段+偏移。光标永远落在可编辑段;点中 chip 则吸附到最近的可编辑边界。 */
|
|
207
|
+
function mapDispToCursor(dispLine, charInLine) {
|
|
208
|
+
const { lines: dispLines, segStart } = buildDisplay();
|
|
209
|
+
let off = 0;
|
|
210
|
+
for (let k = 0; k < dispLine && k < dispLines.length; k++)
|
|
211
|
+
off += dispLines[k].length + 1;
|
|
212
|
+
off += Math.max(0, Math.min(charInLine, (dispLines[dispLine] ?? '').length));
|
|
213
|
+
for (let i = 0; i < segs.length; i++) {
|
|
214
|
+
const start = segStart[i];
|
|
215
|
+
const len = segs[i].frozen ? chipToken(segs[i].text).length : segs[i].text.length;
|
|
216
|
+
// 可编辑段用闭区间;只读块用半开区间——其结束边界归紧随其后的可编辑段,
|
|
217
|
+
// 否则光标恰好停在 chip 后的可编辑段开头时,会被误判落在 chip 上(边界争用)。
|
|
218
|
+
const inside = segs[i].frozen ? off >= start && off < start + len : off >= start && off <= start + len;
|
|
219
|
+
if (inside) {
|
|
220
|
+
if (!segs[i].frozen)
|
|
221
|
+
return { seg: i, off: off - start };
|
|
222
|
+
// 落在只读块内:按就近原则吸附——靠近左沿→其前可编辑段末尾,靠近右沿→其后可编辑段开头。
|
|
223
|
+
const distStart = off - start;
|
|
224
|
+
const distEnd = start + len - off;
|
|
225
|
+
return distStart <= distEnd ? editableEndBefore(i) : editableStartAfter(i);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return lastEditableEnd();
|
|
224
229
|
}
|
|
225
|
-
|
|
230
|
+
function editableStartAfter(i) {
|
|
231
|
+
let j = i + 1;
|
|
232
|
+
while (j < segs.length && segs[j].frozen)
|
|
233
|
+
j++;
|
|
234
|
+
if (j < segs.length)
|
|
235
|
+
return { seg: j, off: 0 };
|
|
236
|
+
return lastEditableEnd();
|
|
237
|
+
}
|
|
238
|
+
function editableEndBefore(i) {
|
|
239
|
+
let j = i - 1;
|
|
240
|
+
while (j >= 0 && segs[j].frozen)
|
|
241
|
+
j--;
|
|
242
|
+
if (j >= 0)
|
|
243
|
+
return { seg: j, off: segs[j].text.length };
|
|
244
|
+
return firstEditableStart();
|
|
245
|
+
}
|
|
246
|
+
function firstEditableStart() {
|
|
247
|
+
for (let j = 0; j < segs.length; j++)
|
|
248
|
+
if (!segs[j].frozen)
|
|
249
|
+
return { seg: j, off: 0 };
|
|
250
|
+
return { seg: 0, off: 0 };
|
|
251
|
+
}
|
|
252
|
+
function lastEditableEnd() {
|
|
253
|
+
for (let j = segs.length - 1; j >= 0; j--)
|
|
254
|
+
if (!segs[j].frozen)
|
|
255
|
+
return { seg: j, off: segs[j].text.length };
|
|
256
|
+
return { seg: 0, off: 0 };
|
|
257
|
+
}
|
|
258
|
+
/** 在光标处插入文本(含换行则落进当前可编辑段,原样保留)。供短粘贴与可打印字符共用。 */
|
|
226
259
|
function insertText(text) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const newLines = [before + parts[0]];
|
|
231
|
-
for (let i = 1; i < parts.length; i++)
|
|
232
|
-
newLines.push(parts[i]);
|
|
233
|
-
newLines[newLines.length - 1] += after;
|
|
234
|
-
lines.splice(cl, 1, ...newLines);
|
|
235
|
-
cl = cl + parts.length - 1;
|
|
236
|
-
// 光标置于插入文本末尾 = 原 before + 末段长度(指向插入文本之后、after 之前)。
|
|
237
|
-
// 旧值 parts[...].length 漏算 before → 光标落到插入文本内部(行中间)→ 后续打字插到中间(bug)。
|
|
238
|
-
cc = before.length + parts[parts.length - 1].length;
|
|
260
|
+
const s = segs[curSeg].text;
|
|
261
|
+
segs[curSeg].text = s.slice(0, curOff) + text + s.slice(curOff);
|
|
262
|
+
curOff += text.length;
|
|
239
263
|
}
|
|
240
|
-
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
264
|
+
/** 落一段粘贴文本:长粘贴 → 在此处插入一个只读 chip(把当前可编辑段从光标切开成「前段 + chip + 后段」),
|
|
265
|
+
* 短粘贴 → 作为可编辑文本插入。连续长粘贴会产生相邻 chip,各自独立;两次粘贴之间敲的字是可编辑段,
|
|
266
|
+
* 可直接编辑、也可退格删掉任一 chip。光标落在 chip 之后的可编辑段(即使为空也保留,保证光标有落点)。 */
|
|
243
267
|
function applyPastedText(buf) {
|
|
244
268
|
if (!buf)
|
|
245
269
|
return;
|
|
246
270
|
const isLong = buf.split('\n').length > 8 || buf.length > 200;
|
|
247
|
-
if (isLong) {
|
|
248
|
-
const before = lines[cl].slice(0, cc);
|
|
249
|
-
const after = lines[cl].slice(cc);
|
|
250
|
-
if (chip == null) {
|
|
251
|
-
// 首次起 chip:光标前的行(含之前的整行)转入 chipPre,光标后的部分留作 suffix 首行。
|
|
252
|
-
chipPre = [...lines.slice(0, cl), before].join('\n');
|
|
253
|
-
chip = buf;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
// 已有 chip:光标前若已打了字(chip 之后、本次粘贴之前),并入 chip 尾部保留(不丢字),
|
|
257
|
-
// 光标后的部分仍留作 suffix 首行。
|
|
258
|
-
chip = before ? chip + '\n' + before + '\n' + buf : chip + '\n' + buf;
|
|
259
|
-
}
|
|
260
|
-
lines = [after, ...lines.slice(cl + 1)];
|
|
261
|
-
cl = 0;
|
|
262
|
-
cc = 0;
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
271
|
+
if (!isLong) {
|
|
265
272
|
insertText(buf);
|
|
273
|
+
computeFiltered();
|
|
274
|
+
redraw();
|
|
275
|
+
return;
|
|
266
276
|
}
|
|
277
|
+
const s = segs[curSeg].text;
|
|
278
|
+
const before = s.slice(0, curOff);
|
|
279
|
+
const after = s.slice(curOff);
|
|
280
|
+
const newSegs = [];
|
|
281
|
+
if (before)
|
|
282
|
+
newSegs.push({ frozen: false, text: before });
|
|
283
|
+
newSegs.push({ frozen: true, text: buf });
|
|
284
|
+
newSegs.push({ frozen: false, text: after }); // 始终保留可编辑后缀(可为空),保证光标有落点
|
|
285
|
+
segs.splice(curSeg, 1, ...newSegs);
|
|
286
|
+
curSeg = curSeg + (before ? 2 : 1); // 落到 chip 之后的可编辑段
|
|
287
|
+
curOff = 0;
|
|
267
288
|
computeFiltered();
|
|
268
289
|
redraw();
|
|
269
290
|
}
|
|
@@ -297,11 +318,12 @@ export async function promptWithSlashMenu(opts) {
|
|
|
297
318
|
}
|
|
298
319
|
}
|
|
299
320
|
function computeFiltered() {
|
|
300
|
-
|
|
301
|
-
|
|
321
|
+
const firstLine = buildDisplay().lines[0] ?? '';
|
|
322
|
+
if (firstLine.startsWith('/')) {
|
|
323
|
+
const { nodes, prefix } = menuContext(firstLine);
|
|
302
324
|
filtered = nodes
|
|
303
325
|
.map((node) => ({ node, input: `${prefix}${node.name}` }))
|
|
304
|
-
.filter((item) => item.input.startsWith(
|
|
326
|
+
.filter((item) => item.input.startsWith(firstLine));
|
|
305
327
|
menuOpen = filtered.length > 0;
|
|
306
328
|
if (selected >= filtered.length) {
|
|
307
329
|
selected = 0;
|
|
@@ -315,37 +337,38 @@ export async function promptWithSlashMenu(opts) {
|
|
|
315
337
|
}
|
|
316
338
|
}
|
|
317
339
|
function redraw() {
|
|
340
|
+
const { lines } = buildDisplay();
|
|
341
|
+
const cur = cursorDisp();
|
|
342
|
+
// 空输入引导:缓冲为空时输入框内画 dim ghost 占位(含 /help 提示),让用户知道怎么开始;
|
|
343
|
+
// 一旦有输入(打字/粘贴/补全)isEmpty 变 false,下一帧占位即消失。
|
|
344
|
+
const isEmpty = segs.length === 1 && !segs[0].frozen && segs[0].text === '';
|
|
318
345
|
layout.paintInput({
|
|
319
346
|
prompt: opts.prompt,
|
|
320
|
-
lines
|
|
321
|
-
cursorLine:
|
|
322
|
-
cursorCol:
|
|
347
|
+
lines,
|
|
348
|
+
cursorLine: cur.line,
|
|
349
|
+
cursorCol: cur.col,
|
|
323
350
|
menu: menuLines().length ? { lines: menuLines() } : null,
|
|
351
|
+
placeholder: isEmpty ? t('prompt.emptyPlaceholder') : undefined,
|
|
324
352
|
});
|
|
325
353
|
}
|
|
326
354
|
/** 鼠标点击输入框:layout 已把"屏 tap 位置"以 (flatIdx, inSegVis) 形式传进来
|
|
327
|
-
* (flatIdx = flat 中的绝对段号;inSegVis = 段内显示列)。
|
|
328
|
-
*
|
|
329
|
-
* chip 模式自动扣 chip prefix:
|
|
330
|
-
* - dispLines() 把 chipPre 末行 + chipPrefix + suffix[0] 拼成一行 "mergedRow"
|
|
331
|
-
* - flat 中对应的"合并行段"是 mergedRow 的折行段,段内 inChar 累加从 mergedRow 起
|
|
332
|
-
* - 要得到 lines[cl=0] 内的字符索引,扣掉 mergedRow 起始到 lines[0] 起始的字符数
|
|
333
|
-
* = chipPre 末行字符数 + chipPrefix 字符数。 */
|
|
355
|
+
* (flatIdx = flat 中的绝对段号;inSegVis = 段内显示列)。本函数把点击位还原成 (段, 偏移) → redraw。
|
|
356
|
+
* 点中只读粘贴块时吸附到最近的可编辑边界(见 mapDispToCursor)。 */
|
|
334
357
|
function applyExternalCursor(flatIdx, inSegVis) {
|
|
335
358
|
const g = layout.getGeo();
|
|
336
359
|
const promptW = displayWidth(opts.prompt);
|
|
337
360
|
const W = Math.max(1, g.cols - promptW);
|
|
338
|
-
const dispLs =
|
|
361
|
+
const { lines: dispLs } = buildDisplay();
|
|
339
362
|
const lineVis = dispLs.map((l) => wrapByDisplayWidth(l, W));
|
|
340
363
|
const flat = [];
|
|
341
364
|
for (const lv of lineVis)
|
|
342
365
|
for (const r of lv)
|
|
343
366
|
flat.push(r);
|
|
344
|
-
let
|
|
345
|
-
let
|
|
367
|
+
let dispLine;
|
|
368
|
+
let charInLine;
|
|
346
369
|
if (flat.length === 0) {
|
|
347
|
-
|
|
348
|
-
|
|
370
|
+
dispLine = 0;
|
|
371
|
+
charInLine = 0;
|
|
349
372
|
}
|
|
350
373
|
else {
|
|
351
374
|
const safeIdx = Math.max(0, Math.min(flatIdx, flat.length - 1));
|
|
@@ -354,60 +377,26 @@ export async function promptWithSlashMenu(opts) {
|
|
|
354
377
|
const safeInSeg = Math.max(0, Math.min(inSegVis, segW));
|
|
355
378
|
const inChar = visColToCharCol(seg, safeInSeg);
|
|
356
379
|
// safeIdx → (dispLine, segInLine)
|
|
357
|
-
let
|
|
380
|
+
let dl = 0;
|
|
358
381
|
let segInLine = safeIdx;
|
|
359
|
-
while (
|
|
360
|
-
segInLine -= lineVis[
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
if (dispLine >= lineVis.length) {
|
|
364
|
-
dispLine = lineVis.length - 1;
|
|
365
|
-
segInLine = lineVis[dispLine].length - 1;
|
|
366
|
-
}
|
|
367
|
-
// dispLine → cl(chip-aware)
|
|
368
|
-
if (chip) {
|
|
369
|
-
const preLen = chipPreLines().length;
|
|
370
|
-
if (dispLine < preLen - 1) {
|
|
371
|
-
// 落在 chipPre 内(非末行)→ 走出 chip 到 suffix 起点
|
|
372
|
-
newCl = 0;
|
|
373
|
-
}
|
|
374
|
-
else if (dispLine === preLen - 1) {
|
|
375
|
-
// 合并行 = suffix 第 0 行(cl=0)
|
|
376
|
-
newCl = 0;
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
newCl = dispLine - (preLen - 1);
|
|
380
|
-
}
|
|
382
|
+
while (dl < lineVis.length && segInLine >= lineVis[dl].length) {
|
|
383
|
+
segInLine -= lineVis[dl].length;
|
|
384
|
+
dl++;
|
|
381
385
|
}
|
|
382
|
-
|
|
383
|
-
|
|
386
|
+
if (dl >= lineVis.length) {
|
|
387
|
+
dl = lineVis.length - 1;
|
|
388
|
+
segInLine = lineVis[dl].length - 1;
|
|
384
389
|
}
|
|
385
|
-
newCl = Math.max(0, Math.min(newCl, lines.length - 1));
|
|
386
|
-
const lineText = lines[newCl] ?? '';
|
|
387
|
-
// cc 必须在 lines[newCl] 内。flat 中 seg 是 dispLs[dispLine] 的某折行段,
|
|
388
|
-
// seg 内 inChar 是段内字符偏移,但 lines[newCl] 的字符是按 (dispLs 前段累加) + (本段 inChar) 算的。
|
|
389
|
-
// ——非 chip 模式下:dispLine = newCl, segStartChars = sum(lineVis[dispLine][0..segInLine-1].length)
|
|
390
|
-
// ccInLine = segStartChars + inChar
|
|
391
|
-
// ——chip 模式下 newCl=0(合并行):dispLine 是合并行 dispLine,segStartChars 同上,
|
|
392
|
-
// 但 lines[0] 在合并行中"起始处"在 (mergedRow.length - lines[0].length) 字符处,
|
|
393
|
-
// 故 ccInLine = segStartChars + inChar - chipOverheadChars。
|
|
394
390
|
let segStartChars = 0;
|
|
395
|
-
const
|
|
391
|
+
const segsVis = lineVis[dl];
|
|
396
392
|
for (let s = 0; s < segInLine; s++)
|
|
397
|
-
segStartChars +=
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const pre = chipPreLines();
|
|
401
|
-
const mergedRow = pre[pre.length - 1] ?? '';
|
|
402
|
-
const prefix = chipPrefix();
|
|
403
|
-
// mergedRow 起始到 lines[0] 起始的字符数 = (chipPreLast + chipPrefix) 字符数
|
|
404
|
-
const chipOverheadChars = mergedRow.length + prefix.length - lineText.length;
|
|
405
|
-
ccInLine = ccInLine - chipOverheadChars;
|
|
406
|
-
}
|
|
407
|
-
newCc = Math.max(0, Math.min(ccInLine, lineText.length));
|
|
393
|
+
segStartChars += segsVis[s]?.length ?? 0;
|
|
394
|
+
charInLine = Math.max(0, Math.min(segStartChars + inChar, dispLs[dl]?.length ?? 0));
|
|
395
|
+
dispLine = dl;
|
|
408
396
|
}
|
|
409
|
-
|
|
410
|
-
|
|
397
|
+
const { seg, off } = mapDispToCursor(dispLine, charInLine);
|
|
398
|
+
curSeg = seg;
|
|
399
|
+
curOff = off;
|
|
411
400
|
// 收起菜单/过滤态:点击输入框关闭菜单,回到纯文本编辑。
|
|
412
401
|
if (menuOpen) {
|
|
413
402
|
menuOpen = false;
|
|
@@ -454,18 +443,18 @@ export async function promptWithSlashMenu(opts) {
|
|
|
454
443
|
if (!menuOpen || !item)
|
|
455
444
|
return submitLeaf;
|
|
456
445
|
if (item.node.children?.length) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
446
|
+
segs = [{ frozen: false, text: `${item.input} ` }];
|
|
447
|
+
curSeg = 0;
|
|
448
|
+
curOff = segs[0].text.length;
|
|
460
449
|
selected = 0;
|
|
461
450
|
menuTop = 0;
|
|
462
451
|
computeFiltered();
|
|
463
452
|
redraw();
|
|
464
453
|
return false;
|
|
465
454
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
455
|
+
segs = [{ frozen: false, text: item.node.value ?? item.input }];
|
|
456
|
+
curSeg = 0;
|
|
457
|
+
curOff = segs[0].text.length;
|
|
469
458
|
computeFiltered();
|
|
470
459
|
if (!submitLeaf || item.node.submit === false) {
|
|
471
460
|
redraw();
|
|
@@ -473,44 +462,85 @@ export async function promptWithSlashMenu(opts) {
|
|
|
473
462
|
}
|
|
474
463
|
return true;
|
|
475
464
|
}
|
|
476
|
-
/**
|
|
465
|
+
/** 提交:菜单打开时先应用选中项;分支进入子菜单,需要参数的叶子只补全。
|
|
466
|
+
* 段序列直接按原文(可编辑段原文 + 粘贴块原文)拼接——所见即所得,不再额外插换行。 */
|
|
477
467
|
function submit() {
|
|
478
468
|
if (menuOpen && !applySelected(true))
|
|
479
469
|
return;
|
|
480
|
-
const
|
|
481
|
-
let content = suffix;
|
|
482
|
-
if (chip) {
|
|
483
|
-
content = suffix.length > 0 ? chip + '\n' + suffix : chip;
|
|
484
|
-
if (chipPre)
|
|
485
|
-
content = chipPre + '\n' + content;
|
|
486
|
-
}
|
|
470
|
+
const content = segs.map((s) => s.text).join('');
|
|
487
471
|
finish(content === '' ? [''] : content.split('\n'));
|
|
488
472
|
}
|
|
489
|
-
/**
|
|
473
|
+
/** 插换行:在光标处断行(落进当前可编辑段)。 */
|
|
490
474
|
function insertNewline() {
|
|
491
|
-
|
|
492
|
-
lines[cl] = lines[cl].slice(0, cc);
|
|
493
|
-
lines.splice(cl + 1, 0, after);
|
|
494
|
-
cl++;
|
|
495
|
-
cc = 0;
|
|
475
|
+
insertText('\n');
|
|
496
476
|
computeFiltered();
|
|
497
477
|
redraw();
|
|
498
478
|
}
|
|
499
|
-
/**
|
|
479
|
+
/** 光标移到当前可编辑段内的行首/行尾(不含段内 \n)。 */
|
|
480
|
+
function moveToLineStart() {
|
|
481
|
+
const s = segs[curSeg].text;
|
|
482
|
+
curOff = s.lastIndexOf('\n', curOff - 1) + 1;
|
|
483
|
+
redraw();
|
|
484
|
+
}
|
|
485
|
+
function moveToLineEnd() {
|
|
486
|
+
const s = segs[curSeg].text;
|
|
487
|
+
const ne = s.indexOf('\n', curOff);
|
|
488
|
+
curOff = ne < 0 ? s.length : ne;
|
|
489
|
+
redraw();
|
|
490
|
+
}
|
|
491
|
+
/** Ctrl+U:删到行首(当前可编辑段内的当前行;已在行首则 no-op,不跨入相邻粘贴块)。 */
|
|
492
|
+
function deleteToLineStart() {
|
|
493
|
+
const s = segs[curSeg].text;
|
|
494
|
+
const ls = s.lastIndexOf('\n', curOff - 1) + 1;
|
|
495
|
+
if (ls === curOff)
|
|
496
|
+
return;
|
|
497
|
+
segs[curSeg].text = s.slice(0, ls) + s.slice(curOff);
|
|
498
|
+
curOff = ls;
|
|
499
|
+
computeFiltered();
|
|
500
|
+
redraw();
|
|
501
|
+
}
|
|
502
|
+
/** Ctrl+K:删到行尾(当前可编辑段内的当前行;已在行尾则 no-op,不跨入相邻粘贴块)。 */
|
|
503
|
+
function deleteToLineEnd() {
|
|
504
|
+
const s = segs[curSeg].text;
|
|
505
|
+
const ne = s.indexOf('\n', curOff);
|
|
506
|
+
if (ne < 0) {
|
|
507
|
+
if (curOff === s.length)
|
|
508
|
+
return;
|
|
509
|
+
segs[curSeg].text = s.slice(0, curOff);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
segs[curSeg].text = s.slice(0, curOff) + s.slice(ne + 1);
|
|
513
|
+
}
|
|
514
|
+
computeFiltered();
|
|
515
|
+
redraw();
|
|
516
|
+
}
|
|
517
|
+
/** Ctrl+Z:还原被 Ctrl+C 清掉的输入(单次)。 */
|
|
518
|
+
function restoreUndo() {
|
|
519
|
+
const snap = undoSnapshot;
|
|
520
|
+
if (!snap)
|
|
521
|
+
return;
|
|
522
|
+
segs = snap.segs.map((s) => ({ ...s }));
|
|
523
|
+
curSeg = Math.min(snap.curSeg, segs.length - 1);
|
|
524
|
+
curOff = Math.min(snap.curOff, segs[curSeg].text.length);
|
|
525
|
+
undoSnapshot = null;
|
|
526
|
+
computeFiltered();
|
|
527
|
+
redraw();
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 清空输入(含粘贴块 / 斜杠菜单 / 粘贴缓冲):Ctrl+C 在有内容时调用——清空而非退出。
|
|
531
|
+
* 清空前留一份快照供 Ctrl+Z 还原——手滑按在输入框上时,正在写的长 prompt 不至于白打。
|
|
532
|
+
*/
|
|
500
533
|
function clearInput() {
|
|
501
534
|
if (layout.isScrolled())
|
|
502
535
|
layout.resetScroll(); // 回尾(若滚动回看),再清空
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
chipPre = '';
|
|
536
|
+
undoSnapshot = { segs: segs.map((s) => ({ ...s })), curSeg, curOff };
|
|
537
|
+
segs = [{ frozen: false, text: '' }];
|
|
538
|
+
curSeg = 0;
|
|
539
|
+
curOff = 0;
|
|
508
540
|
menuOpen = false;
|
|
509
541
|
filtered = [];
|
|
510
542
|
selected = 0;
|
|
511
543
|
menuTop = 0;
|
|
512
|
-
historyIndex = queryHistory.length;
|
|
513
|
-
historyDraft = null;
|
|
514
544
|
if (pasteTimer) {
|
|
515
545
|
clearTimeout(pasteTimer);
|
|
516
546
|
pasteTimer = null;
|
|
@@ -521,13 +551,10 @@ export async function promptWithSlashMenu(opts) {
|
|
|
521
551
|
computeFiltered();
|
|
522
552
|
redraw();
|
|
523
553
|
}
|
|
524
|
-
/** Ctrl+C:有内容(已打字 / 多行 /
|
|
554
|
+
/** Ctrl+C:有内容(已打字 / 多行 / 粘贴块 / 菜单草稿 / 粘贴缓冲 / 粘贴中)则清空,再按一次(空)才退出(仿 fish)。 */
|
|
525
555
|
function onCtrlC() {
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
lines.some((l) => l !== '') ||
|
|
529
|
-
pasteParts.length > 0 ||
|
|
530
|
-
pasting;
|
|
556
|
+
const flat = segs.map((s) => s.text).join('');
|
|
557
|
+
const hasContent = flat.length > 0 || menuOpen || pasteParts.length > 0 || pasting;
|
|
531
558
|
if (hasContent) {
|
|
532
559
|
clearInput();
|
|
533
560
|
return;
|
|
@@ -574,7 +601,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
574
601
|
return;
|
|
575
602
|
}
|
|
576
603
|
// 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,Ctrl+↑/↓ 每次 5 行。
|
|
577
|
-
// 裸 ↑/↓
|
|
604
|
+
// 裸 ↑/↓ 优先给斜杠菜单与多行光标,二者都不适用时按滚轮量滚动回看(与鼠标滚轮同一入口)。
|
|
578
605
|
if (key.name === 'pageup' ||
|
|
579
606
|
key.name === 'pagedown' ||
|
|
580
607
|
(key.ctrl && (key.name === 'up' || key.name === 'down'))) {
|
|
@@ -592,18 +619,38 @@ export async function promptWithSlashMenu(opts) {
|
|
|
592
619
|
// 滚动回看时打字 / 编辑不回尾(保持历史视图,便于边看历史边编辑);仅 Enter 提交时回尾(见下方 submit 前)。
|
|
593
620
|
// Ctrl+C 已在 onKey 顶部统一处理(清空或退出);Ctrl+D:空缓冲退出,否则忽略
|
|
594
621
|
if (key.ctrl && key.name === 'd') {
|
|
595
|
-
if (
|
|
622
|
+
if (segs.map((s) => s.text).join('') === '')
|
|
596
623
|
finish(null);
|
|
597
624
|
return;
|
|
598
625
|
}
|
|
599
626
|
if (key.ctrl && key.name === 'a') {
|
|
600
|
-
|
|
601
|
-
|
|
627
|
+
moveToLineStart();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (key.ctrl && key.name === 'q') {
|
|
631
|
+
// 行首的对称搭档:与 Ctrl+E(行尾)同处顶行相邻(Q/E),左=起始右=结束。
|
|
632
|
+
// Ctrl+A 仍作别名保留(全 app 一致:运行中预填、权限面板都用 Ctrl+A 跳开头)。
|
|
633
|
+
moveToLineStart();
|
|
602
634
|
return;
|
|
603
635
|
}
|
|
604
636
|
if (key.ctrl && key.name === 'e') {
|
|
605
|
-
|
|
606
|
-
|
|
637
|
+
moveToLineEnd();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// Ctrl+Z:还原被 Ctrl+C 清掉的输入。无快照时 no-op。
|
|
641
|
+
if (key.ctrl && key.name === 'z') {
|
|
642
|
+
restoreUndo();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// 行级删除:Ctrl+U 删到行首,Ctrl+K 删到行尾(均在当前可编辑段内,不跨入相邻粘贴块)。
|
|
646
|
+
// 词级删除/跳转(Ctrl+W / Ctrl+← 等)刻意不做:终端序列各终端发得不一样
|
|
647
|
+
// (Ctrl+Backspace 在多数终端发 \x08,与裸退格无从区分),做一半会变成按了没反应的哑键。
|
|
648
|
+
if (key.ctrl && key.name === 'u') {
|
|
649
|
+
deleteToLineStart();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (key.ctrl && key.name === 'k') {
|
|
653
|
+
deleteToLineEnd();
|
|
607
654
|
return;
|
|
608
655
|
}
|
|
609
656
|
const isReturn = key.name === 'return' || key.name === 'enter';
|
|
@@ -629,48 +676,71 @@ export async function promptWithSlashMenu(opts) {
|
|
|
629
676
|
return;
|
|
630
677
|
}
|
|
631
678
|
switch (key.name) {
|
|
632
|
-
case 'backspace':
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
679
|
+
case 'backspace': {
|
|
680
|
+
const s = segs[curSeg].text;
|
|
681
|
+
if (curOff > 0) {
|
|
682
|
+
segs[curSeg].text = s.slice(0, curOff - 1) + s.slice(curOff);
|
|
683
|
+
curOff--;
|
|
636
684
|
computeFiltered();
|
|
637
685
|
redraw();
|
|
638
686
|
}
|
|
639
|
-
else if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
687
|
+
else if (curSeg > 0) {
|
|
688
|
+
const prev = segs[curSeg - 1];
|
|
689
|
+
if (prev.frozen) {
|
|
690
|
+
// 紧贴粘贴块后沿退格:删整块(原子)
|
|
691
|
+
segs.splice(curSeg - 1, 1);
|
|
692
|
+
curSeg--;
|
|
693
|
+
curOff = 0;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
// 并回上一个可编辑段
|
|
697
|
+
prev.text += segs[curSeg].text;
|
|
698
|
+
const joinOff = prev.text.length - segs[curSeg].text.length;
|
|
699
|
+
segs.splice(curSeg, 1);
|
|
700
|
+
curSeg--;
|
|
701
|
+
curOff = joinOff;
|
|
702
|
+
}
|
|
646
703
|
computeFiltered();
|
|
647
704
|
redraw();
|
|
648
705
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
case 'delete': {
|
|
709
|
+
const s = segs[curSeg].text;
|
|
710
|
+
if (curOff < s.length) {
|
|
711
|
+
segs[curSeg].text = s.slice(0, curOff) + s.slice(curOff + 1);
|
|
712
|
+
computeFiltered();
|
|
713
|
+
redraw();
|
|
714
|
+
}
|
|
715
|
+
else if (curSeg < segs.length - 1) {
|
|
716
|
+
const next = segs[curSeg + 1];
|
|
717
|
+
if (next.frozen)
|
|
718
|
+
segs.splice(curSeg + 1, 1); // 并掉下一个粘贴块
|
|
719
|
+
else {
|
|
720
|
+
segs[curSeg].text += next.text;
|
|
721
|
+
segs.splice(curSeg + 1, 1);
|
|
722
|
+
}
|
|
658
723
|
computeFiltered();
|
|
659
724
|
redraw();
|
|
660
725
|
}
|
|
661
726
|
return;
|
|
727
|
+
}
|
|
662
728
|
case 'up':
|
|
663
729
|
if (menuOpen && filtered.length) {
|
|
664
730
|
selected = (selected - 1 + filtered.length) % filtered.length;
|
|
665
731
|
redraw();
|
|
666
732
|
}
|
|
667
|
-
else if (cl > 0) {
|
|
668
|
-
cl--;
|
|
669
|
-
cc = Math.min(cc, lines[cl].length);
|
|
670
|
-
redraw();
|
|
671
|
-
}
|
|
672
733
|
else {
|
|
673
|
-
|
|
734
|
+
const { line, col } = cursorDisp();
|
|
735
|
+
if (line > 0) {
|
|
736
|
+
const m = mapDispToCursor(line - 1, dispColToCharInLine(line - 1, col));
|
|
737
|
+
curSeg = m.seg;
|
|
738
|
+
curOff = m.off;
|
|
739
|
+
redraw();
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
layout.scrollWheel(1); // 等同鼠标滚轮上滚一格
|
|
743
|
+
}
|
|
674
744
|
}
|
|
675
745
|
return;
|
|
676
746
|
case 'down':
|
|
@@ -678,13 +748,18 @@ export async function promptWithSlashMenu(opts) {
|
|
|
678
748
|
selected = (selected + 1) % filtered.length;
|
|
679
749
|
redraw();
|
|
680
750
|
}
|
|
681
|
-
else if (cl < lines.length - 1) {
|
|
682
|
-
cl++;
|
|
683
|
-
cc = Math.min(cc, lines[cl].length);
|
|
684
|
-
redraw();
|
|
685
|
-
}
|
|
686
751
|
else {
|
|
687
|
-
|
|
752
|
+
const { lines: dispLines } = buildDisplay();
|
|
753
|
+
const { line, col } = cursorDisp();
|
|
754
|
+
if (line < dispLines.length - 1) {
|
|
755
|
+
const m = mapDispToCursor(line + 1, dispColToCharInLine(line + 1, col));
|
|
756
|
+
curSeg = m.seg;
|
|
757
|
+
curOff = m.off;
|
|
758
|
+
redraw();
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
layout.scrollWheel(-1); // 等同鼠标滚轮下滚一格
|
|
762
|
+
}
|
|
688
763
|
}
|
|
689
764
|
return;
|
|
690
765
|
case 'tab':
|
|
@@ -692,12 +767,12 @@ export async function promptWithSlashMenu(opts) {
|
|
|
692
767
|
applySelected(false);
|
|
693
768
|
return;
|
|
694
769
|
case 'escape': {
|
|
695
|
-
const { prefix } = menuContext(lines[0]);
|
|
770
|
+
const { prefix } = menuContext(buildDisplay().lines[0] ?? '');
|
|
696
771
|
if (prefix) {
|
|
697
772
|
// 子层 Esc 回到父节点;例如 /model sw → /model。
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
773
|
+
segs = [{ frozen: false, text: prefix.trimEnd() }];
|
|
774
|
+
curSeg = 0;
|
|
775
|
+
curOff = segs[0].text.length;
|
|
701
776
|
selected = 0;
|
|
702
777
|
menuTop = 0;
|
|
703
778
|
computeFiltered();
|
|
@@ -711,42 +786,52 @@ export async function promptWithSlashMenu(opts) {
|
|
|
711
786
|
redraw();
|
|
712
787
|
return;
|
|
713
788
|
}
|
|
714
|
-
case 'left':
|
|
715
|
-
if (
|
|
716
|
-
|
|
789
|
+
case 'left': {
|
|
790
|
+
if (curOff > 0) {
|
|
791
|
+
curOff--;
|
|
717
792
|
redraw();
|
|
718
793
|
}
|
|
719
|
-
else if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
794
|
+
else if (curSeg > 0) {
|
|
795
|
+
let p = curSeg - 1;
|
|
796
|
+
while (p >= 0 && segs[p].frozen)
|
|
797
|
+
p--;
|
|
798
|
+
if (p >= 0) {
|
|
799
|
+
curSeg = p;
|
|
800
|
+
curOff = segs[p].text.length;
|
|
801
|
+
redraw();
|
|
802
|
+
}
|
|
723
803
|
}
|
|
724
804
|
return;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
805
|
+
}
|
|
806
|
+
case 'right': {
|
|
807
|
+
const s = segs[curSeg].text;
|
|
808
|
+
if (curOff < s.length) {
|
|
809
|
+
curOff++;
|
|
728
810
|
redraw();
|
|
729
811
|
}
|
|
730
|
-
else if (
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
812
|
+
else if (curSeg < segs.length - 1) {
|
|
813
|
+
let nx = curSeg + 1;
|
|
814
|
+
while (nx < segs.length && segs[nx].frozen)
|
|
815
|
+
nx++;
|
|
816
|
+
if (nx < segs.length) {
|
|
817
|
+
curSeg = nx;
|
|
818
|
+
curOff = 0;
|
|
819
|
+
redraw();
|
|
820
|
+
}
|
|
734
821
|
}
|
|
735
822
|
return;
|
|
823
|
+
}
|
|
736
824
|
case 'home':
|
|
737
|
-
|
|
738
|
-
redraw();
|
|
825
|
+
moveToLineStart();
|
|
739
826
|
return;
|
|
740
827
|
case 'end':
|
|
741
|
-
|
|
742
|
-
redraw();
|
|
828
|
+
moveToLineEnd();
|
|
743
829
|
return;
|
|
744
830
|
}
|
|
745
831
|
// 可打印字符(>= 空格,非 ctrl/meta)
|
|
746
832
|
const s = key.sequence ?? '';
|
|
747
833
|
if (s && s >= ' ' && !key.ctrl && !key.meta) {
|
|
748
|
-
|
|
749
|
-
cc += s.length;
|
|
834
|
+
insertText(s);
|
|
750
835
|
computeFiltered();
|
|
751
836
|
redraw();
|
|
752
837
|
}
|