diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 400a945..ebe24a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: dope2: specifier: git+https://deemz.org/git/joeri/dope2.git - version: git+https://deemz.org/git/joeri/dope2.git#a166ebb0d703dc77f3939a35cf4cc186cd0cc1d8 + version: git+https://deemz.org/git/joeri/dope2.git#3b8548e9af5528069f07ec62519e263511ecc34b react: specifier: ^19.1.0 version: 19.1.0 @@ -633,8 +633,8 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - dope2@git+https://deemz.org/git/joeri/dope2.git#a166ebb0d703dc77f3939a35cf4cc186cd0cc1d8: - resolution: {commit: a166ebb0d703dc77f3939a35cf4cc186cd0cc1d8, repo: https://deemz.org/git/joeri/dope2.git, type: git} + dope2@git+https://deemz.org/git/joeri/dope2.git#3b8548e9af5528069f07ec62519e263511ecc34b: + resolution: {commit: 3b8548e9af5528069f07ec62519e263511ecc34b, repo: https://deemz.org/git/joeri/dope2.git, type: git} version: 0.0.1 dunder-proto@1.0.1: @@ -1758,7 +1758,7 @@ snapshots: depd@2.0.0: {} - dope2@git+https://deemz.org/git/joeri/dope2.git#a166ebb0d703dc77f3939a35cf4cc186cd0cc1d8: + dope2@git+https://deemz.org/git/joeri/dope2.git#3b8548e9af5528069f07ec62519e263511ecc34b: dependencies: functional-red-black-tree: 1.0.1 diff --git a/src/App.css b/src/App.css index 6d6243c..f609a8f 100644 --- a/src/App.css +++ b/src/App.css @@ -6,7 +6,7 @@ "content content content" "footer footer footer"; - grid-template-columns: 200px 1fr 200px; + grid-template-columns: 100px 1fr 100px; grid-template-rows: auto 1fr auto; /* grid-gap: 10px; */ diff --git a/src/Editor.css b/src/Editor.css index 69ae62b..366386e 100644 --- a/src/Editor.css +++ b/src/Editor.css @@ -1,46 +1,87 @@ -.text-block { - display: inline-block; - /* border: solid 1px lightgrey; */ - /* margin-right: 4px; */ - /* padding-left: 2px; */ - /* padding-right: 2px; */ - user-select: none; - cursor: text; +.suggest { + color: #aaa; +} +.suggestions { + position: absolute; + border: solid 1px lightgrey; + cursor: pointer; + max-height: calc(100vh - 48px); + overflow: scroll; } - .selected { background-color: darkseagreen; color: white; } -.suggest { - color: #aaa; + +[contenteditable] { + outline: 0px solid transparent; } -.cursor { - display: none; +/* TYPES */ + +.infix { + margin-left: 1px; + margin-right: 1px; } -div:focus .cursor { - animation: blink 1s linear infinite; +.type { + margin:1px; display: inline-block; - margin-left: -1px; - margin-right: -3px; - vertical-align: text-bottom; - height: 20px; - width: 2px; - background-color: black; } -.suggestions { - position: absolute; - border: solid 1px lightgrey; - cursor: pointer; +.functionType { + border: solid 1px darkred; + background-color: bisque; + color:darkred; } -@keyframes blink { - 50% { - opacity: 0; - } -} \ No newline at end of file +.productType { + border: solid 1px darkblue; + background-color: aliceblue; + color:darkblue; +} + +.sumType { + border: solid 1px #333; + background-color: lightyellow; + color:#333 +} + +.dictType { + border-radius: 5px; + border: solid 1px darkblue; + background-color: rgb(206, 232, 255); + color:darkblue +} + +.setType { + border-radius: 5px; + padding-left: 2px; + padding-right: 2px; + border: solid 1px darkblue; + background-color: rgb(206, 232, 255); + color:darkblue +} + +.listType { + padding-left: 2px; + padding-right: 2px; + border: solid 1px darkblue; + background-color: rgb(206, 232, 255); + color:darkblue +} + +.iteratorType { + border-style: dashed; + animation: flickerAnimation 500ms steps(1) normal infinite; +} + +/* Animations */ + +@keyframes flickerAnimation { + 0% { opacity:1; } + 50% { opacity:0; } + 100% { opacity:1; } +} diff --git a/src/Editor.tsx b/src/Editor.tsx index 86d7361..caeb66c 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,262 +1,138 @@ -import { useState } from "react"; -import {growPrefix, suggest, getType, prettyT} from "dope2"; +import { useEffect, useRef, useState } from "react"; +import {growPrefix, suggest, getType, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, symbolSetIterator, symbolDictIterator, getSymbol, getHumanReadableName} from "dope2"; import "./Editor.css"; -interface NodeState { - text: string; - children: NodeState[]; - cursor: CursorState; - selection: SelectState; -} - -interface SelectState { - from: number; - to: number; -} - -interface CursorState { - mode: "text" | "child" | "none"; - pos: number; -} - export function Editor({env}) { - const [root, setRoot] = useState({ - text: "comp", - children: [], - selection: { - from: 4, - to: 4, - }, - cursor: { - mode: "text", - pos: 4, - }, - }); - const [i, setI] = useState(0); - - const type = char => { - const newPos = root.selection.from + char.length; - setRoot({ - text: root.text.slice(0, root.selection.from)+char+root.text.slice(root.selection.to), - children: root.children, - selection: { - from: newPos, - to: newPos, - }, - cursor: { - mode: root.cursor.mode, - pos: newPos, - }, - }); - } - - const isSelection = root.selection.from !== root.selection.to; - - const growShrinkSelection = (newPos) => { - let selectFrom, selectTo; - if (root.cursor.pos === root.selection.to) { - // grow/shrink selectTo - selectFrom = root.selection.from; - selectTo = newPos; - } - else if (root.cursor.pos === root.selection.from) { - // grow/shrink selectFrom - selectFrom = newPos; - selectTo = root.selection.to; - } - else { - throw new Error("did not expect this") - } - if (selectFrom > selectTo) { - [selectFrom, selectTo] = [selectTo, selectFrom]; // swap - } - console.log({newPos, selectFrom, selectTo}); - setRoot({ - text: root.text, - children: root.children, - cursor: { - mode: root.cursor.mode, - pos: newPos, - }, - selection: { - from: selectFrom, - to: selectTo, - }, - }); - } - - const updateCursorSelect = (newPos, selectFrom, selectTo) => { - setRoot({ - text: root.text, - children: root.children, - cursor: { - mode: root.cursor.mode, - pos: newPos, - }, - selection: { - from: selectFrom, - to: selectTo, - }, - }); - } - - const handleArrow = (e) => { - let newPos = e.key === "ArrowLeft" - ? Math.max(0, root.cursor.pos-1) // to the left - : Math.min(root.cursor.pos+1, root.text.length); // to the right - if (e.shiftKey) { - return growShrinkSelection(newPos); - } - // shift not down... - let selectTo, selectFrom; - if (isSelection) { - // get rid of selection + move cursor - if (e.key === "ArrowLeft") { - selectTo = selectFrom = newPos = root.selection.from; - } - else { - selectTo = selectFrom = newPos = root.selection.to; - } - } - else { - // just move cursor - selectFrom = selectTo = newPos; - } - updateCursorSelect(newPos, selectFrom, selectTo); + return ; } - const handleJump = (e, newPos) => { - if (e.shiftKey) { - return growShrinkSelection(newPos); - } - // shift not down... - // get rid of selection (if there is any) + move cursor - updateCursorSelect(newPos, newPos, newPos); +function getCursorPosition() { + const selection = window.getSelection(); + if (selection) { + const range = selection.getRangeAt(0); + const clonedRange = range.cloneRange(); + return clonedRange.startOffset; } +} - const keydown = e => { +function setCursorPosition(elem, pos) { + const range = document.createRange(); + range.selectNode(elem); + range.setStart(elem, pos); + range.setEnd(elem, pos); + const selection = window.getSelection(); + if (!selection) { + console.log('no selection!') + } + selection?.removeAllRanges(); + selection?.addRange(range); +} + +function Block({env}) { + const [text, setText] = useState("edit me!"); + const ref = useRef(null); + const singleSuggestion = growPrefix(env.name2dyn)(text); + const suggestions = suggest(env.name2dyn)(text)(16); + const [i, setI] = useState(0); + const resetFocus = () => { + ref.current?.focus(); + }; + useEffect(resetFocus, [ref.current]) + const onSelect = ([name]) => { + setText(name); + ref.current.innerText = name; + setCursorPosition(ref.current.lastChild, name.length); + setI(0); + } + const onInput = e => { + const pos = getCursorPosition(); + setText(e.target.innerText); + setCursorPosition(e.target.lastChild, pos); + }; + const onKeyDown = e => { if (e.key === "Tab") { + const newText = text + singleSuggestion; + setText(newText); + ref.current.innerText = newText; + setCursorPosition(ref.current.lastChild, newText.length); e.preventDefault(); - const newText = root.text + growPrefix(env.name2dyn)(root.text) - return setRoot({ - text: newText, - cursor: { - mode: root.cursor.mode, - pos: newText.length, - }, - selection: { from: newText.length, to: newText.length }, - children: root.children, - }); - } - // console.log(e); - if (e.key === "ArrowRight") { - return handleArrow(e); - } - else if (e.key === "ArrowLeft") { - return handleArrow(e); - } - else if (e.key === "ArrowDown") { - setI((i+1)); - } - else if (e.key === "ArrowUp") { - setI((i-1)); - } - else if (e.key === "Backspace") { - if (isSelection) { - type(''); - } - else { - const newPos = Math.max(0, root.cursor.pos-1); - setRoot({ - text: root.text.slice(0, root.cursor.pos-1)+root.text.slice(root.cursor.pos), - children: root.children, - selection: { - from: newPos, - to: newPos, - }, - cursor: { - mode: root.cursor.mode, - pos: newPos, - } - }); - } - } - else if (e.key === "Delete") { - if (isSelection) { - type(''); - } - else { - setRoot({ - text: root.text.slice(0, root.cursor.pos)+root.text.slice(root.cursor.pos+1), - children: root.children, - cursor: { - mode: root.cursor.mode, - pos: root.cursor.pos, - }, - selection: { - from: root.cursor.pos, - to: root.cursor.pos, - }, - }); - } - } - else if (e.key === "Home") { - return handleJump(e, 0); - } - else if (e.key === "End") { - return handleJump(e, root.text.length); - } - else if (e.key === "Enter") { return; } - else if (!e.metaKey && !e.altKey && !e.ctrlKey && e.key !== "Shift") { - // only type real characters - type(e.key); + if (e.key === "ArrowDown") { + setI((i + 1) % suggestions.length); + e.preventDefault(); + return; } - } - - return
- -
; -} - -interface BlockProperties { - node: NodeState; - env: any; - i: number; - setI: any; -} - -function Block(props: BlockProperties) { - const {node, env, i, setI} = props; - const {selection, cursor, text} = node; - const completion = growPrefix(env.name2dyn)(text); - const suggestions = suggest(env.name2dyn)(text)(10); - return { - [...text].map((char,i) => - = selection.from && i < selection.to) ? ["selected"] : []).join(' ')}> - { (i === cursor.pos) ? : <> } - {char} - ) + if (e.key === "ArrowUp") { + setI((i - 1) % suggestions.length); + e.preventDefault(); + return; } - { (cursor.pos === text.length) ? : <> } - { - [...completion].map((char, i) => - {char} - ) + if (e.key === "Enter") { + onSelect(suggestions[i]); + e.preventDefault(); + return; } + }; + return + { + // hacky, but couldn't find another way: + setTimeout(resetFocus, 0); + }}> + + {singleSuggestion} ; } -function Cursor({suggestions, i, setI}) { - return
-
- {suggestions.length > 0 ? -
- {suggestions.map(([name, dynamic], j) =>
setI(j)}>{name} :: {prettyT(getType(dynamic))}
)} -
- : <> - } -
; +function Suggestions({suggestions, onSelect, i, setI}) { + return (suggestions.length > 0) ? +
+ {suggestions.map(([name, dynamic], j) =>
setI(j)} onDoubleClick={() => onSelect(suggestions[i])}>{name} ::
)} +
+ : <>; } + + +function Type({type}) { + const symbol = getSymbol(type); + switch (symbol) { + case symbolFunction: + return ; + case symbolProduct: + return ; + case symbolSum: + return ; + case symbolDict: + return ; + case symbolSet: + return ; + case symbolList: + return ; + case symbolSetIterator: + return ; + case symbolDictIterator: + return ; + + default: + return
{getHumanReadableName(symbol)}
+ } +} + +function BinaryType({type, cssClass, infix, prefix, suffix}) { + return
+ {prefix} + + {infix} + + {suffix} +
+} + +function UnaryType({type, cssClass, prefix, suffix}) { + return
+ {prefix} + + {suffix} +
+} \ No newline at end of file