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==