diff --git a/src/components/ui/SqlEditorWrapper.tsx b/src/components/ui/SqlEditorWrapper.tsx index 353eb34e..26600acd 100644 --- a/src/components/ui/SqlEditorWrapper.tsx +++ b/src/components/ui/SqlEditorWrapper.tsx @@ -3,7 +3,7 @@ import MonacoEditor, { type OnMount, type BeforeMount } from "@monaco-editor/rea import type * as Monaco from "monaco-editor"; import { useEditorTheme } from "../../hooks/useEditorTheme"; import { loadMonacoTheme } from "../../themes/themeUtils"; -import { readText } from "@tauri-apps/plugin-clipboard-manager"; +import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager"; import { useSettings } from "../../hooks/useSettings"; import { useKeybindings } from "../../hooks/useKeybindings"; import { getFontCSS } from "../../utils/settings"; @@ -132,11 +132,79 @@ const SqlEditorInternal = ({ } }; + const getSelectedText = (ed: Monaco.editor.ICodeEditor): string => { + const model = ed.getModel(); + const selections = ed.getSelections(); + if (!model || !selections || selections.length === 0) return ''; + const nonEmpty = selections.filter((sel) => !sel.isEmpty()); + // With no selection, Monaco's copy/cut act on the whole current line. + if (nonEmpty.length === 0) { + const line = ed.getPosition()?.lineNumber; + return line ? model.getLineContent(line) + '\n' : ''; + } + return nonEmpty.map((sel) => model.getValueInRange(sel)).join('\n'); + }; + + const tauriCopy = async (ed: Monaco.editor.ICodeEditor) => { + try { + const text = getSelectedText(ed); + if (text) await writeText(text); + } catch (err) { + console.error('Failed to write clipboard:', err); + } + }; + + const tauriCut = async (ed: Monaco.editor.ICodeEditor) => { + try { + const text = getSelectedText(ed); + if (!text) return; + await writeText(text); + const model = ed.getModel(); + const selections = ed.getSelections(); + if (!model || !selections) return; + const nonEmpty = selections.filter((sel) => !sel.isEmpty()); + if (nonEmpty.length > 0) { + ed.executeEdits('cut', nonEmpty.map((sel) => ({ range: sel, text: '' }))); + } else { + const line = ed.getPosition()?.lineNumber; + if (line) { + const range = new monacoRef.current!.Range( + line, 1, line + 1, 1, + ); + ed.executeEdits('cut', [{ range, text: '' }]); + } + } + ed.pushUndoStop(); + } catch (err) { + console.error('Failed to cut to clipboard:', err); + } + }; + const handleEditorMount: OnMount = (editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco; - // Register custom Paste action using Tauri clipboard API + // Register custom Cut/Copy/Paste actions using the Tauri clipboard API. + // Monaco's built-ins rely on document.execCommand, which silently fails + // inside the WebView on some Linux setups. + editor.addAction({ + id: 'tauri.clipboardCut', + label: 'Cut', + contextMenuGroupId: '9_cutcopypaste', + contextMenuOrder: 0, + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyX], + run: tauriCut, + }); + + editor.addAction({ + id: 'tauri.clipboardCopy', + label: 'Copy', + contextMenuGroupId: '9_cutcopypaste', + contextMenuOrder: 1, + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC], + run: tauriCopy, + }); + editor.addAction({ id: 'tauri.clipboardPaste', label: 'Paste', @@ -146,7 +214,7 @@ const SqlEditorInternal = ({ run: tauriPaste, }); - // Remove the built-in Paste from context menu (doesn't work in Tauri) + // Remove the built-in Cut/Copy/Paste from the context menu (they don't work in Tauri) const contextMenuContrib = editor.getContribution('editor.contrib.contextmenu'); if (contextMenuContrib) { const contrib = contextMenuContrib as unknown as Record; @@ -154,7 +222,12 @@ const SqlEditorInternal = ({ if (typeof orig === 'function') { contrib._getMenuActions = function (...args: unknown[]) { const actions: { id?: string }[] = (orig as (...a: unknown[]) => { id?: string }[]).apply(this, args); - return actions.filter((a) => a.id !== 'editor.action.clipboardPasteAction'); + const builtIns = new Set([ + 'editor.action.clipboardCutAction', + 'editor.action.clipboardCopyAction', + 'editor.action.clipboardPasteAction', + ]); + return actions.filter((a) => !a.id || !builtIns.has(a.id)); }; } }