diff --git a/src/ast.ts b/src/ast.ts index e862a75..36cb678 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -173,6 +173,13 @@ export type AST = | Definition | Rebind +const infixOps: { [key: string]: string } = { + cat: '&', add: '+', sub: '-', mul: '*', div: '/', mod: '%', + 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); @@ -195,23 +202,43 @@ export function prettyPrint(ast: AST, indent = 0): string { return ast.name; case 'apply': + // infix ops + 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(' '); - return `(${func} ${args})` + if (ast.func.kind === 'lambda' || ast.func.kind === 'match' || ast.func.kind === 'let') { + return `(${func} ${args})` + } + 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)) - 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}]`; } @@ -231,8 +258,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}` } @@ -251,8 +278,11 @@ 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'); + .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 `${expr}\n${cases}`; case 'rebind': 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/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 9540e8f..d503538 100644 --- a/src/cg/06-textEditor.cg +++ b/src/cg/06-textEditor.cg @@ -1,10 +1,52 @@ +textEditorBuffers = []; 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 + source = getAt [buffersKey, name] + | None \ getSource name + | Some v \ v; - source = getSource 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] }; + + 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 @@ -23,6 +65,36 @@ 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, + 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 @@ -64,139 +136,155 @@ 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 \ - 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 \ ( + 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 = [] }; { - view = ctx \ ui.stateful { - focusable = True, - autoFocus = True, + view = ctx \ - key = "textEditor-" & name, + autoScroll = state \ + cursorY = state.cursorRow * lineH; + newScrollY = (cursorY < state.scrollY + | True \ cursorY + | False \ (cursorY + lineH > state.scrollY + ctx.h + | True \ cursorY + lineH - ctx.h + | False \ state.scrollY)); - init = { - lines = lines, - cursorRow = 0, - cursorCol = 0, - scrollX = 0, - scrollY = 0, - undoStack = [], - redoStack = [], - mode = Normal # Normal | Insert | Visual - }, + cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; + 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, scrollX = newScrollX }; - update = state event \ event - | Key { key = "ArrowDown" } \ downArrow state - | Key { key = "j" } \ (state.mode - | Insert \ insertChar "j" state - | Normal \ downArrow state) + withScroll = result \ + { state = autoScroll result.state, emit = result.emit }; - | Key { key = "ArrowUp" } \ upArrow state - | Key { key = "k" } \ (state.mode - | Insert \ insertChar "k" state - | Normal \ upArrow state) + scrollHalfUp = state ctx \ ( + diff = floor ((ctx.h / 2) / lineH); + newRow = state.cursorRow - diff; + withScroll { state = clampCursor state.{ cursorRow = newRow }, emit = [] }); - | Key { key = "ArrowLeft" } \ leftArrow state - | Key { key = "h" } \ (state.mode - | Insert \ insertChar "h" state - | Normal \ leftArrow state) + scrollHalfDown = state ctx \ ( + diff = floor ((ctx.h / 2) / lineH); + newRow = state.cursorRow + diff; + withScroll { state = clampCursor state.{ cursorRow = newRow }, emit = [] }); - | Key { key = "ArrowRight" } \ rightArrow state - | Key { key = "l" } \ (state.mode - | Insert \ insertChar "l" state - | Normal \ rightArrow state) + ui.stateful { + focusable = True, + autoFocus = True, - | Key { key = "i" } \ (state.mode - | Insert \ insertChar "i" state - | Normal \ insertMode state) + key = "textEditor-" & name, - | Key { key = "u" } \ (state.mode - | Insert \ insertChar "u" state - | Normal \ undo state) + init = { + lines = lines, + cursorRow = 0, + cursorCol = 0, + scrollX = 0, + scrollY = 0, + undoStack = [], + redoStack = [], + mode = Normal, # Normal | Insert | Visual + pending = None # Some "d" | Some "g" | etc. + }, - | Key { key = "r", ctrl = True } \ (state.mode - | Insert \ insertChar "R" state - | Normal \ redo state) + 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 = "Escape" } \ escape state + | 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 = [] } + ), - | Key { key = "Backspace" } \ (state.mode - | Insert \ backspace state - | _ \ { state = state, emit = [] }) + view = state emit \ + cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; + cursorY = state.cursorRow * lineH; - | Key { key = "Enter" } \ (state.mode - | Insert \ enter state - | _ \ { state = state, emit = [] }) + cursor = ui.positioned { + x = cursorX, + y = cursorY, + child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } + }; - # any other key - | Key { key = key, printable = True } \ (state.mode - | Insert \ insertChar key state - | _ \ { state = state, emit = [] }) + maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; + totalWidth = maxLineLen * charW * scale + maxLineLen * charGap; + totalHeight = len state.lines * lineH; - | _ \ { state = state, emit = [] }, + # 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; - view = state emit \ - buffer = map (l \ text l) state.lines; - - scale = 2; - charH = 12; - charW = 5; - lineGap = 1; - charGap = 2; - - cursor = ui.positioned { - x = state.cursorCol * charW * scale + charGap * state.cursorCol, - y = state.cursorRow * charH * scale + lineGap * state.cursorRow, - child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } - }; - - scrollable { - w = ctx.w, - h = ctx.h, - totalWidth = 1000, - totalHeight = 1000, - scrollX = state.scrollX, - scrollY = state.scrollY, - child = ui.stack { - children = [ - ui.column { - gap = 1, - children = buffer - }, - cursor - ] + 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 + ] + } } - } - } + } }; 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 { 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..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), @@ -160,6 +161,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);