From 1dd66074fc354be010f7d10ee5ef62ad27bdd6fb Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sun, 5 Oct 2025 21:48:27 +0200 Subject: [PATCH] hacky vim copy mode Signed-off-by: Ronni Skansing --- .../src/lib/components/editor/Editor.svelte | 10 +++ .../components/editor/SimpleCodeEditor.svelte | 10 +++ frontend/src/lib/utils/vimClipboard.js | 72 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 frontend/src/lib/utils/vimClipboard.js diff --git a/frontend/src/lib/components/editor/Editor.svelte b/frontend/src/lib/components/editor/Editor.svelte index b9610ad..c58b427 100644 --- a/frontend/src/lib/components/editor/Editor.svelte +++ b/frontend/src/lib/components/editor/Editor.svelte @@ -6,6 +6,10 @@ import { BiMap } from '$lib/utils/maps'; import { previewQR as generateQR } from '$lib/utils/qrPreview'; import { vimModeEnabled } from '$lib/store/vimMode.js'; + import { + setupVimClipboardIntegration, + destroyVimClipboardIntegration + } from '$lib/utils/vimClipboard.js'; /** @type {'domain'|'page'|'email'} */ export let contentType; @@ -160,6 +164,9 @@ .then((vimModule) => { const statusNode = vimStatusBar; vimModeInstance = vimModule.initVimMode(editor, statusNode); + + // integrate system clipboard with vim registers + setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco); }) .catch((e) => { console.error('vim mode not available', e); @@ -170,6 +177,9 @@ const destroyVimMode = () => { if (vimModeInstance) { try { + // cleanup clipboard integration first + destroyVimClipboardIntegration(vimModeInstance); + // use official monaco-vim dispose method vimModeInstance.dispose(); } catch (e) { diff --git a/frontend/src/lib/components/editor/SimpleCodeEditor.svelte b/frontend/src/lib/components/editor/SimpleCodeEditor.svelte index 153f78e..33c0b8c 100644 --- a/frontend/src/lib/components/editor/SimpleCodeEditor.svelte +++ b/frontend/src/lib/components/editor/SimpleCodeEditor.svelte @@ -4,6 +4,10 @@ import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import { vimModeEnabled } from '$lib/store/vimMode.js'; + import { + setupVimClipboardIntegration, + destroyVimClipboardIntegration + } from '$lib/utils/vimClipboard.js'; export let value = ''; export let height = 'medium'; @@ -119,6 +123,9 @@ .then((vimModule) => { const statusNode = vimStatusBar; vimModeInstance = vimModule.initVimMode(editor, statusNode); + + // integrate system clipboard with vim registers + setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco); }) .catch(() => { console.warn('vim mode not available - monaco-vim package not installed'); @@ -129,6 +136,9 @@ const destroyVimMode = () => { if (vimModeInstance) { try { + // cleanup clipboard integration first + destroyVimClipboardIntegration(vimModeInstance); + // use official monaco-vim dispose method vimModeInstance.dispose(); } catch (e) { diff --git a/frontend/src/lib/utils/vimClipboard.js b/frontend/src/lib/utils/vimClipboard.js new file mode 100644 index 0000000..6648091 --- /dev/null +++ b/frontend/src/lib/utils/vimClipboard.js @@ -0,0 +1,72 @@ +/** + * hacky vim copy to clipboard on visual mode d or y key down + */ + +/** + * sets up clipboard integration for vim mode in monaco editor + * @param {Object} editor - monaco editor instance + * @param {Object} vimModeInstance - vim mode instance from monaco-vim + * @param {boolean} localVimMode - current vim mode state + * @param {Object} monaco - monaco editor module + * @returns {Function} cleanup function to remove event listeners + */ +export function setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco) { + if (!editor || !vimModeInstance || !localVimMode) { + return () => {}; + } + + // check if clipboard api is available + if (typeof navigator === 'undefined' || !navigator.clipboard) { + console.warn('clipboard api not available'); + return () => {}; // noop cleanup + } + + // listen for y and d key presses to copy to system clipboard + const keyDownDisposable = editor.onKeyDown(async (e) => { + if (!localVimMode) return; + + // detect y or d key presses + if (e.keyCode === monaco.KeyCode.KeyY || e.keyCode === monaco.KeyCode.KeyD) { + // get current selection immediately + const selection = editor.getSelection(); + + // only copy to clipboard if we have selected text (visual mode) + if (selection && !selection.isEmpty()) { + // copy selected text to clipboard + try { + const selectedText = editor.getModel().getValueInRange(selection); + if (selectedText && selectedText.trim()) { + await navigator.clipboard.writeText(selectedText); + } + } catch (e) { + console.error(e); + } + } + } + }); + + // cleanup function to remove event listener + const cleanup = () => { + if (keyDownDisposable) { + keyDownDisposable.dispose(); + } + }; + + // store cleanup function on vim instance for later use + if (vimModeInstance) { + vimModeInstance._clipboardCleanup = cleanup; + } + + return cleanup; +} + +/** + * destroys vim clipboard integration + * @param {Object} vimModeInstance - vim mode instance from monaco-vim + */ +export function destroyVimClipboardIntegration(vimModeInstance) { + if (vimModeInstance && vimModeInstance._clipboardCleanup) { + vimModeInstance._clipboardCleanup(); + delete vimModeInstance._clipboardCleanup; + } +}