From e255f8b3a30303f609448429327abb3f28efa45f Mon Sep 17 00:00:00 2001 From: Luca Forstner <8118419+lforst@users.noreply.github.com> Date: Mon, 27 Sep 2021 19:48:34 +0200 Subject: [PATCH] Rework VS Code integration API This commit reworks a large part of the VS Code integration package by updating the user-facing API and adding editor features. At the heart of the user-facing API lies the "GlspVscodeConnector". This component acts as a bridge between a diagram client and a graphical language server implementation, both of which the extension authors provide via the connector's options. The connector will propagate any message sent by the server to the client and vice-versa. It intercepts messages of certain types to add VS Code native functionality, for example, it processes the "setMarkers" action sent by the server to render diagnostics within VS Code. The VS Code connector exposes methods to interact with active clients. The "sendActionToActiveClient" method can be used to send various GLSP-Protocol Actions to the currently active client. Extension authors can use this action to interact with the diagram through user-triggered VS Code commands. Extension authors can use interceptor functions in the connector configuration to control how the VS Code integration behaves when it receives messages. The new package API now provides "quickstart components" that reduce the necessary boilerplate code to get started. Quickstart components include a helper class to launch the default GLSP language server, a default editor provider to render webviews, and a helper class to connect a language server to the VS Code integration. Complete Changelog: - Added native editor functionality - Clipboard support - SVG Export - Native menu contributions - Rework API for extension authors - Component to bridge client and server (GlspVscodeConnector) - Interceptors - Method to interact with currently active diagram editor (sendActionToActiveClient) - Exposure of currently selected elements within the diagram editor - Add quickstart components - GLSP server launcher - Default editor provider - Helper class to connect language server to VS Code integration - Add documentation --- example/workflow/extension/package.json | 104 +++- .../extension/src/workflow-editor-provider.ts | 64 ++ .../extension/src/workflow-extension.ts | 98 ++- .../workflow-glsp-diagram-editor-context.ts | 64 -- example/workflow/scripts/download.ts | 16 +- .../src/glsp-starter.ts | 20 +- .../src/glsp-vscode-diagramserver.ts | 16 +- packages/vscode-integration/README.md | 571 +++++++++++++++++- packages/vscode-integration/package.json | 1 - .../src/action/action-handler.ts | 33 - .../src/action/external-navigation.ts | 62 -- .../action.ts} | 27 +- .../export.ts} | 31 +- .../external-navigation.ts} | 29 +- .../src/{action => actions}/index.ts | 11 +- .../src/{action => actions}/markers.ts | 5 +- .../src/actions/navigation.ts | 60 ++ .../src/{action => actions}/operation.ts | 5 +- .../action.ts => actions/save-state.ts} | 36 +- .../navigation.ts => actions/selection.ts} | 23 +- .../vscode-integration/src/glsp-commands.ts | 46 -- .../src/glsp-diagram-document.ts | 144 ----- .../src/glsp-diagram-editor-context.ts | 136 ----- .../src/glsp-diagram-editor-provider.ts | 100 --- .../src/glsp-vscode-connector.ts | 461 ++++++++++++++ .../vscode-integration/src/glsp-webview.ts | 228 ------- packages/vscode-integration/src/index.ts | 11 +- .../java-socket-server-connection-provider.ts | 150 ----- .../glsp-editor-provider.ts | 159 +++++ .../glsp-server-launcher.ts | 124 ++++ .../{utils => quickstart-components}/index.ts | 10 +- .../socket-glsp-vscode-server.ts | 114 ++++ .../src/server-connection-provider.ts | 21 - packages/vscode-integration/src/types.ts | 197 ++++++ .../src/utils/disposable.ts | 58 -- .../vscode-integration/src/utils/promise.ts | 31 - yarn.lock | 2 +- 37 files changed, 2040 insertions(+), 1228 deletions(-) create mode 100644 example/workflow/extension/src/workflow-editor-provider.ts delete mode 100644 example/workflow/extension/src/workflow-glsp-diagram-editor-context.ts delete mode 100644 packages/vscode-integration/src/action/action-handler.ts delete mode 100644 packages/vscode-integration/src/action/external-navigation.ts rename packages/vscode-integration/src/{action/action-dispatcher.ts => actions/action.ts} (57%) rename packages/vscode-integration/src/{utils/glsp-java-server-args.ts => actions/export.ts} (54%) rename packages/vscode-integration/src/{utils/glsp-env-var.ts => actions/external-navigation.ts} (55%) rename packages/vscode-integration/src/{action => actions}/index.ts (87%) rename packages/vscode-integration/src/{action => actions}/markers.ts (93%) create mode 100644 packages/vscode-integration/src/actions/navigation.ts rename packages/vscode-integration/src/{action => actions}/operation.ts (95%) rename packages/vscode-integration/src/{action/action.ts => actions/save-state.ts} (64%) rename packages/vscode-integration/src/{action/navigation.ts => actions/selection.ts} (60%) delete mode 100644 packages/vscode-integration/src/glsp-commands.ts delete mode 100644 packages/vscode-integration/src/glsp-diagram-document.ts delete mode 100644 packages/vscode-integration/src/glsp-diagram-editor-context.ts delete mode 100644 packages/vscode-integration/src/glsp-diagram-editor-provider.ts create mode 100644 packages/vscode-integration/src/glsp-vscode-connector.ts delete mode 100644 packages/vscode-integration/src/glsp-webview.ts delete mode 100644 packages/vscode-integration/src/java-socket-server-connection-provider.ts create mode 100644 packages/vscode-integration/src/quickstart-components/glsp-editor-provider.ts create mode 100644 packages/vscode-integration/src/quickstart-components/glsp-server-launcher.ts rename packages/vscode-integration/src/{utils => quickstart-components}/index.ts (81%) create mode 100644 packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts delete mode 100644 packages/vscode-integration/src/server-connection-provider.ts create mode 100644 packages/vscode-integration/src/types.ts delete mode 100644 packages/vscode-integration/src/utils/disposable.ts delete mode 100644 packages/vscode-integration/src/utils/promise.ts diff --git a/example/workflow/extension/package.json b/example/workflow/extension/package.json index ff69ffe..b7d136b 100644 --- a/example/workflow/extension/package.json +++ b/example/workflow/extension/package.json @@ -41,60 +41,128 @@ ], "commands": [ { - "command": "workflow.diagram.fit", + "command": "workflow.fit", "title": "Fit to Screen", "category": "Workflow Diagram", - "enablement": "glsp-workflow-diagram-focused" + "enablement": "activeCustomEditorId == 'workflow.glspDiagram'" }, { - "command": "workflow.diagram.center", + "command": "workflow.center", "title": "Center selection", "category": "Workflow Diagram", - "enablement": "glsp-workflow-diagram-focused" + "enablement": "activeCustomEditorId == 'workflow.glspDiagram'" }, { - "command": "workflow.diagram.layout", + "command": "workflow.layout", "title": "Layout diagram", "category": "Workflow Diagram", - "enablement": "glsp-workflow-diagram-focused" + "enablement": "activeCustomEditorId == 'workflow.glspDiagram'" }, { "command": "workflow.goToNextNode", "title": "Go to next Node", "category": "Workflow Navigation", - "enablement": "glsp-workflow-diagram-focused" + "enablement": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" }, { "command": "workflow.goToPreviousNode", "title": "Go to previous Node", "category": "Workflow Navigation", - "enablement": "glsp-workflow-diagram-focused" + "enablement": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" }, { "command": "workflow.showDocumentation", "title": "Show documentation...", "category": "Workflow Diagram", - "enablement": "glsp-workflow-diagram-focused" + "enablement": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" + }, + { + "command": "workflow.exportAsSVG", + "title": "Export as SVG", + "category": "Workflow Diagram", + "enablement": "activeCustomEditorId == 'workflow.glspDiagram'" } ], + "submenus": [ + { + "id": "workflow.editor.title", + "label": "Diagram" + } + ], + "menus": { + "editor/title": [ + { + "submenu": "workflow.editor.title", + "group": "bookmarks" + }, + { + "command": "workflow.goToNextNode", + "group": "navigation", + "when": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" + }, + { + "command": "workflow.goToPreviousNode", + "group": "navigation", + "when": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" + }, + { + "command": "workflow.showDocumentation", + "group": "navigation", + "when": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" + } + ], + "workflow.editor.title": [ + { + "command": "workflow.fit", + "group": "navigation", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" + }, + { + "command": "workflow.center", + "group": "navigation", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" + }, + { + "command": "workflow.layout", + "group": "navigation", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" + }, + { + "command": "workflow.exportAsSVG", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" + } + ] + }, "keybindings": [ { "key": "alt+f", "mac": "alt+f", - "command": "workflow.diagram.fit", - "when": "glsp-workflow-diagram-focused" + "command": "workflow.fit", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" }, { "key": "alt+c", "mac": "alt+c", - "command": "workflow.diagram.center", - "when": "glsp-workflow-diagram-focused" + "command": "workflow.center", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" }, { "key": "alt+l", "mac": "alt+l", - "command": "workflow.diagram.layout", - "when": "glsp-workflow-diagram-focused" + "command": "workflow.layout", + "when": "activeCustomEditorId == 'workflow.glspDiagram'" + }, + { + "key": "Ctrl+4", + "mac": "cmd+4", + "command": "workflow.goToNextNode", + "when": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" + }, + { + "key": "Ctrl+3", + "mac": "cmd+3", + "command": "workflow.goToPreviousNode", + "when": "activeCustomEditorId == 'workflow.glspDiagram' && workflow.editorSelectedElementsAmount == 1" } ] }, @@ -109,15 +177,15 @@ ], "main": "./lib/workflow-extension", "devDependencies": { + "@eclipse-glsp/vscode-integration": "0.9.0", "@types/node": "^8.0.0", "path": "^0.12.7", "rimraf": "^2.6.3", - "@eclipse-glsp/vscode-integration": "0.9.0", "ts-loader": "^6.2.1", "ts-node": "^9.1.1", - "workflow-glsp-webview": "0.9.0", "typescript": "^3.1.3", - "vscode": "^1.1.21" + "vscode": "^1.1.21", + "workflow-glsp-webview": "0.9.0" }, "scripts": { "prepare": "npx vscode-install && yarn clean && yarn build && yarn lint", diff --git a/example/workflow/extension/src/workflow-editor-provider.ts b/example/workflow/extension/src/workflow-editor-provider.ts new file mode 100644 index 0000000..d2104e1 --- /dev/null +++ b/example/workflow/extension/src/workflow-editor-provider.ts @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.01 + ********************************************************************************/ + +import * as vscode from 'vscode'; +import * as path from 'path'; + +import { GlspVscodeConnector } from '@eclipse-glsp/vscode-integration'; +import { GlspEditorProvider } from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; + +export default class WorkflowEditorProvider extends GlspEditorProvider { + diagramType = 'workflow-diagram'; + + constructor( + protected readonly extensionContext: vscode.ExtensionContext, + protected readonly glspVscodeConnector: GlspVscodeConnector + ) { + super(glspVscodeConnector); + } + + setUpWebview(_document: vscode.CustomDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken, clientId: string): void { + const localResourceRootsUri = vscode.Uri.file( + path.join(this.extensionContext.extensionPath, './pack') + ); + + const webviewScriptSourceUri = vscode.Uri.file( + path.join(this.extensionContext.extensionPath, './pack/webview.js') + ); + + webviewPanel.webview.options = { + localResourceRoots: [localResourceRootsUri], + enableScripts: true + }; + + webviewPanel.webview.html = ` + + + + + + + + +
+ + + `; + } +} diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 4845105..9925433 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -13,37 +13,99 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GlspDiagramEditorContext, NavigateAction } from '@eclipse-glsp/vscode-integration'; -import { join, resolve } from 'path'; + import * as vscode from 'vscode'; +import * as process from 'process'; +import * as path from 'path'; + +import { + GlspVscodeConnector, + NavigateAction, + LayoutOperation, + FitToScreenAction, + CenterAction, + RequestExportSvgAction +} from '@eclipse-glsp/vscode-integration'; + +import { + GlspServerLauncher, + SocketGlspVscodeServer +} from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; + +import WorkflowEditorProvider from './workflow-editor-provider'; + +const DEFAULT_SERVER_PORT = '5007'; -import { WorkflowGlspDiagramEditorContext } from './workflow-glsp-diagram-editor-context'; +export async function activate(context: vscode.ExtensionContext): Promise { + // Start server process using quickstart component + if (process.env.GLSP_SERVER_DEBUG !== 'true') { + const serverProcess = new GlspServerLauncher({ + jarPath: path.join(__dirname, '../server/org.eclipse.glsp.example.workflow-0.9.0-SNAPSHOT-glsp.jar'), + serverPort: JSON.parse(process.env.GLSP_SERVER_PORT || DEFAULT_SERVER_PORT), + additionalArgs: ['--fileLog', 'true', '--logDir', path.join(__dirname, '../server')], + logging: true + }); + context.subscriptions.push(serverProcess); + await serverProcess.start(); + } + + // Wrap server with quickstart component + const workflowServer = new SocketGlspVscodeServer({ + clientId: 'glsp.workflow', + clientName: 'workflow', + serverPort: JSON.parse(process.env.GLSP_SERVER_PORT || DEFAULT_SERVER_PORT) + }); -export const SERVER_DIR = join(__dirname, '..', 'server'); -export const JAR_FILE = resolve(join(SERVER_DIR, 'org.eclipse.glsp.example.workflow-0.9.0-SNAPSHOT-glsp.jar')); + // Initialize GLSP-VSCode connector with server wrapper + const glspVscodeConnector = new GlspVscodeConnector({ + server: workflowServer, + logging: true + }); -let editorContext: GlspDiagramEditorContext; + const customEditorProvider = vscode.window.registerCustomEditorProvider( + 'workflow.glspDiagram', + new WorkflowEditorProvider(context, glspVscodeConnector), + { + webviewOptions: { retainContextWhenHidden: true }, + supportsMultipleEditorsPerDocument: false + } + ); -export function activate(context: vscode.ExtensionContext): void { - editorContext = new WorkflowGlspDiagramEditorContext(context); + context.subscriptions.push(workflowServer, glspVscodeConnector, customEditorProvider); + workflowServer.start(); + // Keep track of selected elements + let selectedElements: string[] = []; context.subscriptions.push( + glspVscodeConnector.onSelectionUpdate(n => { + selectedElements = n; + vscode.commands.executeCommand('setContext', 'workflow.editorSelectedElementsAmount', n.length); + }) + ); + + // Register various commands + context.subscriptions.push( + vscode.commands.registerCommand('workflow.fit', () => { + glspVscodeConnector.sendActionToActiveClient(new FitToScreenAction(selectedElements)); + }), + vscode.commands.registerCommand('workflow.center', () => { + glspVscodeConnector.sendActionToActiveClient(new CenterAction(selectedElements)); + }), + vscode.commands.registerCommand('workflow.layout', () => { + glspVscodeConnector.sendActionToActiveClient(new LayoutOperation()); + }), vscode.commands.registerCommand('workflow.goToNextNode', () => { - editorContext.dispatchActionToWebview(new NavigateAction('next')); + glspVscodeConnector.sendActionToActiveClient(new NavigateAction('next')); }), vscode.commands.registerCommand('workflow.goToPreviousNode', () => { - editorContext.dispatchActionToWebview(new NavigateAction('previous')); + glspVscodeConnector.sendActionToActiveClient(new NavigateAction('previous')); }), vscode.commands.registerCommand('workflow.showDocumentation', () => { - editorContext.dispatchActionToWebview(new NavigateAction('documentation')); + glspVscodeConnector.sendActionToActiveClient(new NavigateAction('documentation')); + }), + vscode.commands.registerCommand('workflow.exportAsSVG', () => { + glspVscodeConnector.sendActionToActiveClient(new RequestExportSvgAction()); }) ); } -export function deactivate(): Thenable { - if (!editorContext) { - return Promise.resolve(undefined); - } - return editorContext.deactivateGLSPClient(); -} - diff --git a/example/workflow/extension/src/workflow-glsp-diagram-editor-context.ts b/example/workflow/extension/src/workflow-glsp-diagram-editor-context.ts deleted file mode 100644 index faa77b4..0000000 --- a/example/workflow/extension/src/workflow-glsp-diagram-editor-context.ts +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { - GlspDiagramEditorContext, - GLSPEnvVariable, - GLSPJavaServerArgs, - GLSPWebView, - JavaSocketServerConnectionProvider -} from '@eclipse-glsp/vscode-integration'; -import { ServerConnectionProvider } from '@eclipse-glsp/vscode-integration/lib/server-connection-provider'; -import { join, resolve } from 'path'; -import { SprottyDiagramIdentifier } from 'sprotty-vscode-protocol'; -import * as vscode from 'vscode'; - -export const DEFAULT_PORT = 5007; -export const PORT_ARG_KEY = 'WF_GLSP'; -export const SERVER_DIR = join(__dirname, '..', 'server'); -export const JAR_FILE = resolve(join(SERVER_DIR, 'org.eclipse.glsp.example.workflow-0.9.0-SNAPSHOT-glsp.jar')); - -export class WorkflowGlspDiagramEditorContext extends GlspDiagramEditorContext { - readonly id = 'glsp.workflow'; - readonly diagramType = 'workflow-diagram'; - - get extensionPrefix(): string { - return 'workflow'; - } - - protected getConnectionProvider(): ServerConnectionProvider { - const launchOptions = { - jarPath: JAR_FILE, - serverPort: GLSPEnvVariable.getServerPort() || DEFAULT_PORT, - isRunning: GLSPEnvVariable.isServerDebug(), - noConsoleLog: true, - additionalArgs: GLSPJavaServerArgs.enableFileLogging(SERVER_DIR) - }; - return new JavaSocketServerConnectionProvider(launchOptions); - } - - createWebview(webviewPanel: vscode.WebviewPanel, identifier: SprottyDiagramIdentifier): GLSPWebView { - const webview = new GLSPWebView({ - editorContext: this, - identifier, - localResourceRoots: [ - this.getExtensionFileUri('pack') - ], - scriptUri: this.getExtensionFileUri('pack', 'webview.js'), - webviewPanel - }); - return webview; - } -} diff --git a/example/workflow/scripts/download.ts b/example/workflow/scripts/download.ts index 1f892f4..8a21e1f 100644 --- a/example/workflow/scripts/download.ts +++ b/example/workflow/scripts/download.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import download from 'mvn-artifact-download'; import { join } from 'path'; +import { existsSync } from 'fs'; const downloadDir = join(__dirname, '../extension/server'); const mavenRepository = 'https://oss.sonatype.org/content/repositories/snapshots/'; @@ -23,8 +24,13 @@ const artifactId = 'org.eclipse.glsp.example.workflow'; const version = '0.9.0'; const classifier = 'glsp'; -console.log('Downloading latest version of the Workflow Example Java Server from the maven repository...'); -download({ groupId, artifactId, version, classifier, isSnapShot: true }, downloadDir, mavenRepository) - .then(() => console.log('Download completed. Start the server using this command: \njava -jar org.eclipse.glsp.example.workflow-' - + version + '-SNAPSHOT-glsp.jar org.eclipse.glsp.example.workflow.launch.ExampleServerLauncher\n\n')) - .catch(err => console.error(err)); +if (!existsSync(`${__dirname}/../extension/server/${artifactId}-${version}-SNAPSHOT-${classifier}.jar`)) { + console.log('Downloading latest version of the Workflow Example Java Server from the maven repository...'); + download({ groupId, artifactId, version, classifier, isSnapShot: true }, downloadDir, mavenRepository) + .then(() => console.log('Download completed. Start the server using this command: \njava -jar org.eclipse.glsp.example.workflow-' + + version + '-SNAPSHOT-glsp.jar org.eclipse.glsp.example.workflow.launch.ExampleServerLauncher\n\n')) + .catch(err => console.error(err)); +} else { + console.log('Server jar already exists. Skipping download.'); +} + diff --git a/packages/vscode-integration-webview/src/glsp-starter.ts b/packages/vscode-integration-webview/src/glsp-starter.ts index 6dd1a55..da81e13 100644 --- a/packages/vscode-integration-webview/src/glsp-starter.ts +++ b/packages/vscode-integration-webview/src/glsp-starter.ts @@ -13,8 +13,18 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { DiagramServer, NavigateToExternalTargetAction, TYPES } from '@eclipse-glsp/client'; import { Container } from 'inversify'; +import { + DiagramServer, + NavigateToExternalTargetAction, + ExportSvgAction, + SelectAction, + TYPES +} from '@eclipse-glsp/client'; +import { + RequestClipboardDataAction, + SetClipboardDataAction +} from '@eclipse-glsp/client/lib/features/copy-paste/copy-paste-actions'; import { SprottyDiagramIdentifier, SprottyStarter, @@ -52,6 +62,12 @@ export abstract class GLSPStarter extends SprottyStarter { * All kinds of actions that should (also) be delegated to and handled by the vscode extension */ protected get extensionActionKinds(): string[] { - return [NavigateToExternalTargetAction.KIND]; + return [ + NavigateToExternalTargetAction.KIND, + RequestClipboardDataAction.KIND, + SetClipboardDataAction.KIND, + SelectAction.KIND, + ExportSvgAction.KIND + ]; } } diff --git a/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts b/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts index 6b43c33..03b3f44 100644 --- a/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts +++ b/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts @@ -23,7 +23,8 @@ import { isDeleteElementOperation, isSetEditModeAction, registerDefaultGLSPServerActions, - SetEditModeAction + SetEditModeAction, + ICopyPasteHandler } from '@eclipse-glsp/client'; import { SelectionService } from '@eclipse-glsp/client/lib/features/select/selection-service'; import { inject } from 'inversify'; @@ -34,6 +35,7 @@ export const localDispatchProperty = '__localDispatch'; export class GLSPVscodeDiagramServer extends VscodeDiagramServer { @inject(GLSP_TYPES.SelectionService) protected selectionService: SelectionService; + @inject(GLSP_TYPES.ICopyPasteHandler) protected copyPasteHandler: ICopyPasteHandler; initialize(registry: ActionHandlerRegistry): void { registerDefaultGLSPServerActions(registry, this); @@ -43,6 +45,18 @@ export class GLSPVscodeDiagramServer extends VscodeDiagramServer { this.messageReceived(message.data); } }); + + window.addEventListener('copy', (e: ClipboardEvent) => { + this.copyPasteHandler.handleCopy(e); + }); + + window.addEventListener('cut', (e: ClipboardEvent) => { + this.copyPasteHandler.handleCut(e); + }); + + window.addEventListener('paste', (e: ClipboardEvent) => { + this.copyPasteHandler.handlePaste(e); + }); } handleLocally(action: Action): boolean { diff --git a/packages/vscode-integration/README.md b/packages/vscode-integration/README.md index 389e7f6..c9c0e73 100644 --- a/packages/vscode-integration/README.md +++ b/packages/vscode-integration/README.md @@ -1,16 +1,571 @@ # Eclipse GLSP VSCode Integration +This package contains the glue code for integrating [GLSP](https://www.eclipse.org/glsp/) +diagrams in VS Code. This library enables the implementation of GLSP Diagram editors +for VS Code base on the [Custom Editor API](https://code.visualstudio.com/api/extension-guides/custom-editors). -This package contains the glue code for integrating [GLSP](https://www.eclipse.org/glsp/) diagrams in VS Code. This libaray enables -the implementation of GLSP Diagram editors for VS Code base on the [Custom Editor API](https://code.visualstudio.com/api/extension-guides/custom-editors). - - -### Where to find the sources? - +## Where to find the sources? In addition to this repository, the related source code can be found here: - https://github.com/eclipse-glsp/glsp-server - https://github.com/eclipse-glsp/glsp-client -## More information +## Getting started +This section will show you how to get your first GLPS extension up and running using +the *GLSP VSCode Integration*. + +### Example Extension +You can find a complete example extension that uses this package +[here](https://github.com/eclipse-glsp/glsp-vscode-integration/tree/master/example/workflow). +It makes heavy use of default implementations like the default GLSP server, the +default GLSP client and the default GLSP Sprotty client. + +### Example Code +There are a few steps that are absolutely necessary for the GLSP VSCode Integration +to work. They are outlined in this section. + +#### Extension +First you need to set up your extension starter code. This is done by setting the +`"main"` field of your `package.json` to the entry point of your extension and exporting +an `activate()` function from that file. For more information on how to set up your +VSCode extension please visit https://code.visualstudio.com/api. + +
Code Example + +```typescript +import * as vscode from 'vscode'; + +export async function activate(context: vscode.ExtensionContext): Promise { + // Your extension code here. +} +``` +
+ +#### Server +Next we will start a GLSP server from within the activate function. If you have +already have the server running through some other process you can skip this step. + +If you are using the default GLSP server implementation provided at https://github.com/eclipse-glsp/glsp-server, +you can use the `GlspServerLauncher` [quickstart component](#Quickstart-Components) +to start the server with very little code: + +
Code Example + +```typescript +import { GlspServerLauncher } from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; + +export async function activate(context: vscode.ExtensionContext): Promise { + const serverProcess = new GlspServerLauncher({ + jarPath: '/your/path/to/server.jar', + serverPort: 5007 + }); + context.subscriptions.push(serverProcess); + await serverProcess.start(); +} +``` +
+ +#### Server Interface and GLSP VSCode Connector +A connector class is needed to provide an interface for the *GLSP VSCode integration* +to communicate with the server. If you are using the default GLSP server which communicates +over JSON-RPC, you can make use of the `SocketGlspVscodeServer` [quickstart component](#Quickstart-Components) +to implement the needed interface with very little boilerplate code. + +If we have a server component providing the needed interface we can create an instance +of the `GlspVscodeConnector` and pass the server component. The `GlspVscodeConnector` +lies at the core of this package and provides all the needed functionality. + +
Code Example + +```typescript +import { GlspVscodeConnector } from '@eclipse-glsp/vscode-integration'; +import { SocketGlspVscodeServer } from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; + +export async function activate(context: vscode.ExtensionContext): Promise { + // (Server startup code from above here...) + + const workflowServer = new SocketGlspVscodeServer({ + clientId: 'some.client.id', + clientName: 'SomeClientName', + serverPort: 5007 + }); + + const glspVscodeConnector = new GlspVscodeConnector({ server: workflowServer }); + + context.subscriptions.push(workflowServer, glspVscodeConnector) +} +``` +
+ +#### Custom Editor Provider +In order to have a custom editor in VSCode a component implementing the `vscode.CustomEditorProvider` +needs to be registered from within the extension (more information on custom editors +[here](https://code.visualstudio.com/api/extension-guides/custom-editors)). + +The GLSP VSCode integration package gives you free reign over how you implement +your `CustomEditorProvider`, however a few function calls at certain places are +needed for the integration to work properly: + +- The `onDidChangeCustomDocument` of your `CustomEditorProvider` should always fire + at least when `GlspVscodeConnector.onDidChangeCustomDocument` fires. +- `GlspVscodeConnector.saveDocument(document)` should be called when `CustomEditorProvider.saveCustomDocument` + is called. +- `GlspVscodeConnector.saveDocument(document, destination)` should be called when + `CustomEditorProvider.saveCustomDocumentAs` is called. +- `GlspVscodeConnector.revertDocument()` should be called when `CustomEditorProvider.revertCustomDocument` + is called. + +You can use the `GlspEditorProvider` [quickstart component](#Quickstart-Components) +to set up such an editor provider without much boilerplate code. + +If you chose to create a `CustomEditorProvider` yourself, the `resolveCustomEditor` +function of the `CustomEditorProvider` act as an excellent place to register your +GLSP clients. You can do this with the `GlspVscodeConnector.registerClient(client)` +function. You are free to choose on how your clients implement the needed interface, +however if you need inspiration on how to do it with the default GLSP components, +you can take a look at the example +[here](https://github.com/eclipse-glsp/glsp-vscode-integration/tree/master/example/workflow). + +
Code Example + +```typescript +import MyCustomEditorProvider from './my-custom-editor-provider.ts'; + +export async function activate(context: vscode.ExtensionContext): Promise { + // (Server startup code from above here...) + // (GlspVscodeConnector code from above here...) + + const customEditorProvider = vscode.window.registerCustomEditorProvider( + 'your.custom.editor', + new MyCustomEditorProvider(glspVscodeConnector), + { + webviewOptions: { retainContextWhenHidden: true }, + supportsMultipleEditorsPerDocument: false + } + ); + + context.subscriptions.push(customEditorProvider); +} +``` + +```typescript +// my-custom-editor-provider.ts +export default class WorkflowEditorProvider implements vscode.CustomEditorProvider { + + onDidChangeCustomDocument: vscode.Event>; + + constructor( + private readonly glspVscodeConnector: GlspVscodeConnector + ) { + this.onDidChangeCustomDocument = glspVscodeConnector.onDidChangeCustomDocument; // necessary + } + + // (Any other methods needed for the vscode.CustomEditorProvider interface here.) + + saveCustomDocument(document: vscode.CustomDocument, cancellation: vscode.CancellationToken): Thenable { + return this.glspVscodeConnector.saveDocument(document); // necessary + } + + saveCustomDocumentAs(document: vscode.CustomDocument, destination: vscode.Uri, cancellation: vscode.CancellationToken): Thenable { + return this.glspVscodeConnector.saveDocument(document, destination); // necessary + } + + revertCustomDocument(document: vscode.CustomDocument, cancellation: vscode.CancellationToken): Thenable { + return this.glspVscodeConnector.revertDocument(document, 'your.diagram.type'); // necessary + } + + resolveCustomEditor(document: + vscode.CustomDocument, + webviewPanel: vscode.WebviewPanel, + token: vscode.CancellationToken + ): void | Thenable { + + const onSendToClientEmitter = new vscode.EventEmitter(); + const onClientMessage = new vscode.EventEmitter(); + + // (Your code to send event content to webview here using onSendToClientEmitter.) + // (Your code to emit messages from client with onClientSendEmitter.) + + this.glspVscodeConnector.registerClient({ + clientId: 'your.glsp.client.id.here', // Should be different each time resolve custom Editor is called - must be equal to the ids the client will send in its messages + document: document, + webviewPanel: webviewPanel, + onClientMessage: onClientSendEmitter.event, + onSendToClientEmitter: onSendToClientEmitter + }); + + webviewPanel.webview.html = `(Your webview HTML here)`; + } +} +``` +
+ +#### Final touches +All that's left to do is a final call to start the server and the extension +should be up and running. + +
Code Example + +```typescript + +export async function activate(context: vscode.ExtensionContext): Promise { + // (Server startup code from above here...) + // (GlspVscodeConnector code from above here...) + // (CustomEditorProvider code from above here...) + + workflowServer.start(); +} +``` +
-For more information, please visit the [Eclipse GLSP Umbrella repository](https://github.com/eclipse-glsp/glsp) and the [Eclipse GLSP Website](https://www.eclipse.org/glsp/). If you have questions, contact us on our [spectrum chat](https://spectrum.chat/glsp/) and have a look at our [communication and support options](https://www.eclipse.org/glsp/contact/). +## API +This package exports a number of members, the most important one being the `GlspVscodeConnector`-Class. + +### GlspVscodeConnector +This is the core of the VSCode integration and provides various functionality. It +primarily intercepts certain GLSP Actions sent from the clients or server to trigger +VSCode specific contributions. This currently includes: + +- File dirty state +- File "Save" and "Save as..." +- File reverting +- Diagnostics or "markers" and "validations" +- External target navigation +- Exporting as SVG (with dialog window) +- Providing element selection context to extensions + +#### Options +The `GlspVscodeConnector` takes one constructor argument - an object containing its configuration. + +```typescript +export interface GlspVscodeConnectorOptions { + + /** + * The GLSP server (or its wrapper) that the VSCode integration should use. + */ + server: GlspVscodeServer; + + /** + * Wether the GLSP-VSCode integration should log various events. This is useful + * if you want to find out what events the VSCode integration is receiving from + * and sending to the server and clients. + * + * Defaults to `false`. + */ + logging?: boolean; + + /** + * Optional property to intercept (and/or modify) messages sent from the client + * to the VSCode integration via `GlspVscodeClient.onClientSend`. + * + * @param message Contains the original message sent by the client. + * @param callback A callback to control how messages are handled further. + */ + onBeforeReceiveMessageFromClient?: (message: unknown, callback: InterceptorCallback) => void; + + /** + * Optional property to intercept (and/or modify) messages sent from the server + * to the VSCode integration via `GlspVscodeServer.onServerSend`. + * + * @param message Contains the original message sent by the client. + * @param callback A callback to control how messages are handled further. + */ + onBeforeReceiveMessageFromServer?(message: unknown, callback: InterceptorCallback): void; + + /** + * Optional property to intercept (and/or modify) messages sent from the VSCode + * integration to the server via the `GlspVscodeServer.onServerReceiveEmitter`. + * + * The returned value from this function is the message that will be propagated + * to the server. It can be modified to anything. Returning `undefined` will + * cancel the propagation. + * + * @param originalMessage The original message received by the VSCode integration + * from the client. + * @param processedMessage If the VSCode integration modified the received message + * in any way, this parameter will contain the modified message. If the VSCode + * integration did not modify the message, this parameter will be identical to + * `originalMessage`. + * @param messageChanged This parameter will indicate wether the VSCode integration + * modified the incoming message. In other words: Whether `originalMessage` + * and `processedMessage` are different. + * @returns A message that will be propagated to the server. If the message + * is `undefined` the propagation will be cancelled. + */ + onBeforePropagateMessageToServer?( + originalMessage: unknown, processedMessage: unknown, messageChanged: boolean + ): unknown | undefined; + + /** + * Optional property to intercept (and/or modify) messages sent from the VSCode + * integration to a client via the `GlspVscodeClient.onClientReceiveEmitter`. + * + * The returned value from this function is the message that will be propagated + * to the client. It can be modified to anything. Returning `undefined` will + * cancel the propagation. + * + * @param originalMessage The original message received by the VSCode integration + * from the server. + * @param processedMessage If the VSCode integration modified the received message + * in any way, this parameter will contain the modified message. If the VSCode + * integration did not modify the message, this parameter will be identical to + * `originalMessage`. + * @param messageChanged This parameter will indicate wether the VSCode integration + * modified the incoming message. In other words: Whether `originalMessage` + * and `processedMessage` are different. + * @returns A message that will be propagated to the client. If the message + * is `undefined` the propagation will be cancelled. + */ + onBeforePropagateMessageToClient?( + originalMessage: unknown, processedMessage: unknown, messageChanged: boolean + ): unknown | undefined; +} +``` + +#### Methods and Fields + +```typescript +interface GlspVscodeConnector extends vscode.Disposable { + + /** + * A subscribable event which fires with an array containing the IDs of all + * selected elements when the selection of the editor changes. + */ + onSelectionUpdate: vscode.Event; + + /** + * A subscribable event which fires when a document changed. The event body + * will contain that document. Use this event for the onDidChangeCustomDocument + * on your implementation of the `CustomEditorProvider`. + */ + onDidChangeCustomDocument: vscode.Event>; + + /** + * Register a client on the GLSP-VSCode connector. All communication will subsequently + * run through the VSCode integration. Clients do not need to be unregistered + * as they are automatically disposed of when the panel they belong to is closed. + * + * @param client The client to register. + */ + registerClient(client: GlspVscodeClient): void; + + /** + * Send an action to the client/panel that is currently focused. If no registered + * panel is focused, the message will not be sent. + * + * @param action The action to send to the active client. + */ + sendActionToActiveClient(action: Action): void; + + /** + * Saves a document. Make sure to call this function in the `saveCustomDocument` + * and `saveCustomDocumentAs` functions of your `CustomEditorProvider` implementation. + * + * @param document The document to save. + * @param destination Optional parameter. When this parameter is provided the + * file will instead be saved at this location. + * @returns A promise that resolves when the file has been successfully saved. + */ + async saveDocument(document: D, destination?: vscode.Uri): Promise; + + /** + * Reverts a document. Make sure to call this function in the `revertCustomDocument` + * functions of your `CustomEditorProvider` implementation. + * + * @param document Document to revert. + * @param diagramType Diagram type as it is configured on the server. + * @returns A promise that resolves when the file has been successfully reverted. + */ + async revertDocument(document: D, diagramType: string): Promise; +} +``` + +### GlspVscodeServer + +```typescript +/** + * The server or server wrapper used by the VSCode integration needs to implement + * this interface. + */ +export interface GlspVscodeServer { + + /** + * An event emitter used by the VSCode extension to send messages to the server. + * + * You should subscribe to the event attached to this emitter to receive messages + * from the client/VSCode integration and pass them to the server. + * + * Use the properties `onBeforeReceiveMessageFromClient` and `onBeforePropagateMessageToServer` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onSendToServerEmitter: vscode.EventEmitter; + + /** + * An event the VSCode integration uses to receive messages from the server. + * The messages are then propagated to the client or processed by the VSCode + * integration to provide functionality. + * + * Fire this event with the message the server wants to send to the client. + * + * Use the properties `onBeforeReceiveMessageFromServer` and `onBeforePropagateMessageToClient` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onServerMessage: vscode.Event; +} +``` + +### GlspVscodeClient + +```typescript +/** + * Any clients registered on the GLSP VSCode integration need to implement this + * interface. + */ +export interface GlspVscodeClient { + + /** + * A unique identifier for the client/panel with which the client will be registered + * on the server. + */ + readonly clientId: string; + + /** + * The webview belonging to the client. + */ + readonly webviewPanel: vscode.WebviewPanel; + + /** + * The document object belonging to the client; + */ + readonly document: D; + + /** + * This event emitter is used by the VSCode integration to pass messages/actions + * to the client. These messages can come from the server or the VSCode integration + * itself. + * + * You should subscribe to the attached event and pass contents of the event + * to the webview. + * + * Use the properties `onBeforeReceiveMessageFromServer` and `onBeforePropagateMessageToClient` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onSendToClientEmitter: vscode.EventEmitter; + + /** + * The VSCode integration will subscribe to this event to listen to messages + * from the client. + * + * Fire this event with the message the client wants to send to the server. + * + * Use the properties `onBeforeReceiveMessageFromClient` and `onBeforePropagateMessageToServer` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onClientMessage: vscode.Event; +} +``` + +### Quickstart Components +This package also exposes components which can be taken advantage of if you are using the default GLSP components. + +They can be imported using + +```ts +import * as QuickstartComponents from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; +``` + +#### GlspServerLauncher +A small class used to start a default implementation GLSP server. + +```ts +interface GlspServerLauncher extends vscode.Disposable { + constructor(options: JavaSocketServerLauncherOptions); + + /** + * Starts up the server. + */ + async start(): Promise; + + /** + * Stops the server. + */ + stop(): void; +} + +interface JavaSocketServerLauncherOptions { + /** Path to the location of the jar file that should be launched as process */ + readonly jarPath: string; + /** Port on which the server should listen for new client connections */ + readonly serverPort: number; + /** Set to `true` if server stdout and stderr should be printed in extension host console. Default: `false` */ + readonly logging?: boolean; + /** Additional arguments that should be passed when starting the server process. */ + readonly additionalArgs?: string[]; +} +``` + +#### SocketGlspVscodeServer +A can component that provides the right interface for the GLSP VSCode integration +to be used as server and which can connect to a default implementation GLSP server. + +```ts +interface SocketGlspVscodeServerOptions { + /** Port of the running server. */ + readonly serverPort: number; + /** Client ID to register the jsonRPC client with on the server. */ + readonly clientId: string; + /** Name to register the client with on the server. */ + readonly clientName: string; +} + +interface SocketGlspVscodeServer extends GlspVscodeServer, vscode.Disposable { + + constructor(private readonly options: SocketGlspVscodeServerOptions); + + /** + * Starts up the JSON-RPC client and connects it to a running server. + */ + async start(): Promise; + + /** + * Stops the client. It cannot be restarted. + */ + async stop(): Promise; +} +``` + +#### GlspEditorProvider +An extensible base class to create a CustomEditorProvider that takes care of diagram +initialization and custom document events. + +Webview setup needs to be implemented. + +```ts +export abstract class GlspEditorProvider implements vscode.CustomEditorProvider { + /** + * The diagram type identifier the diagram server is responsible for. + */ + abstract diagramType: string; + + constructor(protected readonly glspVscodeConnector: GlspVscodeConnector); + + /** + * Used to set up the webview within the webview panel. + */ + abstract setUpWebview( + document: vscode.CustomDocument, + webviewPanel: vscode.WebviewPanel, + token: vscode.CancellationToken, + clientId: string + ): void; +} +``` + +## More information +For more information, please visit the [Eclipse GLSP Umbrella repository](https://github.com/eclipse-glsp/glsp) +and the [Eclipse GLSP Website](https://www.eclipse.org/glsp/). If you have questions, +contact us on our [spectrum chat](https://spectrum.chat/glsp/) and have a look at our +[communication and support options](https://www.eclipse.org/glsp/contact/). diff --git a/packages/vscode-integration/package.json b/packages/vscode-integration/package.json index fb2a46d..4c10f1a 100644 --- a/packages/vscode-integration/package.json +++ b/packages/vscode-integration/package.json @@ -35,7 +35,6 @@ }, "dependencies": { "@eclipse-glsp/protocol": "next", - "sprotty-vscode-protocol": "0.0.5", "vscode-jsonrpc": "^4.0.0" }, "devDependencies": { diff --git a/packages/vscode-integration/src/action/action-handler.ts b/packages/vscode-integration/src/action/action-handler.ts deleted file mode 100644 index 91a008a..0000000 --- a/packages/vscode-integration/src/action/action-handler.ts +++ /dev/null @@ -1,33 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; - -/** - * Used to locally intercept and handle actions in the VS Code extension. - */ -export interface ExtensionActionHandler { - - /** - * List of action names that the action handler will intercept. - */ - readonly kinds: string[]; - - /** - * @returns true when the action should be further progagated to the glsp server or the - * webview - */ - handleAction(action: Action): Thenable; -} diff --git a/packages/vscode-integration/src/action/external-navigation.ts b/packages/vscode-integration/src/action/external-navigation.ts deleted file mode 100644 index 23965df..0000000 --- a/packages/vscode-integration/src/action/external-navigation.ts +++ /dev/null @@ -1,62 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import * as vscode from 'vscode'; -import { ExtensionActionHandler } from './action-handler'; -import { Action } from 'sprotty-vscode-protocol'; - -interface Args { [key: string]: string | number | boolean } - -interface NavigationTarget { - uri: string; - label?: string; - args?: Args; -} - -export class NavigateToExternalTargetAction implements Action { - static readonly KIND = 'navigateToExternalTarget'; - readonly kind = NavigateToExternalTargetAction.KIND; - constructor(readonly target: NavigationTarget) { } - static is(action?: Action): action is NavigateToExternalTargetAction { - return action !== undefined && (action.kind === NavigateToExternalTargetAction.KIND) - && (action as NavigateToExternalTargetAction).target !== undefined; - } -} - -export class NavigateToExternalTargetHandler implements ExtensionActionHandler { - static SHOW_OPTIONS = 'jsonOpenerOptions'; - - kinds = [NavigateToExternalTargetAction.KIND]; - - async handleAction(action: Action): Promise { - if (NavigateToExternalTargetAction.is(action)) { - const { uri, args } = action.target; - let showOptions = { ...args }; - - // Give server the possibility to provide options through the `showOptions` field by providing a - // stringified version of the `TextDocumentShowOptions` - // See: https://code.visualstudio.com/api/references/vscode-api#TextDocumentShowOptions - const showOptionsField = args?.[NavigateToExternalTargetHandler.SHOW_OPTIONS]; - if (showOptionsField) { - showOptions = { ...args, ...(JSON.parse(showOptionsField.toString())) }; - } - - vscode.window.showTextDocument(vscode.Uri.parse(uri), showOptions); - } - - return false; - } -} diff --git a/packages/vscode-integration/src/action/action-dispatcher.ts b/packages/vscode-integration/src/actions/action.ts similarity index 57% rename from packages/vscode-integration/src/action/action-dispatcher.ts rename to packages/vscode-integration/src/actions/action.ts index cb1cdff..dfeb728 100644 --- a/packages/vscode-integration/src/action/action-dispatcher.ts +++ b/packages/vscode-integration/src/actions/action.ts @@ -13,18 +13,23 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; -import { GLSPWebViewRegistry } from 'src/glsp-webview'; +/* eslint-disable no-null/no-null */ -export interface ExtensionActionDispatcher { - dispatch(action: Action): void; +export interface Action { + readonly kind: string; } -export namespace ExtensionActionDispatcher { - export function dispatch(registry: GLSPWebViewRegistry, action: Action): void { - const activeWebview = registry.getActiveWebview(); - if (activeWebview) { - activeWebview.dispatch(action); - } - } +export interface ActionMessage { + clientId: string; + action: A; +} + +export function isAction(object: any): object is Action { + return typeof object === 'object' && object !== null && 'kind' in object && typeof object['kind'] === 'string'; +} + +export function isActionMessage(object: any): object is ActionMessage { + return typeof object === 'object' && object !== null && + 'clientId' in object && typeof object['clientId'] === 'string' && + 'action' in object && isAction(object.action); } diff --git a/packages/vscode-integration/src/utils/glsp-java-server-args.ts b/packages/vscode-integration/src/actions/export.ts similarity index 54% rename from packages/vscode-integration/src/utils/glsp-java-server-args.ts rename to packages/vscode-integration/src/actions/export.ts index 7cbaed4..7cc469e 100644 --- a/packages/vscode-integration/src/utils/glsp-java-server-args.ts +++ b/packages/vscode-integration/src/actions/export.ts @@ -14,19 +14,22 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export namespace GLSPJavaServerArgs { - /** - * Utility function to create the additional launch args for a GLSP Java Server - * to enable file logging. - * @param logDir Path to the directy where the log files should be stored - * @param disableConsolelogging Flag to indicate wether default console logging should be disabled - */ - export function enableFileLogging(logDir: string, disableConsolelogging = true): string[] { - const additionalArgs = ['--fileLog', 'true', '--logDir', logDir]; - if (disableConsolelogging) { - additionalArgs.push('--consoleLog'); - additionalArgs.push('false'); - } - return additionalArgs; +import { Action } from './action'; + +export class ExportSvgAction implements Action { + static readonly KIND = 'exportSvg'; + constructor(public readonly svg: string, public readonly kind = ExportSvgAction.KIND) { } + + static is(action?: Action): action is ExportSvgAction { + return action !== undefined && action.kind === ExportSvgAction.KIND && 'svg' in action; + } +} + +export class RequestExportSvgAction implements Action { + static readonly KIND = 'requestExportSvg'; + constructor(public readonly kind = RequestExportSvgAction.KIND) { } + + static is(action?: Action): action is RequestExportSvgAction { + return action !== undefined && action.kind === RequestExportSvgAction.KIND; } } diff --git a/packages/vscode-integration/src/utils/glsp-env-var.ts b/packages/vscode-integration/src/actions/external-navigation.ts similarity index 55% rename from packages/vscode-integration/src/utils/glsp-env-var.ts rename to packages/vscode-integration/src/actions/external-navigation.ts index 469fed5..a67abe6 100644 --- a/packages/vscode-integration/src/utils/glsp-env-var.ts +++ b/packages/vscode-integration/src/actions/external-navigation.ts @@ -13,20 +13,23 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export namespace GLSPEnvVariable { - export const SERVER_DEBUG = 'GLSP_SERVER_DEBUG'; - export const SERVER_PORT = 'GLSP_SERVER_PORT'; - export function isServerDebug(): boolean { - const envVar = process.env[SERVER_DEBUG]; - return envVar !== undefined && JSON.parse(envVar); - } +import { Action } from './action'; + +interface Args { [key: string]: string | number | boolean } + +interface NavigationTarget { + uri: string; + label?: string; + args?: Args; +} - export function getServerPort(): number | undefined { - const envVar = process.env[SERVER_PORT]; - if (envVar) { - return JSON.parse(envVar); - } - return; +export class NavigateToExternalTargetAction implements Action { + static readonly KIND = 'navigateToExternalTarget'; + readonly kind = NavigateToExternalTargetAction.KIND; + constructor(readonly target: NavigationTarget) { } + static is(action?: Action): action is NavigateToExternalTargetAction { + return action !== undefined && (action.kind === NavigateToExternalTargetAction.KIND) + && (action as NavigateToExternalTargetAction).target !== undefined; } } diff --git a/packages/vscode-integration/src/action/index.ts b/packages/vscode-integration/src/actions/index.ts similarity index 87% rename from packages/vscode-integration/src/action/index.ts rename to packages/vscode-integration/src/actions/index.ts index 2df55ef..9c68392 100644 --- a/packages/vscode-integration/src/action/index.ts +++ b/packages/vscode-integration/src/actions/index.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. + * Copyright (c) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -14,10 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './action-handler'; -export * from './action-dispatcher'; export * from './action'; -export * from './operation'; -export * from './navigation'; +export * from './export'; +export * from './save-state'; export * from './external-navigation'; export * from './markers'; +export * from './navigation'; +export * from './operation'; +export * from './selection'; diff --git a/packages/vscode-integration/src/action/markers.ts b/packages/vscode-integration/src/actions/markers.ts similarity index 93% rename from packages/vscode-integration/src/action/markers.ts rename to packages/vscode-integration/src/actions/markers.ts index 6305974..8fafd48 100644 --- a/packages/vscode-integration/src/action/markers.ts +++ b/packages/vscode-integration/src/actions/markers.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. + * Copyright (c) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -13,7 +13,8 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; + +import { Action } from './action'; export interface Marker { readonly label: string; diff --git a/packages/vscode-integration/src/actions/navigation.ts b/packages/vscode-integration/src/actions/navigation.ts new file mode 100644 index 0000000..07b371e --- /dev/null +++ b/packages/vscode-integration/src/actions/navigation.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { Action } from './action'; + +interface Args { + [key: string]: string | number | boolean; +} + +export class NavigateAction implements Action { + static readonly KIND = 'navigate'; + readonly kind = NavigateAction.KIND; + constructor(readonly targetTypeId: string, readonly args?: Args) { } + + static is(action?: Action): action is NavigateAction { + return action !== undefined && action.kind === NavigateAction.KIND && 'targetTypeId' in action; + } +} + +export class FitToScreenAction implements Action { + static readonly KIND = 'fit'; + constructor(public readonly elementIds: string[], + public readonly padding?: number, + public readonly maxZoom?: number, + public readonly animate: boolean = true, + public readonly kind = FitToScreenAction.KIND) { + } + + static is(action?: Action): action is FitToScreenAction { + return action !== undefined && action.kind === FitToScreenAction.KIND + && 'elementIds' in action && 'animate' in action; + } +} + +export class CenterAction implements Action { + static readonly KIND = 'center'; + constructor(public readonly elementIds: string[], + public readonly animate: boolean = true, + public readonly retainZoom: boolean = false, + public readonly kind = CenterAction.KIND) { + } + + static is(action?: Action): action is CenterAction { + return action !== undefined && action.kind === CenterAction.KIND + && 'elementIds' in action && 'animate' in action && 'retainZoom' in action; + } +} diff --git a/packages/vscode-integration/src/action/operation.ts b/packages/vscode-integration/src/actions/operation.ts similarity index 95% rename from packages/vscode-integration/src/action/operation.ts rename to packages/vscode-integration/src/actions/operation.ts index 3ff54f7..dab41b0 100644 --- a/packages/vscode-integration/src/action/operation.ts +++ b/packages/vscode-integration/src/actions/operation.ts @@ -13,9 +13,10 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; -export type Operation = Action; +import { Action } from './action'; + +type Operation = Action; export class UndoOperation implements Operation { static readonly KIND = 'glspUndo'; diff --git a/packages/vscode-integration/src/action/action.ts b/packages/vscode-integration/src/actions/save-state.ts similarity index 64% rename from packages/vscode-integration/src/action/action.ts rename to packages/vscode-integration/src/actions/save-state.ts index 0f5e5e1..da62847 100644 --- a/packages/vscode-integration/src/action/action.ts +++ b/packages/vscode-integration/src/actions/save-state.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. + * Copyright (c) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -13,45 +13,17 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; + +import { Action } from './action'; export type JsonPrimitive = string | number | boolean; + export class RequestModelAction implements Action { static readonly KIND = 'requestModel'; readonly kind = RequestModelAction.KIND; constructor(public readonly options?: { [key: string]: JsonPrimitive }, public readonly requestId = '') { } - -} - -export class FitToScreenAction implements Action { - static readonly KIND = 'fit'; - constructor(public readonly elementIds: string[], - public readonly padding?: number, - public readonly maxZoom?: number, - public readonly animate: boolean = true, - public readonly kind = FitToScreenAction.KIND) { - } - - static is(action?: Action): action is FitToScreenAction { - return action !== undefined && action.kind === FitToScreenAction.KIND - && 'elementIds' in action && 'animate' in action; - } -} - -export class CenterAction implements Action { - static readonly KIND = 'center'; - constructor(public readonly elementIds: string[], - public readonly animate: boolean = true, - public readonly retainZoom: boolean = false, - public readonly kind = CenterAction.KIND) { - } - - static is(action?: Action): action is CenterAction { - return action !== undefined && action.kind === CenterAction.KIND - && 'elementIds' in action && 'animate' in action && 'retainZoom' in action; - } } export class SaveModelAction implements Action { diff --git a/packages/vscode-integration/src/action/navigation.ts b/packages/vscode-integration/src/actions/selection.ts similarity index 60% rename from packages/vscode-integration/src/action/navigation.ts rename to packages/vscode-integration/src/actions/selection.ts index 9f7f0c0..58d42ae 100644 --- a/packages/vscode-integration/src/action/navigation.ts +++ b/packages/vscode-integration/src/actions/selection.ts @@ -13,18 +13,21 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; -interface Args { - [key: string]: string | number | boolean; -} +import { Action } from './action'; + +export class SelectAction implements Action { + static readonly KIND = 'elementSelected'; -export class NavigateAction implements Action { - static readonly KIND = 'navigate'; - readonly kind = NavigateAction.KIND; - constructor(readonly targetTypeId: string, readonly args?: Args) { } + constructor( + public readonly selectedElementsIDs: string[] = [], + public readonly deselectedElementsIDs: string[] = [], + public readonly kind = SelectAction.KIND + ) { } - static is(action?: Action): action is NavigateAction { - return action !== undefined && action.kind === NavigateAction.KIND && 'targetTypeId' in action; + static is(action?: Action): action is SelectAction { + return action !== undefined && action.kind === SelectAction.KIND + && 'selectedElementsIDs' in action + && 'deselectedElementsIDs' in action; } } diff --git a/packages/vscode-integration/src/glsp-commands.ts b/packages/vscode-integration/src/glsp-commands.ts deleted file mode 100644 index 0b0b8dc..0000000 --- a/packages/vscode-integration/src/glsp-commands.ts +++ /dev/null @@ -1,46 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { Action } from 'sprotty-vscode-protocol'; -import * as vscode from 'vscode'; - -import { ExtensionActionDispatcher } from './action'; -import { GLSPWebViewRegistry } from './glsp-webview'; - -export interface GLSPCommandOptions { - readonly command: string; - readonly extensionPrefix: string; - readonly registry: GLSPWebViewRegistry; - readonly action: Action; - readonly context: vscode.ExtensionContext; -} -export namespace GLSPCommand { - export const FIT_TO_SCREEN = 'diagram.fit'; - export const CENTER = 'diagram.center'; - export const LAYOUT = 'diagram.layout'; - - export function commandId(extensionPrefix: string, commandKey: string): string { - return `${extensionPrefix}.${commandKey}`; - } - - export function registerActionCommand(options: GLSPCommandOptions): void { - options.context.subscriptions.push( - vscode.commands.registerCommand(commandId(options.extensionPrefix, options.command), - () => ExtensionActionDispatcher.dispatch(options.registry, options.action)) - ); - } - -} - diff --git a/packages/vscode-integration/src/glsp-diagram-document.ts b/packages/vscode-integration/src/glsp-diagram-document.ts deleted file mode 100644 index 7d7c93c..0000000 --- a/packages/vscode-integration/src/glsp-diagram-document.ts +++ /dev/null @@ -1,144 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { Action, SprottyDiagramIdentifier } from 'sprotty-vscode-protocol'; -import * as vscode from 'vscode'; - -import { - DirtyStateChangeReason, - ExtensionActionDispatcher, - RedoOperation, - RequestModelAction, - SaveModelAction, - SetDirtyStateAction, - UndoOperation, - SetMarkersAction -} from './action'; -import { ExtensionActionHandler } from './action/action-handler'; -import { waitForEventWithTimeout } from './utils'; -import { Disposable } from './utils/disposable'; - -export interface DiagramEditEvent { - undo(): void; - redo(): void; -} -export class GlspDiagramDocument extends Disposable implements vscode.CustomDocument, ExtensionActionHandler { - static async create(uri: vscode.Uri, _backupId?: string): Promise { - return new GlspDiagramDocument(uri); - } - - protected readonly _onDidChange: vscode.EventEmitter; - protected readonly _onDidSave: vscode.EventEmitter; - protected actionDispatcher?: ExtensionActionDispatcher; - protected diagramIdentifier: SprottyDiagramIdentifier; - - private readonly diagnostics: vscode.DiagnosticCollection; - - kinds = [ - SetDirtyStateAction.KIND, - SetMarkersAction.KIND - ]; - - private constructor(readonly uri: vscode.Uri) { - super(); - this._onDidChange = this.addDisposable(new vscode.EventEmitter()); - this._onDidSave = this.addDisposable(new vscode.EventEmitter()); - this.diagnostics = this.addDisposable(vscode.languages.createDiagnosticCollection()); - } - - initialize(diagramIdentifier: SprottyDiagramIdentifier, actionDispatcher: ExtensionActionDispatcher): void { - this.diagramIdentifier = diagramIdentifier; - this.actionDispatcher = actionDispatcher; - } - - get onDidChange(): vscode.Event { - return this._onDidChange.event; - } - - get onDidSave(): vscode.Event { - return this._onDidSave.event; - } - - protected dispatchAction(action: Action): void { - if (!this.actionDispatcher) { - throw new Error(`Cannot dispatch action "${action.kind}" for GlspDocument with uri: "${this.uri}". - No action dispatcher has been set`); - } - this.actionDispatcher.dispatch(action); - } - - async backup(destination: vscode.Uri, _cancellation: vscode.CancellationToken): Promise { - // No need to implement a custom backup. The server holds the current model state anyways. - return { - id: destination.toString(), - delete: () => {/** */ } - }; - } - - async save(_cancellation: vscode.CancellationToken): Promise { - return this.dispatchAction(new SaveModelAction()); - } - - async saveAs(destination: vscode.Uri, cancellation: vscode.CancellationToken): Promise { - this.dispatchAction(new SaveModelAction(destination.path)); - return waitForEventWithTimeout(this.onDidSave, 2000, 'onDidSave'); - } - - async revert(cancellation: vscode.CancellationToken): Promise { - this.dispatchAction(new RequestModelAction({ - sourceUri: this.uri.toString(), - diagramType: this.diagramIdentifier.diagramType - })); - } - - async handleAction(action: Action): Promise { - if (SetDirtyStateAction.is(action)) { - const reason = action.reason || ''; - if (reason === DirtyStateChangeReason.SAVE) { - this._onDidSave.fire(); - } else if (reason === DirtyStateChangeReason.OPERATION && action.isDirty) { - this._onDidChange.fire({ - undo: () => { - this.dispatchAction(new UndoOperation()); - }, - redo: () => { - this.dispatchAction(new RedoOperation()); - } - }); - } - } - - if (SetMarkersAction.is(action)) { - const SEVERITY_MAP = { - 'info': 2, - 'warning': 1, - 'error': 0 - }; - - const updatedDiagnostics = action.markers.map(marker => new vscode.Diagnostic( - new vscode.Range(0, 0, 0, 0), // Must have be defined as such - no workarounds - marker.description, - SEVERITY_MAP[marker.kind] - )); - - this.diagnostics.set(this.uri, updatedDiagnostics); - - return true; // needs to be sent to webview - } - - return false; - } -} - diff --git a/packages/vscode-integration/src/glsp-diagram-editor-context.ts b/packages/vscode-integration/src/glsp-diagram-editor-context.ts deleted file mode 100644 index eae8b2f..0000000 --- a/packages/vscode-integration/src/glsp-diagram-editor-context.ts +++ /dev/null @@ -1,136 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { ActionMessageHandler, ApplicationIdProvider, BaseJsonrpcGLSPClient, GLSPClient } from '@eclipse-glsp/protocol'; -import * as net from 'net'; -import * as path from 'path'; -import { Action, ActionMessage, SprottyDiagramIdentifier } from 'sprotty-vscode-protocol'; -import * as vscode from 'vscode'; -import { - createMessageConnection, - Emitter, - MessageConnection, - SocketMessageReader, - SocketMessageWriter -} from 'vscode-jsonrpc'; - -import { CenterAction, FitToScreenAction, LayoutOperation, ExtensionActionDispatcher, NavigateToExternalTargetHandler } from './action'; -import { GLSPCommand } from './glsp-commands'; -import { GlspDiagramEditorProvider } from './glsp-diagram-editor-provider'; -import { GLSPWebView } from './glsp-webview'; -import { ServerConnectionProvider } from './server-connection-provider'; -import { Disposable } from './utils/disposable'; - -export abstract class GlspDiagramEditorContext extends Disposable { - protected _glspClient: BaseJsonrpcGLSPClient; - protected onReady: Promise = Promise.resolve(); - - protected onMessageFromGLSPServerEmitter = new Emitter(); - - public abstract readonly id: string; - public abstract readonly diagramType: string; - public abstract readonly extensionPrefix: string; - - private editorProvider: GlspDiagramEditorProvider; - - constructor(readonly context: vscode.ExtensionContext) { - super(); - this.addDisposable(this.registerEditorProvider()); - this.initializeGLSPClient(); - this.registerCommands(); - } - - protected abstract getConnectionProvider(): ServerConnectionProvider; - - protected registerEditorProvider(): vscode.Disposable { - this.editorProvider = new GlspDiagramEditorProvider(this.context, this); - const viewType = `${this.extensionPrefix}.${GlspDiagramEditorProvider.VIEW_TYPE}`; - return vscode.window.registerCustomEditorProvider(viewType, - this.editorProvider - , { - webviewOptions: { retainContextWhenHidden: true }, - supportsMultipleEditorsPerDocument: false - }); - } - - protected initializeGLSPClient(): void { - this._glspClient = new BaseJsonrpcGLSPClient({ - id: this.id, - name: this.extensionPrefix, - connectionProvider: () => this.getConnectionProvider().createConnection() - }); - this.onReady = this._glspClient.start().then(() => { - this._glspClient.initializeServer({ applicationId: ApplicationIdProvider.get() }); - this._glspClient.onActionMessage(message => this.onMessageFromGLSPServerEmitter.fire(message)); - }); - } - - abstract createWebview(webviewPanel: vscode.WebviewPanel, identifier: SprottyDiagramIdentifier): GLSPWebView; - - registerActionHandlers(webview: GLSPWebView): void { - webview.addActionHandler(new NavigateToExternalTargetHandler()); - } - - getExtensionFileUri(...segments: string[]): vscode.Uri { - return vscode.Uri - .file(path.join(this.context.extensionPath, ...segments)); - } - - onMessageFromGLSPServer(listener: ActionMessageHandler): vscode.Disposable { - return this.onMessageFromGLSPServerEmitter.event(listener); - } - - deactivateGLSPClient(): Thenable { - this.dispose(); - return Promise.resolve(undefined); - } - - async glspClient(): Promise { - await this.onReady; - return this._glspClient; - } - - protected registerCommands(): void { - this.registerActionCommand(GLSPCommand.FIT_TO_SCREEN, new FitToScreenAction([])); - this.registerActionCommand(GLSPCommand.CENTER, new CenterAction([])); - this.registerActionCommand(GLSPCommand.LAYOUT, new LayoutOperation()); - } - - /** - * Register a command that dispatches a new action when triggered. - * @param command Command id without extension prefix. - * @param action The action that should be dispatched. - */ - protected registerActionCommand(command: string, action: Action): void { - GLSPCommand.registerActionCommand({ - command, - action, - context: this.context, - registry: this.editorProvider.webviewRegistry, - extensionPrefix: this.extensionPrefix - }); - } - - async dispatchActionToWebview(action: Action): Promise { - ExtensionActionDispatcher.dispatch(this.editorProvider.webviewRegistry, action); - } - -} - -export function createSocketConnection(outSocket: net.Socket, inSocket: net.Socket): MessageConnection { - const reader = new SocketMessageReader(inSocket); - const writer = new SocketMessageWriter(outSocket); - return createMessageConnection(reader, writer); -} diff --git a/packages/vscode-integration/src/glsp-diagram-editor-provider.ts b/packages/vscode-integration/src/glsp-diagram-editor-provider.ts deleted file mode 100644 index 2620734..0000000 --- a/packages/vscode-integration/src/glsp-diagram-editor-provider.ts +++ /dev/null @@ -1,100 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { SprottyDiagramIdentifier } from 'sprotty-vscode-protocol'; -import * as vscode from 'vscode'; - -import { GlspDiagramDocument } from './glsp-diagram-document'; -import { GlspDiagramEditorContext } from './glsp-diagram-editor-context'; -import { GLSPWebView, GLSPWebViewRegistry } from './glsp-webview'; -import { disposeAll } from './utils/disposable'; - -export class GlspDiagramEditorProvider implements vscode.CustomEditorProvider { - public static VIEW_TYPE = 'glspDiagram'; - readonly webviewRegistry: GLSPWebViewRegistry; - private _onDidChangeCustomDocument: vscode.EventEmitter>; - - constructor(protected readonly context: vscode.ExtensionContext, - protected readonly editorContext: GlspDiagramEditorContext) { - this.webviewRegistry = new GLSPWebViewRegistry(); - this._onDidChangeCustomDocument = new vscode.EventEmitter>(); - } - - get onDidChangeCustomDocument(): vscode.Event> { - return this._onDidChangeCustomDocument.event; - } - - saveCustomDocument(document: GlspDiagramDocument, cancellation: vscode.CancellationToken): Thenable { - return document.save(cancellation); - } - - saveCustomDocumentAs(document: GlspDiagramDocument, destination: vscode.Uri, cancellation: vscode.CancellationToken): Thenable { - return document.saveAs(destination, cancellation); - } - revertCustomDocument(document: GlspDiagramDocument, cancellation: vscode.CancellationToken): Thenable { - return document.revert(cancellation); - } - - backupCustomDocument(document: GlspDiagramDocument, context: vscode.CustomDocumentBackupContext, cancellation: vscode.CancellationToken): - Thenable { - return document.backup(context.destination, cancellation); - } - - async openCustomDocument(uri: vscode.Uri, openContext: vscode.CustomDocumentOpenContext, token: vscode.CancellationToken): Promise { - const document = await GlspDiagramDocument.create(uri, openContext.backupId); - - const listeners: vscode.Disposable[] = []; - listeners.push(document.onDidChange(e => { - this._onDidChangeCustomDocument.fire({ - document, - ...e - }); - - })); - document.onDidDispose(() => disposeAll(listeners)); - return document; - } - - resolveCustomEditor(document: GlspDiagramDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken): void | Thenable { - const identifier = this.createDiagramIdentifier(document); - const webview = this.editorContext.createWebview(webviewPanel, identifier); - this.webviewRegistry.add(document.uri, webview); - webview.addActionHandler(document); - this.editorContext.registerActionHandlers(webview); - document.initialize(identifier, webview); - - return webview.connect(); - } - - protected createDiagramIdentifier(document: GlspDiagramDocument): SprottyDiagramIdentifier { - const diagramType = this.editorContext.diagramType; - const clientId = diagramType + '_' + GLSPWebView.viewCount++; - return { - diagramType, - uri: serializeUri(document.uri), - clientId - }; - } - -} - -export function serializeUri(uri: vscode.Uri): string { - let uriString = uri.toString(); - const match = uriString.match(/file:\/\/\/([a-z])%3A/i); - if (match) { - uriString = 'file:///' + match[1] + ':' + uriString.substring(match[0].length); - } - return uriString; -} diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts new file mode 100644 index 0000000..0ac82cb --- /dev/null +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -0,0 +1,461 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; + +import { + isActionMessage, + SaveModelAction, + RequestModelAction, + SetDirtyStateAction, + DirtyStateChangeReason, + UndoOperation, + RedoOperation, + SetMarkersAction, + NavigateToExternalTargetAction, + SelectAction, + ExportSvgAction, + Action, + ActionMessage +} from './actions'; + +import { GlspVscodeConnectorOptions, GlspVscodeClient } from './types'; + +export enum MessageOrigin { + CLIENT, + SERVER +} + +export interface MessageProcessingResult { + processedMessage: unknown; + messageChanged: boolean; +} + +/** + * The `GlspVscodeConnector` acts as the bridge between GLSP-Clients and the GLSP-Server + * and is at the core of the Glsp-VSCode integration. + * + * It works by being providing a server that implements the `GlspVscodeServer` + * interface and registering clients using the `GlspVscodeConnector.registerClient` + * function. Messages sent between the clients and the server are then intercepted + * by the connector to provide functionality based on the content of the messages. + * + * Messages can be intercepted using the interceptor properties in the options + * argument. + * + * Selection updates can be listened to using the `onSelectionUpdate` property. + */ +export class GlspVscodeConnector implements vscode.Disposable { + + /** Maps clientId to corresponding GlspVscodeClient. */ + protected readonly clientMap = new Map>(); + /** Maps Documents to corresponding clientId. */ + protected readonly documentMap = new Map(); + /** Maps clientId to selected elementIDs for that client. */ + protected readonly clientSelectionMap = new Map(); + + protected readonly options: Required; + protected readonly diagnostics = vscode.languages.createDiagnosticCollection(); + protected readonly selectionUpdateEmitter = new vscode.EventEmitter(); + protected readonly onDocumentSavedEmitter = new vscode.EventEmitter(); + protected readonly onDidChangeCustomDocumentEventEmitter = new vscode.EventEmitter>(); + protected readonly disposables: vscode.Disposable[] = []; + + /** + * A subscribable event which fires with an array containing the IDs of all + * selected elements when the selection of the editor changes. + */ + public onSelectionUpdate: vscode.Event; + + /** + * A subscribable event which fires when a document changed. The event body + * will contain that document. Use this event for the onDidChangeCustomDocument + * on your implementation of the `CustomEditorProvider`. + */ + public onDidChangeCustomDocument: vscode.Event>; + + constructor(options: GlspVscodeConnectorOptions) { + // Create default options + this.options = { + logging: false, + onBeforeReceiveMessageFromClient: (message, callback) => { + callback(message, true); + }, + onBeforeReceiveMessageFromServer: (message, callback) => { + callback(message, true); + }, + onBeforePropagateMessageToClient: (_originalMessage, processedMessage) => processedMessage, + onBeforePropagateMessageToServer: (_originalMessage, processedMessage) => processedMessage, + ...options + }; + + this.onSelectionUpdate = this.selectionUpdateEmitter.event; + this.onDidChangeCustomDocument = this.onDidChangeCustomDocumentEventEmitter.event; + + // Set up message listener for server + const serverMessageListener = this.options.server.onServerMessage(message => { + if (this.options.logging) { + if (isActionMessage(message)) { + console.log(`Server (${message.clientId}): ${message.action.kind}`, message.action); + } else { + console.log('Server (no action message):', message); + } + } + + // Run message through first user-provided interceptor (pre-receive) + this.options.onBeforeReceiveMessageFromServer(message, (newMessage, shouldBeProcessedByConnector = true) => { + const { processedMessage, messageChanged } = shouldBeProcessedByConnector ? + this.processMessage(newMessage, MessageOrigin.SERVER) : + { processedMessage: message, messageChanged: false }; + + // Run message through second user-provided interceptor (pre-send) - processed + const filteredMessage = this.options.onBeforePropagateMessageToClient(newMessage, processedMessage, messageChanged); + if (typeof filteredMessage !== 'undefined' && isActionMessage(filteredMessage)) { + this.sendMessageToClient(filteredMessage.clientId, filteredMessage); + } + }); + }); + + this.disposables.push( + this.diagnostics, + this.selectionUpdateEmitter, + serverMessageListener + ); + } + + /** + * Register a client on the GLSP-VSCode connector. All communication will subsequently + * run through the VSCode integration. Clients do not need to be unregistered + * as they are automatically disposed of when the panel they belong to is closed. + * + * @param client The client to register. + */ + public registerClient(client: GlspVscodeClient): void { + this.clientMap.set(client.clientId, client); + this.documentMap.set(client.document, client.clientId); + + // Set up message listener for client + const clientMessageListener = client.onClientMessage(message => { + if (this.options.logging) { + if (isActionMessage(message)) { + console.log(`Client (${message.clientId}): ${message.action.kind}`, message.action); + } else { + console.log('Client (no action message):', message); + } + } + + // Run message through first user-provided interceptor (pre-receive) + this.options.onBeforeReceiveMessageFromClient(message, (newMessage, shouldBeProcessedByConnector = true) => { + const { processedMessage, messageChanged } = shouldBeProcessedByConnector ? + this.processMessage(newMessage, MessageOrigin.CLIENT) : + { processedMessage: message, messageChanged: false }; + + const filteredMessage = this.options.onBeforePropagateMessageToServer(newMessage, processedMessage, messageChanged); + + if (typeof filteredMessage !== 'undefined') { + this.options.server.onSendToServerEmitter.fire(filteredMessage); + } + }); + }); + + const viewStateListener = client.webviewPanel.onDidChangeViewState(e => { + if (e.webviewPanel.active) { + this.selectionUpdateEmitter.fire(this.clientSelectionMap.get(client.clientId) || []); + } + }); + + // Cleanup when client panel is closed + const panelOnDisposeListener = client.webviewPanel.onDidDispose(() => { + this.diagnostics.set(client.document.uri, undefined); // this clears the diagnostics for the file + this.clientMap.delete(client.clientId); + this.documentMap.delete(client.document); + this.clientSelectionMap.delete(client.clientId); + viewStateListener.dispose(); + clientMessageListener.dispose(); + panelOnDisposeListener.dispose(); + }); + } + + /** + * Send an action to the client/panel that is currently focused. If no registered + * panel is focused, the message will not be sent. + * + * @param action The action to send to the active client. + */ + public sendActionToActiveClient(action: Action): void { + this.clientMap.forEach(client => { + if (client.webviewPanel.active) { + client.onSendToClientEmitter.fire({ + clientId: client.clientId, + action: action, + __localDispatch: true + }); + } + }); + } + + /** + * Send message to registered client by id. + * + * @param clientId Id of client. + * @param message Message to send. + */ + protected sendMessageToClient(clientId: string, message: unknown): void { + const client = this.clientMap.get(clientId); + if (client) { + client.onSendToClientEmitter.fire(message); + } + } + + /** + * Send action to registered client by id. + * + * @param clientId Id of client. + * @param action Action to send. + */ + protected sendActionToClient(clientId: string, action: Action): void { + this.sendMessageToClient(clientId, { + clientId: clientId, + action: action, + __localDispatch: true + }); + } + + /** + * Provides the functionality of the VSCode integration. + * + * Incoming messages (unless intercepted) will run through this function and + * be acted upon by providing default functionality for VSCode. + * + * @param message The original received message. + * @param origin The origin of the received message. + * @returns An object containing the processed message and an indicator wether + * the message was modified. + */ + protected processMessage(message: unknown, origin: MessageOrigin): MessageProcessingResult { + if (isActionMessage(message)) { + const client = this.clientMap.get(message.clientId); + + // Dirty state & save actions + if (SetDirtyStateAction.is(message.action)) { + return this.handleSetDirtyStateAction(message as ActionMessage, client, origin); + } + + // Diagnostic actions + if (SetMarkersAction.is(message.action)) { + return this.handleSetMarkersAction(message as ActionMessage, client, origin); + } + + // External targets action + if (NavigateToExternalTargetAction.is(message.action)) { + return this.handleNavigateToExternalTargetAction(message as ActionMessage, client, origin); + } + + // Selection action + if (SelectAction.is(message.action)) { + return this.handleSelectAction(message as ActionMessage, client, origin); + } + + // Export SVG action + if (ExportSvgAction.is(message.action)) { + return this.handleExportSvgAction(message as ActionMessage, client, origin); + } + } + + // Propagate unchanged message + return { processedMessage: message, messageChanged: false }; + } + + protected handleSetDirtyStateAction( + message: ActionMessage, + client: GlspVscodeClient | undefined, + _origin: MessageOrigin + ): MessageProcessingResult { + if (client) { + const reason = message.action.reason || ''; + if (reason === DirtyStateChangeReason.SAVE) { + this.onDocumentSavedEmitter.fire(client.document); + } else if (reason === DirtyStateChangeReason.OPERATION && message.action.isDirty) { + this.onDidChangeCustomDocumentEventEmitter.fire({ + document: client.document, + undo: () => { + this.sendActionToClient(client.clientId, new UndoOperation()); + }, + redo: () => { + this.sendActionToClient(client.clientId, new RedoOperation()); + } + }); + } + } + + // Propagate unchanged message + return { processedMessage: message, messageChanged: false }; + } + + protected handleSetMarkersAction( + message: ActionMessage, + client: GlspVscodeClient | undefined, + _origin: MessageOrigin + ): MessageProcessingResult { + if (client) { + const SEVERITY_MAP = { + 'info': vscode.DiagnosticSeverity.Information, + 'warning': vscode.DiagnosticSeverity.Warning, + 'error': vscode.DiagnosticSeverity.Error + }; + + const updatedDiagnostics = message.action.markers.map(marker => new vscode.Diagnostic( + new vscode.Range(0, 0, 0, 0), // Must have be defined as such - no workarounds + marker.description, + SEVERITY_MAP[marker.kind] + )); + + this.diagnostics.set(client.document.uri, updatedDiagnostics); + } + + // Propagate unchanged message + return { processedMessage: message, messageChanged: false }; + } + + protected handleNavigateToExternalTargetAction( + message: ActionMessage, + _client: GlspVscodeClient | undefined, + _origin: MessageOrigin + ): MessageProcessingResult { + const SHOW_OPTIONS = 'jsonOpenerOptions'; + const { uri, args } = message.action.target; + let showOptions = { ...args }; + + // Give server the possibility to provide options through the `showOptions` field by providing a + // stringified version of the `TextDocumentShowOptions` + // See: https://code.visualstudio.com/api/references/vscode-api#TextDocumentShowOptions + const showOptionsField = args?.[SHOW_OPTIONS]; + if (showOptionsField) { + showOptions = { ...args, ...JSON.parse(showOptionsField.toString()) }; + } + + vscode.window.showTextDocument(vscode.Uri.parse(uri), showOptions) + .then( + () => undefined, // onFulfilled: Do nothing. + () => undefined // onRejected: Do nothing - This is needed as error handling in case the navigationTarget does not exist. + ); + + // Do not propagate action + return { processedMessage: undefined, messageChanged: true }; + } + + protected handleSelectAction( + message: ActionMessage, + client: GlspVscodeClient | undefined, + origin: MessageOrigin + ): MessageProcessingResult { + if (client) { + this.clientSelectionMap.set(client.clientId, message.action.selectedElementsIDs); + this.selectionUpdateEmitter.fire(message.action.selectedElementsIDs); + } + + if (origin === MessageOrigin.CLIENT) { + // Do not propagate action if it comes from client to avoid an infinite loop, as both, client and server will mirror the Selection action + return { processedMessage: undefined, messageChanged: true }; + } else { + // Propagate unchanged message + return { processedMessage: message, messageChanged: false }; + } + } + + protected handleExportSvgAction( + message: ActionMessage, + _client: GlspVscodeClient | undefined, + _origin: MessageOrigin + ): MessageProcessingResult { + vscode.window.showSaveDialog({ + filters: { 'SVG': ['svg'] }, + saveLabel: 'Export', + title: 'Export as SVG' + }).then( + (uri: vscode.Uri | undefined) => { + if (uri) { + fs.writeFile(uri.fsPath, message.action.svg, { encoding: 'utf-8' }, err => { + if (err) { + console.error(err); + } + }); + } + }, + console.error + ); + + // Do not propagate action to avoid an infinite loop, as both, client and server will mirror the Export SVG action + return { processedMessage: undefined, messageChanged: true }; + } + + /** + * Saves a document. Make sure to call this function in the `saveCustomDocument` + * and `saveCustomDocumentAs` functions of your `CustomEditorProvider` implementation. + * + * @param document The document to save. + * @param destination Optional parameter. When this parameter is provided the + * file will instead be saved at this location. + * @returns A promise that resolves when the file has been successfully saved. + */ + public async saveDocument(document: D, destination?: vscode.Uri): Promise { + const clientId = this.documentMap.get(document); + if (clientId) { + return new Promise(resolve => { + const listener = this.onDocumentSavedEmitter.event(savedDocument => { + if (document === savedDocument) { + listener.dispose(); + resolve(); + } + }); + this.sendActionToClient(clientId, new SaveModelAction(destination?.path)); + }); + } else { + if (this.options.logging) { + console.error('Saving failed: Document not registered'); + } + throw new Error('Saving failed.'); + } + } + + /** + * Reverts a document. Make sure to call this function in the `revertCustomDocument` + * functions of your `CustomEditorProvider` implementation. + * + * @param document Document to revert. + * @param diagramType Diagram type as it is configured on the server. + * @returns A promise that resolves when the file has been successfully reverted. + */ + public async revertDocument(document: D, diagramType: string): Promise { + const clientId = this.documentMap.get(document); + if (clientId) { + this.sendActionToClient(clientId, new RequestModelAction({ + sourceUri: document.uri.toString(), + diagramType + })); + } else { + if (this.options.logging) { + console.error('Backup failed: Document not registered'); + } + throw new Error('Backup failed.'); + } + } + + public dispose(): void { + this.disposables.forEach(disposable => disposable.dispose()); + } +} diff --git a/packages/vscode-integration/src/glsp-webview.ts b/packages/vscode-integration/src/glsp-webview.ts deleted file mode 100644 index fdc851d..0000000 --- a/packages/vscode-integration/src/glsp-webview.ts +++ /dev/null @@ -1,228 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2020-2021 TypeFox and others. - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -// Implementation is based on https://github.com/eclipse/sprotty-vscode/blob/master/sprotty-vscode-extension/src/sprotty-webview.ts -import { GLSPClient } from '@eclipse-glsp/protocol'; -import { - Action, - ActionMessage, - isActionMessage, - isWebviewReadyMessage, - SprottyDiagramIdentifier -} from 'sprotty-vscode-protocol'; -import * as vscode from 'vscode'; -import { ResponseMessage } from 'vscode-jsonrpc/lib/messages'; - -import { ExtensionActionDispatcher } from './action'; -import { ExtensionActionHandler } from './action/action-handler'; -import { GlspDiagramEditorContext } from './glsp-diagram-editor-context'; -import { Disposable } from './utils/disposable'; - -export class GLSPWebViewRegistry { - private registry: Map = new Map(); - - get(uri: vscode.Uri): GLSPWebView[] { - return this.registry.get(uri.toString()) || []; - } - - add(uri: vscode.Uri, webview: GLSPWebView): void { - const key = uri.toString(); - const webviews = this.registry.get(key) || []; - webviews.push(webview); - this.registry.set(key, webviews); - - webview.onDidDispose(() => this.delete(uri, webview)); - - } - - delete(uri: vscode.Uri, webview: GLSPWebView): void { - const key = uri.toString(); - const webviews = this.registry.get(key) || []; - const index = webviews.indexOf(webview); - if (index > -1) { - webviews.splice(index, 1); - this.registry.set(key, webviews); - } - } - - getActiveWebview(): GLSPWebView | undefined { - let activeWebview: GLSPWebView | undefined; - this.registry.forEach((webviews: GLSPWebView[], key: string) => { - const activeViews = webviews.filter(webview => webview.diagramPanel.active); - if (activeViews.length === 1) { - activeWebview = activeViews[0]; - return; - } - }); - return activeWebview; - } -} - -export interface GLSPWebviewOptions { - editorContext: GlspDiagramEditorContext; - identifier: SprottyDiagramIdentifier; - localResourceRoots: vscode.Uri[]; - scriptUri: vscode.Uri; - webviewPanel: vscode.WebviewPanel; -} - -export type GLSPWebviewMessage = ActionMessage | SprottyDiagramIdentifier | ResponseMessage; - -export class GLSPWebView extends Disposable implements ExtensionActionDispatcher { - static viewCount = 0; - - protected readonly editorContext: GlspDiagramEditorContext; - protected readonly scriptUri: vscode.Uri; - protected readonly diagramIdentifier: SprottyDiagramIdentifier; - readonly diagramPanel: vscode.WebviewPanel; - protected readonly actionHandlers = new Map(); - - protected messageQueue: (ActionMessage | SprottyDiagramIdentifier | ResponseMessage)[] = []; - private resolveWebviewReady: () => void; - // eslint-disable-next-line no-invalid-this - private readonly webviewReady = new Promise(resolve => this.resolveWebviewReady = resolve); - - constructor(options: GLSPWebviewOptions) { - super(); - this.editorContext = options.editorContext; - this.diagramIdentifier = options.identifier; - this.scriptUri = options.scriptUri; - this.diagramPanel = this.initializeDiagramPanel(options.webviewPanel, options.localResourceRoots); - } - - protected ready(): Promise { - return this.webviewReady; - } - - protected glspClient(): Promise { - return this.editorContext.glspClient(); - } - - protected initializeDiagramPanel(webViewPanel: vscode.WebviewPanel, localResourceRoots: vscode.Uri[]): vscode.WebviewPanel { - webViewPanel.webview.options = { - localResourceRoots, - enableScripts: true - }; - this.initializeWebview(webViewPanel.webview); - webViewPanel.onDidDispose(() => this.dispose()); - return webViewPanel; - } - - protected initializeWebview(webview: vscode.Webview): void { - webview.html = ` - - - - - - - - -
- - - `; - } - - public async connect(): Promise { - this.addDisposable(this.diagramPanel.onDidChangeViewState(event => { - if (event.webviewPanel.visible) { - this.messageQueue.forEach(message => this.sendToWebview(message)); - this.messageQueue = []; - } - this.setWebviewActiveContext(event.webviewPanel.active); - })); - - this.setWebviewActiveContext(this.diagramPanel.active); - - this.addDisposable(this.diagramPanel.webview.onDidReceiveMessage(message => this.receiveFromWebview(message))); - this.addDisposable(this.editorContext.onMessageFromGLSPServer(message => { - // only handle messages that are meant for this webview - if (message.clientId === this.diagramIdentifier.clientId) { - this.sendToWebview(message); - } - })); - - this.sendDiagramIdentifier(); - } - - protected setWebviewActiveContext(isActive: boolean): void { - vscode.commands.executeCommand('setContext', 'glsp-' + this.diagramIdentifier.diagramType + '-focused', isActive); - } - - protected async sendToWebview(message: GLSPWebviewMessage): Promise { - if (this.diagramPanel.visible) { - if (isActionMessage(message)) { - const shouldForwardToWebview = await this.handleLocally(message.action); - if (shouldForwardToWebview) { - this.diagramPanel.webview.postMessage(message); - } - } else { - this.diagramPanel.webview.postMessage(message); - } - } else { - this.messageQueue.push(message); - } - } - - protected async sendDiagramIdentifier(): Promise { - await this.ready(); - this.sendToWebview(this.diagramIdentifier); - } - - protected async receiveFromWebview(message: any): Promise { - if (isWebviewReadyMessage(message)) { - this.resolveWebviewReady(); - } else if (isActionMessage(message)) { - const shouldForwardToServer = await this.handleLocally(message.action); - if (shouldForwardToServer) { - this.forwardToGlspServer(message); - } - } - } - - protected async forwardToGlspServer(message: ActionMessage): Promise { - const glspClient = await this.glspClient(); - glspClient.sendActionMessage(message); - } - - dispatch(action: Action): void { - this.sendToWebview({ - clientId: this.diagramIdentifier.clientId, - action, - __localDispatch: true - } as ActionMessage); // TODO: Type Messages properly so that this casting isn't necessary - } - - /** - * Handle the action locally if a local action handler is present. - * @param action The action that should be handled. - * @returns true if the action should be further propagated - * (either the glspServer ot the webview), false otherwise. - */ - protected handleLocally(action: Action): Thenable { - const actionHandler = this.actionHandlers.get(action.kind); - if (actionHandler) { - return actionHandler.handleAction(action); - } - return Promise.resolve(true); - } - - addActionHandler(actionHandler: ExtensionActionHandler): void { - actionHandler.kinds.forEach(kind => this.actionHandlers.set(kind, actionHandler)); - } -} diff --git a/packages/vscode-integration/src/index.ts b/packages/vscode-integration/src/index.ts index ba45db9..44a7641 100644 --- a/packages/vscode-integration/src/index.ts +++ b/packages/vscode-integration/src/index.ts @@ -13,10 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './utils'; -export * from './action'; -export * from './glsp-diagram-editor-context'; -export * from './glsp-diagram-document'; -export * from './glsp-diagram-editor-provider'; -export * from './glsp-webview'; -export * from './java-socket-server-connection-provider'; + +export * from './actions'; +export * from './glsp-vscode-connector'; +export * from './types'; diff --git a/packages/vscode-integration/src/java-socket-server-connection-provider.ts b/packages/vscode-integration/src/java-socket-server-connection-provider.ts deleted file mode 100644 index 306737e..0000000 --- a/packages/vscode-integration/src/java-socket-server-connection-provider.ts +++ /dev/null @@ -1,150 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { ChildProcess, spawn, SpawnOptions } from 'child_process'; -import * as fs from 'fs'; -import * as net from 'net'; -import { MessageConnection } from 'vscode-jsonrpc'; - -import { createSocketConnection } from './glsp-diagram-editor-context'; -import { ServerConnectionProvider } from './server-connection-provider'; - -export interface JavaSocketServerLaunchOptions { - /** Path to the location of the jar file that should be launched as process */ - readonly jarPath: string; - /** Port on which the server should listen for new client connections */ - serverPort: number; - /** Indicates wether the server is already running or should be started in an embedded process. */ - isRunning: boolean; - /** Indicates wether console logging of server process should be disabled*/ - noConsoleLog: boolean; - /** Additional arguments that should be passed when starting the server process. */ - additionalArgs?: string[]; -} - -export namespace JavaSocketServerLaunchOptions { - export function createDefaultOptions(): JavaSocketServerLaunchOptions { - return { - jarPath: '', - serverPort: NaN, - isRunning: false, - noConsoleLog: false - }; - } - - export function createOptions(options?: Partial): JavaSocketServerLaunchOptions { - return options ? { - ...createDefaultOptions, - ...options - } as JavaSocketServerLaunchOptions : createDefaultOptions(); - } - - export const START_UP_COMPLETE_MSG = '[GLSP-Server]:Startup completed'; - -} -export class JavaSocketServerConnectionProvider implements ServerConnectionProvider { - protected readonly options: JavaSocketServerLaunchOptions; - protected resolveReady: (value?: void | PromiseLike | undefined) => void; - // eslint-disable-next-line no-invalid-this - onReady: Promise = new Promise(resolve => this.resolveReady = resolve); - - constructor(partialOptions?: Partial) { - this.options = JavaSocketServerLaunchOptions.createOptions(partialOptions); - } - - public async createConnection(): Promise { - - const port = this.options.serverPort; - - if (isNaN(port)) { - throw new Error(`Could not launch GLSP Server. The given server port is not a number: ${port}`); - } - - await this.launchServer(); - - const socket = new net.Socket(); - const connection = createSocketConnection(socket, socket); - socket.connect(port); - return connection; - } - - protected async launchServer(): Promise { - if (this.options.isRunning) { - this.resolveReady(); - return this.onReady; - } - const jarPath = this.options.jarPath; - if (!fs.existsSync(jarPath)) { - throw Error(`Could not launch GLSP server. The given jar path is not valid: ${jarPath}`); - } - let args = ['-jar', this.options.jarPath, '--port', `${this.options.serverPort}`]; - if (this.options.additionalArgs) { - args = [...args, ...this.options.additionalArgs]; - } - await this.spawnProcessAsync('java', args); - return this.onReady; - - } - - protected get processName(): string { - return 'GLSP-Server'; - } - - protected spawnProcessAsync(command: string, args?: string[], options?: SpawnOptions): Promise { - const rawProcess = spawn(command, args, options); - rawProcess.stderr.on('data', this.processLogError.bind(this)); - rawProcess.stdout.on('data', this.processLogInfo.bind(this)); - return new Promise((resolve, reject) => { - rawProcess.on('error', error => { - this.onDidFailSpawnProcess(error); - if (error.message.includes('ENOENT')) { - const guess = command.split(/\s+/).shift(); - if (guess) { - reject(new Error(`Failed to spawn ${guess}\nPerhaps it is not on the PATH.`)); - return; - } - } - reject(error); - }); - - process.nextTick(() => resolve(rawProcess)); - }); - } - - protected onDidFailSpawnProcess(error: Error): void { - if (!this.options.noConsoleLog) { - console.error(`${this.processName}: ${error}`); - } - } - - protected processLogError(data: string | Buffer): void { - if (data && !this.options.noConsoleLog) { - console.error(`${this.processName}: ${data}`); - } - } - - protected processLogInfo(data: string | Buffer): void { - if (data) { - const message = data.toString(); - if (message.startsWith(JavaSocketServerLaunchOptions.START_UP_COMPLETE_MSG)) { - this.resolveReady(); - } - if (!this.options.noConsoleLog) { - console.log(`${this.processName}: ${data}`); - } - } - } - -} diff --git a/packages/vscode-integration/src/quickstart-components/glsp-editor-provider.ts b/packages/vscode-integration/src/quickstart-components/glsp-editor-provider.ts new file mode 100644 index 0000000..5e6874e --- /dev/null +++ b/packages/vscode-integration/src/quickstart-components/glsp-editor-provider.ts @@ -0,0 +1,159 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.01 + ********************************************************************************/ + +import * as vscode from 'vscode'; + +import { isActionMessage, isWebviewReadyMessage } from 'sprotty-vscode-protocol'; +import { GlspVscodeConnector } from '../glsp-vscode-connector'; + +/** + * An extensible base class to create a CustomEditorProvider that takes care of + * diagram initialization and custom document events. + * + * Webview setup needs to be implemented. + */ +export abstract class GlspEditorProvider implements vscode.CustomEditorProvider { + + /** The diagram type identifier the diagram server is responsible for. */ + abstract diagramType: string; + + /** Used to generate continuous and unique clientIds - TODO: consider replacing this with uuid. */ + private viewCount = 0; + + onDidChangeCustomDocument: vscode.Event>; + + constructor( + protected readonly glspVscodeConnector: GlspVscodeConnector + ) { + this.onDidChangeCustomDocument = glspVscodeConnector.onDidChangeCustomDocument; + } + + saveCustomDocument(document: vscode.CustomDocument, _cancellation: vscode.CancellationToken): Thenable { + return this.glspVscodeConnector.saveDocument(document); + } + + saveCustomDocumentAs(document: vscode.CustomDocument, destination: vscode.Uri, _cancellation: vscode.CancellationToken): Thenable { + return this.glspVscodeConnector.saveDocument(document, destination); + } + + revertCustomDocument(document: vscode.CustomDocument, _cancellation: vscode.CancellationToken): Thenable { + return this.glspVscodeConnector.revertDocument(document, this.diagramType); + } + + backupCustomDocument( + _document: vscode.CustomDocument, + context: vscode.CustomDocumentBackupContext, + _cancellation: vscode.CancellationToken + ): Thenable { + // Basically do the bare minimum - which is nothing + return Promise.resolve({ id: context.destination.toString(), delete: () => undefined }); + } + + openCustomDocument(uri: vscode.Uri, _openContext: vscode.CustomDocumentOpenContext, _token: vscode.CancellationToken): vscode.CustomDocument | Thenable { + // Return the most basic implementation possible. + return { uri, dispose: () => undefined }; + } + + resolveCustomEditor(document: vscode.CustomDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken): void | Thenable { + // This is used to initialize sprotty for our diagram + const sprottyDiagramIdentifier = { + diagramType: this.diagramType, + uri: serializeUri(document.uri), + clientId: `${this.diagramType}_${this.viewCount++}` + }; + + // Promise that resolves when sprotty sends its ready-message + const webviewReadyPromise = new Promise(resolve => { + const messageListener = webviewPanel.webview.onDidReceiveMessage((message: unknown) => { + if (isWebviewReadyMessage(message)) { + resolve(); + messageListener.dispose(); + } + }); + }); + + const sendMessageToWebview = async (message: unknown): Promise => { + webviewReadyPromise.then(() => { + if (webviewPanel.active) { + webviewPanel.webview.postMessage(message); + } else { + console.log('Message stalled for webview:', document.uri.path, message); + const viewStateListener = webviewPanel.onDidChangeViewState(() => { + viewStateListener.dispose(); + sendMessageToWebview(message); + }); + } + }); + }; + + const receiveMessageFromServerEmitter = new vscode.EventEmitter(); + const sendMessageToServerEmitter = new vscode.EventEmitter(); + + webviewPanel.onDidDispose(() => { + receiveMessageFromServerEmitter.dispose(); + sendMessageToServerEmitter.dispose(); + }); + + // Listen for Messages from webview (only after ready-message has been received) + webviewReadyPromise.then(() => { + webviewPanel.webview.onDidReceiveMessage((message: unknown) => { + if (isActionMessage(message)) { + sendMessageToServerEmitter.fire(message); + } + }); + }); + + // Listen for Messages from server + receiveMessageFromServerEmitter.event(message => { + if (isActionMessage(message)) { + sendMessageToWebview(message); + } + }); + + // Register document/diagram panel/model in vscode connector + this.glspVscodeConnector.registerClient({ + clientId: sprottyDiagramIdentifier.clientId, + document: document, + webviewPanel: webviewPanel, + onClientMessage: sendMessageToServerEmitter.event, + onSendToClientEmitter: receiveMessageFromServerEmitter + }); + + // Initialize diagram + sendMessageToWebview(sprottyDiagramIdentifier); + + this.setUpWebview(document, webviewPanel, token, sprottyDiagramIdentifier.clientId); + } + + /** + * Used to set up the webview within the webview panel. + */ + abstract setUpWebview( + document: vscode.CustomDocument, + webviewPanel: vscode.WebviewPanel, + token: vscode.CancellationToken, + clientId: string + ): void; +} + +function serializeUri(uri: vscode.Uri): string { + let uriString = uri.toString(); + const match = uriString.match(/file:\/\/\/([a-z])%3A/i); + if (match) { + uriString = 'file:///' + match[1] + ':' + uriString.substring(match[0].length); + } + return uriString; +} diff --git a/packages/vscode-integration/src/quickstart-components/glsp-server-launcher.ts b/packages/vscode-integration/src/quickstart-components/glsp-server-launcher.ts new file mode 100644 index 0000000..087867f --- /dev/null +++ b/packages/vscode-integration/src/quickstart-components/glsp-server-launcher.ts @@ -0,0 +1,124 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as vscode from 'vscode'; + +const START_UP_COMPLETE_MSG = '[GLSP-Server]:Startup completed'; + +interface JavaSocketServerLauncherOptions { + /** Path to the location of the jar file that should be launched as process */ + readonly jarPath: string; + /** Port on which the server should listen for new client connections */ + readonly serverPort: number; + /** Set to `true` if server stdout and stderr should be printed in extension host console. Default: `false` */ + readonly logging?: boolean; + /** Additional arguments that should be passed when starting the server process. */ + readonly additionalArgs?: string[]; +} + +/** + * This component can be used to bootstrap your extension when using the default + * GLSP server implementation, which you can find here: + * https://github.com/eclipse-glsp/glsp-server + * + * It simply starts up a server JAR located at a specified path on a specified port. + * You can pass additional launch arguments through the options. + * + * If you need a component to quickly connect your default GLSP server to the GLSP-VSCode + * integration, take a look at the `SocketGlspVscodeServer` quickstart component. + */ +export class GlspServerLauncher implements vscode.Disposable { + protected readonly options: Required; + protected serverProcess?: childProcess.ChildProcess; + + constructor(options: JavaSocketServerLauncherOptions) { + // Create default options + this.options = { + logging: false, + additionalArgs: [], + ...options + }; + } + + /** + * Starts up the server. + */ + async start(): Promise { + return new Promise(resolve => { + const jarPath = this.options.jarPath; + + if (!fs.existsSync(jarPath)) { + throw Error(`Could not launch GLSP server. The given jar path is not valid: ${jarPath}`); + } + + const args = [ + '-jar', this.options.jarPath, + '--port', `${this.options.serverPort}`, + ...this.options.additionalArgs + ]; + + const process = childProcess.spawn('java', args); + this.serverProcess = process; + + process.stdout.on('data', data => { + if (data.toString().includes(START_UP_COMPLETE_MSG)) { + resolve(); + } + + this.handleStdoutData(data); + }); + + process.stderr.on('data', this.handleStderrData); + process.on('error', this.handleProcessError); + }); + } + + protected handleStdoutData(data: string | Buffer): void { + if (this.options.logging) { + console.log('GLSP-Server:', data.toString()); + } + } + + protected handleStderrData(data: string | Buffer): void { + if (data && this.options.logging) { + console.error('GLSP-Server:', data.toString()); + } + } + + protected handleProcessError(error: Error): never { + if (this.options.logging) { + console.error('GLSP-Server:', error); + } + + throw error; + } + + /** + * Stops the server. + */ + stop(): void { + if (this.serverProcess && !this.serverProcess.killed) { + this.serverProcess.kill('SIGINT'); + // TODO: Think of a process that does this elegantly with the same consistency. + } + } + + dispose(): void { + this.stop(); + } +} diff --git a/packages/vscode-integration/src/utils/index.ts b/packages/vscode-integration/src/quickstart-components/index.ts similarity index 81% rename from packages/vscode-integration/src/utils/index.ts rename to packages/vscode-integration/src/quickstart-components/index.ts index 2771213..25e0cdf 100644 --- a/packages/vscode-integration/src/utils/index.ts +++ b/packages/vscode-integration/src/quickstart-components/index.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. + * Copyright (c) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './disposable'; -export * from './promise'; -export * from './glsp-env-var'; -export * from './glsp-java-server-args'; + +export * from './glsp-editor-provider'; +export * from './glsp-server-launcher'; +export * from './socket-glsp-vscode-server'; diff --git a/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts new file mode 100644 index 0000000..ddd4642 --- /dev/null +++ b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts @@ -0,0 +1,114 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as net from 'net'; +import * as vscode from 'vscode'; +import { createMessageConnection, SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc'; +import { ApplicationIdProvider, BaseJsonrpcGLSPClient } from '@eclipse-glsp/protocol'; +import { isActionMessage } from '../actions'; + +import { GlspVscodeServer } from '../types'; + +interface SocketGlspVscodeServerOptions { + /** Port of the running server. */ + readonly serverPort: number; + /** Client ID to register the jsonRPC client with on the server. */ + readonly clientId: string; + /** Name to register the client with on the server. */ + readonly clientName: string; +} + +/** + * This component can be used to bootstrap your extension when using the default + * GLSP server implementation, which you can find here: + * https://github.com/eclipse-glsp/glsp-server + * + * It sets up a JSON-RPC connection to a server running on a specified port and + * provides an interface, ready to be used by the `GlspVscodeConnector` for the + * GLSP-VSCode integration. + * + * If you need a component to quickly start your default GLSP server, take a look + * at the `GlspServerStarter` quickstart component. + */ +export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposable { + readonly onSendToServerEmitter = new vscode.EventEmitter(); + readonly onServerMessage: vscode.Event; + + protected readonly onServerSendEmitter = new vscode.EventEmitter(); + + protected readonly socket = new net.Socket(); + protected readonly glspClient: BaseJsonrpcGLSPClient; + + protected readonly onReady: Promise; + protected setReady: () => void; + + constructor(protected readonly options: SocketGlspVscodeServerOptions) { + this.onReady = new Promise(resolve => { + this.setReady = resolve; + }); + + this.onServerMessage = this.onServerSendEmitter.event; + + const reader = new SocketMessageReader(this.socket); + const writer = new SocketMessageWriter(this.socket); + const connection = createMessageConnection(reader, writer); + + this.glspClient = new BaseJsonrpcGLSPClient({ + id: options.clientId, + name: options.clientName, + connectionProvider: connection + }); + + this.onSendToServerEmitter.event(message => { + this.onReady.then(() => { + if (isActionMessage(message)) { + this.glspClient.sendActionMessage(message); + } + }); + }); + } + + /** + * Starts up the JSON-RPC client and connects it to a running server. + */ + async start(): Promise { + this.socket.connect(this.options.serverPort); + + await this.glspClient.start(); + await this.glspClient.initializeServer({ applicationId: ApplicationIdProvider.get() }); + + // The listener cant be registered before `glspClient.start()` because the + // glspClient will reject the listener if it has not connected to the server yet. + this.glspClient.onActionMessage(message => { + this.onServerSendEmitter.fire(message); + }); + + this.setReady(); + } + + /** + * Stops the client. It cannot be restarted. + */ + async stop(): Promise { + return this.glspClient.stop(); + } + + dispose(): void { + this.onSendToServerEmitter.dispose(); + this.onServerSendEmitter.dispose(); + this.stop(); + } +} diff --git a/packages/vscode-integration/src/server-connection-provider.ts b/packages/vscode-integration/src/server-connection-provider.ts deleted file mode 100644 index b519145..0000000 --- a/packages/vscode-integration/src/server-connection-provider.ts +++ /dev/null @@ -1,21 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { MaybePromise } from '@eclipse-glsp/protocol'; -import { MessageConnection } from 'vscode-jsonrpc'; - -export interface ServerConnectionProvider { - createConnection(): MaybePromise; -} diff --git a/packages/vscode-integration/src/types.ts b/packages/vscode-integration/src/types.ts new file mode 100644 index 0000000..8a32391 --- /dev/null +++ b/packages/vscode-integration/src/types.ts @@ -0,0 +1,197 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as vscode from 'vscode'; + +/** + * Any clients registered on the GLSP VSCode integration need to implement this + * interface. + */ +export interface GlspVscodeClient { + + /** + * A unique identifier for the client/panel with which the client will be registered + * on the server. + */ + readonly clientId: string; + + /** + * The webview belonging to the client. + */ + readonly webviewPanel: vscode.WebviewPanel; + + /** + * The document object belonging to the client; + */ + readonly document: D; + + /** + * This event emitter is used by the VSCode integration to pass messages/actions + * to the client. These messages can come from the server or the VSCode integration + * itself. + * + * You should subscribe to the attached event and pass contents of the event + * to the webview. + * + * Use the properties `onBeforeReceiveMessageFromServer` and `onBeforePropagateMessageToClient` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onSendToClientEmitter: vscode.EventEmitter; + + /** + * The VSCode integration will subscribe to this event to listen to messages + * from the client. + * + * Fire this event with the message the client wants to send to the server. + * + * Use the properties `onBeforeReceiveMessageFromClient` and `onBeforePropagateMessageToServer` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onClientMessage: vscode.Event; +} + +/** + * The server or server wrapper used by the VSCode integration needs to implement + * this interface. + */ +export interface GlspVscodeServer { + + /** + * An event emitter used by the VSCode extension to send messages to the server. + * + * You should subscribe to the event attached to this emitter to receive messages + * from the client/VSCode integration and pass them to the server. + * + * Use the properties `onBeforeReceiveMessageFromClient` and `onBeforePropagateMessageToServer` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onSendToServerEmitter: vscode.EventEmitter; + + /** + * An event the VSCode integration uses to receive messages from the server. + * The messages are then propagated to the client or processed by the VSCode + * integration to provide functionality. + * + * Fire this event with the message the server wants to send to the client. + * + * Use the properties `onBeforeReceiveMessageFromServer` and `onBeforePropagateMessageToClient` + * of the GlspVscodeConnector in order to control what messages are propagated + * and processed. + */ + readonly onServerMessage: vscode.Event; +} + +interface InterceptorCallback { + /** + * This callback controls what message should be propagated to the VSCode integration + * and whether the VSCode integration should process it (ie. provide functionality + * based on the message). + * + * @param newMessage The message to be propagated. This value can be anything, + * however if it is `undefined` the message will not be propagated further. + * @param shouldBeProcessedByConnector Optional parameter indicating whether the + * VSCode integration should process the message. That usually means providing + * functionality based on the message but also modifying it or blocking it from + * being propagated further. + */ + (newMessage: unknown | undefined, shouldBeProcessedByConnector?: boolean): void; +} + +export interface GlspVscodeConnectorOptions { + + /** + * The GLSP server (or its wrapper) that the VSCode integration should use. + */ + server: GlspVscodeServer; + + /** + * Wether the GLSP-VSCode integration should log various events. This is useful + * if you want to find out what events the VSCode integration is receiving from + * and sending to the server and clients. + * + * Defaults to `false`. + */ + logging?: boolean; + + /** + * Optional property to intercept (and/or modify) messages sent from the client + * to the VSCode integration via `GlspVscodeClient.onClientSend`. + * + * @param message Contains the original message sent by the client. + * @param callback A callback to control how messages are handled further. + */ + onBeforeReceiveMessageFromClient?: (message: unknown, callback: InterceptorCallback) => void; + + /** + * Optional property to intercept (and/or modify) messages sent from the server + * to the VSCode integration via `GlspVscodeServer.onServerSend`. + * + * @param message Contains the original message sent by the client. + * @param callback A callback to control how messages are handled further. + */ + onBeforeReceiveMessageFromServer?(message: unknown, callback: InterceptorCallback): void; + + /** + * Optional property to intercept (and/or modify) messages sent from the VSCode + * integration to the server via the `GlspVscodeServer.onServerReceiveEmitter`. + * + * The returned value from this function is the message that will be propagated + * to the server. It can be modified to anything. Returning `undefined` will + * cancel the propagation. + * + * @param originalMessage The original message received by the VSCode integration + * from the client. + * @param processedMessage If the VSCode integration modified the received message + * in any way, this parameter will contain the modified message. If the VSCode + * integration did not modify the message, this parameter will be identical to + * `originalMessage`. + * @param messageChanged This parameter will indicate wether the VSCode integration + * modified the incoming message. In other words: Whether `originalMessage` + * and `processedMessage` are different. + * @returns A message that will be propagated to the server. If the message + * is `undefined` the propagation will be cancelled. + */ + onBeforePropagateMessageToServer?( + originalMessage: unknown, processedMessage: unknown, messageChanged: boolean + ): unknown | undefined; + + /** + * Optional property to intercept (and/or modify) messages sent from the VSCode + * integration to a client via the `GlspVscodeClient.onClientReceiveEmitter`. + * + * The returned value from this function is the message that will be propagated + * to the client. It can be modified to anything. Returning `undefined` will + * cancel the propagation. + * + * @param originalMessage The original message received by the VSCode integration + * from the server. + * @param processedMessage If the VSCode integration modified the received message + * in any way, this parameter will contain the modified message. If the VSCode + * integration did not modify the message, this parameter will be identical to + * `originalMessage`. + * @param messageChanged This parameter will indicate wether the VSCode integration + * modified the incoming message. In other words: Whether `originalMessage` + * and `processedMessage` are different. + * @returns A message that will be propagated to the client. If the message + * is `undefined` the propagation will be cancelled. + */ + onBeforePropagateMessageToClient?( + originalMessage: unknown, processedMessage: unknown, messageChanged: boolean + ): unknown | undefined; +} diff --git a/packages/vscode-integration/src/utils/disposable.ts b/packages/vscode-integration/src/utils/disposable.ts deleted file mode 100644 index 59a96e2..0000000 --- a/packages/vscode-integration/src/utils/disposable.ts +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import * as vscode from 'vscode'; - -export function disposeAll(toDispose: vscode.Disposable[]): void { - while (toDispose.length) { - const disposable = toDispose.pop(); - if (disposable) { - disposable.dispose(); - } - } -} - -export abstract class Disposable implements vscode.Disposable { - - private _isDisposed = false; - - protected disposables: vscode.Disposable[] = []; - - protected _onDidDispose: vscode.EventEmitter; - - constructor() { - this._onDidDispose = this.addDisposable(new vscode.EventEmitter()); - } - - get onDidDispose(): vscode.Event { - return this._onDidDispose.event; - } - protected addDisposable(disposable: T): T { - if (this._isDisposed) { - disposable.dispose(); - } else { - this.disposables.push(disposable); - } - return disposable; - } - - dispose(): any { - if (!this._isDisposed) { - this._onDidDispose.fire(); - this._isDisposed = true; - disposeAll(this.disposables); - } - } -} diff --git a/packages/vscode-integration/src/utils/promise.ts b/packages/vscode-integration/src/utils/promise.ts deleted file mode 100644 index c88fd01..0000000 --- a/packages/vscode-integration/src/utils/promise.ts +++ /dev/null @@ -1,31 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2021 EclipseSource and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import * as vscode from 'vscode'; - -export function waitForEventWithTimeout(event: vscode.Event, timeout: number, eventName?: string): Promise { - return new Promise((resolve, reject) => { - const timer: ReturnType = setTimeout(() => { - listener.dispose(); - reject(new Error('Timeout waiting for ' + eventName || event.toString())); - }, timeout); - - const listener = event((e: E) => { - clearTimeout(timer); - listener.dispose(); - resolve(); - }); - }); -} diff --git a/yarn.lock b/yarn.lock index 17adc6e..5b75435 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4928,7 +4928,7 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sprotty-vscode-protocol@0.0.5, sprotty-vscode-protocol@^0.0.5: +sprotty-vscode-protocol@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/sprotty-vscode-protocol/-/sprotty-vscode-protocol-0.0.5.tgz#6d0578d0094b224ea50549786ebd59dadace7fcf" integrity sha512-nhuOLHgWEczQRjYgHE3C8oHv7cxGsv2mAacdETkMYnnBkLG3t28N68ydIt9a/lv6EhMJDwh9dLqyDBbvHPA3Wg==