From 72c159a2707661655d4493ecf4c0e8aea5e70276 Mon Sep 17 00:00:00 2001 From: Alec Ames Date: Thu, 5 Feb 2026 16:21:59 -0500 Subject: [PATCH 1/2] (feat) add vim mode toggle --- CHANGELOG.md | 7 +++++++ README.md | 1 + package-lock.json | 11 +++++++++- package.json | 3 ++- src/lib/components/Editor.svelte | 34 +++++++++++++++++++++++++++++++ src/lib/stores/settings.svelte.ts | 8 ++++++++ 6 files changed, 62 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a8e49..eb3ffc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### Unreleased +- Added Vim mode +- Added theme toggle +- Fixed titlebar icon color + +### Released + ## v2.4.2 - Added full width toggle diff --git a/README.md b/README.md index 2b3992e..7450d24 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Download from [markpad.sftwr.dev](https://markpad.sftwr.dev) - Split view - Syntax highlighting both in editor and code blocks - Mermaid diagram support +- Vim mode - Image and YouTube embeds - Familiar GitHub styled markdown rendering - Tiny memory usage (~10MB) diff --git a/package-lock.json b/package-lock.json index a6e511a..f0b0078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "highlight.js": "^11.11.1", "katex": "^0.16.27", "mermaid": "^11.12.2", - "monaco-editor": "^0.55.1" + "monaco-editor": "^0.55.1", + "monaco-vim": "^0.4.4" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.6", @@ -2575,6 +2576,14 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/monaco-vim": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.4.tgz", + "integrity": "sha512-LNChAb//WEm/W+eyeHG/0+pdVEHotk2hLTN+M3sQZx5E8cAlSWSgqcxpcRuQnxDybSln7pfHF9i63HmbIQvrWw==", + "peerDependencies": { + "monaco-editor": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/package.json b/package.json index eab4128..5906fb8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "highlight.js": "^11.11.1", "katex": "^0.16.27", "mermaid": "^11.12.2", - "monaco-editor": "^0.55.1" + "monaco-editor": "^0.55.1", + "monaco-vim": "^0.4.4" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.6", diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 23a8840..f51fdac 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -9,6 +9,7 @@ import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; + import { initVimMode } from 'monaco-vim'; let { value = $bindable(), @@ -47,6 +48,7 @@ }>(); let container: HTMLDivElement; + let vimStatusNode: HTMLDivElement; let editor: monaco.editor.IStandaloneCodeEditor; const currentTabId = tabManager.activeTabId; @@ -155,6 +157,14 @@ }, }); + editor.addAction({ + id: 'toggle-vim-mode', + label: 'Toggle Vim Mode', + run: () => { + settings.toggleVimMode(); + }, + }); + const updateTheme = () => { monaco.editor.setTheme(getTheme()); }; @@ -452,9 +462,21 @@ monaco.editor.setTheme(targetTheme); } }); + + $effect(() => { + if (editor && settings.vimMode && vimStatusNode) { + const vim = initVimMode(editor, vimStatusNode); + return () => { + vim.dispose(); + }; + } + });
+{#if settings.vimMode} +
+{/if} diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts index 537b4e6..54fc1e7 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -2,22 +2,26 @@ export class SettingsStore { minimap = $state(false); wordWrap = $state('on'); lineNumbers = $state('on'); + vimMode = $state(false); constructor() { if (typeof localStorage !== 'undefined') { const savedMinimap = localStorage.getItem('editor.minimap'); const savedWordWrap = localStorage.getItem('editor.wordWrap'); const savedLineNumbers = localStorage.getItem('editor.lineNumbers'); + const savedVimMode = localStorage.getItem('editor.vimMode'); if (savedMinimap !== null) this.minimap = savedMinimap === 'true'; if (savedWordWrap !== null) this.wordWrap = savedWordWrap; if (savedLineNumbers !== null) this.lineNumbers = savedLineNumbers; + if (savedVimMode !== null) this.vimMode = savedVimMode === 'true'; $effect.root(() => { $effect(() => { localStorage.setItem('editor.minimap', String(this.minimap)); localStorage.setItem('editor.wordWrap', this.wordWrap); localStorage.setItem('editor.lineNumbers', this.lineNumbers); + localStorage.setItem('editor.vimMode', String(this.vimMode)); }); }); } @@ -34,6 +38,10 @@ export class SettingsStore { toggleLineNumbers() { this.lineNumbers = this.lineNumbers === 'on' ? 'off' : 'on'; } + + toggleVimMode() { + this.vimMode = !this.vimMode; + } } export const settings = new SettingsStore(); From ca2c7e8ee19bd10f8665d6a40d2fbbafb3021584 Mon Sep 17 00:00:00 2001 From: Alec Ames Date: Thu, 5 Feb 2026 16:22:47 -0500 Subject: [PATCH 2/2] (feat) add status bar with wordcount --- CHANGELOG.md | 2 + src/lib/components/Editor.svelte | 108 +++++++++++++++++++++++++++++- src/lib/stores/settings.svelte.ts | 16 +++++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3ffc8..e8a1159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Added Vim mode - Added theme toggle - Fixed titlebar icon color +- Added togglable status bar +- Added optional word count in status bar ### Released diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index f51fdac..a42170d 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -48,8 +48,14 @@ }>(); let container: HTMLDivElement; - let vimStatusNode: HTMLDivElement; + let vimStatusNode = $state(); let editor: monaco.editor.IStandaloneCodeEditor; + + let cursorPosition = $state(null); + let selectionCount = $state(0); + let cursorCount = $state(0); + let wordCount = $state(0); + let currentLanguage = $state('markdown'); const currentTabId = tabManager.activeTabId; self.MonacoEnvironment = { @@ -165,6 +171,22 @@ }, }); + editor.addAction({ + id: 'toggle-status-bar', + label: 'Toggle Status Bar', + run: () => { + settings.toggleStatusBar(); + }, + }); + + editor.addAction({ + id: 'toggle-word-count', + label: 'Toggle Word Count', + run: () => { + settings.toggleWordCount(); + }, + }); + const updateTheme = () => { monaco.editor.setTheme(getTheme()); }; @@ -182,8 +204,40 @@ tabManager.updateTabRawContent(tabManager.activeTabId, newValue); } } + + // Update word count + const model = editor.getModel(); + if (model) { + const text = model.getValue(); + wordCount = (text.match(/\S+/g) || []).filter((w) => /\w/.test(w)).length; + } + }); + + editor.onDidChangeCursorPosition((e) => { + cursorPosition = e.position; + }); + + editor.onDidChangeCursorSelection((e) => { + const selections = editor.getSelections() || []; + cursorCount = selections.length; + const model = editor.getModel(); + + if (model && selections.length > 0) { + selectionCount = selections.reduce((acc: number, selection: monaco.Selection) => { + return acc + model.getValueInRange(selection).length; + }, 0); + } else { + selectionCount = 0; + } }); + // Initialize values + if (editor.getModel()) { + currentLanguage = editor.getModel()?.getLanguageId() || 'markdown'; + const text = editor.getModel()?.getValue() || ''; + wordCount = (text.match(/\S+/g) || []).filter((w) => /\w/.test(w)).length; + } + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { if (onsave) onsave(); }); @@ -474,10 +528,41 @@
+ {#if settings.vimMode}
{/if} +{#if settings.statusBar} +
+
+ Ln {cursorPosition?.lineNumber ?? 1}, Col {cursorPosition?.column ?? 1} +
+ {#if selectionCount > 0} +
+ {selectionCount} selected +
+ {:else if cursorCount > 1} +
+ {cursorCount} selections +
+ {/if} + {#if settings.wordCount} +
+ {wordCount} words +
+ {/if} +
+ {zoomLevel}% +
+
+ {currentLanguage} +
+
CRLF
+
UTF-8
+
+{/if} + diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts index 54fc1e7..46cbfd4 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -3,6 +3,8 @@ export class SettingsStore { wordWrap = $state('on'); lineNumbers = $state('on'); vimMode = $state(false); + statusBar = $state(true); + wordCount = $state(false); constructor() { if (typeof localStorage !== 'undefined') { @@ -10,11 +12,15 @@ export class SettingsStore { const savedWordWrap = localStorage.getItem('editor.wordWrap'); const savedLineNumbers = localStorage.getItem('editor.lineNumbers'); const savedVimMode = localStorage.getItem('editor.vimMode'); + const savedStatusBar = localStorage.getItem('editor.statusBar'); + const savedWordCount = localStorage.getItem('editor.wordCount'); if (savedMinimap !== null) this.minimap = savedMinimap === 'true'; if (savedWordWrap !== null) this.wordWrap = savedWordWrap; if (savedLineNumbers !== null) this.lineNumbers = savedLineNumbers; if (savedVimMode !== null) this.vimMode = savedVimMode === 'true'; + if (savedStatusBar !== null) this.statusBar = savedStatusBar === 'true'; + if (savedWordCount !== null) this.wordCount = savedWordCount === 'true'; $effect.root(() => { $effect(() => { @@ -22,6 +28,8 @@ export class SettingsStore { localStorage.setItem('editor.wordWrap', this.wordWrap); localStorage.setItem('editor.lineNumbers', this.lineNumbers); localStorage.setItem('editor.vimMode', String(this.vimMode)); + localStorage.setItem('editor.statusBar', String(this.statusBar)); + localStorage.setItem('editor.wordCount', String(this.wordCount)); }); }); } @@ -42,6 +50,14 @@ export class SettingsStore { toggleVimMode() { this.vimMode = !this.vimMode; } + + toggleStatusBar() { + this.statusBar = !this.statusBar; + } + + toggleWordCount() { + this.wordCount = !this.wordCount; + } } export const settings = new SettingsStore();