Compare commits

...

10 commits

7 changed files with 277 additions and 121 deletions

View file

@ -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(' ');
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':

View file

@ -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
| [] \ []

View file

@ -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 \ [])

View file

@ -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,42 +136,57 @@ 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 {
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,
autoFocus = True,
@ -113,87 +200,88 @@ 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
| 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 \ 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 = "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 \
buffer = map (l \ text l) state.lines;
scale = 2;
charH = 12;
charW = 5;
lineGap = 1;
charGap = 2;
cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol;
cursorY = state.cursorRow * lineH;
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;
# 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 = 1000,
totalHeight = 1000,
totalWidth = totalWidth,
totalHeight = totalHeight,
scrollX = state.scrollX,
scrollY = state.scrollY,
onScroll = delta \ emit (Scrolled delta),
child = ui.stack {
children = [
ui.column {
gap = 1,
children = buffer
},
...buffer,
cursor
]
}

View file

@ -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 "<fn>"
| _ \ 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 {

View file

@ -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;

View file

@ -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);