first pass at line wrapping (press F2 to toggle)

Hunter
Wednesday, August 6th, 2025 at 4:24 PM

index.html+109-5

index.html
  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