From 87c4f4d16756cb1a52ae9a78e837bebd586e64da Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sun, 5 Oct 2025 10:39:52 +0200 Subject: [PATCH] vim mode for editors Signed-off-by: Ronni Skansing --- frontend/package-lock.json | 10 ++ frontend/package.json | 1 + .../src/lib/components/editor/Editor.svelte | 96 ++++++++++++++++- .../components/editor/SimpleCodeEditor.svelte | 102 +++++++++++++++++- .../lib/components/editor/VimToggle.svelte | 16 +++ frontend/src/lib/store/vimMode.js | 26 +++++ frontend/src/lib/utils/cookies.js | 57 ++++++++++ frontend/src/routes/api-sender/+page.svelte | 13 ++- 8 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 frontend/src/lib/components/editor/VimToggle.svelte create mode 100644 frontend/src/lib/store/vimMode.js create mode 100644 frontend/src/lib/utils/cookies.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d28f688..7edd8db 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "devalue": "^5.3.2", "license-checker": "^25.0.1", "monaco-editor": "^0.53.0", + "monaco-vim": "^0.4.2", "nanoid": "^5.1.5", "npm-check-updates": "^17.1.3", "papaparse": "^5.5.3", @@ -2461,6 +2462,15 @@ "@types/trusted-types": "^1.0.6" } }, + "node_modules/monaco-vim": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.2.tgz", + "integrity": "sha512-rdbQC3O2rmpwX2Orzig/6gZjZfH7q7TIeB+uEl49sa+QyNm3jCKJOw5mwxBdFzTqbrPD+URfg6A2lEkuL5kymw==", + "license": "MIT", + "peerDependencies": { + "monaco-editor": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index c1e30b8..018a596 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "devalue": "^5.3.2", "license-checker": "^25.0.1", "monaco-editor": "^0.53.0", + "monaco-vim": "^0.4.2", "nanoid": "^5.1.5", "npm-check-updates": "^17.1.3", "papaparse": "^5.5.3", diff --git a/frontend/src/lib/components/editor/Editor.svelte b/frontend/src/lib/components/editor/Editor.svelte index 3fcb16d..ad7dd4d 100644 --- a/frontend/src/lib/components/editor/Editor.svelte +++ b/frontend/src/lib/components/editor/Editor.svelte @@ -5,6 +5,7 @@ import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; import { BiMap } from '$lib/utils/maps'; import { previewQR as generateQR } from '$lib/utils/qrPreview'; + import { vimModeEnabled } from '$lib/store/vimMode.js'; /** @type {'domain'|'page'|'email'} */ export let contentType; @@ -12,6 +13,8 @@ export let baseURL = 'example.test'; export let domainMap = new BiMap({}); export let selectedDomain = ''; + export let externalVimMode = null; // allow external control of vim mode + let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled; let editor = null; let previewFrame = null; let previewRenderDelayID = null; @@ -24,6 +27,7 @@ let externalFrameRef = null; let fileInputRef; let shadowContainer = null; + let vimStatusBar = null; const apiTemplates = [ { label: 'Custom Field 1', text: '{{.CustomField1}}' }, @@ -123,6 +127,11 @@ } }); + // enable vim mode if preference is enabled + if (localVimMode) { + initializeVimMode(); + } + editor.getModel().onDidChangeContent((e) => { if (previewRenderDelayID) { clearTimeout(previewRenderDelayID); @@ -136,6 +145,10 @@ return () => { document.body.classList.remove('overflow-hidden'); + if (vimModeInstance && vimModeInstance.dispose) { + vimModeInstance.dispose(); + vimModeInstance = null; + } if (editor) { editor.dispose(); monaco.editor.getModels().forEach((model) => model.dispose()); @@ -143,6 +156,52 @@ }; }); + // track vim mode state to prevent duplicate initialization + let vimModeInstance = null; + + const initializeVimMode = () => { + if (localVimMode && editor && !vimModeInstance) { + import('monaco-vim') + .then((vimModule) => { + const statusNode = vimStatusBar; + vimModeInstance = vimModule.initVimMode(editor, statusNode); + }) + .catch((e) => { + console.error('vim mode not available', e); + }); + } + }; + + const destroyVimMode = () => { + if (vimModeInstance) { + // use official monaco-vim dispose method + vimModeInstance.dispose(); + + // clear vim status bar + if (vimStatusBar) { + vimStatusBar.textContent = ''; + } + + vimModeInstance = null; + } + }; + + // sync with external vim mode control + $: if (externalVimMode !== null) { + localVimMode = externalVimMode; + } else { + localVimMode = $vimModeEnabled; + } + + // Watch for vim mode changes after initial load + $: if (editor && typeof localVimMode === 'boolean') { + if (localVimMode && !vimModeInstance) { + initializeVimMode(); + } else if (!localVimMode && vimModeInstance) { + destroyVimMode(); + } + } + const selectPreviewDomain = () => { baseURL = selectedDomain ? selectedDomain : baseURL; updatePreview(); @@ -564,6 +623,30 @@ Preview + + +