Skip to content

Commit

Permalink
added a lox showcase
Browse files Browse the repository at this point in the history
  • Loading branch information
Emil Krebs committed Oct 17, 2023
1 parent 13d90e5 commit b41514a
Show file tree
Hide file tree
Showing 5 changed files with 617 additions and 1 deletion.
72 changes: 72 additions & 0 deletions hugo/assets/scripts/lox/lox-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export interface LoxMessage {
type: LoxMessageType;
content: unknown;
};

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" } },
],
}
};
257 changes: 257 additions & 0 deletions hugo/assets/scripts/lox/lox.tsx
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 {
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 />);
14 changes: 14 additions & 0 deletions hugo/content/showcase/lox.html
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
---
3 changes: 2 additions & 1 deletion hugo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
"build:worker/monaco-editor-react": "vite --config vite.bundle-monaco-editor-react.ts build",
"build:worker/arithmetics": "esbuild ../node_modules/langium-arithmetics-dsl/out/language-server/main-browser.js --bundle --format=iife --outfile=./static/showcase/libs/worker/arithmeticsServerWorker.js",
"build:worker/domainmodel": "esbuild ../node_modules/langium-domainmodel-dsl/out/language-server/main-browser.js --bundle --format=iife --outfile=./static/showcase/libs/worker/domainmodelServerWorker.js",
"build:worker/lox": "esbuild ../node_modules/langium-lox/out/language-server/main-browser.js --bundle --format=iife --outfile=./static/showcase/libs/worker/loxServerWorker.js",
"build:worker/playground-langium": "esbuild ./content/playground/langium-worker.ts --bundle --format=iife --outfile=./static/playground/libs/worker/langiumServerWorker.js",
"build:worker/playground-user": "esbuild ./content/playground/user-worker.ts --bundle --format=iife --outfile=./static/playground/libs/worker/userServerWorker.js",
"build:worker/playground-common": "esbuild ./content/playground/common.ts --bundle --format=esm --outfile=./static/playground/libs/worker/common.js",
"build:worker/sql": "esbuild ./assets/scripts/sql/language-server.ts --bundle --format=iife --outfile=./static/showcase/libs/worker/sqlServerWorker.js",
"build:worker/minilogo": "esbuild ../node_modules/langium-minilogo/out/language-server/main-browser.js --bundle --format=iife --outfile=./static/showcase/libs/worker/minilogoServerWorker.js",
"build:static": "npm run clean:static && npm run build:worker/monaco-editor-react && npm run build:worker/statemachine && npm run build:worker/domainmodel && npm run build:worker/sql && npm run build:worker/minilogo && npm run build:worker/arithmetics && npm run build:worker/playground-common && npm run build:worker/playground-langium && npm run build:worker/playground-user && npm run copy:monaco-editor-wrapper && npm run copy:monaco-workers",
"build:static": "npm run clean:static && npm run build:worker/monaco-editor-react && npm run build:worker/statemachine && npm run build:worker/lox && npm run build:worker/domainmodel && npm run build:worker/sql && npm run build:worker/minilogo && npm run build:worker/arithmetics && npm run build:worker/playground-common && npm run build:worker/playground-langium && npm run build:worker/playground-user && npm run copy:monaco-editor-wrapper && npm run copy:monaco-workers",
"build": "npm run build:static && cross-env NODE_ENV=production hugo --config ./config.toml -b / -d ../public --gc --minify ",
"watch": "npm run build:static && cross-env NODE_ENV=development hugo server --config ./config.toml -D -b localhost:1313 -d ../public --appendPort=false",
"watch:gitpod": "npm run build:static && cross-env NODE_ENV=development hugo server --config ./config.toml -D -b `gp url 1313` -d ../public --appendPort=false"
Expand Down
Loading

0 comments on commit b41514a

Please sign in to comment.