implement better line wrapping (#8)

Hunter
Wednesday, August 6th, 2025 at 9:12 PM

index.html+67-4

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"
 10 +		import {EditorView, keymap, placeholder, lineNumbers, Decoration} from "https://esm.sh/@codemirror/view@6"
 11  		import {EditorState, Compartment} 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"

 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};
 20  	</script>
 21  	<style>
 22  		:root {

197  		const storageKey = 'html-lab-content';
198  		let isFullscreen = false;
199  		let showLineNumbers = false;
200 +		let enableLineWrapping = false;
201  		let lineNumbersCompartment;
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 +				enableLineWrapping = localStorage.getItem('editor-line-wrapping') === 'true';
388  			} catch (e) {
389  				console.warn('Could not load editor settings from localStorage:', e);
390  				showLineNumbers = false;
391 +				enableLineWrapping = false;
392  			}
393  		}
394  

410  			});
411  		}
412  
413 +		function createLineWrappingExtension() {
414 +			const {EditorView, Decoration} = window.CodeMirror;
415 +			
416 +			return [
417 +				EditorView.lineWrapping,
418 +				EditorView.decorations.of((view) => {
419 +					const decorations = [];
420 +					
421 +					for (let {from, to} of view.visibleRanges) {
422 +						for (let pos = from; pos <= to;) {
423 +							const line = view.state.doc.lineAt(pos);
424 +							const lineText = line.text;
425 +							
426 +							// Calculate indentation level (count leading whitespace)
427 +							let indentChars = 0;
428 +							for (let i = 0; i < lineText.length; i++) {
429 +								if (lineText[i] === '\t') {
430 +									indentChars += 2; // Convert tab to 2 spaces for calculation
431 +								} else if (lineText[i] === ' ') {
432 +									indentChars += 1;
433 +								} else {
434 +									break;
435 +								}
436 +							}
437 +							
438 +							// Apply hanging indent if line has indentation
439 +							if (indentChars > 0) {
440 +								const indentDecoration = Decoration.line({
441 +									attributes: {
442 +										style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);`
443 +									}
444 +								});
445 +								decorations.push(indentDecoration.range(line.from));
446 +							}
447 +							
448 +							pos = line.to + 1;
449 +						}
450 +					}
451 +					
452 +					return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none;
453 +				}),
454 +			];
455 +		}
456 +
457 +		function toggleLineWrapping() {
458 +			enableLineWrapping = !enableLineWrapping;
459 +			saveEditorSetting('editor-line-wrapping', enableLineWrapping);
460 +			
461 +			const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : [];
462 +			
463 +			editorView.dispatch({
464 +				effects: lineWrappingCompartment.reconfigure(lineWrappingExtension)
465 +			});
466 +		}
467  
468  		// File operations
469  		window.saveFile = function() {

504  				return;
505  			}
506  
    -			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;
507 +			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, DecorationSet} = window.CodeMirror;
508  			
509  			// Load saved content and editor settings
510  			const savedContent = loadFromStorage();

512  			
513  			// Create compartments for dynamic extensions
514  			lineNumbersCompartment = new Compartment();
515 +			lineWrappingCompartment = new Compartment();
516 +			
517 +			const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : [];
518  			
519  			// Create CodeMirror editor
520  			editorView = new EditorView({

531  							{key: "Mod-o", run: () => { window.loadFile(); return true; }},
532  							{key: "Mod-s", run: () => { window.saveFile(); return true; }},
533  							{key: "F1", run: () => { toggleLineNumbers(); return true; }},
534 +							{key: "F2", run: () => { toggleLineWrapping(); return true; }},
535  							indentWithTab, 
536  							...searchKeymap.filter(binding => binding.key !== "Mod-f"),
537  							...defaultKeymap

554  							'autocapitalize': 'off',
555  							'spellcheck': 'false'
556  						}),
    -						lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : [])
557 +						lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []),
558 +						lineWrappingCompartment.of(initialLineWrappingExtension)
559  					]
560  				}),
561  				parent: document.getElementById('editor')