diff --git a/package-lock.json b/package-lock.json index 92c7231ec..e03cb63d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2628,6 +2628,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/emscripten": { "version": "1.39.13", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", @@ -4475,6 +4485,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/corser": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", @@ -8177,6 +8200,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -11782,6 +11814,7 @@ "@codingame/monaco-vscode-typescript-basics-default-extension": "~10.1.1", "@codingame/monaco-vscode-typescript-language-features-default-extension": "~10.1.1", "@typefox/monaco-editor-react": "~6.0.0-next.5", + "cors": "^2.8.5", "express": "~4.21.1", "langium": "~3.2.0", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~10.1.1", @@ -11801,6 +11834,7 @@ "wtd-core": "~4.0.1" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/emscripten": "~1.39.13", "@types/express": "~5.0.0", "@types/ws": "~8.5.12", diff --git a/packages/examples/package.json b/packages/examples/package.json index 186018acc..9e96a7ca9 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -74,6 +74,7 @@ "@codingame/monaco-vscode-typescript-basics-default-extension": "~10.1.1", "@codingame/monaco-vscode-typescript-language-features-default-extension": "~10.1.1", "@typefox/monaco-editor-react": "~6.0.0-next.5", + "cors": "^2.8.5", "express": "~4.21.1", "langium": "~3.2.0", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~10.1.1", @@ -93,6 +94,7 @@ "wtd-core": "~4.0.1" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/express": "~5.0.0", "@types/ws": "~8.5.12", "@types/emscripten": "~1.39.13", diff --git a/packages/examples/src/json/server/main.ts b/packages/examples/src/json/server/main.ts index f233ad49e..b05b9ae53 100644 --- a/packages/examples/src/json/server/main.ts +++ b/packages/examples/src/json/server/main.ts @@ -4,9 +4,11 @@ * ------------------------------------------------------------------------------------------ */ import { resolve } from 'node:path'; +import cors from 'cors'; + import { runLanguageServer } from '../../common/node/language-server-runner.js'; import { LanguageName } from '../../common/node/server-commons.js'; - +import express from 'express'; export const runJsonServer = (baseDir: string, relativeDir: string) => { const processRunPath = resolve(baseDir, relativeDir); runLanguageServer({ @@ -23,4 +25,22 @@ export const runJsonServer = (baseDir: string, relativeDir: string) => { perMessageDeflate: false } }); + + startMockHttpServerForSavingCodeFromEditor(); +}; + +export const startMockHttpServerForSavingCodeFromEditor = () => { + const app = express(); + app.use(cors()); + app.use(express.json()); + app.post('/save-code', (req, res) => { + const { code } = req.body; + console.log('Received code:', code); + res.json({ success: true, message: code}); + }); + + const PORT = 3003; + app.listen(PORT, () => { + console.log(`JSON server running on port ${PORT}`); + }); }; diff --git a/verify/angular/src/app/app.component.css b/verify/angular/src/app/app.component.css deleted file mode 100644 index 3a7e0476d..000000000 --- a/verify/angular/src/app/app.component.css +++ /dev/null @@ -1,3 +0,0 @@ -.monaco-editor { - height: 50vh; -} diff --git a/verify/angular/src/app/app.component.html b/verify/angular/src/app/app.component.html index a86adb4fd..c5c54137d 100644 --- a/verify/angular/src/app/app.component.html +++ b/verify/angular/src/app/app.component.html @@ -1,4 +1,11 @@ -

Monaco Language Client Angular Client Example

-
-
+

Angular Monaco Editor demo with saving code to a mock HTTP server

