mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-05 22:12:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39d4fcfee6 | ||
|
|
fd0792d83d | ||
|
|
fdb948267d |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { stringCellWidth } from "../autocomplete/terminalStringCellWidth";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { ImageAddon } from "@xterm/addon-image";
|
||||
import { SearchAddon } from "@xterm/addon-search";
|
||||
@@ -1065,7 +1066,43 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
overlay.style.fontSize = `${currentTerminalFontSize()}px`;
|
||||
overlay.style.fontFamily = String(term.options.fontFamily ?? fontFamily);
|
||||
overlay.style.lineHeight = String(term.options.lineHeight ?? lineHeight);
|
||||
overlay.textContent = previewRows.map((row) => row.text).join("\n");
|
||||
// Native font advances differ from xterm's device-pixel-rounded cells.
|
||||
// Fit each row to its terminal cell width, including double-width glyphs,
|
||||
// rather than applying a single-cell correction once per Unicode glyph.
|
||||
overlay.style.fontWeight = String(term.options.fontWeight ?? "normal");
|
||||
overlay.style.fontKerning = "none";
|
||||
overlay.style.fontVariantLigatures = "none";
|
||||
const screenRect = term.element?.querySelector(".xterm-screen")?.getBoundingClientRect();
|
||||
if (screenRect && screenRect.width > 0 && screenRect.height > 0
|
||||
&& term.cols > 0 && term.rows > 0) {
|
||||
overlay.style.lineHeight = `${screenRect.height / term.rows}px`;
|
||||
const rowElements = previewRows.map((row) => {
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.verticalAlign = "top";
|
||||
span.style.transformOrigin = "left top";
|
||||
span.textContent = row.text;
|
||||
return span;
|
||||
});
|
||||
const fragment = document.createDocumentFragment();
|
||||
rowElements.forEach((span, index) => {
|
||||
if (index > 0) fragment.appendChild(document.createTextNode("\n"));
|
||||
fragment.appendChild(span);
|
||||
});
|
||||
overlay.replaceChildren(fragment);
|
||||
// Batch all measurements before writing transforms to avoid one forced
|
||||
// layout per row. Text nodes and explicit newlines stay unchanged for copy.
|
||||
const nativeWidths = rowElements.map((span) => span.getBoundingClientRect().width);
|
||||
rowElements.forEach((span, index) => {
|
||||
const nativeWidth = nativeWidths[index];
|
||||
const cells = stringCellWidth(previewRows[index].text, term);
|
||||
if (nativeWidth > 0 && cells > 0) {
|
||||
span.style.transform = `scaleX(${cells * screenRect.width / term.cols / nativeWidth})`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
overlay.textContent = previewRows.map((row) => row.text).join("\n");
|
||||
}
|
||||
overlay.setAttribute(HISTORY_PREVIEW_WRAP_ATTR, encodeHistoryPreviewWrapFlags(previewRows));
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -17,10 +17,30 @@ import {
|
||||
isHistoryPreviewDismissClick,
|
||||
joinHistoryPreviewSelectionText,
|
||||
nextHistoryPreviewTop,
|
||||
selectHistoryPreviewAll,
|
||||
shouldHideHistoryPreviewOnMouseDown,
|
||||
shouldKeepHistoryPreviewOnKey,
|
||||
} from "./terminalHistoryScrollOverride.ts";
|
||||
|
||||
test("select all includes every preview row and preserves soft-wrap copying", async () => {
|
||||
const { JSDOM } = await import("jsdom");
|
||||
const dom = new JSDOM("<pre><span>中文abc</span>\n<span>def</span>\n<span>last</span></pre>");
|
||||
try {
|
||||
const overlay = dom.window.document.querySelector("pre")!;
|
||||
overlay.setAttribute(HISTORY_PREVIEW_WRAP_ATTR, "010");
|
||||
assert.equal(selectHistoryPreviewAll(overlay), true);
|
||||
assert.equal(dom.window.getSelection()!.toString(), "中文abc\ndef\nlast");
|
||||
assert.equal(getHistoryPreviewSelectionText(overlay, dom.window.getSelection()), "中文abcdef\nlast");
|
||||
overlay.textContent = "plain\ntext";
|
||||
assert.equal(selectHistoryPreviewAll(overlay), true);
|
||||
assert.equal(dom.window.getSelection()!.toString(), "plain\ntext");
|
||||
overlay.textContent = "";
|
||||
assert.equal(selectHistoryPreviewAll(overlay), false);
|
||||
} finally {
|
||||
dom.window.close();
|
||||
}
|
||||
});
|
||||
|
||||
const wheel = (
|
||||
over: Partial<Parameters<typeof forcedHistoryScrollLinesForWheel>[0]> = {},
|
||||
) => ({
|
||||
@@ -352,3 +372,23 @@ test("history preview right-click is recognized as an app-menu target", () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("nested preview rows retain soft-wrap copy and partial selections", async () => {
|
||||
const { JSDOM } = await import("jsdom");
|
||||
const dom = new JSDOM("<pre><span>中文abc</span>\n<span>def</span></pre>");
|
||||
const overlay = dom.window.document.querySelector("pre")!;
|
||||
overlay.setAttribute(HISTORY_PREVIEW_WRAP_ATTR, "01");
|
||||
const first = overlay.firstChild!.firstChild!;
|
||||
const last = overlay.lastChild!.firstChild!;
|
||||
const selection = {
|
||||
rangeCount: 1, anchorNode: first, focusNode: last,
|
||||
anchorOffset: 1, focusOffset: 2, toString: () => "文abc\ndef",
|
||||
};
|
||||
assert.equal(getHistoryPreviewSelectionText(overlay, selection), "文abcde");
|
||||
assert.equal(getHistoryPreviewSelectionText(overlay, {
|
||||
...selection, anchorNode: overlay, focusNode: overlay,
|
||||
anchorOffset: 0, focusOffset: overlay.childNodes.length,
|
||||
}), "中文abcdef");
|
||||
dom.window.close();
|
||||
});
|
||||
@@ -147,6 +147,7 @@ export type HistoryPreviewSelectionLike = {
|
||||
};
|
||||
|
||||
export type HistoryPreviewNodeLike = {
|
||||
ownerDocument?: Pick<Document, "createRange">;
|
||||
contains(node: { nodeType?: number } | null): boolean;
|
||||
firstChild?: { nodeType?: number } | null;
|
||||
getAttribute?(name: string): string | null;
|
||||
@@ -271,6 +272,16 @@ const resolveOverlayTextOffset = (
|
||||
node: { nodeType?: number } | null,
|
||||
offset: number,
|
||||
): number | null => {
|
||||
if (overlay.ownerDocument && node) {
|
||||
try {
|
||||
const prefix = overlay.ownerDocument.createRange();
|
||||
prefix.selectNodeContents(overlay as unknown as Node);
|
||||
prefix.setEnd(node as Node, offset);
|
||||
return prefix.toString().length;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (node === textNode) return offset;
|
||||
if (node === overlay) return offset <= 0 ? 0 : overlay.textContent?.length ?? 0;
|
||||
return null;
|
||||
@@ -347,9 +358,7 @@ export const selectHistoryPreviewAll = (overlay: HTMLElement | null | undefined)
|
||||
const selection = overlay.ownerDocument.getSelection();
|
||||
if (!selection) return false;
|
||||
const range = overlay.ownerDocument.createRange();
|
||||
const textNode = overlay.firstChild;
|
||||
if (textNode) range.selectNodeContents(textNode);
|
||||
else range.selectNodeContents(overlay);
|
||||
range.selectNodeContents(overlay);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
return !selection.isCollapsed;
|
||||
|
||||
Reference in new issue
Block a user