-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Emil Krebs
committed
Oct 17, 2023
1 parent
13d90e5
commit b41514a
Showing
5 changed files
with
617 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } }, | ||
], | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.