dsh-neotui 0.1.8 → 0.1.10
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/package.json +1 -1
- package/src/widgets.js +71 -26
package/package.json
CHANGED
package/src/widgets.js
CHANGED
|
@@ -257,7 +257,7 @@ export class Input extends Widget {
|
|
|
257
257
|
this.expanded = false; // Ctrl+L: drop the line cap and fill the window
|
|
258
258
|
this.app = opts.app ?? null;
|
|
259
259
|
this.pendingPaste = null; // large-paste stage 1: held-back clipboard text
|
|
260
|
-
this.
|
|
260
|
+
this.pasteMark = null; // code-point span of the immutable "[已复制…]" token
|
|
261
261
|
this.onChange = opts.onChange ?? null;
|
|
262
262
|
this.allowEmptyEnter = opts.allowEmptyEnter ?? false;
|
|
263
263
|
this.history = [];
|
|
@@ -325,19 +325,45 @@ export class Input extends Widget {
|
|
|
325
325
|
this.onChange?.();
|
|
326
326
|
}
|
|
327
327
|
insert(text) {
|
|
328
|
-
const cps = this.selectAll ? [] : this.#cps();
|
|
329
328
|
const at = this.selectAll ? 0 : this.cursor;
|
|
330
|
-
this.selectAll
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
329
|
+
if (this.selectAll) {
|
|
330
|
+
this.selectAll = false;
|
|
331
|
+
this.value = "";
|
|
332
|
+
this.cursor = 0;
|
|
333
|
+
this.#touch();
|
|
334
|
+
}
|
|
335
|
+
this.#edit(at, at, text);
|
|
334
336
|
this.onChange?.();
|
|
335
337
|
}
|
|
336
|
-
|
|
338
|
+
/** EVERY edit goes through here so the immutable paste token behaves as
|
|
339
|
+
* one unit: deleting any part of it removes it whole, typing inside it
|
|
340
|
+
* replaces it, edits elsewhere just shift it. */
|
|
341
|
+
#edit(from, to, text = "") {
|
|
337
342
|
const cps = this.#cps();
|
|
338
|
-
|
|
343
|
+
const t = Array.from(text);
|
|
344
|
+
const m = this.pasteMark;
|
|
345
|
+
if (m && to > m.start && from < m.end) {
|
|
346
|
+
// the edit touches the token: apply it over the whole token span
|
|
347
|
+
const lo = Math.min(from, m.start);
|
|
348
|
+
const hi = Math.max(to, m.end);
|
|
349
|
+
cps.splice(lo, hi - lo, ...t);
|
|
350
|
+
this.cursor = lo + t.length;
|
|
351
|
+
this.pasteMark = null;
|
|
352
|
+
this.pendingPaste = null;
|
|
353
|
+
this.value = cps.join("");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (m && to <= m.start) {
|
|
357
|
+
const delta = t.length - (to - from);
|
|
358
|
+
m.start += delta; m.end += delta;
|
|
359
|
+
}
|
|
360
|
+
cps.splice(from, to - from, ...t);
|
|
361
|
+
this.cursor = from + t.length;
|
|
339
362
|
this.value = cps.join("");
|
|
340
363
|
}
|
|
364
|
+
#deleteAt(idx) {
|
|
365
|
+
this.#edit(idx, idx + 1);
|
|
366
|
+
}
|
|
341
367
|
/** Scroll offset that keeps the cursor's visual row inside the window. */
|
|
342
368
|
#scrollStart(h) {
|
|
343
369
|
const rows = this.#visualRows();
|
|
@@ -407,6 +433,7 @@ export class Input extends Widget {
|
|
|
407
433
|
const row = start + Math.max(0, Math.min(h - 1, ev.y - this.y));
|
|
408
434
|
const rx = ev.x - this.x - (row === 0 ? strWidth(this.prompt) : 1);
|
|
409
435
|
this.cursor = this.#indexAtVisual(row, Math.max(0, rx));
|
|
436
|
+
this.#snapCursor();
|
|
410
437
|
} else {
|
|
411
438
|
const rx = ev.x - this.x - strWidth(this.prompt);
|
|
412
439
|
let w = 0, idx = 0;
|
|
@@ -417,34 +444,50 @@ export class Input extends Widget {
|
|
|
417
444
|
idx++;
|
|
418
445
|
}
|
|
419
446
|
this.cursor = idx;
|
|
447
|
+
this.#snapCursor();
|
|
420
448
|
}
|
|
421
449
|
return true;
|
|
422
450
|
}
|
|
423
451
|
return false;
|
|
424
452
|
}
|
|
425
|
-
/**
|
|
426
|
-
*
|
|
427
|
-
|
|
453
|
+
/** The paste token is a single cursor unit: the caret never rests inside
|
|
454
|
+
* its span — LEFT from the end hops to its start, RIGHT from the start
|
|
455
|
+
* hops to its end, and clicks/moves snap to the nearest boundary.
|
|
456
|
+
* dir: -1 = leftward movement → start, +1 = rightward → end, 0 = nearest. */
|
|
457
|
+
#snapCursor(dir = 0) {
|
|
458
|
+
const m = this.pasteMark;
|
|
459
|
+
if (!m) return;
|
|
460
|
+
if (this.cursor > m.start && this.cursor < m.end) {
|
|
461
|
+
if (dir < 0) this.cursor = m.start;
|
|
462
|
+
else if (dir > 0) this.cursor = m.end;
|
|
463
|
+
else this.cursor = this.cursor - m.start < m.end - this.cursor ? m.start : m.end;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** Cancel the held-back paste AND its token. */
|
|
467
|
+
#touch() { this.pendingPaste = null; this.pasteMark = null; }
|
|
428
468
|
/** Claude-Code-style two-stage paste: the first Ctrl+Shift+V of a large
|
|
429
|
-
* clipboard shows a "[已复制 N 行内容]" placeholder
|
|
430
|
-
*
|
|
469
|
+
* clipboard shows a "[已复制 N 行内容]" placeholder — an IMMUTABLE token:
|
|
470
|
+
* deleting/typing into it consumes it whole, edits around it keep it, and
|
|
471
|
+
* pasting the same content again replaces exactly that token. */
|
|
431
472
|
#paste(text) {
|
|
432
473
|
text = String(text ?? "");
|
|
433
474
|
const large = text.includes("\n") || text.length > 300;
|
|
434
475
|
if (large) {
|
|
435
476
|
if (this.pendingPaste && this.pendingPaste.text === text) {
|
|
436
477
|
const full = this.pendingPaste.text;
|
|
437
|
-
const
|
|
478
|
+
const m = this.pasteMark;
|
|
438
479
|
this.#touch();
|
|
439
|
-
if (this.
|
|
480
|
+
if (m) this.#edit(m.start, m.end, full);
|
|
440
481
|
else this.insert(full);
|
|
441
482
|
this.app?.toast?.("已粘贴完整内容");
|
|
442
483
|
return true;
|
|
443
484
|
}
|
|
444
485
|
const lines = text.split("\n").length;
|
|
486
|
+
const placeholder = `[已复制 ${lines} 行内容]`;
|
|
487
|
+
const at = this.selectAll ? 0 : this.cursor;
|
|
488
|
+
this.insert(placeholder);
|
|
489
|
+
this.pasteMark = { start: at, end: at + Array.from(placeholder).length };
|
|
445
490
|
this.pendingPaste = { text };
|
|
446
|
-
this.pendingPlaceholder = `[已复制 ${lines} 行内容]`;
|
|
447
|
-
this.insert(this.pendingPlaceholder);
|
|
448
491
|
this.app?.toast?.("再次 Ctrl+Shift+V 粘贴完整内容(Ctrl+L 展开输入栏)");
|
|
449
492
|
return true;
|
|
450
493
|
}
|
|
@@ -457,28 +500,30 @@ export class Input extends Widget {
|
|
|
457
500
|
if (ev.type !== "key") return false;
|
|
458
501
|
switch (ev.name) {
|
|
459
502
|
case "backspace":
|
|
460
|
-
if (this.cursor > 0) { this.#
|
|
503
|
+
if (this.cursor > 0) { this.#edit(this.cursor - 1, this.cursor); this.onChange?.(); }
|
|
461
504
|
return true;
|
|
462
505
|
case "delete":
|
|
463
|
-
if (this.cursor < this.#cps().length) { this.#
|
|
506
|
+
if (this.cursor < this.#cps().length) { this.#edit(this.cursor, this.cursor + 1); this.onChange?.(); }
|
|
464
507
|
return true;
|
|
465
|
-
case "left": this.selectAll = false; this.cursor = Math.max(0, this.cursor - 1); return true;
|
|
466
|
-
case "right": this.selectAll = false; this.cursor = Math.min(this.#cps().length, this.cursor + 1); return true;
|
|
508
|
+
case "left": this.selectAll = false; this.cursor = Math.max(0, this.cursor - 1); this.#snapCursor(-1); return true;
|
|
509
|
+
case "right": this.selectAll = false; this.cursor = Math.min(this.#cps().length, this.cursor + 1); this.#snapCursor(1); return true;
|
|
467
510
|
case "home": {
|
|
468
511
|
if (this.multi) { const rows = this.#visualRows(); const { row } = this.#cursorVisual(); this.cursor = rows[row].start; }
|
|
469
512
|
else this.cursor = 0;
|
|
513
|
+
this.#snapCursor();
|
|
470
514
|
return true;
|
|
471
515
|
}
|
|
472
516
|
case "end": {
|
|
473
517
|
if (this.multi) { const rows = this.#visualRows(); const { row } = this.#cursorVisual(); this.cursor = rows[row].end; }
|
|
474
518
|
else this.cursor = this.#cps().length;
|
|
519
|
+
this.#snapCursor();
|
|
475
520
|
return true;
|
|
476
521
|
}
|
|
477
522
|
case "up":
|
|
478
523
|
if (this.multi) {
|
|
479
524
|
const rows = this.#visualRows();
|
|
480
525
|
const { row, col } = this.#cursorVisual();
|
|
481
|
-
if (row > 0) this.cursor = this.#indexAtVisual(row - 1, col);
|
|
526
|
+
if (row > 0) { this.cursor = this.#indexAtVisual(row - 1, col); this.#snapCursor(); }
|
|
482
527
|
} else if (this.history.length) {
|
|
483
528
|
this.histIdx = this.histIdx < 0 ? this.history.length - 1 : Math.max(0, this.histIdx - 1);
|
|
484
529
|
this.setValue(this.history[this.histIdx] ?? "");
|
|
@@ -488,7 +533,7 @@ export class Input extends Widget {
|
|
|
488
533
|
if (this.multi) {
|
|
489
534
|
const rows = this.#visualRows();
|
|
490
535
|
const { row, col } = this.#cursorVisual();
|
|
491
|
-
if (row < rows.length - 1) this.cursor = this.#indexAtVisual(row + 1, col);
|
|
536
|
+
if (row < rows.length - 1) { this.cursor = this.#indexAtVisual(row + 1, col); this.#snapCursor(); }
|
|
492
537
|
} else if (this.histIdx >= 0) {
|
|
493
538
|
this.histIdx++;
|
|
494
539
|
if (this.histIdx >= this.history.length) { this.histIdx = -1; this.setValue(""); }
|
|
@@ -500,8 +545,8 @@ export class Input extends Widget {
|
|
|
500
545
|
switch (ev.key) {
|
|
501
546
|
case "j": if (this.multi) { this.insert("\n"); return true; } return false;
|
|
502
547
|
case "c": this.#touch(); this.value = ""; this.cursor = 0; this.selectAll = false; this.onChange?.(); this.app?.toast?.("已清空输入栏"); return true;
|
|
503
|
-
case "u": this.#
|
|
504
|
-
case "k": this.#
|
|
548
|
+
case "u": this.#edit(0, this.cursor); this.onChange?.(); return true;
|
|
549
|
+
case "k": this.#edit(this.cursor, this.#cps().length); this.onChange?.(); return true;
|
|
505
550
|
case "l": {
|
|
506
551
|
// expand/collapse the input: expanded drops the 6-line cap and
|
|
507
552
|
// lets the editor fill the window above the input
|
|
@@ -527,13 +572,13 @@ export class Input extends Widget {
|
|
|
527
572
|
}
|
|
528
573
|
return false;
|
|
529
574
|
}
|
|
530
|
-
this.#touch();
|
|
531
575
|
this.insert(ev.text);
|
|
532
576
|
return true;
|
|
533
577
|
case "enter":
|
|
534
578
|
if (ev.shift && this.multi) { this.insert("\n"); return true; } // Shift+Enter = newline
|
|
535
579
|
if (this.value.trim() === "" && !this.allowEmptyEnter) return false;
|
|
536
580
|
const v = this.value;
|
|
581
|
+
this.#touch();
|
|
537
582
|
this.history.push(v);
|
|
538
583
|
this.histIdx = -1;
|
|
539
584
|
this.value = "";
|