From f1ff6c29d609316aeaaa9d8b7910ba70e961b067 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 26 Feb 2026 16:51:34 -0700 Subject: [PATCH 01/10] textEditor can save to a textEditorBuffers list with the W command. it can apply back to the original store ref with the A command. Crazy man --- src/cg/06-textEditor.cg | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index 9540e8f..5c0414f 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -1,10 +1,31 @@ textEditor = name \ # defaults = {}; # c = { ...defaults, ...config }; + buffersKey = "textEditorBuffers"; + + # load from staging buffers if it exists there. if not, load from source + source = getAt [buffersKey, name] + | None \ getSource name + | Some v \ v; - source = getSource name; lines = split "\n" source; + write = state \ + content = join "\n" state.lines; + { state = state, emit = [rebindAt [buffersKey, name] content] }; + + apply = state \ + content = name & " = " & (join "\n" state.lines) & ";"; + result = eval! content; + _ = debug! "apply" [content, result]; + result + | Defined _ \ { state = state, emit = [] } + | Err msg \ ( + _ = debug! "error applying" []; + { state = state, emit = [] } + ) + | _ \ { state = state, emit = [] }; + insertChar = char state \ newLines = updateAt state.cursorRow (line \ insertCharAt line state.cursorCol char @@ -146,9 +167,21 @@ textEditor = name \ | Normal \ undo state) | Key { key = "r", ctrl = True } \ (state.mode - | Insert \ insertChar "R" state + | Insert \ { state = state, emit = [] } | Normal \ redo state) + | Key { key = "W", shift = True } \ (state.mode + | Insert \ insertChar "W" state + | Normal \ write state) + + | Key { key = "W", shift = True } \ (state.mode + | Insert \ insertChar "W" state + | Normal \ write state) + + | Key { key = "A", shift = True } \ (state.mode + | Insert \ insertChar "A" state + | Normal \ apply state) + | Key { key = "Escape" } \ escape state | Key { key = "Backspace" } \ (state.mode From f54b8ca65e466293b2631c1bfbfb14ec3907eedf Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 26 Feb 2026 20:29:37 -0700 Subject: [PATCH 02/10] Better pretty printing. scrolling in the textEditor --- src/ast.ts | 3 ++- src/cg/06-textEditor.cg | 40 ++++++++++++++++++++++++++++++++-------- src/cg/06-tree.cg | 4 ++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index e862a75..8085c52 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -207,7 +207,8 @@ export function prettyPrint(ast: AST, indent = 0): string { return `(${func} ${args})` case 'let': - return `${ast.name} = ${prettyPrint(ast.value, indent + 1)};\n${i}${prettyPrint(ast.body, indent)}` + const sep = indent === 0 ? '\n\n' : '\n'; + return `${ast.name} = ${prettyPrint(ast.value, indent + 1)};${sep}${i}${prettyPrint(ast.body, indent)}` case 'list': { const elems = ast.elements.map(e => prettyPrint(e, indent + 1)) diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index 5c0414f..113b82c 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -138,6 +138,11 @@ textEditor = name \ }, update = state event \ event + | Scrolled delta \ ( + newY = max 0 (state.scrollY + delta.deltaY); + newX = max 0 (state.scrollX + delta.deltaX); + { state = state.{ scrollY = newY, scrollX = newX }, emit = [] }) + | Key { key = "ArrowDown" } \ downArrow state | Key { key = "j" } \ (state.mode | Insert \ insertChar "j" state @@ -200,27 +205,46 @@ textEditor = name \ | _ \ { state = state, emit = [] }, view = state emit \ - buffer = map (l \ text l) state.lines; - scale = 2; charH = 12; charW = 5; lineGap = 1; charGap = 2; + lineH = charH * scale + lineGap; + + cursorY = state.cursorRow * lineH; + scrollY = (cursorY < state.scrollY + | True \ cursorY + | False \ (cursorY + lineH > state.scrollY + ctx.h + | True \ cursorY + lineH - ctx.h + | False \ state.scrollY)); + + cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; + scrollX = (cursorX < state.scrollX + | True \ cursorX + | False \ (cursorX + charW * scale > state.scrollX + ctx.w + | True \ cursorX + charW * scale - ctx.w + | False \ state.scrollX)); + + buffer = map (l \ text l) state.lines; cursor = ui.positioned { - x = state.cursorCol * charW * scale + charGap * state.cursorCol, - y = state.cursorRow * charH * scale + lineGap * state.cursorRow, + x = cursorX, + y = cursorY, child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } }; + maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; + totalWidth = maxLineLen * charW * scale + maxLineLen * charGap; + totalHeight = len state.lines * lineH; + scrollable { w = ctx.w, h = ctx.h, - totalWidth = 1000, - totalHeight = 1000, - scrollX = state.scrollX, - scrollY = state.scrollY, + totalWidth = totalWidth, + totalHeight = totalHeight, + scrollX = scrollX, + scrollY = scrollY, child = ui.stack { children = [ ui.column { diff --git a/src/cg/06-tree.cg b/src/cg/06-tree.cg index 7e3121b..f563800 100644 --- a/src/cg/06-tree.cg +++ b/src/cg/06-tree.cg @@ -65,7 +65,7 @@ treeNode = config \ valueLabel = value \ value | NumberValue n \ Some (show n) - | StringValue n \ Some ("\"" & n & "\"") + | StringValue n \ Some ("\"" & r & "\"") | ConstructorValue { tag = tag } \ Some tag | FunctionValue _ \ Some "" | _ \ None; @@ -105,7 +105,7 @@ treeNode = config \ h = 30, onSubmit = onSubmit } - | False \ simple config.path (config.prefix & "\"" & n & "\"") "#f6a" (Some (_ \ config.onEditLeaf config.path))) + | False \ simple config.path (config.prefix & "\"" & (slice n 0 30) & "..\"") "#f6a" (Some (_ \ config.onEditLeaf config.path))) | ConstructorValue { tag = tag } \ (isEditing | True \ textInput { From 3267d5bc39655c2844ef630f394ed3f9792a1cf1 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 26 Feb 2026 21:03:08 -0700 Subject: [PATCH 03/10] textEditor scrolling. only rendering the lines that are in the viewport. smooth. better prettyPrinting of match inside lambda --- src/ast.ts | 9 +- src/cg/06-textEditor.cg | 282 ++++++++++++++++++++++------------------ 2 files changed, 159 insertions(+), 132 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 8085c52..028c8c8 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -252,9 +252,12 @@ export function prettyPrint(ast: AST, indent = 0): string { case 'match': const expr = prettyPrint(ast.expr, indent); const cases = ast.cases - .map(c => `${i}| ${prettyPrintPattern(c.pattern)} \\ ${prettyPrint(c.result, indent + 1)}`) - .join('\n'); - return `${expr}\n${cases}`; + .map(c => { + const result = prettyPrint(c.result, indent + 1); + const needsParens = c.result.kind === 'match' || c.result.kind === 'let'; + return `${i}| ${prettyPrintPattern(c.pattern)} \\ ${needsParens ? '(' : ''}${result}${needsParens ? ')' : ''}`; + }).join('\n'); + return `${i}${expr}\n${cases}`; case 'rebind': return `${prettyPrint(ast.target, indent)} := ${prettyPrint(ast.value, indent)}`; diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index 113b82c..7238a7e 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -85,9 +85,6 @@ textEditor = name \ newLines = updateAt state.cursorRow (_ \ before) state.lines; newLines2 = insertAt (state.cursorRow + 1) after newLines; - _ = debug! "enter" { before = before, after = after, newLines2 = newLines2, newRow = - state.cursorRow + 1 }; - { state = state.{ lines = newLines2, cursorCol = 0, cursorRow = state.cursorRow + 1 }, emit = [] }; clampCursor = state \ @@ -103,157 +100,184 @@ textEditor = name \ state.{ cursorRow = newRow2, cursorCol = newCol2 }; - upArrow = state \ ( + upArrow = state \ newState = clampCursor state.{ cursorRow = state.cursorRow - 1 }; - { state = newState, emit = [] }); + { state = newState, emit = [] }; - downArrow = state \ ( + downArrow = state \ newState = clampCursor state.{ cursorRow = state.cursorRow + 1 }; - { state = newState, emit = [] }); + { state = newState, emit = [] }; - leftArrow = state \ ( + leftArrow = state \ newState = clampCursor state.{ cursorCol = state.cursorCol - 1 }; - { state = newState, emit = [] }); + { state = newState, emit = [] }; - rightArrow = state \ ( + rightArrow = state \ newState = clampCursor state.{ cursorCol = state.cursorCol + 1 }; - { state = newState, emit = [] }); + { state = newState, emit = [] }; + + # todo: scrollHalfDown = state \ (); + # todo: scrollHalfUp = state \ (); { - view = ctx \ ui.stateful { - focusable = True, - autoFocus = True, + view = ctx \ - key = "textEditor-" & name, - - init = { - lines = lines, - cursorRow = 0, - cursorCol = 0, - scrollX = 0, - scrollY = 0, - undoStack = [], - redoStack = [], - mode = Normal # Normal | Insert | Visual - }, - - update = state event \ event - | Scrolled delta \ ( - newY = max 0 (state.scrollY + delta.deltaY); - newX = max 0 (state.scrollX + delta.deltaX); - { state = state.{ scrollY = newY, scrollX = newX }, emit = [] }) - - | Key { key = "ArrowDown" } \ downArrow state - | Key { key = "j" } \ (state.mode - | Insert \ insertChar "j" state - | Normal \ downArrow state) - - | Key { key = "ArrowUp" } \ upArrow state - | Key { key = "k" } \ (state.mode - | Insert \ insertChar "k" state - | Normal \ upArrow state) - - | Key { key = "ArrowLeft" } \ leftArrow state - | Key { key = "h" } \ (state.mode - | Insert \ insertChar "h" state - | Normal \ leftArrow state) - - | Key { key = "ArrowRight" } \ rightArrow state - | Key { key = "l" } \ (state.mode - | Insert \ insertChar "l" state - | Normal \ rightArrow state) - - | Key { key = "i" } \ (state.mode - | Insert \ insertChar "i" state - | Normal \ insertMode state) - - | Key { key = "u" } \ (state.mode - | Insert \ insertChar "u" state - | Normal \ undo state) - - | Key { key = "r", ctrl = True } \ (state.mode - | Insert \ { state = state, emit = [] } - | Normal \ redo state) - - | Key { key = "W", shift = True } \ (state.mode - | Insert \ insertChar "W" state - | Normal \ write state) - - | Key { key = "W", shift = True } \ (state.mode - | Insert \ insertChar "W" state - | Normal \ write state) - - | Key { key = "A", shift = True } \ (state.mode - | Insert \ insertChar "A" state - | Normal \ apply state) - - | Key { key = "Escape" } \ escape state - - | Key { key = "Backspace" } \ (state.mode - | Insert \ backspace state - | _ \ { state = state, emit = [] }) - - | Key { key = "Enter" } \ (state.mode - | Insert \ enter state - | _ \ { state = state, emit = [] }) - - # any other key - | Key { key = key, printable = True } \ (state.mode - | Insert \ insertChar key state - | _ \ { state = state, emit = [] }) - - | _ \ { state = state, emit = [] }, - - view = state emit \ - scale = 2; - charH = 12; - charW = 5; - lineGap = 1; - charGap = 2; - lineH = charH * scale + lineGap; + scale = 2; + charH = 12; + charW = 5; + lineGap = 1; + charGap = 2; + lineH = charH * scale + lineGap; + autoScroll = state \ cursorY = state.cursorRow * lineH; - scrollY = (cursorY < state.scrollY + newScrollY = (cursorY < state.scrollY | True \ cursorY | False \ (cursorY + lineH > state.scrollY + ctx.h | True \ cursorY + lineH - ctx.h | False \ state.scrollY)); cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; - scrollX = (cursorX < state.scrollX + newScrollX = (cursorX < state.scrollX | True \ cursorX | False \ (cursorX + charW * scale > state.scrollX + ctx.w | True \ cursorX + charW * scale - ctx.w | False \ state.scrollX)); + state.{ scrollY = newScrollY, cursorY = cursorY, scrollX = newScrollX, cursorX = cursorX }; - buffer = map (l \ text l) state.lines; + withScroll = result \ + { state = autoScroll result.state, emit = result.emit }; - cursor = ui.positioned { - x = cursorX, - y = cursorY, - child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } - }; + ui.stateful { + focusable = True, + autoFocus = True, - maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; - totalWidth = maxLineLen * charW * scale + maxLineLen * charGap; - totalHeight = len state.lines * lineH; + key = "textEditor-" & name, - scrollable { - w = ctx.w, - h = ctx.h, - totalWidth = totalWidth, - totalHeight = totalHeight, - scrollX = scrollX, - scrollY = scrollY, - child = ui.stack { - children = [ - ui.column { - gap = 1, - children = buffer - }, - cursor - ] + init = { + lines = lines, + cursorRow = 0, + cursorCol = 0, + scrollX = 0, + scrollY = 0, + undoStack = [], + redoStack = [], + mode = Normal # Normal | Insert | Visual + }, + + update = state event \ event + | Scrolled delta \ ( + maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; + totalW = maxLineLen * charW * scale + maxLineLen * charGap; + totalH = len state.lines * lineH; + newX = max 0 (min (totalW - ctx.w) (state.scrollX + delta.deltaX)); + newY = max 0 (min (totalH - ctx.h) (state.scrollY + delta.deltaY)); + { state = state.{ scrollY = newY, scrollX = newX }, emit = [] }) + + | Key { key = "ArrowDown" } \ withScroll (downArrow state) + | Key { key = "j" } \ (state.mode + | Insert \ insertChar "j" state + | Normal \ withScroll (downArrow state)) + + | Key { key = "ArrowUp" } \ withScroll (upArrow state) + | Key { key = "k" } \ (state.mode + | Insert \ insertChar "k" state + | Normal \ withScroll(upArrow state)) + + | Key { key = "ArrowLeft" } \ withScroll (leftArrow state) + | Key { key = "h" } \ (state.mode + | Insert \ insertChar "h" state + | Normal \ withScroll(leftArrow state)) + + | Key { key = "ArrowRight" } \ withScroll (rightArrow state) + | Key { key = "l" } \ (state.mode + | Insert \ insertChar "l" state + | Normal \ withScroll(rightArrow state)) + + | Key { key = "i" } \ (state.mode + | Insert \ insertChar "i" state + | Normal \ insertMode state) + + | Key { key = "u" } \ (state.mode + | Insert \ insertChar "u" state + | Normal \ undo state) + + | Key { key = "r", ctrl = True } \ (state.mode + | Insert \ { state = state, emit = [] } + | Normal \ redo state) + + | Key { key = "W", shift = True } \ (state.mode + | Insert \ insertChar "W" state + | Normal \ write state) + + | Key { key = "W", ctrl = True, shift = True } \ (state.mode + | Insert \ insertChar "W" state + | Normal \ write state) + + | Key { key = "A", ctrl = True, shift = True } \ (state.mode + | Insert \ insertChar "A" state + | Normal \ apply state) + + | Key { key = "d", ctrl = True } \ scrollHalfDown state + | Key { key = "u", ctrl = True } \ scrollHalfUp state + + | Key { key = "Escape" } \ escape state + + | Key { key = "Backspace" } \ (state.mode + | Insert \ backspace state + | _ \ { state = state, emit = [] }) + + | Key { key = "Enter" } \ (state.mode + | Insert \ enter state + | _ \ { state = state, emit = [] }) + + # any other key + | Key { key = key, printable = True } \ (state.mode + | Insert \ insertChar key state + | _ \ { state = state, emit = [] }) + + | _ \ { state = state, emit = [] }, + + view = state emit \ + _ = debug! "here" state; + cursor = ui.positioned { + x = state.cursorX, + y = state.cursorY, + child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } + }; + + maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; + totalWidth = maxLineLen * charW * scale + maxLineLen * charGap; + totalHeight = len state.lines * lineH; + + # buffer = map (l \ text l) state.lines; + # only render visible lines + startLine = max 0 (floor (state.scrollY / lineH)); + visibleCount = floor (ctx.h / lineH) + 2; + endLine = min (len state.lines) (startLine + visibleCount); + visibleLines = slice state.lines startLine endLine; + buffer = mapWithIndex (l i \ + ui.positioned { + x = 0, + y = (startLine + i) * lineH, + child = text l + } + ) visibleLines; + + scrollable { + w = ctx.w, + h = ctx.h, + totalWidth = totalWidth, + totalHeight = totalHeight, + scrollX = state.scrollX, + scrollY = state.scrollY, + onScroll = delta \ emit (Scrolled delta), + child = ui.stack { + children = [ + ...buffer, + cursor + ] + } } - } - } + } }; From 15f6124173e93b73617a4274716fd514075152e4 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 26 Feb 2026 21:21:49 -0700 Subject: [PATCH 04/10] Ability to delete from store --- src/runtime-compiled.ts | 24 ++++++++++++++++++++++++ src/runtime-js.ts | 3 +++ 2 files changed, 27 insertions(+) diff --git a/src/runtime-compiled.ts b/src/runtime-compiled.ts index a06a6bf..7426bb4 100644 --- a/src/runtime-compiled.ts +++ b/src/runtime-compiled.ts @@ -1,4 +1,6 @@ import { render, hitTest, scrollHitTest } from './ui'; +import { syncToAst, saveDefinitions } from './runtime-js'; +import { definitions } from './compiler'; type UIValue = any; @@ -217,6 +219,28 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) { return; } + if (event._tag === 'DeleteAt') { + const path = event._0; + const name = path[0]; + if (path.length === 1) { + delete store[name]; + definitions.delete(name); + saveDefinitions(); + } else { + let obj = store[name]; + for (let i = 1; i < path.length - 1; i++) { + if (obj === undefined || obj === null) return; + obj = obj[path[i]]; + } + if (obj !== undefined && obj !== null) { + delete obj[path[path.length - 1]]; + } + } + syncToAst(name); + + return; + } + if (event._tag === 'Focus') { setFocus(event._0); return; diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 2734775..ec3ce21 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -160,6 +160,9 @@ export const _rt = { return { _tag: 'Rebind', _0: name, _1: rest, _2: value }; }, + deleteAt: (path: any[]) => { + return { _tag: 'DeleteAt', _0: path }; + }, "undefine!": (name: string) => { delete store[name]; definitions.delete(name); From 4ba3eda57aa6fc4230695e7da7aed5f9d094e104 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 26 Feb 2026 21:39:11 -0700 Subject: [PATCH 05/10] More pretty printer stuff. fixing infix. fixing indents --- src/ast.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 028c8c8..5b4ab12 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -173,6 +173,11 @@ export type AST = | Definition | Rebind +const infixOps: { [key: string]: string } = { + cat: '&', add: '+', sub: '-', mul: '*', div: '/', mod: '%', + pow: '^', eq: '==', neq: '!=', gt: '>', lt: '<', gte: '>=', lte: '<=' +}; + export function prettyPrint(ast: AST, indent = 0): string { const i = ' '.repeat(indent); @@ -195,6 +200,13 @@ export function prettyPrint(ast: AST, indent = 0): string { return ast.name; case 'apply': + // infix ops + if (ast.func.kind === 'variable' && ast.args.length === 2 && infixOps[ast.func.name]) { + const left = prettyPrint(ast.args[0], indent); + const right = prettyPrint(ast.args[1], indent); + return `${left} ${infixOps[ast.func.name]} ${right}`; + } + const func = prettyPrint(ast.func, indent); const args = ast.args.map(a => { const printed = prettyPrint(a, indent); @@ -204,6 +216,9 @@ export function prettyPrint(ast: AST, indent = 0): string { return printed; }).join(' '); + if (ast.func.kind === 'variable' || ast.func.kind === 'constructor') { + return `${func} ${args}` + } return `(${func} ${args})` case 'let': @@ -212,7 +227,8 @@ export function prettyPrint(ast: AST, indent = 0): string { case 'list': { const elems = ast.elements.map(e => prettyPrint(e, indent + 1)) - if (elems.length <= 1) return `[${elems.join(', ')}]`; + const oneLine = `[${elems.join(', ')}]`; + if (oneLine.length <= 60 || elems.length <= 1) return oneLine; const inner = elems.map(e => `${' '.repeat(indent + 1)}${e}`).join(',\n'); return `[\n${inner}\n${i}]`; } @@ -232,8 +248,8 @@ export function prettyPrint(ast: AST, indent = 0): string { const body = prettyPrint(ast.body, indent + 1); const isComplex = ast.body.kind === 'match' || ast.body.kind === 'let'; if (isComplex) { - return `${params} \\\n${body}` - + const ii = ' '.repeat(indent + 1); + return `${params} \\\n${ii}${body}` } return `${params} \\ ${body}` } @@ -257,7 +273,7 @@ export function prettyPrint(ast: AST, indent = 0): string { const needsParens = c.result.kind === 'match' || c.result.kind === 'let'; return `${i}| ${prettyPrintPattern(c.pattern)} \\ ${needsParens ? '(' : ''}${result}${needsParens ? ')' : ''}`; }).join('\n'); - return `${i}${expr}\n${cases}`; + return `${expr}\n${cases}`; case 'rebind': return `${prettyPrint(ast.target, indent)} := ${prettyPrint(ast.value, indent)}`; From 4904b7df7f0793654b0bc04c350cd73368db0d0f Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 26 Feb 2026 21:53:54 -0700 Subject: [PATCH 06/10] ctrl-d and ctrl-u for half scrolling --- src/cg/06-textEditor.cg | 43 +++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index 7238a7e..215b632 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -1,6 +1,13 @@ textEditor = name \ # defaults = {}; # c = { ...defaults, ...config }; + scale = 2; + charH = 12; + charW = 5; + lineGap = 1; + charGap = 2; + lineH = charH * scale + lineGap; + buffersKey = "textEditorBuffers"; # load from staging buffers if it exists there. if not, load from source @@ -116,19 +123,9 @@ textEditor = name \ newState = clampCursor state.{ cursorCol = state.cursorCol + 1 }; { state = newState, emit = [] }; - # todo: scrollHalfDown = state \ (); - # todo: scrollHalfUp = state \ (); - { view = ctx \ - scale = 2; - charH = 12; - charW = 5; - lineGap = 1; - charGap = 2; - lineH = charH * scale + lineGap; - autoScroll = state \ cursorY = state.cursorRow * lineH; newScrollY = (cursorY < state.scrollY @@ -143,11 +140,21 @@ textEditor = name \ | False \ (cursorX + charW * scale > state.scrollX + ctx.w | True \ cursorX + charW * scale - ctx.w | False \ state.scrollX)); - state.{ scrollY = newScrollY, cursorY = cursorY, scrollX = newScrollX, cursorX = cursorX }; + state.{ scrollY = newScrollY, scrollX = newScrollX }; withScroll = result \ { state = autoScroll result.state, emit = result.emit }; + scrollHalfUp = state ctx \ ( + diff = floor ((ctx.h / 2) / lineH); + newRow = state.cursorRow - diff; + withScroll { state = clampCursor state.{ cursorRow = newRow }, emit = [] }); + + scrollHalfDown = state ctx \ ( + diff = floor ((ctx.h / 2) / lineH); + newRow = state.cursorRow + diff; + withScroll { state = clampCursor state.{ cursorRow = newRow }, emit = [] }); + ui.stateful { focusable = True, autoFocus = True, @@ -198,6 +205,9 @@ textEditor = name \ | Insert \ insertChar "i" state | Normal \ insertMode state) + | Key { key = "d", ctrl = True } \ scrollHalfDown state ctx + | Key { key = "u", ctrl = True } \ scrollHalfUp state ctx + | Key { key = "u" } \ (state.mode | Insert \ insertChar "u" state | Normal \ undo state) @@ -218,9 +228,6 @@ textEditor = name \ | Insert \ insertChar "A" state | Normal \ apply state) - | Key { key = "d", ctrl = True } \ scrollHalfDown state - | Key { key = "u", ctrl = True } \ scrollHalfUp state - | Key { key = "Escape" } \ escape state | Key { key = "Backspace" } \ (state.mode @@ -239,10 +246,12 @@ textEditor = name \ | _ \ { state = state, emit = [] }, view = state emit \ - _ = debug! "here" state; + cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; + cursorY = state.cursorRow * lineH; + cursor = ui.positioned { - x = state.cursorX, - y = state.cursorY, + x = cursorX, + y = cursorY, child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } }; From 295a8a5e24242ef768e851d0f420adcf9715ec99 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Fri, 27 Feb 2026 21:40:58 -0700 Subject: [PATCH 07/10] More pretty printer --- src/ast.ts | 26 ++++++++++++++++++-------- src/cg/03-ui-components.cg | 4 ++-- src/cg/06-textEditor.cg | 1 + 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 5b4ab12..36cb678 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -178,6 +178,8 @@ const infixOps: { [key: string]: string } = { pow: '^', eq: '==', neq: '!=', gt: '>', lt: '<', gte: '>=', lte: '<=' }; +const isInfix = (a: AST) => a.kind === 'apply' && a.func.kind === 'variable' && a.args.length === 2 && infixOps[a.func.name]; + export function prettyPrint(ast: AST, indent = 0): string { const i = ' '.repeat(indent); @@ -201,25 +203,33 @@ export function prettyPrint(ast: AST, indent = 0): string { case 'apply': // infix ops - if (ast.func.kind === 'variable' && ast.args.length === 2 && infixOps[ast.func.name]) { - const left = prettyPrint(ast.args[0], indent); - const right = prettyPrint(ast.args[1], indent); - return `${left} ${infixOps[ast.func.name]} ${right}`; + if (isInfix(ast)) { + const wrapIfNeeded = (a: AST) => { + const printed = prettyPrint(a, indent); + if (a.kind === 'apply' || a.kind === 'lambda' || a.kind === 'match' || a.kind === 'let') { + return `(${printed})`; + } + return `${printed}`; + + } + const left = wrapIfNeeded(ast.args[0]); + const right = wrapIfNeeded(ast.args[1]); + return `${left} ${infixOps[(ast.func as Variable).name]} ${right}`; } const func = prettyPrint(ast.func, indent); const args = ast.args.map(a => { const printed = prettyPrint(a, indent); - if (a.kind === 'lambda' || a.kind === 'match' || a.kind === 'let' || a.kind === 'rebind') { + if (a.kind === 'lambda' || a.kind === 'match' || a.kind === 'let' || a.kind === 'rebind' || a.kind === 'apply') { return `(${printed})`; } return printed; }).join(' '); - if (ast.func.kind === 'variable' || ast.func.kind === 'constructor') { - return `${func} ${args}` + if (ast.func.kind === 'lambda' || ast.func.kind === 'match' || ast.func.kind === 'let') { + return `(${func} ${args})` } - return `(${func} ${args})` + return `${func} ${args}` case 'let': const sep = indent === 0 ? '\n\n' : '\n'; diff --git a/src/cg/03-ui-components.cg b/src/cg/03-ui-components.cg index 5ca8f0e..d91ce72 100644 --- a/src/cg/03-ui-components.cg +++ b/src/cg/03-ui-components.cg @@ -134,8 +134,8 @@ scrollable = config \ | False \ []), ...(showHBar | True \ [ui.positioned { - x = c.h - 4, - y = hBarX, + x = hBarX, + y = c.h - 4, child = ui.rect { h = 4, w = hBarWidth, color = "rgba(255,255,255,0.3)", radius = 2 } }] | False \ []) diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index 215b632..c626821 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -1,3 +1,4 @@ +textEditorBuffers = []; textEditor = name \ # defaults = {}; # c = { ...defaults, ...config }; From 7a97ec3f90d7b08e047b1bb72c012acf83e37b01 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Wed, 4 Mar 2026 17:56:53 -0700 Subject: [PATCH 08/10] texteditor. 'o' key --- src/cg/01-stdlib.cg | 10 ++++++++++ src/cg/06-textEditor.cg | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/cg/01-stdlib.cg b/src/cg/01-stdlib.cg index cfb63bf..b05de06 100644 --- a/src/cg/01-stdlib.cg +++ b/src/cg/01-stdlib.cg @@ -77,6 +77,16 @@ join = s list \ list | [x] \ x | [x, ...xs] \ (x & s & (join s xs)); +# repeatStr : Int \ String \ String +repeatStr = n str \ n + | 0 \ "" + | m \ str & (repeatStr (m - 1) str); + +# repeat : Int \ a \ List a +repeat = n a \ n + | 0 \ [] + | m \ [a, ...repeat (m - 1) a]; + # zipWith : (a \ b \ c) \ List a \ List b \ List c zipWith = f l1 l2 \ l1 | [] \ [] diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index c626821..81de2ca 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -52,6 +52,22 @@ textEditor = name \ emit = [] }; + # 'o' key + openLine = state \ + stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; + newUndoStack = [stateSnapshot, ...state.undoStack]; + cursorRow = state.cursorRow + 1; + newLines = insertAt cursorRow "" state.lines; + { + state = state.{ + lines = newLines, + cursorRow = cursorRow, + undoStack = newUndoStack, + mode = Insert + }, + emit = [] + }; + escape = state \ { state = state.{ mode = Normal }, emit = [] }; undo = state \ state.undoStack @@ -206,6 +222,10 @@ textEditor = name \ | Insert \ insertChar "i" state | Normal \ insertMode state) + | Key { key = "o" } \ (state.mode + | Insert \ insertChar "o" state + | Normal \ openLine state) + | Key { key = "d", ctrl = True } \ scrollHalfDown state ctx | Key { key = "u", ctrl = True } \ scrollHalfUp state ctx From fcce2ac4c05b671180a6bbfce49b82bfd022d295 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Thu, 12 Mar 2026 21:04:52 -0600 Subject: [PATCH 09/10] refactoring key events so all Insert mode are together... may regret that later but its cleaner now. Getting delete line working --- src/cg/06-textEditor.cg | 155 +++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 89 deletions(-) diff --git a/src/cg/06-textEditor.cg b/src/cg/06-textEditor.cg index 81de2ca..d503538 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -18,6 +18,19 @@ textEditor = name \ lines = split "\n" source; + clampCursor = state \ + line = nth state.cursorRow state.lines ~ unwrapOr ""; + + newRow = max 0 state.cursorRow; + newRow2 = min (len state.lines - 1) newRow; + + maxCol = state.mode + | Insert \ len line + | Normal \ max 0 (len line - 1); + newCol2 = min maxCol (max 0 state.cursorCol); + + state.{ cursorRow = newRow2, cursorCol = newCol2 }; + write = state \ content = join "\n" state.lines; { state = state, emit = [rebindAt [buffersKey, name] content] }; @@ -62,12 +75,26 @@ textEditor = name \ state = state.{ lines = newLines, cursorRow = cursorRow, + cursorCol = 0, undoStack = newUndoStack, mode = Insert }, emit = [] }; + deleteLine = state \ + stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; + newUndoStack = [stateSnapshot, ...state.undoStack]; + newLines = [...(take (state.cursorRow) state.lines), ...(drop (state.cursorRow + 1) state.lines)]; + { + state = clampCursor state.{ + lines = newLines, + undoStack = newUndoStack, + pending = None + }, + emit = [] + }; + escape = state \ { state = state.{ mode = Normal }, emit = [] }; undo = state \ state.undoStack @@ -111,19 +138,6 @@ textEditor = name \ { state = state.{ lines = newLines2, cursorCol = 0, cursorRow = state.cursorRow + 1 }, emit = [] }; - clampCursor = state \ - line = nth state.cursorRow state.lines ~ unwrapOr ""; - - newRow = max 0 state.cursorRow; - newRow2 = min (len state.lines - 1) newRow; - - maxCol = state.mode - | Insert \ len line - | Normal \ max 0 (len line - 1); - newCol2 = min maxCol (max 0 state.cursorCol); - - state.{ cursorRow = newRow2, cursorCol = newCol2 }; - upArrow = state \ newState = clampCursor state.{ cursorRow = state.cursorRow - 1 }; { state = newState, emit = [] }; @@ -186,85 +200,48 @@ textEditor = name \ scrollY = 0, undoStack = [], redoStack = [], - mode = Normal # Normal | Insert | Visual + mode = Normal, # Normal | Insert | Visual + pending = None # Some "d" | Some "g" | etc. }, - update = state event \ event - | Scrolled delta \ ( - maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; - totalW = maxLineLen * charW * scale + maxLineLen * charGap; - totalH = len state.lines * lineH; - newX = max 0 (min (totalW - ctx.w) (state.scrollX + delta.deltaX)); - newY = max 0 (min (totalH - ctx.h) (state.scrollY + delta.deltaY)); - { state = state.{ scrollY = newY, scrollX = newX }, emit = [] }) + update = state event \ state.mode + | Insert \ (event + | Key { key = "Escape" } \ escape state + | Key { key = "Control" } \ escape state + | Key { key = "Backspace" } \ backspace state + | Key { key = "Enter" } \ enter state + | Key { key = k } \ insertChar k state) + | Normal \ (event + | Scrolled delta \ ( + maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; + totalW = maxLineLen * charW * scale + maxLineLen * charGap; + totalH = len state.lines * lineH; + newX = max 0 (min (totalW - ctx.w) (state.scrollX + delta.deltaX)); + newY = max 0 (min (totalH - ctx.h) (state.scrollY + delta.deltaY)); + { state = state.{ scrollY = newY, scrollX = newX }, emit = [] }) - | Key { key = "ArrowDown" } \ withScroll (downArrow state) - | Key { key = "j" } \ (state.mode - | Insert \ insertChar "j" state - | Normal \ withScroll (downArrow state)) - - | Key { key = "ArrowUp" } \ withScroll (upArrow state) - | Key { key = "k" } \ (state.mode - | Insert \ insertChar "k" state - | Normal \ withScroll(upArrow state)) - - | Key { key = "ArrowLeft" } \ withScroll (leftArrow state) - | Key { key = "h" } \ (state.mode - | Insert \ insertChar "h" state - | Normal \ withScroll(leftArrow state)) - - | Key { key = "ArrowRight" } \ withScroll (rightArrow state) - | Key { key = "l" } \ (state.mode - | Insert \ insertChar "l" state - | Normal \ withScroll(rightArrow state)) - - | Key { key = "i" } \ (state.mode - | Insert \ insertChar "i" state - | Normal \ insertMode state) - - | Key { key = "o" } \ (state.mode - | Insert \ insertChar "o" state - | Normal \ openLine state) - - | Key { key = "d", ctrl = True } \ scrollHalfDown state ctx - | Key { key = "u", ctrl = True } \ scrollHalfUp state ctx - - | Key { key = "u" } \ (state.mode - | Insert \ insertChar "u" state - | Normal \ undo state) - - | Key { key = "r", ctrl = True } \ (state.mode - | Insert \ { state = state, emit = [] } - | Normal \ redo state) - - | Key { key = "W", shift = True } \ (state.mode - | Insert \ insertChar "W" state - | Normal \ write state) - - | Key { key = "W", ctrl = True, shift = True } \ (state.mode - | Insert \ insertChar "W" state - | Normal \ write state) - - | Key { key = "A", ctrl = True, shift = True } \ (state.mode - | Insert \ insertChar "A" state - | Normal \ apply state) - - | Key { key = "Escape" } \ escape state - - | Key { key = "Backspace" } \ (state.mode - | Insert \ backspace state - | _ \ { state = state, emit = [] }) - - | Key { key = "Enter" } \ (state.mode - | Insert \ enter state - | _ \ { state = state, emit = [] }) - - # any other key - | Key { key = key, printable = True } \ (state.mode - | Insert \ insertChar key state - | _ \ { state = state, emit = [] }) - - | _ \ { state = state, emit = [] }, + | Key { key = "ArrowDown" } \ withScroll (downArrow state) + | Key { key = "j" } \ withScroll (downArrow state) + | Key { key = "ArrowUp" } \ withScroll (upArrow state) + | Key { key = "k" } \ withScroll(upArrow state) + | Key { key = "ArrowLeft" } \ withScroll (leftArrow state) + | Key { key = "h" } \ withScroll(leftArrow state) + | Key { key = "ArrowRight" } \ withScroll (rightArrow state) + | Key { key = "l" } \ withScroll(rightArrow state) + | Key { key = "i" } \ insertMode state + | Key { key = "o" } \ openLine state + | Key { key = "d", ctrl = True } \ scrollHalfDown state ctx + | Key { key = "u", ctrl = True } \ scrollHalfUp state ctx + | Key { key = "u" } \ undo state + | Key { key = "d" } \ (state.pending + | Some "d" \ deleteLine state + | _ \ { state = state.{ pending = Some "d" }, emit = [] }) + | Key { key = "r", ctrl = True } \ redo state + | Key { key = "W", ctrl = True, shift = True } \ write state + | Key { key = "A", ctrl = True, shift = True } \ apply state + # any other key or event + | _ \ { state = state, emit = [] } + ), view = state emit \ cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; From 78fa27f70a25f680659700992840e3caaf6ed5c0 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sun, 15 Mar 2026 20:08:34 -0600 Subject: [PATCH 10/10] isWordChar --- src/runtime-js.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runtime-js.ts b/src/runtime-js.ts index ec3ce21..db04c8a 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -80,6 +80,7 @@ export const _rt = { return String(value); }, chars: (s: string) => s.split(''), + isWordChar: (s: string) => ({ _tag: /^\w$/.test(s) ? 'True' : 'False' }), // join: (delim: string) => (xs: string[]) => xs.join(delim), split: (delim: string) => (xs: string) => xs.split(delim), slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end),