[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
163
components/terminal/autocomplete/terminalStringCellWidth.ts
Normal file
163
components/terminal/autocomplete/terminalStringCellWidth.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Terminal cell-column width for autocomplete / ghost positioning.
|
||||
*
|
||||
* When an xterm instance is available, prefer its active Unicode provider
|
||||
* (`15-graphemes` via UnicodeGraphemesAddon) so emoji / VS-16 clusters match
|
||||
* the cursor advance. Fall back to a small East-Asian-Width-style classifier
|
||||
* for unit fakes that lack `_core.unicodeService`.
|
||||
*/
|
||||
|
||||
import type { Terminal as XTerm } from "@xterm/xterm";
|
||||
|
||||
type UnicodeServiceLike = {
|
||||
getStringCellWidth?: (s: string) => number;
|
||||
};
|
||||
|
||||
type TermWithUnicodeService = {
|
||||
_core?: {
|
||||
unicodeService?: UnicodeServiceLike;
|
||||
};
|
||||
};
|
||||
|
||||
const unicodeMarkPattern = /\p{Mark}/u;
|
||||
|
||||
function codePointCellWidth(cp: number): number {
|
||||
// Zero-width joiners / format / variation selectors / marks — xterm
|
||||
// folds these into the surrounding grapheme (wcwidth 0 or shouldJoin).
|
||||
if (
|
||||
cp === 0x00ad ||
|
||||
cp === 0x200d || // ZWJ
|
||||
(cp >= 0x200b && cp <= 0x200f) || // ZWSP..RLM
|
||||
(cp >= 0x202a && cp <= 0x202e) || // bidi overrides
|
||||
(cp >= 0x2060 && cp <= 0x206f) || // word joiner, invisible ops
|
||||
(cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors
|
||||
cp === 0xfeff ||
|
||||
(cp >= 0x1f3fb && cp <= 0x1f3ff) || // Emoji skin-tone modifiers
|
||||
(cp >= 0xe0100 && cp <= 0xe01ef) || // Variation Selectors Supplement
|
||||
unicodeMarkPattern.test(String.fromCodePoint(cp))
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
if (
|
||||
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
||||
(cp >= 0x2e80 && cp <= 0x303e) || // CJK Radicals, Kangxi
|
||||
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana, Katakana, CJK Compat
|
||||
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Extension A
|
||||
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified Ideographs
|
||||
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
||||
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
|
||||
(cp >= 0xf900 && cp <= 0xfaff) || // CJK Compat Ideographs
|
||||
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK Compat Forms
|
||||
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
|
||||
(cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth signs
|
||||
(cp >= 0x1f300 && cp <= 0x1faff) || // Emoji blocks
|
||||
(cp >= 0x20000 && cp <= 0x3fffd) // CJK Extension B-F, G
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function graphemeCellWidth(grapheme: string): number {
|
||||
let max = 0;
|
||||
for (const ch of grapheme) {
|
||||
const w = codePointCellWidth(ch.codePointAt(0) ?? 0);
|
||||
if (w > max) max = w;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
const graphemeSegmenter =
|
||||
typeof Intl !== "undefined" && "Segmenter" in Intl
|
||||
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
||||
: null;
|
||||
|
||||
function fallbackStringCellWidth(s: string): number {
|
||||
if (graphemeSegmenter) {
|
||||
let w = 0;
|
||||
for (const { segment } of graphemeSegmenter.segment(s)) {
|
||||
w += graphemeCellWidth(segment);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
// Fallback without Segmenter: sum code-point widths (ZWJ/marks already 0).
|
||||
let w = 0;
|
||||
for (const ch of s) {
|
||||
w += codePointCellWidth(ch.codePointAt(0) ?? 0);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
/** Terminal cell columns occupied by `s` (wide glyphs / grapheme clusters). */
|
||||
export function stringCellWidth(
|
||||
s: string,
|
||||
term?: XTerm | TermWithUnicodeService | null,
|
||||
): number {
|
||||
if (!s) return 0;
|
||||
const unicodeService = (term as TermWithUnicodeService | null | undefined)
|
||||
?._core?.unicodeService;
|
||||
const getWidth = unicodeService?.getStringCellWidth;
|
||||
if (typeof getWidth === "function") {
|
||||
return getWidth.call(unicodeService, s);
|
||||
}
|
||||
return fallbackStringCellWidth(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice a terminal line string by cell columns (xterm `cursorX` units).
|
||||
*
|
||||
* `translateToString()` returns characters, but `buffer.cursorX` is a cell
|
||||
* column. Mixing them with `String#substring(cursorX)` pulls padding spaces
|
||||
* into user input whenever the prompt contains wide glyphs (CJK paths in
|
||||
* Windows CMD / PowerShell), which breaks autocomplete matching (#2813).
|
||||
*/
|
||||
export function sliceStringByCellColumns(
|
||||
text: string,
|
||||
startCell: number,
|
||||
endCell?: number,
|
||||
term?: XTerm | TermWithUnicodeService | null,
|
||||
): string {
|
||||
if (!text) return "";
|
||||
const start = Math.max(0, startCell);
|
||||
const end = endCell === undefined ? Number.POSITIVE_INFINITY : Math.max(start, endCell);
|
||||
if (end === 0) return "";
|
||||
|
||||
let cell = 0;
|
||||
let startIndex = 0;
|
||||
let endIndex = text.length;
|
||||
let sawStart = false;
|
||||
|
||||
const advance = (segment: string, index: number, segmentLength: number): boolean => {
|
||||
const width = stringCellWidth(segment, term);
|
||||
const nextCell = cell + width;
|
||||
if (!sawStart && nextCell > start) {
|
||||
startIndex = index;
|
||||
sawStart = true;
|
||||
}
|
||||
if (nextCell >= end) {
|
||||
endIndex = nextCell === end ? index + segmentLength : index;
|
||||
if (!sawStart) {
|
||||
startIndex = index;
|
||||
sawStart = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
cell = nextCell;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (graphemeSegmenter) {
|
||||
for (const { segment, index } of graphemeSegmenter.segment(text)) {
|
||||
if (advance(segment, index, segment.length)) break;
|
||||
}
|
||||
} else {
|
||||
let index = 0;
|
||||
for (const ch of text) {
|
||||
if (advance(ch, index, ch.length)) break;
|
||||
index += ch.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sawStart) return "";
|
||||
return text.slice(startIndex, endIndex);
|
||||
}
|
||||
Reference in New Issue
Block a user