diff --git a/apps/vs-code-extension/assets/language-configuration.json b/apps/vs-code-extension/assets/language-configuration.json index 2b1e4e94f..1656f69fa 100644 --- a/apps/vs-code-extension/assets/language-configuration.json +++ b/apps/vs-code-extension/assets/language-configuration.json @@ -1,17 +1,9 @@ { "comments": { - // symbol used for single line comment. Remove this entry if your language does not support line comments "lineComment": "//", - // symbols used for start and end a block comment. Remove this entry if your language does not support block comments "blockComment": ["/*", "*/"] }, - // symbols used as brackets - "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] - ], - // symbols that are auto closed when typing + "brackets": [["{", "}"], ["[", "]"], ["(", ")"]], "autoClosingPairs": [ ["{", "}"], ["[", "]"], @@ -19,7 +11,6 @@ ["\"", "\""], ["'", "'"] ], - // symbols that can be used to surround a selection "surroundingPairs": [ ["{", "}"], ["[", "]"], diff --git a/libs/language-server/langium-config.json b/libs/language-server/langium-config.json index d411e9641..503a22b5b 100644 --- a/libs/language-server/langium-config.json +++ b/libs/language-server/langium-config.json @@ -8,9 +8,6 @@ "textMate": { "out": "../../apps/vs-code-extension/assets/jayvee.tmLanguage.json" }, - "monarch": { - "out": "../../libs/monaco-editor/src/lib/jayvee.monarch.ts" - }, "prism": { "out": "../../apps/docs/src/theme/prism-jayvee.js" } diff --git a/libs/language-server/project.json b/libs/language-server/project.json index c5b7ba2ea..ec9384ca5 100644 --- a/libs/language-server/project.json +++ b/libs/language-server/project.json @@ -9,21 +9,23 @@ "cache": true, "inputs": [ "{projectRoot}/langium-config.json", - "{workspaceRoot}/tools/scripts/fix-monarch-grammar-escape.mjs", + "{workspaceRoot}/apps/vs-code-extension/assets/language-configuration.json", + "{workspaceRoot}/tools/scripts/monaco-editor/copy-textmate-grammar.mjs", "{workspaceRoot}/tools/scripts/language-server/generate-stdlib.mjs", "{projectRoot}/src/grammar/**/*" ], "outputs": [ "{workspaceRoot}/apps/vs-code-extension/assets/jayvee.tmLanguage.json", - "{workspaceRoot}/libs/monaco-editor/src/lib/jayvee.monarch.ts", + "{workspaceRoot}/libs/monaco-editor/src/lib/jayvee.tmLanguage.json", + "{workspaceRoot}/libs/monaco-editor/src/lib/language-configuration.json", "{workspaceRoot}/apps/docs/src/theme/prism-jayvee.js", "{projectRoot}/src/lib/ast/generated" ], "options": { "commands": [ "langium generate -f libs/language-server/langium-config.json", - // Workaround until https://github.com/langium/langium/issues/740 is resolved: - "node tools/scripts/fix-monarch-grammar-escape.mjs", + "node tools/scripts/monaco-editor/copy-textmate-grammar.mjs", + "node tools/scripts/monaco-editor/copy-language-configuration.mjs", "node tools/scripts/language-server/generate-stdlib.mjs" ], "parallel": false diff --git a/libs/monaco-editor/.gitignore b/libs/monaco-editor/.gitignore index 4169fd363..ac5771115 100644 --- a/libs/monaco-editor/.gitignore +++ b/libs/monaco-editor/.gitignore @@ -3,4 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only # file generated by Langium -src/lib/jayvee.monarch.ts +src/lib/jayvee.tmLanguage.json +# file copied from vs-code-extension +src/lib/language-configuration.json diff --git a/libs/monaco-editor/project.json b/libs/monaco-editor/project.json index 07c5964f1..2b3f80f2e 100644 --- a/libs/monaco-editor/project.json +++ b/libs/monaco-editor/project.json @@ -3,13 +3,13 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/monaco-editor/src", "projectType": "library", - "tags": [], "targets": { "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] }, "build": { + "dependsOn": ["^build", "generate"], "executor": "@nx/rollup:rollup", "outputs": ["{options.outputPath}"], "options": { @@ -42,8 +42,6 @@ "options": { "commands": [ "node tools/scripts/relax-peer-dependency-versions.mjs monaco-editor", - "node tools/scripts/monaco-editor/delete-vscode-peer-dependency.mjs monaco-editor", - "node tools/scripts/monaco-editor/relax-react-version.mjs monaco-editor", "node tools/scripts/add-package-json-version.mjs monaco-editor", "node tools/scripts/publish.mjs monaco-editor false" // dry-run ], diff --git a/libs/monaco-editor/src/lib/monaco-editor.ts b/libs/monaco-editor/src/lib/monaco-editor.ts new file mode 100644 index 000000000..1f4e4e57c --- /dev/null +++ b/libs/monaco-editor/src/lib/monaco-editor.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import tmGrammar from './jayvee.tmLanguage.json'; +import config from './language-configuration.json'; + +export function getTextMateGrammar(): unknown { + return tmGrammar; +} + +export function getLanguageConfiguration(): unknown { + return config; +} diff --git a/libs/monaco-editor/src/lib/monaco-editor.tsx b/libs/monaco-editor/src/lib/monaco-editor.tsx deleted file mode 100644 index 39ba8d2eb..000000000 --- a/libs/monaco-editor/src/lib/monaco-editor.tsx +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg -// -// SPDX-License-Identifier: AGPL-3.0-only - -/* - The necessary imports for the Monaco editor can be found here: - https://github.com/TypeFox/monaco-languageclient/blob/c5511b19e95e237c3f95a0fc0588769263f3ba40/packages/examples/browser-lsp/src/client.ts -*/ - -import 'monaco-editor/esm/vs/editor/editor.all.js'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.js'; -import { - CloseAction, - type Disposable, - ErrorAction, - type MessageTransports, - MonacoLanguageClient, - MonacoServices, - State, -} from 'monaco-languageclient'; -import React from 'react'; -import getMessageServiceOverride from 'vscode/service-override/messages'; -import { StandaloneServices } from 'vscode/services'; -import { - BrowserMessageReader, - BrowserMessageWriter, -} from 'vscode-languageserver-protocol/browser.js'; - -import JayveeMonarchConfig from './jayvee.monarch'; - -const LANGUAGE_NAME = 'jayvee'; - -export interface MonacoEditorProps { - startJayveeWorker: () => Worker; - - /** - * The text that shall (initially) be displayed in the editor. - * - * Whenever this Prop changes, the internal Model is re-created. - * This is a pretty expensive operation. - * Thus, you should only change this Prop when absolutely necessary. - * Most importantly: Since the editor holds its own state, you should **not** sync `editorText` with your State. - * Or, in other words: Do not update this Prop whenever {@link onDidChangeEditorText} is called. - * In most applications, it is sufficient to set this Prop once (when the component gets created) and then to just leave it unchanged. - */ - editorText: string; - onDidChangeEditorText: (newText: string) => void; - - /** - * Settings that will be passed to the constructor of the Monaco editor. - * These settings can be used to control the appearance of the editor. - * - * For instance, you can set the color theme of the editor using the following configuration: - * - * ``` ts - * { - * theme: 'vs-dark' - * } - * ``` - * - * A good starting point for experimenting with the editor options is the Monaco Playground, see - * https://microsoft.github.io/monaco-editor/playground.html - * - * **You should memoize this Prop, because whenever it changes, the editor model gets re-created.** - */ - editorConfig?: monaco.editor.IStandaloneEditorConstructionOptions; -} - -export const MonacoEditor: React.FC = (props) => { - return ( - - - - ); -}; - -const MonacoWrapper: React.FC = (props) => { - const [state, setState] = React.useState<{ - model: monaco.editor.ITextModel | undefined; - }>({ - model: undefined, - }); - - React.useEffect(() => { - // For some reason, creating the model too quickly leads to a runtime error saying `Cannot read properties of undefined (reading 'onDidChangeNotification')`. Thus, we create the model "on-the-fly" here. - const model = monaco.editor.createModel(props.editorText, LANGUAGE_NAME); - - setState((state) => ({ ...state, model: model })); - - return () => { - model.dispose(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Whenever the `editorText` Prop changes, update the model. - React.useEffect(() => { - if (!state.model) { - return; - } - - state.model.setValue(props.editorText); - }, [props.editorText, state.model]); - - React.useEffect(() => { - if (!state.model) { - return; - } - - // When the model content (i.e. the text in the editor) changes, call the callback function defined in Props. - state.model.onDidChangeContent(() => { - if (!state.model) { - return; - } - props.onDidChangeEditorText(state.model.getValue()); - }); - }, [props, state.model]); - - React.useEffect(() => { - if (!state.model) { - return; - } - - const defaultEditorConfig: monaco.editor.IStandaloneEditorConstructionOptions = - { - model: state.model, - theme: 'vs-light', - - // Make sure that the editor is automatically resized when the container is resized. - automaticLayout: true, - }; - const editorConfig = { ...defaultEditorConfig, ...props.editorConfig }; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const editor = monaco.editor.create(containerRef.current!, editorConfig); - - return () => { - editor.dispose(); - }; - }, [props.editorConfig, state.model]); - - const containerRef = React.useRef(null); - - return
; -}; - -interface MonacoContextProps extends MonacoEditorProps { - children: React.ReactNode; -} -const MonacoContext: React.FC = (props) => { - React.useLayoutEffect(() => { - const destroy = setUpMonaco(props.startJayveeWorker); - - return () => { - destroy(); - }; - }, [props.startJayveeWorker]); - - return {props.children}; -}; - -function setUpMonaco(startJayveeWorker: () => Worker): () => void { - const disposables: Disposable[] = []; - - window.MonacoEnvironment = {}; - window.MonacoEnvironment.getWorker = (workerId): Worker => { - if (workerId !== 'workerMain.js') { - throw Error('Tried to load a worker with an unknown ID.'); - } - - const worker = new Worker( - new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url), - { - type: 'module', - }, - ); - return worker; - }; - - monaco.languages.register({ - id: LANGUAGE_NAME, - }); - const tokensProvider = monaco.languages.setMonarchTokensProvider( - LANGUAGE_NAME, - JayveeMonarchConfig, - ); - disposables.push(tokensProvider); - - const installedServices = MonacoServices.install() as Disposable; - disposables.push(installedServices); - - StandaloneServices.initialize({ - ...getMessageServiceOverride(), - }); - - const worker = startJayveeWorker(); - const reader = new BrowserMessageReader(worker); - const writer = new BrowserMessageWriter(worker); - - const languageClient = new MonacoLanguageClient({ - name: 'Jayvee Language Client', - clientOptions: { - documentSelector: [{ language: LANGUAGE_NAME }], - errorHandler: { - error: () => ({ action: ErrorAction.Continue }), - closed: () => ({ action: CloseAction.DoNotRestart }), - }, - }, - connectionProvider: { - get: (): Promise => { - return Promise.resolve({ reader, writer }); - }, - }, - }); - - void languageClient.start(); - - return () => { - for (const disposable of disposables) { - disposable.dispose(); - } - - void pollToStopLanguageClient(languageClient, worker); - }; -} - -async function pollToStopLanguageClient( - languageClient: MonacoLanguageClient, - worker: Worker, - attemptsLeft = 5, -): Promise { - if (attemptsLeft < 1) { - console.warn('Failed to stop the language client.'); - return; - } - - if (languageClient.state === State.Running) { - await languageClient.dispose(); - worker.terminate(); - return; - } - - const WAIT_MILLISECONDS = 1000; - window.setTimeout(() => { - void pollToStopLanguageClient(languageClient, worker, attemptsLeft - 1); - }, WAIT_MILLISECONDS); -} diff --git a/tools/scripts/fix-monarch-grammar-escape.mjs b/tools/scripts/fix-monarch-grammar-escape.mjs deleted file mode 100644 index 6eac8c088..000000000 --- a/tools/scripts/fix-monarch-grammar-escape.mjs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg -// -// SPDX-License-Identifier: AGPL-3.0-only - -import {getSourcePath} from "./shared-util.mjs"; -import {readFileSync, writeFileSync} from "fs"; - -// This script solely serves as a temporary workaround until https://github.com/langium/langium/issues/740 is resolved. - -const monarchFilePath = getSourcePath('monaco-editor') + '/lib/jayvee.monarch.ts'; - -const monarchFileContent = readFileSync(monarchFilePath).toString(); - -// Replace unescaped occurrence of '|/'with '|\/': -writeFileSync(monarchFilePath, monarchFileContent.replace(/\|\//g, '|\\/')); diff --git a/tools/scripts/monaco-editor/copy-language-configuration.mjs b/tools/scripts/monaco-editor/copy-language-configuration.mjs new file mode 100644 index 000000000..6ad684cf6 --- /dev/null +++ b/tools/scripts/monaco-editor/copy-language-configuration.mjs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { copyFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { getSourcePath } from '../shared-util.mjs'; + +const confVscode = join( + getSourcePath('vs-code-extension'), + '..', + 'assets', + 'language-configuration.json', +); + +const confMonaco = join( + getSourcePath('monaco-editor'), + 'lib', + 'language-configuration.json', +); + +await copyFile(confVscode, confMonaco); diff --git a/tools/scripts/monaco-editor/copy-textmate-grammar.mjs b/tools/scripts/monaco-editor/copy-textmate-grammar.mjs new file mode 100644 index 000000000..e246f49f7 --- /dev/null +++ b/tools/scripts/monaco-editor/copy-textmate-grammar.mjs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { copyFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { getSourcePath } from '../shared-util.mjs'; + +const tmVscode = join( + getSourcePath('vs-code-extension'), + '..', + 'assets', + 'jayvee.tmLanguage.json', +); + +const tmMonaco = join( + getSourcePath('monaco-editor'), + 'lib', + 'jayvee.tmLanguage.json', +); + +await copyFile(tmVscode, tmMonaco); diff --git a/tools/scripts/monaco-editor/delete-vscode-peer-dependency.mjs b/tools/scripts/monaco-editor/delete-vscode-peer-dependency.mjs deleted file mode 100644 index 1ecb838ce..000000000 --- a/tools/scripts/monaco-editor/delete-vscode-peer-dependency.mjs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg -// -// SPDX-License-Identifier: AGPL-3.0-only - -import { getOutputPath, parsePackageJson, writePackageJson } from "../shared-util.mjs"; - -// Executing this script: node path/to/delete-vscode-peer-dependency.mjs {projectName} -const [, , projectName] = process.argv; -process.chdir(getOutputPath(projectName)); - -const packageJson = parsePackageJson(); - -/* - In our editor, we perform imports from a package called "vscode". - This is however not the true name of the package. - The actual name of the package is `@codingame/monaco-vscode-api`. - `monaco-languageclient` renames this package (for whatever reason), see - https://github.com/TypeFox/monaco-languageclient/blob/c5511b19e95e237c3f95a0fc0588769263f3ba40/packages/client/package.json#L56 - - This is a problem for our bundler, because it seems unable to detect that the package has been renamed. - Thus, it creates an entry in `package.json`, saying that our library depends on `vscode` instead of `@codingame/monaco-vscode-api`. - - Since this package is a peer dependency of `monaco-languageclient` anyways, we can simply remove the entry for `vscode` to fix the problem. -*/ -if (packageJson.peerDependencies) { - delete packageJson.peerDependencies.vscode; -} - -writePackageJson(packageJson); \ No newline at end of file diff --git a/tools/scripts/monaco-editor/relax-react-version.mjs b/tools/scripts/monaco-editor/relax-react-version.mjs deleted file mode 100644 index b84a17665..000000000 --- a/tools/scripts/monaco-editor/relax-react-version.mjs +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg -// -// SPDX-License-Identifier: AGPL-3.0-only - -import { getOutputPath, parsePackageJson, writePackageJson } from "../shared-util.mjs"; - -// Executing this script: node path/to/relax-react-version.mjs {projectName} -const [, , projectName] = process.argv; -process.chdir(getOutputPath(projectName)); - -const packageJson = parsePackageJson(); - -// By default, this value is set to the exact React version we are using. This makes it hard to use the package in environments where a different React version is present. -if (packageJson.peerDependencies) { - packageJson.peerDependencies.react = '>= 17'; -} - -writePackageJson(packageJson); \ No newline at end of file