From 6e62a1285ca5ad1ce35d069e4cf5ca4968c8dd95 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Jan 2026 14:21:54 +0000 Subject: [PATCH] Improve textarea with real tabs and smart editing features - Use real tabs instead of 4 spaces for indentation - Add Shift+Tab to dedent lines/selections - Add auto-indent on Enter (preserves indentation, adds extra after {/(/[) - Add block indent/dedent when text is selected - Add auto-close for brackets and quotes with smart handling - Skip over closing chars when already present - Delete matching pairs with backspace - Update all examples to use tabs --- playground.html | 287 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 213 insertions(+), 74 deletions(-) diff --git a/playground.html b/playground.html index 729cc8d..664c71b 100644 --- a/playground.html +++ b/playground.html @@ -720,8 +720,8 @@ hello: [ '// Hello World in Hemlock', 'fn main() {', - ' print("Hello, World!");', - ' print("Welcome to Hemlock!");', + '\tprint("Hello, World!");', + '\tprint("Welcome to Hemlock!");', '}', '', 'main();' @@ -730,17 +730,17 @@ fibonacci: [ '// Fibonacci sequence', 'fn fib(n) {', - ' if (n <= 1) {', - ' return n;', - ' }', - ' return fib(n - 1) + fib(n - 2);', + '\tif (n <= 1) {', + '\t\treturn n;', + '\t}', + '\treturn fib(n - 1) + fib(n - 2);', '}', '', 'fn main() {', - ' print("Fibonacci sequence:");', - ' for (let i = 0; i < 15; i = i + 1) {', - ' print("fib(" + i + ") = " + fib(i));', - ' }', + '\tprint("Fibonacci sequence:");', + '\tfor (let i = 0; i < 15; i = i + 1) {', + '\t\tprint("fib(" + i + ") = " + fib(i));', + '\t}', '}', '', 'main();' @@ -749,21 +749,21 @@ fizzbuzz: [ '// FizzBuzz', 'fn fizzbuzz(n) {', - ' for (let i = 1; i <= n; i = i + 1) {', - ' if (i % 15 == 0) {', - ' print("FizzBuzz");', - ' } else if (i % 3 == 0) {', - ' print("Fizz");', - ' } else if (i % 5 == 0) {', - ' print("Buzz");', - ' } else {', - ' print(i);', - ' }', - ' }', + '\tfor (let i = 1; i <= n; i = i + 1) {', + '\t\tif (i % 15 == 0) {', + '\t\t\tprint("FizzBuzz");', + '\t\t} else if (i % 3 == 0) {', + '\t\t\tprint("Fizz");', + '\t\t} else if (i % 5 == 0) {', + '\t\t\tprint("Buzz");', + '\t\t} else {', + '\t\t\tprint(i);', + '\t\t}', + '\t}', '}', '', 'fn main() {', - ' fizzbuzz(30);', + '\tfizzbuzz(30);', '}', '', 'main();' @@ -774,24 +774,24 @@ 'import { sleep } from "@stdlib/time";', '', 'async fn fetch_data(id) {', - ' print("Fetching data " + id + "...");', - ' sleep(0.5); // seconds in hemlock!', - ' return "Data from " + id;', + '\tprint("Fetching data " + id + "...");', + '\tsleep(0.5); // seconds in hemlock!', + '\treturn "Data from " + id;', '}', '', 'async fn main() {', - ' let tasks = [', - ' spawn(fetch_data, "A"),', - ' spawn(fetch_data, "B"),', - ' spawn(fetch_data, "C")', - ' ];', + '\tlet tasks = [', + '\t\tspawn(fetch_data, "A"),', + '\t\tspawn(fetch_data, "B"),', + '\t\tspawn(fetch_data, "C")', + '\t];', '', - ' for (task in tasks) {', - ' let result = await task;', - ' print("Got: " + result);', - ' }', + '\tfor (task in tasks) {', + '\t\tlet result = await task;', + '\t\tprint("Got: " + result);', + '\t}', '', - ' print("All done!");', + '\tprint("All done!");', '}', '', 'main();' @@ -802,24 +802,24 @@ 'import { parse, stringify } from "@stdlib/json";', '', 'fn main() {', - ' let data = {', - ' name: "Hemlock",', - ' version: "1.6.4",', - ' features: ["fast", "safe", "fun"],', - ' config: {', - ' debug: true,', - ' threads: 4', - ' }', - ' };', + '\tlet data = {', + '\t\tname: "Hemlock",', + '\t\tversion: "1.6.4",', + '\t\tfeatures: ["fast", "safe", "fun"],', + '\t\tconfig: {', + '\t\t\tdebug: true,', + '\t\t\tthreads: 4', + '\t\t}', + '\t};', '', - ' let json_str = stringify(data);', - ' print("Serialized:");', - ' print(json_str);', - ' print("");', + '\tlet json_str = stringify(data);', + '\tprint("Serialized:");', + '\tprint(json_str);', + '\tprint("");', '', - ' let parsed = parse(json_str);', - ' print("Parsed name: " + parsed["name"]);', - ' print("Features: " + parsed["features"]);', + '\tlet parsed = parse(json_str);', + '\tprint("Parsed name: " + parsed["name"]);', + '\tprint("Features: " + parsed["features"]);', '}', '', 'main();' @@ -830,24 +830,24 @@ '// Expand the "Stdin Input" section below to provide input!', '', 'fn main() {', - ' print("What is your name?");', - ' let name = read_line();', - ' ', - ' if (name == null) {', - ' print("No input provided!");', - ' return;', - ' }', - ' ', - ' print("Hello, " + name + "!");', - ' print("");', - ' ', - ' print("How old are you?");', - ' let age_str = read_line();', - ' ', - ' if (age_str != null) {', - ' let age = i32(age_str);', - ' print("In 10 years you will be " + (age + 10) + "!");', - ' }', + '\tprint("What is your name?");', + '\tlet name = read_line();', + '\t', + '\tif (name == null) {', + '\t\tprint("No input provided!");', + '\t\treturn;', + '\t}', + '\t', + '\tprint("Hello, " + name + "!");', + '\tprint("");', + '\t', + '\tprint("How old are you?");', + '\tlet age_str = read_line();', + '\t', + '\tif (age_str != null) {', + '\t\tlet age = i32(age_str);', + '\t\tprint("In 10 years you will be " + (age + 10) + "!");', + '\t}', '}', '', 'main();' @@ -1063,14 +1063,153 @@ editor.addEventListener('input', updateHighlight); editor.addEventListener('scroll', syncScroll); - // Tab handling + // Enhanced keyboard handling editor.addEventListener('keydown', (e) => { + const start = editor.selectionStart; + const end = editor.selectionEnd; + const value = editor.value; + const hasSelection = start !== end; + + // Tab / Shift+Tab handling if (e.key === 'Tab') { e.preventDefault(); - const start = editor.selectionStart; - const end = editor.selectionEnd; - editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end); - editor.selectionStart = editor.selectionEnd = start + 4; + + if (hasSelection) { + // Block indent/dedent for multi-line selections + const lineStart = value.lastIndexOf('\n', start - 1) + 1; + const lineEnd = value.indexOf('\n', end); + const endPos = lineEnd === -1 ? value.length : lineEnd; + const selectedLines = value.substring(lineStart, endPos); + const lines = selectedLines.split('\n'); + + let newLines; + let deltaFirst = 0; + let deltaTotal = 0; + + if (e.shiftKey) { + // Dedent: remove leading tab or up to 4 spaces + newLines = lines.map((line, i) => { + if (line.startsWith('\t')) { + if (i === 0) deltaFirst = -1; + deltaTotal -= 1; + return line.substring(1); + } else if (line.match(/^ {1,4}/)) { + const spaces = line.match(/^ {1,4}/)[0].length; + if (i === 0) deltaFirst = -spaces; + deltaTotal -= spaces; + return line.substring(spaces); + } + return line; + }); + } else { + // Indent: add tab to each line + newLines = lines.map(line => '\t' + line); + deltaFirst = 1; + deltaTotal = lines.length; + } + + const newText = newLines.join('\n'); + editor.value = value.substring(0, lineStart) + newText + value.substring(endPos); + editor.selectionStart = start + deltaFirst; + editor.selectionEnd = end + deltaTotal; + } else { + if (e.shiftKey) { + // Dedent current line + const lineStart = value.lastIndexOf('\n', start - 1) + 1; + const beforeCursor = value.substring(lineStart, start); + + if (beforeCursor.startsWith('\t')) { + editor.value = value.substring(0, lineStart) + value.substring(lineStart + 1); + editor.selectionStart = editor.selectionEnd = start - 1; + } else if (beforeCursor.match(/^ {1,4}/)) { + const spaces = beforeCursor.match(/^ {1,4}/)[0].length; + editor.value = value.substring(0, lineStart) + value.substring(lineStart + spaces); + editor.selectionStart = editor.selectionEnd = start - spaces; + } + } else { + // Insert real tab + editor.value = value.substring(0, start) + '\t' + value.substring(end); + editor.selectionStart = editor.selectionEnd = start + 1; + } + } + updateHighlight(); + return; + } + + // Enter: auto-indent + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + const lineStart = value.lastIndexOf('\n', start - 1) + 1; + const currentLine = value.substring(lineStart, start); + const indent = currentLine.match(/^[\t ]*/)[0]; + + // Add extra indent after { or ( + const trimmed = currentLine.trimEnd(); + let extraIndent = ''; + if (trimmed.endsWith('{') || trimmed.endsWith('(') || trimmed.endsWith('[')) { + extraIndent = '\t'; + } + + editor.value = value.substring(0, start) + '\n' + indent + extraIndent + value.substring(end); + editor.selectionStart = editor.selectionEnd = start + 1 + indent.length + extraIndent.length; + updateHighlight(); + return; + } + + // Auto-close brackets and quotes + const pairs = { + '(': ')', + '[': ']', + '{': '}', + '"': '"', + "'": "'", + '`': '`' + }; + + if (pairs[e.key] && !hasSelection) { + const nextChar = value[start]; + const prevChar = value[start - 1]; + + // For quotes, only auto-close if not escaping and not inside a word + if (e.key === '"' || e.key === "'" || e.key === '`') { + // Skip if the next char is the same quote (user is closing) + if (nextChar === e.key) { + e.preventDefault(); + editor.selectionStart = editor.selectionEnd = start + 1; + return; + } + // Don't auto-close if preceded by backslash or alphanumeric + if (prevChar === '\\' || /\w/.test(prevChar)) { + return; + } + } + + e.preventDefault(); + editor.value = value.substring(0, start) + e.key + pairs[e.key] + value.substring(end); + editor.selectionStart = editor.selectionEnd = start + 1; + updateHighlight(); + return; + } + + // Skip over closing brackets/quotes if already there + const closers = [')', ']', '}', '"', "'", '`']; + if (closers.includes(e.key) && value[start] === e.key) { + e.preventDefault(); + editor.selectionStart = editor.selectionEnd = start + 1; + return; + } + + // Backspace: delete matching pair if empty + if (e.key === 'Backspace' && !hasSelection) { + const prevChar = value[start - 1]; + const nextChar = value[start]; + if (pairs[prevChar] && pairs[prevChar] === nextChar) { + e.preventDefault(); + editor.value = value.substring(0, start - 1) + value.substring(start + 1); + editor.selectionStart = editor.selectionEnd = start - 1; + updateHighlight(); + return; + } } });