дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(ui): Make read-only content selectable with a Copy context menu (#48)
Root cause: body is user-select: none and each feature had to opt back in; the Markdown preview (and other read-only views) never did, and the app-wide contextmenu guard left no Copy item. Fix: shared selectableText class (src/ui) for read-only content, and a fallback Copy context menu from browserGuards when right-clicking a selection or a selectable area (component menus still win). Siblings covered: Quick Documentation / hover / completion docs, hover errors, Quick Definition, debugger value tooltip, inspection messages, message dialogs (no drag on body text), notification titles, About dialog; existing selectable consoles now get Copy too. Verified in-app on macOS: select in Markdown preview, right-click Copy puts the text on the clipboard; tab context menu unchanged. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
6dfd82de4c
коміт
de84a643aa
17 files changed
+453
-55
No files matched your search
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Read-only text the user can select and copy, like IDEA's Markdown preview,
|
||||
* Quick Documentation and message panes. The workbench body is unselectable
|
||||
* (chrome); content opts back in with `selectableText`. A right-click on a
|
||||
* selection, or anywhere in such content, offers Copy (`copyMenuFor`).
|
||||
*/
|
||||
import { formatBinding } from "@/platform/keymap";
|
||||
import type { MenuItem } from "./Menu";
|
||||
import styles from "./SelectableText.module.css";
|
||||
|
||||
export const selectableText = styles.selectable!;
|
||||
|
||||
const EDITABLE = "input, textarea, [contenteditable=''], [contenteditable='true'], [contenteditable='plaintext-only']";
|
||||
|
||||
function selectionCovers(sel: Selection | null, target: Node): boolean {
|
||||
if (!sel || sel.isCollapsed || !sel.toString()) return false;
|
||||
for (let i = 0; i < sel.rangeCount; i++) {
|
||||
const range = sel.getRangeAt(i);
|
||||
if (range.intersectsNode(target) || target.contains(range.commonAncestorContainer)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The Copy menu for a right-click on `target`, or null when it is not on readable text. */
|
||||
export function copyMenuFor(target: EventTarget | null): MenuItem[] | null {
|
||||
const el = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
||||
if (!el || el.closest(EDITABLE)) return null;
|
||||
const sel = window.getSelection();
|
||||
const text = selectionCovers(sel, el) ? sel!.toString() : "";
|
||||
if (!text && !el.closest(`.${selectableText}`)) return null;
|
||||
return [
|
||||
{
|
||||
id: "copy",
|
||||
label: "Copy",
|
||||
icon: "copy",
|
||||
shortcut: formatBinding("Meta+C"),
|
||||
disabled: !text,
|
||||
run: () => void copyText(text),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Writes `text` to the clipboard; falls back to copying the live selection. */
|
||||
export async function copyText(text: string): Promise<void> {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Clipboard API refused (permissions): the selection is still there.
|
||||
}
|
||||
document.execCommand("copy");
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача