Compare commits

...

10 commits

7 changed files with 277 additions and 121 deletions

View file

@ -173,6 +173,13 @@ export type AST =
| Definition | Definition
| Rebind | 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 { export function prettyPrint(ast: AST, indent = 0): string {
const i = ' '.repeat(indent); const i = ' '.repeat(indent);
@ -195,23 +202,43 @@ export function prettyPrint(ast: AST, indent = 0): string {
return ast.name; return ast.name;
case 'apply': 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 func = prettyPrint(ast.func, indent);
const args = ast.args.map(a => { const args = ast.args.map(a => {
const printed = prettyPrint(a, indent); 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})`;
} }
return printed; return printed;
}).join(' '); }).join(' ');
if (ast.func.kind === 'lambda' || ast.func.kind === 'match' || ast.func.kind === 'let') {
return `(${func} ${args})` return `(${func} ${args})`
}
return `${func} ${args}`
case 'let': 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': { case 'list': {
const elems = ast.elements.map(e => prettyPrint(e, indent + 1)) 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'); const inner = elems.map(e => `${' '.repeat(indent + 1)}${e}`).join(',\n');
return `[\n${inner}\n${i}]`; 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 body = prettyPrint(ast.body, indent + 1);
const isComplex = ast.body.kind === 'match' || ast.body.kind === 'let'; const isComplex = ast.body.kind === 'match' || ast.body.kind === 'let';
if (isComplex) { if (isComplex) {
return `${params} \\\n${body}` const ii = ' '.repeat(indent + 1);
return `${params} \\\n${ii}${body}`
} }
return `${params} \\ ${body}` return `${params} \\ ${body}`
} }
@ -251,8 +278,11 @@ export function prettyPrint(ast: AST, indent = 0): string {
case 'match': case 'match':
const expr = prettyPrint(ast.expr, indent); const expr = prettyPrint(ast.expr, indent);
const cases = ast.cases const cases = ast.cases
.map(c => `${i}| ${prettyPrintPattern(c.pattern)} \\ ${prettyPrint(c.result, indent + 1)}`) .map(c => {
.join('\n'); 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}`; return `${expr}\n${cases}`;
case 'rebind': case 'rebind':

View file

@ -77,6 +77,16 @@ join = s list \ list
| [x] \ x | [x] \ x
| [x, ...xs] \ (x & s & (join s xs)); | [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 : (a \ b \ c) \ List a \ List b \ List c
zipWith = f l1 l2 \ l1 zipWith = f l1 l2 \ l1
| [] \ [] | [] \ []

View file

@ -134,8 +134,8 @@ scrollable = config \
| False \ []), | False \ []),
...(showHBar ...(showHBar
| True \ [ui.positioned { | True \ [ui.positioned {
x = c.h - 4, x = hBarX,
y = hBarX, y = c.h - 4,
child = ui.rect { h = 4, w = hBarWidth, color = "rgba(255,255,255,0.3)", radius = 2 } child = ui.rect { h = 4, w = hBarWidth, color = "rgba(255,255,255,0.3)", radius = 2 }
}] }]
| False \ []) | False \ [])

View file

@ -1,10 +1,52 @@
textEditorBuffers = [];
textEditor = name \ textEditor = name \
# defaults = {}; # defaults = {};
# c = { ...defaults, ...config }; # 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; 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 \ insertChar = char state \
newLines = updateAt state.cursorRow (line \ newLines = updateAt state.cursorRow (line \
insertCharAt line state.cursorCol char insertCharAt line state.cursorCol char
@ -23,6 +65,36 @@ textEditor = name \
emit = [] 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 = [] }; escape = state \ { state = state.{ mode = Normal }, emit = [] };
undo = state \ state.undoStack undo = state \ state.undoStack
@ -64,42 +136,57 @@ textEditor = name \
newLines = updateAt state.cursorRow (_ \ before) state.lines; newLines = updateAt state.cursorRow (_ \ before) state.lines;
newLines2 = insertAt (state.cursorRow + 1) after newLines; 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 = [] }; { state = state.{ lines = newLines2, cursorCol = 0, cursorRow = state.cursorRow + 1 }, emit = [] };
clampCursor = state \ upArrow = 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 }; newState = clampCursor state.{ cursorRow = state.cursorRow - 1 };
{ state = newState, emit = [] }); { state = newState, emit = [] };
downArrow = state \ ( downArrow = state \
newState = clampCursor state.{ cursorRow = state.cursorRow + 1 }; newState = clampCursor state.{ cursorRow = state.cursorRow + 1 };
{ state = newState, emit = [] }); { state = newState, emit = [] };
leftArrow = state \ ( leftArrow = state \
newState = clampCursor state.{ cursorCol = state.cursorCol - 1 }; newState = clampCursor state.{ cursorCol = state.cursorCol - 1 };
{ state = newState, emit = [] }); { state = newState, emit = [] };
rightArrow = state \ ( rightArrow = state \
newState = clampCursor state.{ cursorCol = state.cursorCol + 1 }; newState = clampCursor state.{ cursorCol = state.cursorCol + 1 };
{ state = newState, emit = [] }); { state = newState, emit = [] };
{ {
view = ctx \ ui.stateful { view = ctx \
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));
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 };
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, focusable = True,
autoFocus = True, autoFocus = True,
@ -113,87 +200,88 @@ textEditor = name \
scrollY = 0, scrollY = 0,
undoStack = [], undoStack = [],
redoStack = [], redoStack = [],
mode = Normal # Normal | Insert | Visual mode = Normal, # Normal | Insert | Visual
pending = None # Some "d" | Some "g" | etc.
}, },
update = state event \ event update = state event \ state.mode
| Key { key = "ArrowDown" } \ downArrow state | Insert \ (event
| 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 \ insertChar "R" state
| Normal \ redo state)
| Key { key = "Escape" } \ escape state | 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 = "Backspace" } \ (state.mode | Key { key = "ArrowDown" } \ withScroll (downArrow state)
| Insert \ backspace state | Key { key = "j" } \ withScroll (downArrow state)
| _ \ { state = state, emit = [] }) | Key { key = "ArrowUp" } \ withScroll (upArrow state)
| Key { key = "k" } \ withScroll(upArrow state)
| Key { key = "Enter" } \ (state.mode | Key { key = "ArrowLeft" } \ withScroll (leftArrow state)
| Insert \ enter state | Key { key = "h" } \ withScroll(leftArrow state)
| _ \ { state = state, emit = [] }) | Key { key = "ArrowRight" } \ withScroll (rightArrow state)
| Key { key = "l" } \ withScroll(rightArrow state)
# any other key | Key { key = "i" } \ insertMode state
| Key { key = key, printable = True } \ (state.mode | Key { key = "o" } \ openLine state
| Insert \ insertChar key state | Key { key = "d", ctrl = True } \ scrollHalfDown state ctx
| _ \ { state = state, emit = [] }) | Key { key = "u", ctrl = True } \ scrollHalfUp state ctx
| Key { key = "u" } \ undo state
| _ \ { state = state, emit = [] }, | 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 \ view = state emit \
buffer = map (l \ text l) state.lines; cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol;
cursorY = state.cursorRow * lineH;
scale = 2;
charH = 12;
charW = 5;
lineGap = 1;
charGap = 2;
cursor = ui.positioned { cursor = ui.positioned {
x = state.cursorCol * charW * scale + charGap * state.cursorCol, x = cursorX,
y = state.cursorRow * charH * scale + lineGap * state.cursorRow, y = cursorY,
child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } 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 { scrollable {
w = ctx.w, w = ctx.w,
h = ctx.h, h = ctx.h,
totalWidth = 1000, totalWidth = totalWidth,
totalHeight = 1000, totalHeight = totalHeight,
scrollX = state.scrollX, scrollX = state.scrollX,
scrollY = state.scrollY, scrollY = state.scrollY,
onScroll = delta \ emit (Scrolled delta),
child = ui.stack { child = ui.stack {
children = [ children = [
ui.column { ...buffer,
gap = 1,
children = buffer
},
cursor cursor
] ]
} }

View file

@ -65,7 +65,7 @@ treeNode = config \
valueLabel = value \ value valueLabel = value \ value
| NumberValue n \ Some (show n) | NumberValue n \ Some (show n)
| StringValue n \ Some ("\"" & n & "\"") | StringValue n \ Some ("\"" & r & "\"")
| ConstructorValue { tag = tag } \ Some tag | ConstructorValue { tag = tag } \ Some tag
| FunctionValue _ \ Some "<fn>" | FunctionValue _ \ Some "<fn>"
| _ \ None; | _ \ None;
@ -105,7 +105,7 @@ treeNode = config \
h = 30, h = 30,
onSubmit = onSubmit 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 | ConstructorValue { tag = tag } \ (isEditing
| True \ textInput { | True \ textInput {

View file

@ -1,4 +1,6 @@
import { render, hitTest, scrollHitTest } from './ui'; import { render, hitTest, scrollHitTest } from './ui';
import { syncToAst, saveDefinitions } from './runtime-js';
import { definitions } from './compiler';
type UIValue = any; type UIValue = any;
@ -217,6 +219,28 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
return; 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') { if (event._tag === 'Focus') {
setFocus(event._0); setFocus(event._0);
return; return;

View file

@ -80,6 +80,7 @@ export const _rt = {
return String(value); return String(value);
}, },
chars: (s: string) => s.split(''), chars: (s: string) => s.split(''),
isWordChar: (s: string) => ({ _tag: /^\w$/.test(s) ? 'True' : 'False' }),
// join: (delim: string) => (xs: string[]) => xs.join(delim), // join: (delim: string) => (xs: string[]) => xs.join(delim),
split: (delim: string) => (xs: string) => xs.split(delim), split: (delim: string) => (xs: string) => xs.split(delim),
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end), 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 }; return { _tag: 'Rebind', _0: name, _1: rest, _2: value };
}, },
deleteAt: (path: any[]) => {
return { _tag: 'DeleteAt', _0: path };
},
"undefine!": (name: string) => { "undefine!": (name: string) => {
delete store[name]; delete store[name];
definitions.delete(name); definitions.delete(name);