Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
138 changes: 138 additions & 0 deletions src/lib/components/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -47,7 +48,14 @@
}>();

let container: HTMLDivElement;
let vimStatusNode = $state<HTMLDivElement>();
let editor: monaco.editor.IStandaloneCodeEditor;

let cursorPosition = $state<monaco.Position | null>(null);
let selectionCount = $state(0);
let cursorCount = $state(0);
let wordCount = $state(0);
let currentLanguage = $state('markdown');
const currentTabId = tabManager.activeTabId;

self.MonacoEnvironment = {
Expand Down Expand Up @@ -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());
};
Expand All @@ -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();
});
Expand Down Expand Up @@ -452,14 +516,88 @@
monaco.editor.setTheme(targetTheme);
}
});

$effect(() => {
if (editor && settings.vimMode && vimStatusNode) {
const vim = initVimMode(editor, vimStatusNode);
return () => {
vim.dispose();
};
}
});
</script>

<div class="editor-container" bind:this={container}></div>

{#if settings.vimMode}
<div class="vim-status-bar" bind:this={vimStatusNode}></div>
{/if}

{#if settings.statusBar}
<div class="status-bar">
<div class="status-item">
Ln {cursorPosition?.lineNumber ?? 1}, Col {cursorPosition?.column ?? 1}
</div>
{#if selectionCount > 0}
<div class="status-item">
{selectionCount} selected
</div>
{:else if cursorCount > 1}
<div class="status-item">
{cursorCount} selections
</div>
{/if}
{#if settings.wordCount}
<div class="status-item">
{wordCount} words
</div>
{/if}
<div class="status-item">
{zoomLevel}%
</div>
<div class="status-item">
{currentLanguage}
</div>
<div class="status-item">CRLF</div>
<div class="status-item">UTF-8</div>
</div>
{/if}

<style>
.editor-container {
width: 100%;
height: 100%;
overflow: hidden;
}

.vim-status-bar {
padding: 0 10px;
font-family: monospace;
font-size: 12px;
background: var(--bg-tertiary);
border-top: 1px solid var(--color-border-muted);
color: var(--text-primary);
display: flex;
align-items: center;
min-height: 20px;
}

.status-bar {
padding: 0 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 12px;
background: var(--bg-tertiary);
border-top: 1px solid var(--color-border-muted);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 22px;
gap: 20px;
user-select: none;
}

.status-item {
opacity: 0.8;
}
</style>
24 changes: 24 additions & 0 deletions src/lib/stores/settings.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
}
Expand All @@ -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();