diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a8e49..e8a1159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### Unreleased +- Added Vim mode +- Added theme toggle +- Fixed titlebar icon color +- Added togglable status bar +- Added optional word count in status bar + +### 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..a42170d 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,7 +48,14 @@ }>(); let container: 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 = { @@ -155,6 +163,30 @@ }, }); + editor.addAction({ + id: 'toggle-vim-mode', + label: 'Toggle Vim Mode', + run: () => { + settings.toggleVimMode(); + }, + }); + + 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()); }; @@ -172,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(); }); @@ -452,14 +516,88 @@ monaco.editor.setTheme(targetTheme); } }); + + $effect(() => { + if (editor && settings.vimMode && vimStatusNode) { + const vim = initVimMode(editor, vimStatusNode); + return () => { + vim.dispose(); + }; + } + });
+{#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 537b4e6..46cbfd4 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -2,22 +2,34 @@ export class SettingsStore { minimap = $state(false); wordWrap = $state('on'); lineNumbers = $state('on'); + vimMode = $state(false); + statusBar = $state(true); + wordCount = $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'); + 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(() => { 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)); + localStorage.setItem('editor.statusBar', String(this.statusBar)); + localStorage.setItem('editor.wordCount', String(this.wordCount)); }); }); } @@ -34,6 +46,18 @@ export class SettingsStore { toggleLineNumbers() { this.lineNumbers = this.lineNumbers === 'on' ? 'off' : 'on'; } + + toggleVimMode() { + this.vimMode = !this.vimMode; + } + + toggleStatusBar() { + this.statusBar = !this.statusBar; + } + + toggleWordCount() { + this.wordCount = !this.wordCount; + } } export const settings = new SettingsStore();