mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
fix(run): select and copy the whole console output with Select All (#903)
Root cause: Cmd/Ctrl+A in the Run console fell through to the browser and selected the whole window, and copying only saw the rows the virtualizer had mounted. Select All now selects the console only (also in its context menu), and copying after it — keyboard, context menu or native copy — takes the whole buffer. copySelectionOnKeyDown accepts a textOf callback for other views that do not mount all their text. Refs #746 Co-authored-by: ntbang0901 <[email protected]>
This commit is contained in:
1 parent
b3c0efd804
commit
7c7d656a50
4 files changed
+88
-4
No files matched your search
@@ -82,3 +82,24 @@ test("preserves all lines in a drag selection across virtual windows", async ({
|
||||
expect(text).toContain("output 250\noutput 251");
|
||||
expect(text).toContain("output 284");
|
||||
});
|
||||
|
||||
test("Select All takes the whole buffer, not just the mounted rows", async ({ page }) => {
|
||||
const log = await consoleOutput(page);
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { copied: string[] }).copied = [];
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: { writeText: async (text: string) => (window as unknown as { copied: string[] }).copied.push(text) },
|
||||
});
|
||||
});
|
||||
await log.focus();
|
||||
await page.keyboard.press("Meta+A");
|
||||
// Selection stays inside the console: nothing from the workbench behind it is selected.
|
||||
const selected = await page.evaluate(() => window.getSelection()?.toString() ?? "");
|
||||
expect(selected).not.toContain("Project");
|
||||
await page.keyboard.press("Meta+C");
|
||||
const [copied] = await page.evaluate(() => (window as unknown as { copied: string[] }).copied);
|
||||
expect(copied?.split("\n")).toHaveLength(500);
|
||||
expect(copied).toContain("output 0\n");
|
||||
expect(copied).toMatch(/output 499$/);
|
||||
});
|
||||
@@ -108,6 +108,33 @@ describe("view-local console copy with the Windows keymap (#372)", () => {
|
||||
expect(writeText).toHaveBeenCalledWith("selected application output");
|
||||
});
|
||||
|
||||
it("Ctrl+A selects only the console and copies the whole buffer, not just the mounted rows (#746)", () => {
|
||||
const model = new ConsoleModel();
|
||||
model.append({ kind: "stdout", text: "first\nsecond\n" });
|
||||
vi.spyOn(model, "text").mockReturnValue("whole buffer, including unmounted rows");
|
||||
const outside = document.createElement("p");
|
||||
outside.textContent = "workbench chrome";
|
||||
document.body.append(outside);
|
||||
render(
|
||||
createElement(ConsoleView, {
|
||||
model,
|
||||
version: 1,
|
||||
softWrap: false,
|
||||
follow: false,
|
||||
onFollowChange: () => {},
|
||||
cwd: "/",
|
||||
}),
|
||||
);
|
||||
const output = screen.getByRole("log");
|
||||
const selectAll = copy(output, { key: "a", code: "KeyA", metaKey: true });
|
||||
expect(selectAll.defaultPrevented).toBe(true);
|
||||
const selection = window.getSelection()!;
|
||||
expect(output.contains(selection.anchorNode) && output.contains(selection.focusNode)).toBe(true);
|
||||
expect(selection.toString()).not.toContain("workbench chrome");
|
||||
copy(output);
|
||||
expect(writeText).toHaveBeenCalledWith("whole buffer, including unmounted rows");
|
||||
});
|
||||
|
||||
it.each([false, true])("copies build output and selected message detail (detail=%s)", (detail) => {
|
||||
useBuildRun.setState({
|
||||
status: "failed",
|
||||
|
||||
@@ -179,6 +179,7 @@ export function ConsoleView({
|
||||
...aria
|
||||
}: ConsoleViewProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const lines = model.lines;
|
||||
const patterns = useWorkflowSettings((s) => s.console.foldPatterns);
|
||||
const [expanded, setExpanded] = useState<ReadonlySet<number>>(() => new Set());
|
||||
@@ -377,9 +378,30 @@ export function ConsoleView({
|
||||
scrolledMatch.current = key;
|
||||
}, [findOpen, activeMatch, rowOfLine, rows, virtualizer]);
|
||||
|
||||
/** Rows are virtualized, so "everything" is a selection of the container, not of the mounted rows. */
|
||||
const selectAll = () => {
|
||||
const inner = innerRef.current;
|
||||
if (inner) window.getSelection()?.selectAllChildren(inner);
|
||||
};
|
||||
const allSelected = () => {
|
||||
const inner = innerRef.current;
|
||||
const s = window.getSelection();
|
||||
return (
|
||||
!!inner &&
|
||||
!!s &&
|
||||
!s.isCollapsed &&
|
||||
s.anchorNode === inner &&
|
||||
s.focusNode === inner &&
|
||||
s.anchorOffset === 0 &&
|
||||
s.focusOffset === inner.childNodes.length
|
||||
);
|
||||
};
|
||||
/** The selected text; after Select All the whole buffer, including rows that are not mounted. */
|
||||
const selectedOutput = () => (allSelected() ? model.text() : (window.getSelection()?.toString() ?? ""));
|
||||
|
||||
const onContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const selection = window.getSelection()?.toString() ?? "";
|
||||
const selection = selectedOutput();
|
||||
showContextMenu(e, [
|
||||
{
|
||||
label: "Copy",
|
||||
@@ -388,6 +410,7 @@ export function ConsoleView({
|
||||
disabled: !selection,
|
||||
run: () => void navigator.clipboard?.writeText(selection).catch(() => undefined),
|
||||
},
|
||||
{ label: "Select All", shortcut: formatBinding(hostBinding("Meta+A")), run: selectAll },
|
||||
{ label: "Copy All", run: () => void navigator.clipboard?.writeText(model.text()).catch(() => undefined) },
|
||||
{
|
||||
label: "Find…",
|
||||
@@ -410,7 +433,12 @@ export function ConsoleView({
|
||||
openFind();
|
||||
return;
|
||||
}
|
||||
if (copySelectionOnKeyDown(e)) return;
|
||||
if (hasPrimaryModifier(e) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "a") {
|
||||
e.preventDefault();
|
||||
selectAll();
|
||||
return;
|
||||
}
|
||||
if (copySelectionOnKeyDown(e, selectedOutput)) return;
|
||||
if (!input) return;
|
||||
if (e.key === "d" && e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
@@ -496,10 +524,16 @@ export function ConsoleView({
|
||||
onScroll={onScroll}
|
||||
onContextMenu={onContextMenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onCopy={(e) => {
|
||||
// Native copy (menu, macOS) would take only the mounted rows.
|
||||
if (!allSelected()) return;
|
||||
e.preventDefault();
|
||||
e.clipboardData.setData("text/plain", model.text());
|
||||
}}
|
||||
onPaste={onPaste}
|
||||
data-autofocus=""
|
||||
>
|
||||
<div className={styles.consoleInner} style={{ height: virtualizer.getTotalSize() }}>
|
||||
<div ref={innerRef} className={styles.consoleInner} style={{ height: virtualizer.getTotalSize() }}>
|
||||
{items.map((item) => {
|
||||
const row = rows[item.index];
|
||||
if (!row) {
|
||||
|
||||
@@ -70,6 +70,8 @@ export function copySelectionOnKeyDown(
|
||||
| "preventDefault"
|
||||
| "stopPropagation"
|
||||
>,
|
||||
/** Text to copy instead of `selection.toString()`, for views that do not mount all their text. */
|
||||
textOf: (selection: Selection) => string = (selection) => selection.toString(),
|
||||
): boolean {
|
||||
if (e.defaultPrevented || e.nativeEvent.isComposing || e.altKey || e.shiftKey) return false;
|
||||
const copy =
|
||||
@@ -87,6 +89,6 @@ export function copySelectionOnKeyDown(
|
||||
return false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void copyText(selection.toString());
|
||||
void copyText(textOf(selection));
|
||||
return true;
|
||||
}
|
||||
Reference in new issue
Block a user