[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:
115
components/terminal/terminalStringCellWidth.test.ts
Normal file
115
components/terminal/terminalStringCellWidth.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { stringCellWidth, sliceStringByCellColumns } from "./autocomplete/terminalStringCellWidth.ts";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
test("stringCellWidth counts ASCII as one cell each", () => {
|
||||
assert.equal(stringCellWidth("docker"), 6);
|
||||
});
|
||||
|
||||
test("stringCellWidth counts CJK ideographs as two cells each", () => {
|
||||
assert.equal(stringCellWidth("部署"), 4);
|
||||
});
|
||||
|
||||
test("sliceStringByCellColumns respects wide CJK cells", () => {
|
||||
const prompt = String.raw`C:\Users\用户>`;
|
||||
const line = `${prompt}部署`;
|
||||
const promptCells = stringCellWidth(prompt);
|
||||
assert.equal(sliceStringByCellColumns(line, 0, promptCells), prompt);
|
||||
assert.equal(sliceStringByCellColumns(line, promptCells), "部署");
|
||||
// Padding spaces after the content must not leak into the cursor prefix.
|
||||
const padded = line + " ".repeat(20);
|
||||
assert.equal(
|
||||
sliceStringByCellColumns(padded, 0, stringCellWidth(line)),
|
||||
line,
|
||||
);
|
||||
});
|
||||
|
||||
test("stringCellWidth collapses ZWJ emoji to one wide grapheme", () => {
|
||||
assert.equal(stringCellWidth("👨💻"), 2);
|
||||
});
|
||||
|
||||
test("stringCellWidth ignores combining marks inside a grapheme", () => {
|
||||
// e + combining acute accent
|
||||
assert.equal(stringCellWidth("e\u0301"), 1);
|
||||
});
|
||||
|
||||
test("stringCellWidth matches xterm 15-graphemes for common emoji clusters", () => {
|
||||
class El {
|
||||
tagName: string;
|
||||
style: Record<string, string> = {};
|
||||
children: El[] = [];
|
||||
classList = { add() {}, remove() {}, contains() { return false; } };
|
||||
constructor(tag: string) { this.tagName = tag; }
|
||||
appendChild(c: El) { this.children.push(c); return c; }
|
||||
removeChild(c: El) { return c; }
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
setAttribute() {}
|
||||
getAttribute() { return null; }
|
||||
getBoundingClientRect() { return { left: 0, top: 0, width: 0, height: 0 }; }
|
||||
remove() {}
|
||||
}
|
||||
const previous = {
|
||||
document: globalThis.document,
|
||||
window: globalThis.window,
|
||||
HTMLElement: globalThis.HTMLElement,
|
||||
Element: globalThis.Element,
|
||||
DocumentFragment: globalThis.DocumentFragment,
|
||||
getComputedStyle: globalThis.getComputedStyle,
|
||||
requestAnimationFrame: globalThis.requestAnimationFrame,
|
||||
cancelAnimationFrame: globalThis.cancelAnimationFrame,
|
||||
};
|
||||
// Minimal DOM so @xterm/xterm can construct a Terminal in node tests.
|
||||
(globalThis as { document?: unknown }).document = {
|
||||
createElement: (t: string) => new El(t),
|
||||
createDocumentFragment: () => new El("frag"),
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
};
|
||||
(globalThis as { window?: unknown }).window = globalThis;
|
||||
(globalThis as { HTMLElement?: unknown }).HTMLElement = El;
|
||||
(globalThis as { Element?: unknown }).Element = El;
|
||||
(globalThis as { DocumentFragment?: unknown }).DocumentFragment = El;
|
||||
(globalThis as { getComputedStyle?: unknown }).getComputedStyle = () => ({
|
||||
getPropertyValue: () => "",
|
||||
});
|
||||
(globalThis as { requestAnimationFrame?: unknown }).requestAnimationFrame = (
|
||||
cb: (t: number) => void,
|
||||
) => setTimeout(() => cb(0), 0);
|
||||
(globalThis as { cancelAnimationFrame?: unknown }).cancelAnimationFrame = (
|
||||
id: number,
|
||||
) => clearTimeout(id);
|
||||
|
||||
try {
|
||||
const xterm = require("@xterm/xterm") as typeof import("@xterm/xterm");
|
||||
const graphemes = require("@xterm/addon-unicode-graphemes") as typeof import("@xterm/addon-unicode-graphemes");
|
||||
const term = new xterm.Terminal({ cols: 80, rows: 24, allowProposedApi: true });
|
||||
term.loadAddon(new graphemes.UnicodeGraphemesAddon());
|
||||
term.unicode.activeVersion = "15-graphemes";
|
||||
|
||||
const samples = ["🇨🇳", "1️⃣", "©️", "🖥", "👨💻", "部署", "docker"];
|
||||
for (const s of samples) {
|
||||
const expected = (
|
||||
term as unknown as {
|
||||
_core: { unicodeService: { getStringCellWidth: (v: string) => number } };
|
||||
}
|
||||
)._core.unicodeService.getStringCellWidth(s);
|
||||
assert.equal(
|
||||
stringCellWidth(s, term),
|
||||
expected,
|
||||
`${JSON.stringify(s)} should match xterm width ${expected}`,
|
||||
);
|
||||
}
|
||||
// These disagree with the hand-rolled fallback; the term path must win.
|
||||
assert.equal(stringCellWidth("🇨🇳", term), 2);
|
||||
assert.equal(stringCellWidth("1️⃣", term), 2);
|
||||
assert.equal(stringCellWidth("©️", term), 2);
|
||||
assert.equal(stringCellWidth("🖥", term), 1);
|
||||
} finally {
|
||||
Object.assign(globalThis, previous);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user