+
+ + +
diff --git a/verify/angular/src/app/app.component.scss b/verify/angular/src/app/app.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/verify/angular/src/app/app.component.ts b/verify/angular/src/app/app.component.ts index 77985edaf..0a0b510cf 100644 --- a/verify/angular/src/app/app.component.ts +++ b/verify/angular/src/app/app.component.ts @@ -3,30 +3,50 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ -import { AfterViewInit, Component } from '@angular/core'; -import { MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; +import { AfterViewInit, Component, inject, signal } from '@angular/core'; +import { WrapperConfig } from 'monaco-editor-wrapper'; +import { MonacoAngularWrapperComponent } from '../monaco-angular-wrapper/monaco-angular-wrapper.component'; import { buildJsonClientUserConfig } from 'monaco-languageclient-examples/json-client'; - +import { SaveCodeService } from '../save-code.service'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.css'], - standalone: true + styleUrls: ['./app.component.scss'], + standalone: true, + imports: [MonacoAngularWrapperComponent], }) -export class MonacoEditorComponent implements AfterViewInit { - title = 'angular-client'; - initDone = false; +export class AppComponent implements AfterViewInit { + private saveCodeService = inject(SaveCodeService); + wrapperConfig = signal(undefined); // this can be updated at runtime + + title = 'angular demo for saving code'; + editorId = 'monaco-editor-root'; // this can be parameterized or it can be in a loop to support multiple editors + editorInlineStyle = signal('height: 50vh;'); + readonly codeText = signal(''); async ngAfterViewInit(): Promise { - const wrapper = new MonacoEditorLanguageClientWrapper(); + const editorDom = document.getElementById(this.editorId); + if (editorDom) { + const config = buildJsonClientUserConfig({ + htmlContainer: editorDom, + }); + this.wrapperConfig.set(config); + } + } - const config = buildJsonClientUserConfig({ - htmlContainer: document.getElementById('monaco-editor-root')! - }); + onTextChanged(text: string) { + this.codeText.set(text); + } + + save = async () => { try { - await wrapper.initAndStart(config); - } catch (e) { - console.error(e); + const response = await firstValueFrom( + this.saveCodeService.saveCode(this.codeText()) + ); + alert('Code saved:' + JSON.stringify(response)); + } catch (error) { + console.error('Error saving code:', error); } - } + }; } diff --git a/verify/angular/src/main.ts b/verify/angular/src/main.ts index 40ed6b012..0096b59db 100644 --- a/verify/angular/src/main.ts +++ b/verify/angular/src/main.ts @@ -4,5 +4,8 @@ * ------------------------------------------------------------------------------------------ */ import { bootstrapApplication } from '@angular/platform-browser'; -import { MonacoEditorComponent } from './app/app.component'; -bootstrapApplication(MonacoEditorComponent); +import { provideHttpClient } from '@angular/common/http'; +import { AppComponent } from './app/app.component'; +bootstrapApplication(AppComponent, { + providers: [provideHttpClient()], +}); diff --git a/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.html b/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.html new file mode 100644 index 000000000..25a53b705 --- /dev/null +++ b/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.scss b/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.scss new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.scss @@ -0,0 +1 @@ + diff --git a/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.ts b/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.ts new file mode 100644 index 000000000..fd1b5a1d6 --- /dev/null +++ b/verify/angular/src/monaco-angular-wrapper/monaco-angular-wrapper.component.ts @@ -0,0 +1,95 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { + Component, + effect, + EventEmitter, + input, + OnDestroy, + Output, +} from '@angular/core'; + +import * as monaco from 'monaco-editor'; +import { + MonacoEditorLanguageClientWrapper, + TextChanges, + TextModels, + WrapperConfig , didModelContentChange +} from 'monaco-editor-wrapper'; + +@Component({ + standalone: true, + selector: 'monaco-angular-wrapper', + templateUrl: './monaco-angular-wrapper.component.html', + styleUrls: ['./monaco-angular-wrapper.component.scss'], +}) +export class MonacoAngularWrapperComponent implements OnDestroy { + @Output() onTextChanged = new EventEmitter(); + wrapperConfig = input(); + monacoEditorId = input(); + editorInlineStyle = input(); + private wrapper: MonacoEditorLanguageClientWrapper = + new MonacoEditorLanguageClientWrapper(); + private _subscription: monaco.IDisposable | null = null; + private isRestarting?: Promise; + + constructor() { + effect(async () => { + try { + if (this.wrapperConfig() !== undefined) { + await this.wrapper.initAndStart( + this.wrapperConfig() as WrapperConfig + ); + this.handleOnTextChanged(); + } + } catch (e) { + console.error(e); + } + }); + } + + protected async destroyMonaco(): Promise { + if (this.isRestarting) { + await this.isRestarting; + } + try { + await this.wrapper.dispose(); + } catch { + // The language client may throw an error during disposal. + // This should not prevent us from continue working. + console.error('Error during disposal of the language client.'); + } + if (this._subscription) { + this._subscription.dispose(); + } + } + + async ngOnDestroy() { + this.destroyMonaco(); + } + + handleOnTextChanged() { + const wrapperConfig = this.wrapperConfig(); + const textModels = this.wrapper.getTextModels(); + if (textModels?.text !== undefined && wrapperConfig !== undefined) { + const newSubscriptions: monaco.IDisposable[] = []; + this.emitCodeChange(textModels, wrapperConfig); + newSubscriptions.push( + textModels.text.onDidChangeContent(() => { + this.emitCodeChange(textModels, wrapperConfig); + }) + ); + } + } + + emitCodeChange(textModels: TextModels , wrapperConfig: WrapperConfig ) { + const onTextChanged = (textChanges: TextChanges) => { + this.onTextChanged.emit(textChanges.text); + }; + didModelContentChange(textModels, wrapperConfig.editorAppConfig.codeResources, onTextChanged); + } + +} diff --git a/verify/angular/src/save-code.service.ts b/verify/angular/src/save-code.service.ts new file mode 100644 index 000000000..7e2eaa91f --- /dev/null +++ b/verify/angular/src/save-code.service.ts @@ -0,0 +1,14 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +@Injectable({ providedIn: 'root' }) +export class SaveCodeService { + private http = inject(HttpClient); + saveCode(codeText: string) { + return this.http.post('http://localhost:3003/save-code', { code: codeText }); + } +}