-
Notifications
You must be signed in to change notification settings - Fork 37
Lox Showcase #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lox Showcase #162
Changes from all commits
eedeebf
613f2ab
f6dd30a
a4c91ae
a97fbca
105b566
308d1f0
5fee4ef
23ca2fe
ebf8a84
3a9437c
fb8abda
a365a85
9de599f
568a5e1
1e5beba
9e4f0a3
37af827
aee2c7c
1686025
a5e0931
e06afd6
7bf2248
772f9d7
bb01cf3
12680dd
4806fed
215359d
2d757bc
57a150d
47c22d0
ae68335
77b3908
fbce6a5
11271e9
0a7d50c
42f40be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| export interface LoxMessage { | ||
| type: LoxMessageType; | ||
| content: unknown; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having unknown type is not so nice. There is a easy pattern I use: type XType = 'A'|'B'|'C';
interface XBase {
type: XType;
}
interface A extends XBase {
type: 'A'; // subset of parent definition!
content: string;
}
interface B extends XBase {
type: 'B'; // subset of parent...
content: number;
}
interface C extends XBase {
type: 'C';
content: boolean;
}
type X = A|B|C;
//usage: you can do stuff like this:
const x: X = ... //get from somewhere
if(x.type === 'A') {
//it asserts then that x is of type A, so accessing x.content will be a string, no casts needed!!!
} else if(x.type === 'B') {
//...x.content is number
} else if(x.type === 'C') {
//...x.content is boolean
} else {
assertUnreachable(x.type); //this code is then unreachable (it will be highlighted red when another type D was added)
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The interpreter returns an unknown type, so I could do something like this: export interface LoxMessage {
type: LoxMessageType;
content: MessageContent;
};
type MessageContent = string | number | boolean | null | any; |
||
| }; | ||
|
|
||
| export type LoxMessageType = "notification" | "output" | "error"; | ||
|
|
||
|
|
||
| export const exampleCode = `fun factorial(n: number): number { | ||
| if (n <= 1) { | ||
| return 1; | ||
| } else { | ||
| return n * factorial(n - 1); | ||
| } | ||
| } | ||
| fun binomial(n: number, k: number): number { | ||
| return factorial(n) / (factorial(k) * factorial(n - k)); | ||
| } | ||
| fun pow(x: number, n: number): number { | ||
| var result = 1; | ||
| for (var i = 0; i < n; i = i + 1) { | ||
| result = result * x; | ||
| } | ||
| return result; | ||
| } | ||
| fun mod(x: number, y: number): number { | ||
| return x - y * (x / y); | ||
| } | ||
| fun floor(x: number): number { | ||
| return x - mod(x, 1); | ||
| } | ||
| print("factorial(5) = " + factorial(5)); | ||
| print("binomial(5, 2) = " + binomial(5, 2)); | ||
| print("pow(2, 10) = " + pow(2, 10)); | ||
| print("mod(10, 3) = " + mod(10, 3)); | ||
| print("floor(3.14) = " + floor(3.14)); | ||
| `; | ||
|
|
||
| export const syntaxHighlighting = { | ||
| keywords: [ | ||
| 'and', 'boolean', 'class', 'else', 'false', 'for', 'fun', 'if', 'nil', 'number', 'or', 'print', 'return', 'string', 'super', 'this', 'true', 'var', 'void', 'while' | ||
| ], | ||
| operators: [ | ||
| '-', ',', ';', ':', '!', '!=', '.', '*', '/', '+', '<', '<=', '=', '==', '=>', '>', '>=' | ||
| ], | ||
| symbols: /-|,|;|:|!|!=|\.|\(|\)|\{|\}|\*|\+|<|<=|=|==|=>|>|>=/, | ||
|
|
||
| tokenizer: { | ||
| initial: [ | ||
| { regex: /[_a-zA-Z][\w_]*/, action: { cases: { '@keywords': { "token": "keyword" }, '@default': { "token": "ID" } } } }, | ||
| { regex: /[0-9]+(\.[0-9]+)?/, action: { "token": "number" } }, | ||
| { regex: /"[^"]*"/, action: { "token": "string" } }, | ||
| { include: '@whitespace' }, | ||
| { regex: /@symbols/, action: { cases: { '@operators': { "token": "operator" }, '@default': { "token": "" } } } }, | ||
| ], | ||
| whitespace: [ | ||
| { regex: /\s+/, action: { "token": "white" } }, | ||
| { regex: /\/\*/, action: { "token": "comment", "next": "@comment" } }, | ||
| { regex: /\/\/[^\n\r]*/, action: { "token": "comment" } }, | ||
| ], | ||
| comment: [ | ||
| { regex: /[^\/\*]+/, action: { "token": "comment" } }, | ||
| { regex: /\*\//, action: { "token": "comment", "next": "@pop" } }, | ||
| { regex: /[\/\*]/, action: { "token": "comment" } }, | ||
| ], | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| import { | ||
| MonacoEditorReactComp, | ||
| } from "@typefox/monaco-editor-react/bundle"; | ||
| import { buildWorkerDefinition } from "monaco-editor-workers"; | ||
| import React, { createRef, useRef } from "react"; | ||
| import { createRoot } from "react-dom/client"; | ||
| import { Diagnostic, DocumentChangeResponse } from "../langium-utils/langium-ast"; | ||
| import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string"; | ||
| import { LoxMessage, exampleCode, syntaxHighlighting } from "./lox-tools"; | ||
| import { UserConfig } from "monaco-editor-wrapper"; | ||
| import { createUserConfig } from "../utils"; | ||
|
|
||
| buildWorkerDefinition( | ||
| "../../libs/monaco-editor-workers/workers", | ||
| new URL("", window.location.href).href, | ||
| false | ||
| ); | ||
| let userConfig: UserConfig; | ||
|
|
||
| interface PreviewProps { | ||
| diagnostics?: Diagnostic[]; | ||
| } | ||
|
|
||
| interface PreviewState { | ||
| diagnostics?: Diagnostic[]; | ||
| messages: TerminalMessage[]; | ||
| } | ||
|
|
||
| interface TerminalMessage { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the same trick as above to avoid Union type for content |
||
| type: "notification" | "error" | "output"; | ||
| content: string | string[]; | ||
| } | ||
|
|
||
| class Preview extends React.Component<PreviewProps, PreviewState> { | ||
| terminalContainer: React.RefObject<HTMLDivElement>; | ||
| constructor(props: PreviewProps) { | ||
| super(props); | ||
| this.state = { | ||
| diagnostics: props.diagnostics, | ||
| messages: [], | ||
| }; | ||
|
|
||
| this.terminalContainer = createRef<HTMLDivElement>(); | ||
| } | ||
|
|
||
| println(text: string) { | ||
| this.setState((state) => ({ | ||
| messages: [...state.messages, { type: "output", content: text }], | ||
| })); | ||
| } | ||
|
|
||
| error(text: string) { | ||
| this.setState((state) => ({ | ||
| messages: [...state.messages, { type: "error", content: text }], | ||
| })); | ||
| } | ||
|
|
||
| clear() { | ||
| this.setState({ messages: [] }); | ||
| } | ||
|
|
||
| setDiagnostics(diagnostics: Diagnostic[]) { | ||
| this.setState({ diagnostics: diagnostics }); | ||
|
|
||
| } | ||
|
|
||
| render() { | ||
| // if the code doesn't contain any errors and the diagnostics aren't warnings | ||
| if (this.state.diagnostics == null || this.state.diagnostics.filter((i) => i.severity === 1).length == 0) { | ||
|
|
||
| // auto scroll to bottom | ||
| const terminal = this.terminalContainer.current; | ||
| const newLine = terminal?.lastElementChild; | ||
| if (newLine && terminal) { | ||
| const rect = newLine.getBoundingClientRect(); | ||
| if (rect.bottom <= terminal.getBoundingClientRect().bottom) { | ||
| newLine.scrollIntoView(); | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <div className="text-sm flex flex-col p-4 overflow-x-hidden overflow-y-scroll" ref={this.terminalContainer}> | ||
| {this.state.messages.map((message, index) => | ||
| <p key={index} className={message.type == "error" ? "text-base text-accentRed" : "text-white"}>{message.type == "error" ? "An error occurred: " : ""} {message.content}</p> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // Show the exception | ||
| return ( | ||
| <div className="flex flex-col h-full w-full p-4 justify-start items-center my-10" > | ||
| <div className="text-white border-2 border-solid border-accentRed rounded-md p-4 text-left text-sm cursor-default"> | ||
| {this.state.diagnostics.filter((i) => i.severity === 1).map((diagnostic, index) => | ||
| <details key={index}> | ||
| <summary>{`Line ${diagnostic.range.start.line}-${diagnostic.range.end.line}: ${diagnostic.message}`}</summary> | ||
| <p>Source: {diagnostic.source} | Code: {diagnostic.code}</p> | ||
| </details> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| class App extends React.Component<{}, {}> { | ||
| monacoEditor: React.RefObject<MonacoEditorReactComp>; | ||
| preview: React.RefObject<Preview>; | ||
| copyHint: React.RefObject<HTMLDivElement>; | ||
| shareButton: React.RefObject<HTMLImageElement>; | ||
| constructor(props) { | ||
| super(props); | ||
|
|
||
| // bind 'this' ref for callbacks to maintain parent context | ||
| this.onMonacoLoad = this.onMonacoLoad.bind(this); | ||
| this.onDocumentChange = this.onDocumentChange.bind(this); | ||
| this.copyLink = this.copyLink.bind(this); | ||
| this.monacoEditor = React.createRef(); | ||
| this.preview = React.createRef(); | ||
| this.copyHint = React.createRef(); | ||
| this.shareButton = React.createRef(); | ||
| } | ||
|
|
||
| /** | ||
| * Callback that is invoked when Monaco is finished loading up. | ||
| * Can be used to safely register notification listeners, retrieve data, and the like | ||
| * | ||
| * @throws Error on inability to ref the Monaco component or to get the language client | ||
| */ | ||
| onMonacoLoad() { | ||
| // verify we can get a ref to the editor | ||
| if (!this.monacoEditor.current) { | ||
| throw new Error("Unable to get a reference to the Monaco Editor"); | ||
| } | ||
|
|
||
| // verify we can get a ref to the language client | ||
| const lc = this.monacoEditor.current | ||
| ?.getEditorWrapper() | ||
| ?.getLanguageClient(); | ||
| if (!lc) { | ||
| throw new Error("Could not get handle to Language Client on mount"); | ||
| } | ||
| this.monacoEditor.current.getEditorWrapper()?.getEditor()?.focus(); | ||
| // register to receive DocumentChange notifications | ||
| lc.onNotification("browser/DocumentChange", this.onDocumentChange); | ||
| } | ||
|
|
||
| /** | ||
| * Callback invoked when the document processed by the LS changes | ||
| * Invoked on startup as well | ||
| * @param resp Response data | ||
| */ | ||
| onDocumentChange(resp: DocumentChangeResponse) { | ||
| // decode the received Asts | ||
| const message = JSON.parse(resp.content) as LoxMessage; | ||
| switch (message.type) { | ||
| case "notification": | ||
| switch (message.content) { | ||
| case "startInterpreter": | ||
| this.preview.current?.clear(); | ||
| break; | ||
| } | ||
| break; | ||
| case "error": | ||
| this.preview.current?.error(message.content as string); | ||
| break; | ||
| case "output": | ||
| this.preview.current?.println(message.content as string); | ||
| break; | ||
| } | ||
| this.preview.current?.setDiagnostics(resp.diagnostics); | ||
| } | ||
|
|
||
|
|
||
| async copyLink() { | ||
| const code = this.monacoEditor.current?.getEditorWrapper()?.getEditor()?.getValue()!; | ||
| const url = new URL("/showcase/lox", window.origin); | ||
| url.searchParams.append("code", compressToEncodedURIComponent(code)); | ||
|
|
||
| this.copyHint.current!.style.display = "block"; | ||
| this.shareButton.current!.src = '/assets/checkmark.svg'; | ||
| setTimeout(() => { | ||
| this.shareButton.current!.src = '/assets/share.svg'; | ||
| this.copyHint.current!.style.display = 'none'; | ||
| }, 1000); | ||
|
|
||
| navigator.clipboard.writeText(window.location.href); | ||
|
|
||
| await navigator.clipboard.writeText(url.toString()); | ||
| } | ||
|
|
||
| componentDidMount() { | ||
| this.shareButton.current!.addEventListener('click', this.copyLink); | ||
| } | ||
|
|
||
| render() { | ||
| const style = { | ||
| height: "100%", | ||
| width: "100%", | ||
| }; | ||
| const url = new URL(window.location.toString()); | ||
| let code = url.searchParams.get("code"); | ||
| if (code) { | ||
| code = decompressFromEncodedURIComponent(code); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="justify-center self-center flex flex-col md:flex-row h-full w-full"> | ||
| <div className="float-left w-full h-full flex flex-col"> | ||
| <div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono "> | ||
| <span>Editor</span> | ||
| <div className="flex flex-row justify-end w-full h-full gap-2"> | ||
| <div className="text-sm hidden" ref={this.copyHint}>Link was copied!</div> | ||
| <img src="/assets/share.svg" title="Copy URL to this grammar and content" className="inline w-4 h-4 cursor-pointer" ref={this.shareButton}></img> | ||
| </div> | ||
| </div> | ||
| <div className="wrapper relative bg-white dark:bg-gray-900 border border-emeraldLangium h-full w-full"> | ||
| <MonacoEditorReactComp | ||
| ref={this.monacoEditor} | ||
| onLoad={this.onMonacoLoad} | ||
| userConfig={userConfig} | ||
| style={style} | ||
| /> | ||
| </div> | ||
| </div> | ||
| <div className="float-left w-full h-full flex flex-col" id="preview"> | ||
| <div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono "> | ||
| <span>Output</span> | ||
| </div> | ||
| <div className="border border-emeraldLangium h-full w-full overflow-hidden overflow-y-scroll"> | ||
| <Preview ref={this.preview} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export async function share(code: string): Promise<void> { | ||
| const url = new URL("/showcase/lox", window.origin); | ||
| url.searchParams.append("code", compressToEncodedURIComponent(code)); | ||
| await navigator.clipboard.writeText(url.toString()); | ||
| } | ||
|
|
||
| userConfig = createUserConfig({ | ||
| languageId: 'lox', | ||
| code: exampleCode, | ||
| htmlElement: document.getElementById('root')!, | ||
| worker: '/showcase/libs/worker/loxServerWorker.js', | ||
| monarchGrammar: syntaxHighlighting | ||
| }); | ||
| const root = createRoot(document.getElementById("root") as HTMLElement); | ||
| root.render(<App />); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| title: "Lox" | ||
| weight: 500 | ||
| type: langium | ||
| layout: showcase-page | ||
| url: "/showcase/lox" | ||
| img: "/assets/Langium_Lox.svg" | ||
| file: "scripts/lox/lox.tsx" | ||
| description: A tree-walk interpreter for the Lox language. It is based on the book 'Crafting Interpreters' by Bob Nystrom. | ||
| geekdochidden: true | ||
| draft: false | ||
| beta: true | ||
| noMain: true | ||
| --- |
Uh oh!
There was an error while loading. Please reload this page.