first pass at line wrapping (press F2 to toggle)
Hunter
Wednesday, August 6th, 2025 at 4:24 PM
7 <link rel="icon" href="resources/rollerskate.png">
8 <link rel="manifest" href="manifest.json">
9 <script type="module">
- import {EditorView, keymap, placeholder, lineNumbers} from "https://esm.sh/@codemirror/view@6"
- import {EditorState, Compartment} from "https://esm.sh/@codemirror/state@6"
10 + import {EditorView, keymap, placeholder, lineNumbers, Decoration} from "https://esm.sh/@codemirror/view@6"
11 + import {EditorState, Compartment, StateField, StateEffect} from "https://esm.sh/@codemirror/state@6"
12 import {defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap} from "https://esm.sh/@codemirror/commands@6"
13 import {closeBrackets, closeBracketsKeymap} from "https://esm.sh/@codemirror/autocomplete@6"
14 import {html} from "https://esm.sh/@codemirror/lang-html@6"
16 import {indentUnit} from "https://esm.sh/@codemirror/language@6"
17 import {search, searchKeymap, closeSearchPanel, openSearchPanel} from "https://esm.sh/@codemirror/search@6"
18
- window.CodeMirror = {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers};
19 + window.CodeMirror = {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers, Decoration, StateField, StateEffect};
20 </script>
21 <style>
22 :root {
198 let isFullscreen = false;
199 let showLineNumbers = false;
200 let lineNumbersCompartment;
201 + let showLineWrapping = false;
202 + let lineWrappingCompartment;
203
204 function toggleFullscreen() {
205 const previewPane = document.querySelector('.preview-pane');
384 function loadEditorSettings() {
385 try {
386 showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true';
387 + showLineWrapping = localStorage.getItem('editor-line-wrapping') === 'true';
388 } catch (e) {
389 console.warn('Could not load editor settings from localStorage:', e);
390 showLineNumbers = false;
391 + showLineWrapping = false;
392 }
393 }
394
410 });
411 }
412
413 + function createLineWrappingPlugin() {
414 + const {EditorView, Decoration, StateField, StateEffect} = window.CodeMirror;
415 +
416 + // Line wrapping functionality
417 + const CharacterWidthEffect = StateEffect.define({});
418 + const extra_cycle_character_width = StateField.define({
419 + create() {
420 + return null;
421 + },
422 + update(value, tr) {
423 + for (let effect of tr.effects) {
424 + if (effect.is(CharacterWidthEffect)) return effect.value;
425 + }
426 + return value;
427 + }
428 + });
429 +
430 + const character_width_listener = EditorView.updateListener.of((viewupdate) => {
431 + const width = viewupdate.view.defaultCharacterWidth;
432 + const current_width = viewupdate.view.state.field(extra_cycle_character_width, false);
433 +
434 + if (current_width !== width) {
435 + viewupdate.view.dispatch({
436 + effects: [CharacterWidthEffect.of(width)]
437 + });
438 + }
439 + });
440 +
441 + const ARBITRARY_INDENT_LINE_WRAP_LIMIT = 48;
442 + const line_wrapping_decorations = StateField.define({
443 + create() {
444 + return Decoration.none;
445 + },
446 + update(deco, tr) {
447 + const tabSize = tr.state.tabSize;
448 + const charWidth = tr.state.field(extra_cycle_character_width, false);
449 + if (charWidth == null) return Decoration.none;
450 +
451 + if (!tr.docChanged && deco !== Decoration.none) return deco;
452 +
453 + const decorations = [];
454 +
455 + for (let i = 0; i < tr.state.doc.lines; i++) {
456 + const line = tr.state.doc.line(i + 1);
457 + if (line.length === 0) continue;
458 +
459 + let indented_chars = 0;
460 + for (const ch of line.text) {
461 + if (ch === '\t') {
462 + indented_chars = indented_chars + tabSize;
463 + } else if (ch === ' ') {
464 + indented_chars = indented_chars + 1;
465 + } else {
466 + break;
467 + }
468 + }
469 +
470 + const offset = Math.min(indented_chars, ARBITRARY_INDENT_LINE_WRAP_LIMIT) * charWidth;
471 +
472 + const rules = document.createElement('span').style;
473 + rules.setProperty('--idented', `${offset}px`);
474 + rules.setProperty('text-indent', 'calc(-1 * var(--idented) - 1px)');
475 + rules.setProperty('padding-left', 'calc(var(--idented) + 2px)');
476 +
477 + const linerwapper = Decoration.line({
478 + attributes: { style: rules.cssText }
479 + });
480 +
481 + decorations.push(linerwapper.range(line.from, line.from));
482 + }
483 + return Decoration.set(decorations, true);
484 + },
485 + provide: (f) => EditorView.decorations.from(f)
486 + });
487 +
488 + return [extra_cycle_character_width, character_width_listener, line_wrapping_decorations];
489 + }
490 +
491 + function toggleLineWrapping() {
492 + const {EditorView} = window.CodeMirror;
493 + showLineWrapping = !showLineWrapping;
494 + saveEditorSetting('editor-line-wrapping', showLineWrapping);
495 +
496 + const wrapExtensions = showLineWrapping ? [EditorView.lineWrapping, ...createLineWrappingPlugin()] : [];
497 + editorView.dispatch({
498 + effects: lineWrappingCompartment.reconfigure(wrapExtensions)
499 + });
500 + }
501 +
502
503 // File operations
504 window.saveFile = function() {
539 return;
540 }
541
- const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers} = window.CodeMirror;
542 + const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers, Decoration, StateField, StateEffect} = window.CodeMirror;
543
544 // Load saved content and editor settings
545 const savedContent = loadFromStorage();
547
548 // Create compartments for dynamic extensions
549 lineNumbersCompartment = new Compartment();
550 + lineWrappingCompartment = new Compartment();
551
552 // Create CodeMirror editor
553 editorView = new EditorView({
564 {key: "Mod-o", run: () => { window.loadFile(); return true; }},
565 {key: "Mod-s", run: () => { window.saveFile(); return true; }},
566 {key: "F1", run: () => { toggleLineNumbers(); return true; }},
567 + {key: "F2", run: () => { toggleLineWrapping(); return true; }},
568 indentWithTab,
569 ...searchKeymap.filter(binding => binding.key !== "Mod-f"),
570 ...defaultKeymap
587 'autocapitalize': 'off',
588 'spellcheck': 'false'
589 }),
- lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : [])
590 + lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []),
591 + lineWrappingCompartment.of([])
592 ]
593 }),
594 parent: document.getElementById('editor')
605 handleViewportChange(); // Exit fullscreen mode if keyboard is open
606 });
607
608 + // Initialize line wrapping if it was enabled
609 + if (showLineWrapping) {
610 + const wrapExtensions = [EditorView.lineWrapping, ...createLineWrappingPlugin()];
611 + editorView.dispatch({
612 + effects: lineWrappingCompartment.reconfigure(wrapExtensions)
613 + });
614 + }
615 +
616 // Initial render
617 updatePreview();
618