Add tab indentation and line number toggle to CodeMirror editor

- Wire indentWithTab into keymap (Tab=indent, Shift-Tab=dedent, 4 spaces)
- Set indentUnit to 4 spaces
- Wrap lineNumbers() in a Compartment for live toggle without editor rebuild
- Add Hash toolbar button to toggle line numbers; respects show_line_numbers prop as initial value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-08 15:55:42 -04:00
parent e3a3ab7de8
commit 60ecd221b4
2 changed files with 30 additions and 3 deletions

View File

@@ -81,6 +81,7 @@ type CMCache = {
languages?: any; languages?: any;
oneDark?: any; oneDark?: any;
placeholderExt?: any; placeholderExt?: any;
Compartment?: any;
} | null; } | null;
const GLOBAL_KEY = '__cm_singleton_modules_v1'; const GLOBAL_KEY = '__cm_singleton_modules_v1';
@@ -135,6 +136,7 @@ export async function ensure_CodeMirror_modules(): Promise<CMCache> {
EditorState_allowMultipleSelections: EditorState_allowMultipleSelections:
stateMod.EditorState.allowMultipleSelections, stateMod.EditorState.allowMultipleSelections,
EditorState_readOnly: stateMod.EditorState.readOnly, EditorState_readOnly: stateMod.EditorState.readOnly,
Compartment: stateMod.Compartment,
markdown: markdownMod?.markdown, markdown: markdownMod?.markdown,
markdownLanguage: markdownMod?.markdownLanguage, markdownLanguage: markdownMod?.markdownLanguage,

View File

@@ -11,7 +11,7 @@ import { ensure_CodeMirror_modules } from './codemirror_modules';
// import type { key_val } from '$lib/stores/ae_stores'; // import type { key_val } from '$lib/stores/ae_stores';
// Icons (Standardized to Lucide where possible, or FontAwesome placeholders) // Icons (Standardized to Lucide where possible, or FontAwesome placeholders)
import { Bold, Code, Italic, List } from '@lucide/svelte'; import { Bold, Code, Hash, Italic, List } from '@lucide/svelte';
interface Props { interface Props {
content?: string | null; content?: string | null;
new_content?: string; new_content?: string;
@@ -42,6 +42,8 @@ let {
let editor_container: HTMLDivElement | undefined = $state(); let editor_container: HTMLDivElement | undefined = $state();
let cm: any = $state(); // CodeMirror modules cache let cm: any = $state(); // CodeMirror modules cache
let show_line_nums = $state(untrack(() => show_line_numbers));
let ln_compartment: any = null;
async function create_editor() { async function create_editor() {
if (!browser) return; if (!browser) return;
@@ -55,6 +57,8 @@ async function create_editor() {
editor_view = null; editor_view = null;
} }
ln_compartment = new cm.Compartment();
const extensions = [ const extensions = [
cm.highlightSpecialChars(), cm.highlightSpecialChars(),
cm.history(), cm.history(),
@@ -70,8 +74,9 @@ async function create_editor() {
cm.highlightActiveLine(), cm.highlightActiveLine(),
cm.highlightActiveLineGutter(), cm.highlightActiveLineGutter(),
// Keymaps // Keymaps — indentWithTab must come before defaultKeymap
cm.keymap.of([ cm.keymap.of([
cm.indentWithTab,
...cm.defaultKeymap, ...cm.defaultKeymap,
...cm.searchKeymap, ...cm.searchKeymap,
...cm.historyKeymap, ...cm.historyKeymap,
@@ -80,6 +85,9 @@ async function create_editor() {
...cm.lintKeymap ...cm.lintKeymap
]), ]),
// 4-space indentation unit
cm.indentUnit.of(' '),
// Language Support // Language Support
language === 'markdown' language === 'markdown'
? cm.markdown({ base: cm.markdownLanguage }) ? cm.markdown({ base: cm.markdownLanguage })
@@ -94,7 +102,7 @@ async function create_editor() {
readonly readonly
? cm.EditorState.readOnly.of(true) ? cm.EditorState.readOnly.of(true)
: cm.EditorView.editable.of(true), : cm.EditorView.editable.of(true),
show_line_numbers ? cm.lineNumbers() : null, ln_compartment.of(show_line_nums ? cm.lineNumbers() : []),
wrap_lines ? cm.EditorView_lineWrapping : null, wrap_lines ? cm.EditorView_lineWrapping : null,
placeholder ? cm.placeholderExt(placeholder) : null, placeholder ? cm.placeholderExt(placeholder) : null,
@@ -144,6 +152,14 @@ $effect(() => {
} }
}); });
// Toggle line numbers without rebuilding the editor
$effect(() => {
if (!editor_view || !ln_compartment || !cm) return;
editor_view.dispatch({
effects: ln_compartment.reconfigure(show_line_nums ? cm.lineNumbers() : [])
});
});
// *** Toolbar Helpers // *** Toolbar Helpers
const wrap_selection = (before: string, after: string = before) => { const wrap_selection = (before: string, after: string = before) => {
if (!editor_view) return; if (!editor_view) return;
@@ -244,6 +260,15 @@ const toggle_list = () => {
</button> </button>
<div class="ml-auto flex gap-1"> <div class="ml-auto flex gap-1">
<button
type="button"
class="btn btn-sm {show_line_nums
? 'variant-filled-primary'
: 'variant-soft'} hover:variant-filled-primary"
onclick={() => (show_line_nums = !show_line_nums)}
title="Toggle Line Numbers">
<Hash size="14" />
</button>
<span <span
class="mr-2 self-center text-[10px] font-bold uppercase opacity-50" class="mr-2 self-center text-[10px] font-bold uppercase opacity-50"
>{language}</span> >{language}</span>