Skip to content

Commit

Permalink
Messy messy, but sketchzone-based editor can be prototyped
Browse files Browse the repository at this point in the history
  • Loading branch information
robsimmons committed May 19, 2024
1 parent 23ba6b9 commit 74d6e40
Show file tree
Hide file tree
Showing 13 changed files with 2,795 additions and 1,890 deletions.
31 changes: 18 additions & 13 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,32 @@
href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@500&family=Fira+Sans+Condensed&display=swap"
rel="stylesheet"
/>
<link href="/src/web/reset.css" rel="stylesheet" />
<link href="/node_modules/sketchzone/css/reset.css" rel="stylesheet" />
<link href="/node_modules/sketchzone/css/sketchzone.css" rel="stylesheet" />
<link href="/src/web/oksolar.css" rel="stylesheet" />
<link href="/src/web/dusa.css" rel="stylesheet" />
<link href="/src/web/codemirror.css" rel="stylesheet" />
<link rel="icon" href="/dusa-icon.svg" />
<link rel="mask-icon" href="/dusa-icon.svg" color="#007FBC" />
</head>
<body id="root" class="theme-light">
<main>
<div class="dk-config" id="config-root"></div>
<div class="dk-sessions">
<div class="dk-header">
<div id="dk-tabs" class="dk-tabs"></div>
<div class="dk-logo">Dusa</div>
<body id="body-root">
<main id="main-root">
<div id="sketchzone-config"></div>
<div id="sketchzone-container">
<div id="sketchzone-header">
<div id="sketchzone-tabs"></div>
<div id="sketchzone-logo"></div>
</div>
<div id="session" class="mobile-view-editor">
<div id="codemirror-root"></div>
<div id="session-divider"></div>
<div id="react-root"></div>
<div id="sketchzone-active-sketch" class="active-sketch-is-showing-editor">
<div id="sketchzone-codemirror-root"></div>
<div id="sketchzone-divider"></div>
<div id="sketchzone-inspector-root">
<div id="sketchzone-inspector-controller" class="zone1"></div>
<div id="sketchzone-inspector-contents"></div>
</div>
</div>
</div>
</main>
<div id="modal-root"></div>
<script type="module" src="/src/web/main.tsx"></script>
</body>
</html>
2,359 changes: 1,266 additions & 1,093 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,20 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"@vitest/coverage-istanbul": "^0.34.6",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"@vitest/coverage-istanbul": "^1.6.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"prettier": "^3.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rollup": "^4.5.2",
"sketchzone": "^0.0.11",
"typescript": "^5.0.2",
"vite": "^5.0.13",
"vitest": "^0.34.6"
"vitest": "^1.6.0"
}
}
17 changes: 0 additions & 17 deletions src/web/SolutionViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,6 @@ function TermViewer(props: { term: Term; depth: number }) {
const [show, setShow] = React.useState(props.depth > 0);
if (typeof props.term !== 'object' || show) {
if (props.term === null) return '()';
if (typeof props.term === 'string') return `"${escapeString(props.term)}"`;
if (typeof props.term === 'bigint') return `${props.term}`;
if (typeof props.term === 'boolean') return `#${props.term}`;
if (props.term.name === null) return `#${props.term.value}`;
if (!props.term.args) return props.term.name;
return (
<>
({props.term.name}
{props.term.args.map((arg, i) => (
<span key={i}>
{' '}
<TermViewer term={arg} depth={props.depth - 1} />
</span>
))}
)
</>
);
} else {
return <button onClick={() => setShow(true)}>more...</button>;
}
Expand Down
242 changes: 242 additions & 0 deletions src/web/codemirror-0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { HighlightStyle, StreamLanguage, syntaxHighlighting } from '@codemirror/language';
import { EditorState, RangeSet, StateEffect, StateField } from '@codemirror/state';
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
keymap,
lineNumbers,
tooltips,
} from '@codemirror/view';
import { ParserState, dusaTokenizer } from '../language/dusa-tokenizer.js';
import { StringStream } from '../parsing/string-stream.js';
import { classHighlighter, tags } from '@lezer/highlight';
import { Diagnostic, linter } from '@codemirror/lint';
import { SourcePosition } from '../parsing/source-location.js';
import { Issue, parseWithStreamParser } from '../parsing/parser.js';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { parseTokens } from '../language/dusa-parser.js';
import { ParsedDeclaration, visitPropsInProgram, visitTermsinProgram } from '../language/syntax.js';
import { check } from '../language/check.js';

const bogusPosition = {
start: { line: 1, column: 1 },
end: { line: 1, column: 2 },
};
/** Create a Codemirror-compliant parser from our stream parser.
* The token method is given a Codemirror-style StringStream,
* and we have to use that to implement the StringStream interface
* that our parser expects. Because we're not using the syntax
* tree, we can feed bogus SourceLocation information to matchedLocation.
*/
const parser = StreamLanguage.define<{ state: ParserState }>({
name: 'Dusa',
startState: () => ({ state: dusaTokenizer.startState }),
token: (stream, cell) => {
const stream2: StringStream = {
eat(pattern) {
const result = stream.match(pattern);
if (!result) return null;
if (result === true) {
if (typeof pattern === 'string') return pattern;
return 'bogus';
}
return result[0];
},
peek(pattern) {
const fragment = stream.string.slice(stream.pos);
if (typeof pattern === 'string') {
return fragment.startsWith(pattern) ? pattern : null;
}
return fragment.match(pattern)?.[0] || null;
},
eatToEol() {
const pos = stream.pos;
stream.skipToEnd();
return stream.string.slice(pos);
},
sol: () => stream.sol(),
eol: () => stream.eol(),
matchedLocation: () => bogusPosition,
};

const result = dusaTokenizer.advance(stream2, cell.state);
cell.state = result.state;
return result.tag || null;
},
blankLine: (cell) => {
const stream: StringStream = {
eat: () => null,
peek: () => null,
eatToEol: () => '',
sol: () => true,
eol: () => true,
matchedLocation: () => bogusPosition,
};
const result = dusaTokenizer.advance(stream, cell.state);
cell.state = result.state;
},
copyState: ({ state }) => ({ state }),
indent: () => null,
languageData: {},
tokenTable: {},
});

