@oh-my-pi/pi-tui 13.15.3 → 13.16.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/CHANGELOG.md +17 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +14 -3
- package/src/components/editor.ts +222 -32
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.16.1] - 2026-03-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Support for optional SearchDb parameter in CombinedAutocompleteProvider constructor for improved fuzzy search performance
|
|
10
|
+
- Fuzzy matching filter for autocomplete suggestions to improve relevance of results
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Fuzzy discovery now applies fuzzy matching filter to results for improved relevance of autocomplete suggestions
|
|
15
|
+
- Autocomplete fuzzy discovery now accepts optional SearchDb instance for faster searches
|
|
16
|
+
|
|
17
|
+
## [13.16.0] - 2026-03-27
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Updated tab replacement in editor text sanitization to respect configured tab width setting
|
|
21
|
+
|
|
5
22
|
## [13.15.0] - 2026-03-23
|
|
6
23
|
|
|
7
24
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.16.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"test": "bun test test/*.test.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@oh-my-pi/pi-natives": "13.
|
|
37
|
-
"@oh-my-pi/pi-utils": "13.
|
|
36
|
+
"@oh-my-pi/pi-natives": "13.16.1",
|
|
37
|
+
"@oh-my-pi/pi-utils": "13.16.1",
|
|
38
38
|
"marked": "^17.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/autocomplete.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
4
5
|
import { fuzzyFind } from "@oh-my-pi/pi-natives";
|
|
5
6
|
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
7
|
|
|
@@ -203,11 +204,17 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
203
204
|
// per-directory readdir fast-path for prefix completions. Global fuzzy
|
|
204
205
|
// discovery continues to use native fuzzyFind + shared scan cache.
|
|
205
206
|
#dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
|
|
207
|
+
#searchDb?: SearchDb;
|
|
206
208
|
readonly #DIR_CACHE_TTL = 2000; // 2 seconds
|
|
207
209
|
|
|
208
|
-
constructor(
|
|
210
|
+
constructor(
|
|
211
|
+
commands: (SlashCommand | AutocompleteItem)[] = [],
|
|
212
|
+
basePath: string = getProjectDir(),
|
|
213
|
+
searchDb?: SearchDb,
|
|
214
|
+
) {
|
|
209
215
|
this.#commands = commands;
|
|
210
216
|
this.#basePath = basePath;
|
|
217
|
+
this.#searchDb = searchDb;
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
async getSuggestions(
|
|
@@ -675,11 +682,15 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
675
682
|
const scopedQuery = await this.#resolveScopedFuzzyQuery(query);
|
|
676
683
|
const searchPath = scopedQuery?.baseDir ?? this.#basePath;
|
|
677
684
|
const fuzzyQuery = scopedQuery?.query ?? query;
|
|
678
|
-
const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath));
|
|
685
|
+
const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath), this.#searchDb);
|
|
686
|
+
const lowerQuery = fuzzyQuery.toLowerCase();
|
|
679
687
|
const filteredMatches = result.matches.filter(entry => {
|
|
680
688
|
const p = entry.path.endsWith("/") ? entry.path.slice(0, -1) : entry.path;
|
|
681
689
|
const normalized = p.replaceAll("\\", "/");
|
|
682
|
-
|
|
690
|
+
if (/(^|\/)\.git(\/|$)/.test(normalized)) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
return lowerQuery.length === 0 || fuzzyMatch(lowerQuery, normalized.toLowerCase());
|
|
683
694
|
});
|
|
684
695
|
const topEntries = filteredMatches.slice(0, 20);
|
|
685
696
|
const suggestions: AutocompleteItem[] = [];
|
package/src/components/editor.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
moveWordLeft,
|
|
13
13
|
moveWordRight,
|
|
14
14
|
padding,
|
|
15
|
+
replaceTabs,
|
|
16
|
+
sliceByColumn,
|
|
15
17
|
truncateToWidth,
|
|
16
18
|
visibleWidth,
|
|
17
19
|
} from "../utils";
|
|
@@ -22,6 +24,13 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
|
|
22
24
|
maxPrimaryColumnWidth: 32,
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
function sanitizeLoadedText(text: string): string {
|
|
28
|
+
return replaceTabs(text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"))
|
|
29
|
+
.split("")
|
|
30
|
+
.filter(char => char === "\n" || char.charCodeAt(0) >= 32)
|
|
31
|
+
.join("");
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
const segmenter = getSegmenter();
|
|
26
35
|
|
|
27
36
|
/**
|
|
@@ -318,6 +327,7 @@ export class Editor implements Component, Focusable {
|
|
|
318
327
|
cursorOverride: string | undefined;
|
|
319
328
|
/** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
|
|
320
329
|
cursorOverrideWidth: number | undefined;
|
|
330
|
+
#promptGutter: string | undefined;
|
|
321
331
|
|
|
322
332
|
// Store last layout width for cursor navigation
|
|
323
333
|
#lastLayoutWidth: number = 80;
|
|
@@ -374,6 +384,7 @@ export class Editor implements Component, Focusable {
|
|
|
374
384
|
|
|
375
385
|
// Custom top border (for status line integration)
|
|
376
386
|
#topBorderContent?: EditorTopBorder;
|
|
387
|
+
#borderVisible = true;
|
|
377
388
|
|
|
378
389
|
constructor(theme: EditorTheme) {
|
|
379
390
|
this.#theme = theme;
|
|
@@ -392,13 +403,24 @@ export class Editor implements Component, Focusable {
|
|
|
392
403
|
this.#topBorderContent = content;
|
|
393
404
|
}
|
|
394
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Show or hide the editor border chrome.
|
|
408
|
+
*/
|
|
409
|
+
setBorderVisible(borderVisible: boolean): void {
|
|
410
|
+
this.#borderVisible = borderVisible;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
setPromptGutter(promptGutter: string | undefined): void {
|
|
414
|
+
this.#promptGutter = promptGutter;
|
|
415
|
+
}
|
|
416
|
+
|
|
395
417
|
/**
|
|
396
418
|
* Get the available width for top border content given a total terminal width.
|
|
397
|
-
* Accounts for the border characters and horizontal padding.
|
|
419
|
+
* Accounts for the border characters and horizontal padding when visible.
|
|
398
420
|
*/
|
|
399
421
|
getTopBorderAvailableWidth(terminalWidth: number): number {
|
|
400
422
|
const paddingX = this.#getEditorPaddingX();
|
|
401
|
-
const borderWidth = paddingX
|
|
423
|
+
const borderWidth = this.#getHorizontalChromeWidth(paddingX);
|
|
402
424
|
return Math.max(0, terminalWidth - borderWidth * 2);
|
|
403
425
|
}
|
|
404
426
|
|
|
@@ -492,7 +514,7 @@ export class Editor implements Component, Focusable {
|
|
|
492
514
|
/** Internal setText that doesn't reset history state - used by navigateHistory */
|
|
493
515
|
#setTextInternal(text: string, cursorAnchor: HistoryCursorAnchor = "end"): void {
|
|
494
516
|
this.#undoStack.length = 0;
|
|
495
|
-
const lines = text
|
|
517
|
+
const lines = sanitizeLoadedText(text).split("\n");
|
|
496
518
|
this.#state.lines = lines.length === 0 ? [""] : lines;
|
|
497
519
|
if (cursorAnchor === "start") {
|
|
498
520
|
this.#state.cursorLine = 0;
|
|
@@ -515,18 +537,113 @@ export class Editor implements Component, Focusable {
|
|
|
515
537
|
return Math.max(0, padding);
|
|
516
538
|
}
|
|
517
539
|
|
|
540
|
+
#getHorizontalChromeWidth(paddingX: number): number {
|
|
541
|
+
return this.#borderVisible ? paddingX + 1 : 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#getPromptGutterWidth(width: number, paddingX: number): number {
|
|
545
|
+
if (this.#borderVisible || !this.#promptGutter) return 0;
|
|
546
|
+
const chromeWidth = 2 * this.#getHorizontalChromeWidth(paddingX);
|
|
547
|
+
const availableWidth = Math.max(0, width - chromeWidth);
|
|
548
|
+
return Math.min(visibleWidth(this.#promptGutter), availableWidth);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#getPromptGutter(
|
|
552
|
+
width: number,
|
|
553
|
+
paddingX: number,
|
|
554
|
+
): { firstLine: string; continuation: string; width: number } | undefined {
|
|
555
|
+
if (this.#borderVisible || !this.#promptGutter) return undefined;
|
|
556
|
+
const gutterWidth = this.#getPromptGutterWidth(width, paddingX);
|
|
557
|
+
if (gutterWidth === 0) return undefined;
|
|
558
|
+
return {
|
|
559
|
+
firstLine: sliceByColumn(this.#promptGutter, 0, gutterWidth, true),
|
|
560
|
+
continuation: padding(gutterWidth),
|
|
561
|
+
width: gutterWidth,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
518
565
|
#getContentWidth(width: number, paddingX: number): number {
|
|
519
|
-
|
|
566
|
+
const chromeWidth = 2 * this.#getHorizontalChromeWidth(paddingX);
|
|
567
|
+
return Math.max(0, width - chromeWidth - this.#getPromptGutterWidth(width, paddingX));
|
|
520
568
|
}
|
|
521
569
|
|
|
522
570
|
#getLayoutWidth(width: number, paddingX: number): number {
|
|
523
571
|
const contentWidth = this.#getContentWidth(width, paddingX);
|
|
524
|
-
|
|
572
|
+
const cursorReserve = this.#borderVisible && paddingX === 0 ? 1 : 0;
|
|
573
|
+
// Keep cursor/scroll layout addressable even when a borderless prompt gutter consumes every visible column.
|
|
574
|
+
return Math.max(1, contentWidth - cursorReserve);
|
|
525
575
|
}
|
|
526
576
|
|
|
527
577
|
#getVisibleContentHeight(contentLines: number): number {
|
|
528
578
|
if (this.#maxHeight === undefined) return contentLines;
|
|
529
|
-
|
|
579
|
+
const verticalChrome = this.#borderVisible ? 2 : 0;
|
|
580
|
+
return Math.max(1, this.#maxHeight - verticalChrome);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#getStyledInputCursor(): { text: string; width: number } {
|
|
584
|
+
const cursorChar = this.#theme.symbols.inputCursor;
|
|
585
|
+
return { text: `\x1b[5m${cursorChar}\x1b[0m`, width: visibleWidth(cursorChar) };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
#renderEndOfLineCursorAtWidthLimit(
|
|
589
|
+
before: string,
|
|
590
|
+
marker: string,
|
|
591
|
+
maxWidth: number,
|
|
592
|
+
replacement?: { text: string; width: number },
|
|
593
|
+
): { text: string; width: number } {
|
|
594
|
+
const beforeGraphemes = [...segmenter.segment(before)];
|
|
595
|
+
const lastGrapheme = beforeGraphemes[beforeGraphemes.length - 1]?.segment;
|
|
596
|
+
const lastGraphemeWidth = lastGrapheme ? visibleWidth(lastGrapheme) : 0;
|
|
597
|
+
const builtInCursor = this.#getStyledInputCursor();
|
|
598
|
+
const fallbackReplacement = lastGrapheme
|
|
599
|
+
? { text: `\x1b[7m${lastGrapheme}\x1b[0m`, width: lastGraphemeWidth }
|
|
600
|
+
: builtInCursor;
|
|
601
|
+
const clampReplacement = (candidate: { text: string; width: number }): { text: string; width: number } => {
|
|
602
|
+
let text = sliceByColumn(candidate.text, 0, maxWidth, true);
|
|
603
|
+
let width = visibleWidth(text);
|
|
604
|
+
if (width > maxWidth) {
|
|
605
|
+
text = "";
|
|
606
|
+
width = 0;
|
|
607
|
+
}
|
|
608
|
+
return { text, width };
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
let clampedReplacement = clampReplacement(replacement ?? fallbackReplacement);
|
|
612
|
+
if (replacement && clampedReplacement.width === 0) {
|
|
613
|
+
// A custom override that cannot fit at all should first fall back to the highlighted tail.
|
|
614
|
+
clampedReplacement = clampReplacement(fallbackReplacement);
|
|
615
|
+
}
|
|
616
|
+
if (lastGrapheme && clampedReplacement.width === 0) {
|
|
617
|
+
// If even the highlighted trailing grapheme cannot fit, show the built-in single-column cursor.
|
|
618
|
+
clampedReplacement = clampReplacement(builtInCursor);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const replacedSpanWidth = Math.min(maxWidth, Math.max(lastGraphemeWidth, clampedReplacement.width));
|
|
622
|
+
const prefixWidth = Math.max(0, maxWidth - replacedSpanWidth);
|
|
623
|
+
const beforePrefix = sliceByColumn(before, 0, prefixWidth, true);
|
|
624
|
+
const replacementPad = padding(Math.max(0, replacedSpanWidth - clampedReplacement.width));
|
|
625
|
+
return {
|
|
626
|
+
text: `${beforePrefix}${replacementPad}${clampedReplacement.text}${marker}`,
|
|
627
|
+
width: visibleWidth(beforePrefix) + replacedSpanWidth,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
#renderTerminalCursorMarker(text: string, marker: string, maxWidth: number): string {
|
|
632
|
+
if (!marker) return text;
|
|
633
|
+
if (visibleWidth(text) < maxWidth) {
|
|
634
|
+
return text + marker;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let insertAt = text.length;
|
|
638
|
+
let offset = 0;
|
|
639
|
+
for (const seg of segmenter.segment(text)) {
|
|
640
|
+
if (visibleWidth(seg.segment) > 0) {
|
|
641
|
+
insertAt = offset;
|
|
642
|
+
}
|
|
643
|
+
offset += seg.segment.length;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return `${text.slice(0, insertAt)}${marker}${text.slice(insertAt)}`;
|
|
530
647
|
}
|
|
531
648
|
|
|
532
649
|
#getPageScrollStep(totalVisualLines: number): number {
|
|
@@ -555,13 +672,15 @@ export class Editor implements Component, Focusable {
|
|
|
555
672
|
|
|
556
673
|
render(width: number): string[] {
|
|
557
674
|
const paddingX = this.#getEditorPaddingX();
|
|
675
|
+
const borderVisible = this.#borderVisible;
|
|
676
|
+
const promptGutter = this.#getPromptGutter(width, paddingX);
|
|
558
677
|
const contentAreaWidth = this.#getContentWidth(width, paddingX);
|
|
559
678
|
const layoutWidth = this.#getLayoutWidth(width, paddingX);
|
|
560
679
|
this.#lastLayoutWidth = layoutWidth;
|
|
561
680
|
|
|
562
681
|
// Box-drawing characters for rounded corners
|
|
563
682
|
const box = this.#theme.symbols.boxRound;
|
|
564
|
-
const borderWidth = paddingX
|
|
683
|
+
const borderWidth = this.#getHorizontalChromeWidth(paddingX);
|
|
565
684
|
const topLeft = this.borderColor(`${box.topLeft}${box.horizontal.repeat(paddingX)}`);
|
|
566
685
|
const topRight = this.borderColor(`${box.horizontal.repeat(paddingX)}${box.topRight}`);
|
|
567
686
|
const bottomLeft = this.borderColor(`${box.bottomLeft}${box.horizontal}${padding(Math.max(0, paddingX - 1))}`);
|
|
@@ -575,23 +694,25 @@ export class Editor implements Component, Focusable {
|
|
|
575
694
|
|
|
576
695
|
const result: string[] = [];
|
|
577
696
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
697
|
+
if (borderVisible) {
|
|
698
|
+
// Render top border: ╭─ [status content] ────────────────╮
|
|
699
|
+
const topFillWidth = Math.max(0, width - borderWidth * 2);
|
|
700
|
+
if (this.#topBorderContent) {
|
|
701
|
+
const { content, width: statusWidth } = this.#topBorderContent;
|
|
702
|
+
if (statusWidth <= topFillWidth) {
|
|
703
|
+
// Status fits - add fill after it
|
|
704
|
+
const fillWidth = topFillWidth - statusWidth;
|
|
705
|
+
result.push(topLeft + content + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
|
|
706
|
+
} else {
|
|
707
|
+
// Status too long - truncate it
|
|
708
|
+
const truncated = truncateToWidth(content, Math.max(0, topFillWidth - 1));
|
|
709
|
+
const truncatedWidth = visibleWidth(truncated);
|
|
710
|
+
const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
|
|
711
|
+
result.push(topLeft + truncated + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
|
|
712
|
+
}
|
|
586
713
|
} else {
|
|
587
|
-
|
|
588
|
-
const truncated = truncateToWidth(content, topFillWidth - 1);
|
|
589
|
-
const truncatedWidth = visibleWidth(truncated);
|
|
590
|
-
const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
|
|
591
|
-
result.push(topLeft + truncated + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
|
|
714
|
+
result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
|
|
592
715
|
}
|
|
593
|
-
} else {
|
|
594
|
-
result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
|
|
595
716
|
}
|
|
596
717
|
|
|
597
718
|
// Render each layout line
|
|
@@ -603,15 +724,63 @@ export class Editor implements Component, Focusable {
|
|
|
603
724
|
const inlineHint = this.#getInlineHint();
|
|
604
725
|
const hintStyle = this.#theme.hintStyle ?? ((t: string) => `\x1b[2m${t}\x1b[0m`);
|
|
605
726
|
|
|
606
|
-
for (
|
|
727
|
+
for (let visibleIndex = 0; visibleIndex < visibleLayoutLines.length; visibleIndex++) {
|
|
728
|
+
const layoutLine = visibleLayoutLines[visibleIndex]!;
|
|
607
729
|
let displayText = layoutLine.text;
|
|
608
730
|
let displayWidth = visibleWidth(layoutLine.text);
|
|
609
731
|
let cursorInPadding = false;
|
|
732
|
+
const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
|
|
733
|
+
const gutterText =
|
|
734
|
+
promptGutter === undefined ? "" : showPromptGutter ? promptGutter.firstLine : promptGutter.continuation;
|
|
610
735
|
|
|
611
736
|
// Add cursor if this line has it
|
|
612
737
|
const hasCursor = layoutLine.hasCursor && layoutLine.cursorPos !== undefined;
|
|
613
738
|
const marker = emitCursorMarker ? CURSOR_MARKER : "";
|
|
614
739
|
|
|
740
|
+
if (!borderVisible && displayWidth > lineContentWidth) {
|
|
741
|
+
displayText = sliceByColumn(displayText, 0, lineContentWidth, true);
|
|
742
|
+
displayWidth = visibleWidth(displayText);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (!borderVisible && lineContentWidth === 0) {
|
|
746
|
+
if (hasCursor && !this.#useTerminalCursor) {
|
|
747
|
+
const zeroWidthCursorBudget = visibleWidth(gutterText);
|
|
748
|
+
const zeroWidthCursorReplacement = this.cursorOverride
|
|
749
|
+
? { text: this.cursorOverride, width: this.cursorOverrideWidth ?? 1 }
|
|
750
|
+
: this.#getStyledInputCursor();
|
|
751
|
+
if (showPromptGutter && zeroWidthCursorBudget > 0) {
|
|
752
|
+
// Keep the leading prompt glyph visible when the gutter consumes the whole row.
|
|
753
|
+
const promptGlyph = [...segmenter.segment(gutterText)][0]?.segment ?? "";
|
|
754
|
+
const promptGlyphWidth = visibleWidth(promptGlyph);
|
|
755
|
+
const remainingCursorWidth = Math.max(0, zeroWidthCursorBudget - promptGlyphWidth);
|
|
756
|
+
if (remainingCursorWidth === 0) {
|
|
757
|
+
result.push(`\x1b[7m${promptGlyph}\x1b[0m${marker}`);
|
|
758
|
+
} else {
|
|
759
|
+
const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(
|
|
760
|
+
"",
|
|
761
|
+
marker,
|
|
762
|
+
remainingCursorWidth,
|
|
763
|
+
zeroWidthCursorReplacement,
|
|
764
|
+
);
|
|
765
|
+
result.push(`${promptGlyph}${widthLimitedCursor.text}`);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(
|
|
769
|
+
gutterText,
|
|
770
|
+
marker,
|
|
771
|
+
zeroWidthCursorBudget,
|
|
772
|
+
zeroWidthCursorReplacement,
|
|
773
|
+
);
|
|
774
|
+
result.push(widthLimitedCursor.text);
|
|
775
|
+
}
|
|
776
|
+
} else if (hasCursor && this.#useTerminalCursor) {
|
|
777
|
+
result.push(this.#renderTerminalCursorMarker(gutterText, marker, visibleWidth(gutterText)));
|
|
778
|
+
} else {
|
|
779
|
+
result.push(gutterText + (hasCursor ? marker : ""));
|
|
780
|
+
}
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
|
|
615
784
|
if (hasCursor && this.#useTerminalCursor) {
|
|
616
785
|
if (marker) {
|
|
617
786
|
const before = displayText.slice(0, layoutLine.cursorPos);
|
|
@@ -620,6 +789,8 @@ export class Editor implements Component, Focusable {
|
|
|
620
789
|
const hintText = hintStyle(truncateToWidth(inlineHint, Math.max(0, lineContentWidth - displayWidth)));
|
|
621
790
|
displayText = before + marker + hintText;
|
|
622
791
|
displayWidth += visibleWidth(inlineHint);
|
|
792
|
+
} else if (after.length === 0 && !borderVisible && displayWidth >= lineContentWidth) {
|
|
793
|
+
displayText = this.#renderTerminalCursorMarker(before, marker, lineContentWidth);
|
|
623
794
|
} else {
|
|
624
795
|
displayText = before + marker + after;
|
|
625
796
|
}
|
|
@@ -640,7 +811,16 @@ export class Editor implements Component, Focusable {
|
|
|
640
811
|
} else if (this.cursorOverride) {
|
|
641
812
|
// Cursor override replaces the normal end-of-text cursor glyph
|
|
642
813
|
const overrideWidth = this.cursorOverrideWidth ?? 1;
|
|
643
|
-
if (
|
|
814
|
+
if (!borderVisible && displayWidth + overrideWidth > lineContentWidth) {
|
|
815
|
+
// Borderless editors have no spare padding cell for an end-of-line cursor glyph.
|
|
816
|
+
// Preserve cursorOverride by replacing the tail of the line with it.
|
|
817
|
+
const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(before, marker, lineContentWidth, {
|
|
818
|
+
text: this.cursorOverride,
|
|
819
|
+
width: overrideWidth,
|
|
820
|
+
});
|
|
821
|
+
displayText = widthLimitedCursor.text;
|
|
822
|
+
displayWidth = widthLimitedCursor.width;
|
|
823
|
+
} else if (inlineHint) {
|
|
644
824
|
const availWidth = Math.max(0, lineContentWidth - displayWidth - overrideWidth);
|
|
645
825
|
const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
|
|
646
826
|
displayText = before + marker + this.cursorOverride + hintText;
|
|
@@ -651,16 +831,21 @@ export class Editor implements Component, Focusable {
|
|
|
651
831
|
}
|
|
652
832
|
} else {
|
|
653
833
|
// Cursor is at the end - add thin cursor glyph
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
834
|
+
const { text: cursor, width: cursorWidth } = this.#getStyledInputCursor();
|
|
835
|
+
if (!borderVisible && displayWidth + cursorWidth > lineContentWidth) {
|
|
836
|
+
// Borderless editors have no spare padding cell for an end-of-line cursor glyph.
|
|
837
|
+
// Highlight the last grapheme so the cursor stays visible without consuming width.
|
|
838
|
+
const widthLimitedCursor = this.#renderEndOfLineCursorAtWidthLimit(before, marker, lineContentWidth);
|
|
839
|
+
displayText = widthLimitedCursor.text;
|
|
840
|
+
displayWidth = widthLimitedCursor.width;
|
|
841
|
+
} else if (inlineHint) {
|
|
842
|
+
const availWidth = Math.max(0, lineContentWidth - displayWidth - cursorWidth);
|
|
658
843
|
const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
|
|
659
844
|
displayText = before + marker + cursor + hintText;
|
|
660
|
-
displayWidth +=
|
|
845
|
+
displayWidth += cursorWidth + Math.min(visibleWidth(inlineHint), availWidth);
|
|
661
846
|
} else {
|
|
662
847
|
displayText = before + marker + cursor;
|
|
663
|
-
displayWidth +=
|
|
848
|
+
displayWidth += cursorWidth;
|
|
664
849
|
}
|
|
665
850
|
if (displayWidth > lineContentWidth && paddingX > 0) {
|
|
666
851
|
cursorInPadding = true;
|
|
@@ -668,10 +853,15 @@ export class Editor implements Component, Focusable {
|
|
|
668
853
|
}
|
|
669
854
|
}
|
|
670
855
|
|
|
671
|
-
// All lines have consistent borders based on padding
|
|
672
|
-
const isLastLine = layoutLine === visibleLayoutLines[visibleLayoutLines.length - 1];
|
|
673
856
|
const linePad = padding(Math.max(0, lineContentWidth - displayWidth));
|
|
674
857
|
|
|
858
|
+
if (!borderVisible) {
|
|
859
|
+
result.push(gutterText + displayText + linePad);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// All lines have consistent borders based on padding
|
|
864
|
+
const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
|
|
675
865
|
const rightPaddingWidth = Math.max(0, paddingX - (cursorInPadding ? 1 : 0));
|
|
676
866
|
if (isLastLine) {
|
|
677
867
|
const bottomRightPadding = Math.max(0, paddingX - 1 - (cursorInPadding ? 1 : 0));
|