дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02: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]>
Цей коміт міститься в:
1 джерело
b3c0efd804
коміт
7c7d656a50
4 files changed
+88
-4
No files matched your search
@@ -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) {
|
||||
|
||||
Reference in new issue
Заблокувати користувача