export const highlighter = HighlightStyle.define([{ tag: tags.className, backgroundColor: 'red' }]);

export interface CodeEditorProps {
contents: string;
getContents: React.MutableRefObject<null | (() => string)>;
updateListener: (update: ViewUpdate) => void;
}

function position(state: EditorState, pos: SourcePosition) {
return state.doc.line(pos.line).from + pos.column - 1;
}

function issueToDiagnostic(issues: Issue[]): readonly Diagnostic[] {
return issues
.map((issue): Diagnostic | null => {
if (!issue.loc) return null;
return {
from: position(view.state, issue.loc.start),
to: position(view.state, issue.loc.end),
severity: issue.severity,
message: issue.msg,
};
})
.filter((issue): issue is Diagnostic => issue !== null);
}

function dusaLinter(view: EditorView): readonly Diagnostic[] {
const contents = view.state.doc.toString();
const tokens = parseWithStreamParser(dusaTokenizer, contents);
if (tokens.issues.length > 0) {
return issueToDiagnostic(tokens.issues);
}
const parsed = parseTokens(tokens.document);
const parsedIssues = parsed.filter((decl): decl is Issue => decl.type === 'Issue');
const parsedDecls = parsed.filter((decl): decl is ParsedDeclaration => decl.type !== 'Issue');
if (parsedIssues.length > 0) {
return issueToDiagnostic(parsedIssues);
}
const { errors } = check(parsedDecls);
return issueToDiagnostic(errors);
}

/** highlightPredicates is based on simplifying the Linter infrastructure */
const highlightPredicatesUpdateEffect = StateEffect.define<[number, number][]>();
const highlightPredicatesState = StateField.define<DecorationSet>({
create() {
return RangeSet.empty;
},
update(value, transaction) {
if (transaction.docChanged) {
value = value.map(transaction.changes);
}

for (const effect of transaction.effects) {
if (effect.is(highlightPredicatesUpdateEffect)) {
return RangeSet.of(
effect.value.map(([from, to]) => ({
from,
to,
value: Decoration.mark({ inclusive: true, class: 'tok-predicate' }),
})),
true,
);
}
}
return value;
},
provide(field) {
return EditorView.decorations.from(field);
},
});
const highlightPredicatesPlugin = ViewPlugin.define((view: EditorView) => {
const delay = 750;
let timeout: null | ReturnType<typeof setTimeout> = setTimeout(() => run(), delay);
let nextUpdateCanHappenOnlyAfter = Date.now() + delay;
function run() {
const now = Date.now();
// Debounce logic, part 1
if (now < nextUpdateCanHappenOnlyAfter - 5) {
timeout = setTimeout(run, nextUpdateCanHappenOnlyAfter - now);
} else {
timeout = null;
const contents = view.state.doc.toString();
const tokens = parseWithStreamParser(dusaTokenizer, contents);
if (tokens.issues.length > 0) return;
const parsed = parseTokens(tokens.document);
const ranges: [number, number][] = [];
const preds = new Set<string>();
for (const prop of visitPropsInProgram(parsed)) {
const start = position(view.state, prop.loc.start);
ranges.push([start, start + prop.name.length]);
preds.add(prop.name);
}
for (const term of visitTermsinProgram(parsed)) {
if (term.type === 'const' && preds.has(term.name)) {
const start = position(view.state, term.loc.start);
ranges.push([start, start + term.name.length]);
}
}
view.dispatch({ effects: [highlightPredicatesUpdateEffect.of(ranges)] });
}
}

return {
update(update: ViewUpdate) {
if (update.docChanged) {
// Debounce logic, part 2
nextUpdateCanHappenOnlyAfter = Date.now() + delay;
if (timeout === null) {
timeout = setTimeout(run, delay);
}
}
},
destroy() {
if (timeout !== null) {
clearTimeout(timeout);
}
},
};
});

export const editorChangeListener: { current: null | ((update: ViewUpdate) => void) } = {
current: null,
};

const state = EditorState.create({
doc: '',
extensions: [
parser,
syntaxHighlighting(classHighlighter),
lineNumbers(),
history(),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (editorChangeListener.current !== null) {
editorChangeListener.current(update);
}
}),
linter(dusaLinter),
tooltips({ parent: document.body }),
highlightPredicatesPlugin,
highlightPredicatesState,
keymap.of([...defaultKeymap, ...historyKeymap]),
],
});
const view = new EditorView({ state, parent: document.getElementById('codemirror-root')! });

export function setEditorContents(contents: string) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: contents },
});
}

export function getEditorContents() {
return view.state.doc.toString();
}
Loading

0 comments on commit 74d6e40

Please sign in to comment.