From f2ca9bf80b40ec3f6e4611eb5603182046ea0234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Thu, 13 Apr 2023 18:27:22 +0200 Subject: [PATCH 01/18] Implement first prototype version with 1 server for multiple clients --- .vscode/launch.json | 21 +- example/workflow/extension/package.json | 9 +- .../extension/src/workflow-extension.ts | 10 +- packages/vscode-integration/package.json | 3 +- .../src/glsp-vscode-connector.ts | 29 ++- packages/vscode-integration/src/index.ts | 1 + .../src/liveshare/constants.ts | 19 ++ .../vscode-integration/src/liveshare/index.ts | 16 ++ .../src/liveshare/liveshareService.ts | 223 ++++++++++++++++++ .../vscode-integration/src/liveshare/store.ts | 52 ++++ .../src/liveshare/treeDataProvider.ts | 44 ++++ .../liveshare-glsp-client.ts | 209 ++++++++++++++++ .../socket-glsp-vscode-server.ts | 20 +- packages/vscode-integration/src/types.ts | 5 +- yarn.lock | 77 +++++- 15 files changed, 722 insertions(+), 16 deletions(-) create mode 100644 packages/vscode-integration/src/liveshare/constants.ts create mode 100644 packages/vscode-integration/src/liveshare/index.ts create mode 100644 packages/vscode-integration/src/liveshare/liveshareService.ts create mode 100644 packages/vscode-integration/src/liveshare/store.ts create mode 100644 packages/vscode-integration/src/liveshare/treeDataProvider.ts create mode 100644 packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index d167466..e672a34 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "sourceMaps": true }, { - "name": "Workflow GLSP Example Extension (External GLSP Server)", + "name": "Workflow GLSP Example Extension (External GLSP Server) 5007", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", @@ -37,6 +37,25 @@ "GLSP_SERVER_DEBUG": "true", "GLSP_SERVER_PORT": "5007" } + }, + { + "name": "Workflow GLSP Example Extension (External GLSP Server) 5017", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/example/workflow/workspace", + "--extensionDevelopmentPath=${workspaceFolder}/example/workflow/extension" + ], + "outFiles": [ + "${workspaceFolder}/example/workflow/extension/lib/*.js", + "${workspaceFolder}/vscode-integration/lib/**/*.js" + ], + "sourceMaps": true, + "env": { + "GLSP_SERVER_DEBUG": "true", + "GLSP_SERVER_PORT": "5017" + } } ] } diff --git a/example/workflow/extension/package.json b/example/workflow/extension/package.json index 9989eed..715506e 100644 --- a/example/workflow/extension/package.json +++ b/example/workflow/extension/package.json @@ -28,6 +28,9 @@ "vscode": "^1.54.0" }, "contributes": { + "liveshare.spaces": [ + "vsls-workspace" + ], "customEditors": [ { "viewType": "workflow.glspDiagram", @@ -182,8 +185,12 @@ "webview" ], "dependencies": { - "@vscode/codicons": "^0.0.25" + "@vscode/codicons": "^0.0.25", + "vsls": "^1.0.4753" }, + "extensionDependencies": [ + "ms-vsliveshare.vsliveshare" + ], "main": "./lib/index", "devDependencies": { "@eclipse-glsp/vscode-integration": "1.1.0-next", diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 93eb191..3e37657 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -13,10 +13,10 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GlspVscodeConnector, NavigateAction } from '@eclipse-glsp/vscode-integration'; +import { GlspVscodeConnector, NavigateAction, liveshareService } from '@eclipse-glsp/vscode-integration'; import { - configureDefaultCommands, GlspServerLauncher, + configureDefaultCommands, SocketGlspVscodeServer } from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; import * as path from 'path'; @@ -47,6 +47,8 @@ export async function activate(context: vscode.ExtensionContext): Promise await serverProcess.start(); } + await liveshareService.liveshare(context); + // Wrap server with quickstart component const workflowServer = new SocketGlspVscodeServer({ clientId: 'glsp.workflow', @@ -54,6 +56,9 @@ export async function activate(context: vscode.ExtensionContext): Promise serverPort: JSON.parse(process.env.GLSP_SERVER_PORT || DEFAULT_SERVER_PORT) }); + liveshareService.registerServer(workflowServer); + + // Initialize GLSP-VSCode connector with server wrapper const glspVscodeConnector = new GlspVscodeConnector({ server: workflowServer, @@ -70,6 +75,7 @@ export async function activate(context: vscode.ExtensionContext): Promise ); context.subscriptions.push(workflowServer, glspVscodeConnector, customEditorProvider); + workflowServer.start(); configureDefaultCommands({ extensionContext: context, connector: glspVscodeConnector, diagramPrefix: 'workflow' }); diff --git a/packages/vscode-integration/package.json b/packages/vscode-integration/package.json index 6279e34..8719cf3 100644 --- a/packages/vscode-integration/package.json +++ b/packages/vscode-integration/package.json @@ -35,7 +35,8 @@ }, "dependencies": { "@eclipse-glsp/protocol": "next", - "vscode-jsonrpc": "^8.0.2" + "vscode-jsonrpc": "^8.0.2", + "vsls": "^1.0.4753" }, "devDependencies": { "@types/node": "^12.12.0", diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index bcff8a2..641287b 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -166,6 +166,8 @@ export class GlspVscodeConnector { + const panelOnDisposeListener = client.webviewPanel.onDidDispose(async () => { this.diagnostics.set(client.document.uri, undefined); // this clears the diagnostics for the file this.clientMap.delete(client.clientId); this.documentMap.delete(client.document); @@ -186,19 +190,36 @@ export class GlspVscodeConnector): string { + let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.toString(); + workspacePath = workspacePath?.endsWith('/') ? workspacePath : workspacePath + '/'; + return client.document.uri.toString().replace(workspacePath, ''); + } - protected async createInitializeClientSessionParams(client: GlspVscodeClient): Promise { + protected async createInitializeClientSessionParams(client: GlspVscodeClient, relativeDocumentUri: string): Promise { return { clientSessionId: client.clientId, - diagramType: client.diagramType + diagramType: client.diagramType, + args: { + relativeDocumentUri, + subclientId: 'H' + } }; } diff --git a/packages/vscode-integration/src/index.ts b/packages/vscode-integration/src/index.ts index fca496e..ed5d873 100644 --- a/packages/vscode-integration/src/index.ts +++ b/packages/vscode-integration/src/index.ts @@ -16,4 +16,5 @@ export * from '@eclipse-glsp/protocol'; export * from './client-actions'; export * from './glsp-vscode-connector'; +export * from './liveshare'; export * from './types'; diff --git a/packages/vscode-integration/src/liveshare/constants.ts b/packages/vscode-integration/src/liveshare/constants.ts new file mode 100644 index 0000000..e5e50bd --- /dev/null +++ b/packages/vscode-integration/src/liveshare/constants.ts @@ -0,0 +1,19 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +export const GET_COUNT_REQUEST = 'getCount'; +export const INCREMENT_COUNT_COMMAND = 'liveshare.incrementCount'; +export const INCREMENT_COUNT_NOTIFICATION = 'incrementCount'; +export const SERVICE_NAME = 'counter'; diff --git a/packages/vscode-integration/src/liveshare/index.ts b/packages/vscode-integration/src/liveshare/index.ts new file mode 100644 index 0000000..7ccf72e --- /dev/null +++ b/packages/vscode-integration/src/liveshare/index.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +export * from './liveshareService'; diff --git a/packages/vscode-integration/src/liveshare/liveshareService.ts b/packages/vscode-integration/src/liveshare/liveshareService.ts new file mode 100644 index 0000000..306832a --- /dev/null +++ b/packages/vscode-integration/src/liveshare/liveshareService.ts @@ -0,0 +1,223 @@ +/******************************************************************************** + * 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 {ActionMessage, DisposeClientSessionParameters, GLSPClient, InitializeClientSessionParameters, Operation } from '@eclipse-glsp/protocol'; +import * as vscode from 'vscode'; +import { LiveShare, Role, Session, SharedService, SharedServiceProxy, getApi } from 'vsls'; +import { GlspVscodeServer } from '../types'; +import { INCREMENT_COUNT_NOTIFICATION, SERVICE_NAME } from './constants'; + +class LiveshareService { + public session: Session | null; + public vsls: LiveShare; + public role: Role = Role.None; + private _service: SharedService | SharedServiceProxy | null; + private _glspClient: GLSPClient; + private _server: GlspVscodeServer; + protected readonly clients = new Map(); // relativeDocumentUri, clients + + // TODO DA clientIds are different, confusing + registerServer(server: GlspVscodeServer): void { + this._server = server; + } + + isConnectionOpen(): boolean { + return !!this._service && this._service.isServiceAvailable; + } + + isInVslsMode(): boolean { + return !!this.vsls; + } + + isHost(): boolean { + return this.role === Role.Host; + } + + isGuest(): boolean { + return this.role === Role.Guest; + } + + notify(action: Operation): void { + // only if liveshare is activated + if (this._service) { + this._service!.notify(INCREMENT_COUNT_NOTIFICATION, { + peerNumber: this.vsls.session.peerNumber, + action + }); + } + } + + get guestService(): SharedServiceProxy { + return this._service as SharedServiceProxy; + } + + get hostService(): SharedService { + return this._service as SharedService; + } + + + async liveshare(context: vscode.ExtensionContext): Promise { + this.vsls = (await getApi())!; + /* if (!this.vsls) { + return; + } + + this.session = this.vsls.session; + if (!this.session) { + return; + } + + this.role = this.session.role; + if (this.role === Role.Host) { + this._service = await this.vsls.shareService(SERVICE_NAME); + + this._service!.onRequest('INITIALIZE_SERVER', () => this._server.initializeResult); + } else if (this.role === Role.Guest) { + this._service = await this.vsls.shareService(SERVICE_NAME); + }*/ + + /* + // Register the custom tree provider with Live Share, which + // allows you to augment it however you'd like to. + const treeDataProvider = new CountTreeDataProvider(store); + vsls.registerTreeDataProvider(View.Session, treeDataProvider); + + context.subscriptions.push( + vscode.commands.registerCommand(INCREMENT_COUNT_COMMAND, () => { + store.increment(); + service!.notify(INCREMENT_COUNT_NOTIFICATION, { + peerNumber: vsls.session.peerNumber + }); + }) + ); + */ + // This event will fire whenever an end-user joins + // or leaves a sessionn, either of the host or guest. + this.vsls.onDidChangeSession(async e => { + this.role = e.session.role; + this.session = e.session; + if (e.session.role === Role.Host) { + /* + store.count = 0; + */ + + // Expose a new custom RPC service, that allows + // guests in a Live Share session to retrieve and + // synchronize custom state with each other. In the + // case of this sample, the state being sychronized + // is simply a count, and the only action that + // can be taken on it is to increment the count. + this._service = await this.vsls.shareService(SERVICE_NAME); + + this._service!.onRequest('INITIALIZE_CLIENT_SESSION', async params => { + const initializeClientSessionParams = params[1] as InitializeClientSessionParameters; + const subclientId = params[2] as string; + initializeClientSessionParams.args = { + ...initializeClientSessionParams.args, + subclientId + }; + (await this._server.glspClient).initializeClientSession(initializeClientSessionParams); + }); + + this._service!.onRequest('DISPOSE_CLIENT_SESSION', async params => { + const disposeClientSessionParams = params[1] as DisposeClientSessionParameters; + const subclientId = params[2] as string; + disposeClientSessionParams.args = { + ...disposeClientSessionParams.args, + subclientId + }; + (await this._server.glspClient).disposeClientSession(disposeClientSessionParams); + }); + + this._service!.onRequest('SEND_ACTION_MESSAGE', async params => { + const actionMessage = params[1] as ActionMessage; + const subclientId = params[2] as string; + const newActionMessage = { + ...actionMessage, + subclientId + }; + (await this._server.glspClient).sendActionMessage(newActionMessage); + }); + + // TODO DA here we could expose the file as GModel + // this._service!.onRequest(GET_COUNT_REQUEST, () => store.count); + + this._service!.onNotify(INCREMENT_COUNT_NOTIFICATION, (ev: any) => { + /* + store.increment(); + */ + const { action } = ev; + for (const key of this.clients.keys()) { + const message = { + clientId: key, + action: action, + liveshareState: 'broadcasted' + }; + this._glspClient.sendActionMessage(message); + } + + // Re-broadcast the notification to all other guests. + this._service!.notify(INCREMENT_COUNT_NOTIFICATION, ev); + }); + } else if (e.session.role === Role.Guest) { + // Attempt to grab a proxy reference to the custom + // counter service on the host. If this doesn't exist, + // then it means the host doesn't have this extension + // installed, and therefore, the extension should + // gracefully degrade. + this._service = await this.vsls.getSharedService(SERVICE_NAME); + if (!this._service) { + return; + } + + this._service!.onNotify('ON_ACTION_MESSAGE', (message: any) => { + message = message as ActionMessage; + // message is for this guest + if (e.session.peerNumber === +(message as any)['subclientId']) { + this._server.onServerSendEmitter.fire(message); + } + }); + + // Grab the current count from the host, who is the + // "source of truth" for the state store. + // TODO DA here we could expose the file as GModel + // store.count = await this._service.request(GET_COUNT_REQUEST, []); + + this._service.onNotify(INCREMENT_COUNT_NOTIFICATION, ({ peerNumber, action }: any) => { + // Ignore the notification if it originated with this user. + if (peerNumber === this.vsls.session.peerNumber) { + return; + } + + for (const key of this.clients.keys()) { + const message = { + clientId: key, + action: action, + liveshareState: 'broadcasted' + }; + this._glspClient.sendActionMessage(message); + } + + /* + store.increment(); + */ + }); + } + }); + } +} + +export const liveshareService = new LiveshareService(); diff --git a/packages/vscode-integration/src/liveshare/store.ts b/packages/vscode-integration/src/liveshare/store.ts new file mode 100644 index 0000000..44d13f1 --- /dev/null +++ b/packages/vscode-integration/src/liveshare/store.ts @@ -0,0 +1,52 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +export interface ICountStore { + count: number; + onChange(handler: () => void): void; +} + +// This is a very basic store, which allows the extension +// to centralize state management. For example, the custom +// tree provider can subscribe to count changes, without +// needing to worry about the various places the count +// can actually change. Instead of writing your +// own store implementation, you could also use a library +// like Redux, MobX, etc. +class CountStore implements ICountStore { + private _count = 0; + // eslint-disable-next-line @typescript-eslint/no-empty-function + private _handler: () => void = () => {}; + + get count(): number { + return this._count; + } + + set count(count: number) { + this._count = count; + this._handler(); + } + + increment(): void { + this._count++; + this._handler(); + } + + onChange(handler: () => void): void { + this._handler = handler; + } +} + +export const store = new CountStore(); diff --git a/packages/vscode-integration/src/liveshare/treeDataProvider.ts b/packages/vscode-integration/src/liveshare/treeDataProvider.ts new file mode 100644 index 0000000..5570b86 --- /dev/null +++ b/packages/vscode-integration/src/liveshare/treeDataProvider.ts @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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 { Command, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem } from 'vscode'; +import { INCREMENT_COUNT_COMMAND } from './constants'; +import { ICountStore } from './store'; + +export class CountTreeDataProvider implements TreeDataProvider { + private _command: Command = { + command: INCREMENT_COUNT_COMMAND, + title: 'Increment Count' + }; + + private _onDidChangeTreeData = new EventEmitter(); + public readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + constructor(private store: ICountStore) { + store.onChange(() => { + this._onDidChangeTreeData.fire(this._command); + }); + } + + getChildren(element?: Command): ProviderResult { + return Promise.resolve([this._command]); + } + + getTreeItem(element: Command): TreeItem { + const treeItem = new TreeItem(`Count: ${this.store.count}`); + treeItem.command = element; + return treeItem; + } +} diff --git a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts new file mode 100644 index 0000000..88cca1d --- /dev/null +++ b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts @@ -0,0 +1,209 @@ +/******************************************************************************** + * Copyright (c) 2019-2022 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 { + ActionMessage, + ActionMessageHandler, + ClientState, + DisposeClientSessionParameters, + GLSPClient, + InitializeClientSessionParameters, + InitializeParameters, + InitializeResult +} from '@eclipse-glsp/protocol'; + +import { liveshareService } from '../liveshare'; + +export class LiveshareGlspClient implements GLSPClient { + readonly id: string; + + protected jsonrpcClient: GLSPClient; + // Map subclientId H = host + protected registeredSubclientMap = new Map>(); + // overwritten clientSessionIds for Server: Map + protected serverClientIdMap = new Map(); + // Map + protected requestSubclientMap = new Map(); + + protected nextUniqueRequestId = 10000; + + + constructor(id: string, existingClient: GLSPClient) { + this.id = id; + + this.jsonrpcClient = existingClient; + } + + shutdownServer(): void { + this.jsonrpcClient.shutdownServer(); + } + + initializeServer(params: InitializeParameters): Promise { + return this.jsonrpcClient.initializeServer(params); + } + + async initializeClientSession(params: InitializeClientSessionParameters): Promise { + const relativeDocumentUri = params.args?.relativeDocumentUri as string; + const subclientId = params.args?.subclientId as string; + if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); + const initialized = subclientMap.size > 0; + subclientMap.set(subclientId, params.clientSessionId); + this.registeredSubclientMap.set(relativeDocumentUri, subclientMap); + if (initialized) { + return; + } + // if a guest is initiating document firstly + params.clientSessionId += '_'+subclientId; + this.serverClientIdMap.set(relativeDocumentUri, params.clientSessionId); + return this.jsonrpcClient.initializeClientSession(params); + } else if (liveshareService.isGuest()) { + liveshareService.guestService.request('INITIALIZE_CLIENT_SESSION', [params, ''+liveshareService.session?.peerNumber, this.id]).then(res => { + console.log('INITIALIZE_CLIENT_SESSION', res); + }); + } + } + + async disposeClientSession(params: DisposeClientSessionParameters): Promise { + const relativeDocumentUri = params.args?.relativeDocumentUri as string; + const subclientId = params.args?.subclientId as string; + if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); + subclientMap.delete(subclientId); + this.registeredSubclientMap.set(relativeDocumentUri, subclientMap); + if (subclientMap.size > 0) { + return; + } + this.serverClientIdMap.delete(relativeDocumentUri); + return this.jsonrpcClient.disposeClientSession(params); + } else if (liveshareService.isGuest()) { + liveshareService.guestService.request('DISPOSE_CLIENT_SESSION', [params, ''+liveshareService.session?.peerNumber, this.id]).then(res => { + console.log('DISPOSE_CLIENT_SESSION', res); + }); + } + } + + onActionMessage(handler: ActionMessageHandler): void { + this.jsonrpcClient.onActionMessage((message) => { + if (!liveshareService.isConnectionOpen()) { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; + const subclientId = 'H'; + (message as any)['subclientId'] = subclientId; + message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; + const tempRequestId = (message.action as any)['responseId']; + if (tempRequestId != null) { + const originalRequestId = this.requestSubclientMap.get(tempRequestId)?.originalRequestId || ''; + this.requestSubclientMap.delete(tempRequestId); + (message.action as any)['responseId'] = originalRequestId; + } + handler(message); // only to host + } else if (liveshareService.isHost()){ + const tempRequestId = (message.action as any)['responseId']; + if (tempRequestId != null) { + const subclientId = this.requestSubclientMap.get(tempRequestId)?.subclientId || ''; + const originalRequestId = this.requestSubclientMap.get(tempRequestId)?.originalRequestId || ''; + this.requestSubclientMap.delete(tempRequestId); + (message.action as any)['responseId'] = originalRequestId; + (message as any)['subclientId'] = subclientId; + // find initial local clientId to action + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; + message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; + if (subclientId === 'H') { + handler(message); // notify host + } else { + liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); // notify subclientId + } + } else if (message.action.kind === 'requestBounds') { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; + const subclientMap = this.registeredSubclientMap?.get(relativeDocumentUri) + if (subclientMap?.size! > 0) { + // get host subclientId or first guest subclientId + const subclientId = (subclientMap!.has('H') ? 'H' : Array.from(subclientMap!.keys())[0]) || ''; + (message as any)['subclientId'] = subclientId; + message.clientId = subclientMap?.get(subclientId) || ''; + if (subclientId === 'H') { + handler(message); // host over handler + } else { + liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); + } + } + + } else /*if ( + message.action.kind === 'setModel' || + message.action.kind === 'updateModel' || + message.action.kind === 'setDirtyState' || + message.action.kind === 'serverMessage' || + message.action.kind === 'serverStatus')*/ { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + if (subclientMap) { + for (const [subclientId, clientId] of subclientMap.entries()) { + (message as any)['subclientId'] = subclientId; + message.clientId = clientId; + if (subclientId === 'H') { + handler(message); // host over handler + } else { + liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); + } + } + } + } + } + }); + } + + sendActionMessage(message: ActionMessage): void { + const relativeDocumentUri = (message as any)['relativeDocumentUri'] as string; + if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { + message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; + const originalRequestId = (message.action as any)['requestId']; + if (originalRequestId != null) { + const tempRequestId = this.generateRequestId(); + (message.action as any)['requestId'] = tempRequestId; + const subclientId = (message as any)['subclientId'] as string; + this.requestSubclientMap.set(tempRequestId, { subclientId, originalRequestId }); + } + this.jsonrpcClient.sendActionMessage(message); + } else if (liveshareService.isGuest()) { + liveshareService.guestService.request('SEND_ACTION_MESSAGE', [message, ''+liveshareService.session?.peerNumber, this.id]).then(res => { + console.log('SEND_ACTION_MESSAGE', res); + }); + } + } + + start(): Promise { + return this.jsonrpcClient.start(); + } + + stop(): Promise { + return this.jsonrpcClient.stop(); + } + + get currentState(): ClientState { + return this.jsonrpcClient.currentState; + } + + private generateRequestId(): string { + return (this.nextUniqueRequestId++).toString(); + } + + private getRelativeDocumentUriByServerClientId(serverClientId: string): string | undefined { + for (let [key, value] of this.serverClientIdMap.entries()) { + if (value === serverClientId) + return key; + } + return undefined; + } +} 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 index f60131c..4e6c303 100644 --- a/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts +++ b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts @@ -26,6 +26,7 @@ import * as vscode from 'vscode'; import { createMessageConnection } from 'vscode-jsonrpc'; import { SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node'; import { GlspVscodeServer } from '../types'; +import { LiveshareGlspClient } from './liveshare-glsp-client'; interface SocketGlspVscodeServerOptions { /** Port of the running server. */ @@ -52,10 +53,10 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab readonly onSendToServerEmitter = new vscode.EventEmitter(); readonly onServerMessage: vscode.Event; - protected readonly onServerSendEmitter = new vscode.EventEmitter(); + readonly onServerSendEmitter = new vscode.EventEmitter(); protected readonly socket = new net.Socket(); - protected readonly _glspClient: GLSPClient; + protected _glspClient: GLSPClient; protected readonly onReady: Promise; protected setReady: () => void; @@ -72,10 +73,10 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab const writer = new SocketMessageWriter(this.socket); const connection = createMessageConnection(reader, writer); - this._glspClient = new BaseJsonrpcGLSPClient({ + this._glspClient = new LiveshareGlspClient(options.clientId, new BaseJsonrpcGLSPClient({ id: options.clientId, connectionProvider: connection - }); + })); this.onSendToServerEmitter.event(message => { this.onReady.then(() => { @@ -132,4 +133,15 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab get glspClient(): Promise { return this.onReady.then(() => this._glspClient); } + + async initalizeNewGlspClient(glspClient: GLSPClient): Promise { + // await this._glspClient.stop(); // old client + + this._glspClient = glspClient; + await this._glspClient.start(); + + this._glspClient.onActionMessage(message => { + this.onServerSendEmitter.fire(message); + }); + } } diff --git a/packages/vscode-integration/src/types.ts b/packages/vscode-integration/src/types.ts index adb471b..b076832 100644 --- a/packages/vscode-integration/src/types.ts +++ b/packages/vscode-integration/src/types.ts @@ -85,7 +85,8 @@ export interface GlspVscodeServer { * and processed. */ readonly onSendToServerEmitter: vscode.EventEmitter; - + + readonly onServerSendEmitter: 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 @@ -110,6 +111,8 @@ export interface GlspVscodeServer { * to call this method during the their initialization phase. */ readonly initializeResult: Promise; + + initalizeNewGlspClient(glspClient: GLSPClient): Promise; } interface InterceptorCallback { diff --git a/yarn.lock b/yarn.lock index 71dbfb2..1c0adca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1107,6 +1107,17 @@ npmlog "^6.0.2" write-file-atomic "^4.0.1" +"@microsoft/servicehub-framework@^2.6.74": + version "2.6.74" + resolved "https://registry.yarnpkg.com/@microsoft/servicehub-framework/-/servicehub-framework-2.6.74.tgz#7c45717adea4f6fe2bd0c8bbe572f42c64a13a83" + integrity sha512-QJ//zzvxffupIkzupnVbMYY5YDOP+g5FlG6x0Pl7svRyq8pAouiibckJJcZlMtsMypKWwAnVBKb9/sonEOsUxw== + dependencies: + await-semaphore "^0.1.3" + msgpack-lite "^0.1.26" + nerdbank-streams "2.5.60" + strict-event-emitter-types "^2.0.0" + vscode-jsonrpc "^4.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2074,6 +2085,11 @@ autocompleter@^5.1.0: resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-5.2.0.tgz#9ed3df262614fd557bf4d5bf67ab13cdee008203" integrity sha512-CMYgI+r7RGZFaT0SvXcyBn1hb/Ne6XbjXimWQPc16LcwZgUGFBHg/Pv8honrwkTZE4DbfrD/MzqlG+Bn2u+1ng== +await-semaphore@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" + integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q== + axios@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a" @@ -2267,11 +2283,21 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +cancellationtoken@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cancellationtoken/-/cancellationtoken-2.2.0.tgz#a3d93cb8675f29dd1574a358b72adbde3da89aeb" + integrity sha512-uF4sHE5uh2VdEZtIRJKGoXAD9jm7bFY0tDRCzH4iLp262TOJ2lrtNHjMG2zc8H+GICOpELIpM7CGW5JeWnb3Hg== + caniuse-lite@^1.0.30001400: version "1.0.30001439" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== +caught@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/caught/-/caught-0.1.3.tgz#f63db0d65f1bacea7cb4852cd8f1d72166a2c8bf" + integrity sha512-DTWI84qfoqHEV5jHRpsKNnEisVCeuBDscXXaXyRLXC+4RD6rFftUNuTElcQ7LeO7w622pfzWkA1f6xu5qEAidw== + chai@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" @@ -3217,6 +3243,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-lite@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d" + integrity sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw== + eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -3858,7 +3889,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.1.8: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -3973,6 +4004,11 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^7.0.0" +int64-buffer@^0.1.9: + version "0.1.10" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423" + integrity sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA== + internal-slot@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" @@ -4213,7 +4249,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@~1.0.0: +isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== @@ -4887,6 +4923,16 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpack-lite@^0.1.26: + version "0.1.26" + resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89" + integrity sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw== + dependencies: + event-lite "^0.1.1" + ieee754 "^1.1.8" + int64-buffer "^0.1.9" + isarray "^1.0.0" + multimatch@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" @@ -4962,6 +5008,16 @@ neo-async@^2.6.0, neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nerdbank-streams@2.5.60: + version "2.5.60" + resolved "https://registry.yarnpkg.com/nerdbank-streams/-/nerdbank-streams-2.5.60.tgz#455edb9a71070a0964a1b39eee5afb30ef826cd6" + integrity sha512-saQaMyTtVDAEc+S+BPXKM6K1AF3FyrorFSDzaCkdmtDe2kZzu1aYPQZNLmnxJhxbTcghYrEmYFFoaDxBDVadCw== + dependencies: + await-semaphore "^0.1.3" + cancellationtoken "^2.0.1" + caught "^0.1.3" + msgpack-lite "^0.1.26" + nise@^5.1.2: version "5.1.4" resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" @@ -6293,6 +6349,11 @@ ssri@^9.0.0, ssri@^9.0.1: dependencies: minipass "^3.1.1" +strict-event-emitter-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" + integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -6796,6 +6857,11 @@ vscode-jsonrpc@8.0.2, vscode-jsonrpc@^8.0.2: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz#f239ed2cd6004021b6550af9fd9d3e47eee3cac9" integrity sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ== +vscode-jsonrpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz#a7bf74ef3254d0a0c272fab15c82128e378b3be9" + integrity sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg== + vscode-languageserver-protocol@^3.16.0: version "3.17.2" resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz#beaa46aea06ed061576586c5e11368a9afc1d378" @@ -6821,6 +6887,13 @@ vscode-ws-jsonrpc@^2.0.1: dependencies: vscode-jsonrpc "8.0.2" +vsls@^1.0.4753: + version "1.0.4753" + resolved "https://registry.yarnpkg.com/vsls/-/vsls-1.0.4753.tgz#1b0957fc987fddd2b4d8c03925d086fb701d11fa" + integrity sha512-hmrsMbhjuLoU8GgtVfqhbV4ZkGvDpLV2AFmzx+cCOGNra2qk0Q36dYkfwENqy/vJVQ/2/lhxcn+69FYnKQRhgg== + dependencies: + "@microsoft/servicehub-framework" "^2.6.74" + walk-up-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e" From 168f7a7416b78078367c0229a5d76896a008406c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Fri, 14 Apr 2023 01:12:10 +0200 Subject: [PATCH 02/18] change subclientId from ActionMessage to Action --- .../src/glsp-vscode-connector.ts | 10 ++++-- .../src/liveshare/liveshareService.ts | 9 ++--- .../liveshare-glsp-client.ts | 35 +++++++++++++------ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index 641287b..eb8c59e 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -167,7 +167,7 @@ export class GlspVscodeConnector): string { + // FIXME %20 instead of blankspace let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.toString(); workspacePath = workspacePath?.endsWith('/') ? workspacePath : workspacePath + '/'; return client.document.uri.toString().replace(workspacePath, ''); } - protected async createInitializeClientSessionParams(client: GlspVscodeClient, relativeDocumentUri: string): Promise { + protected async createInitializeClientSessionParams( + client: GlspVscodeClient, + relativeDocumentUri: string + ): Promise { return { clientSessionId: client.clientId, diagramType: client.diagramType, diff --git a/packages/vscode-integration/src/liveshare/liveshareService.ts b/packages/vscode-integration/src/liveshare/liveshareService.ts index 306832a..c41a6a7 100644 --- a/packages/vscode-integration/src/liveshare/liveshareService.ts +++ b/packages/vscode-integration/src/liveshare/liveshareService.ts @@ -145,11 +145,8 @@ class LiveshareService { this._service!.onRequest('SEND_ACTION_MESSAGE', async params => { const actionMessage = params[1] as ActionMessage; const subclientId = params[2] as string; - const newActionMessage = { - ...actionMessage, - subclientId - }; - (await this._server.glspClient).sendActionMessage(newActionMessage); + (actionMessage.action as any)['subclientId'] = subclientId; + (await this._server.glspClient).sendActionMessage(actionMessage); }); // TODO DA here we could expose the file as GModel @@ -186,7 +183,7 @@ class LiveshareService { this._service!.onNotify('ON_ACTION_MESSAGE', (message: any) => { message = message as ActionMessage; // message is for this guest - if (e.session.peerNumber === +(message as any)['subclientId']) { + if (e.session.peerNumber === +((message.action as any)['subclientId'])) { this._server.onServerSendEmitter.fire(message); } }); diff --git a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts index 88cca1d..0ea4ebc 100644 --- a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts +++ b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts @@ -21,8 +21,10 @@ import { GLSPClient, InitializeClientSessionParameters, InitializeParameters, - InitializeResult + InitializeResult, + RequestModelAction } from '@eclipse-glsp/protocol'; +import * as vscode from 'vscode'; import { liveshareService } from '../liveshare'; @@ -32,9 +34,9 @@ export class LiveshareGlspClient implements GLSPClient { protected jsonrpcClient: GLSPClient; // Map subclientId H = host protected registeredSubclientMap = new Map>(); - // overwritten clientSessionIds for Server: Map + // overwritten clientSessionIds for Server: Map protected serverClientIdMap = new Map(); - // Map + // Map protected requestSubclientMap = new Map(); protected nextUniqueRequestId = 10000; @@ -99,8 +101,7 @@ export class LiveshareGlspClient implements GLSPClient { this.jsonrpcClient.onActionMessage((message) => { if (!liveshareService.isConnectionOpen()) { const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; - const subclientId = 'H'; - (message as any)['subclientId'] = subclientId; + const subclientId = (message.action as any)['subclientId'] || 'H'; message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; const tempRequestId = (message.action as any)['responseId']; if (tempRequestId != null) { @@ -116,7 +117,7 @@ export class LiveshareGlspClient implements GLSPClient { const originalRequestId = this.requestSubclientMap.get(tempRequestId)?.originalRequestId || ''; this.requestSubclientMap.delete(tempRequestId); (message.action as any)['responseId'] = originalRequestId; - (message as any)['subclientId'] = subclientId; + (message.action as any)['subclientId'] = subclientId; // find initial local clientId to action const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; @@ -129,9 +130,7 @@ export class LiveshareGlspClient implements GLSPClient { const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; const subclientMap = this.registeredSubclientMap?.get(relativeDocumentUri) if (subclientMap?.size! > 0) { - // get host subclientId or first guest subclientId - const subclientId = (subclientMap!.has('H') ? 'H' : Array.from(subclientMap!.keys())[0]) || ''; - (message as any)['subclientId'] = subclientId; + const subclientId = (message.action as any)['subclientId'] || 'H'; message.clientId = subclientMap?.get(subclientId) || ''; if (subclientId === 'H') { handler(message); // host over handler @@ -150,7 +149,7 @@ export class LiveshareGlspClient implements GLSPClient { const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); if (subclientMap) { for (const [subclientId, clientId] of subclientMap.entries()) { - (message as any)['subclientId'] = subclientId; + (message.action as any)['subclientId'] = subclientId; message.clientId = clientId; if (subclientId === 'H') { handler(message); // host over handler @@ -168,11 +167,19 @@ export class LiveshareGlspClient implements GLSPClient { const relativeDocumentUri = (message as any)['relativeDocumentUri'] as string; if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; + // if requestModel action and originClient not host => change sourceUri + if (message.action.kind === 'requestModel' && (message.action as any)['subclientId'] !== 'H') { + const requestModelAction = message.action as RequestModelAction; + requestModelAction.options = { + ...requestModelAction.options, + sourceUri: this.getFullDocumentUri(relativeDocumentUri) + } + } const originalRequestId = (message.action as any)['requestId']; if (originalRequestId != null) { const tempRequestId = this.generateRequestId(); (message.action as any)['requestId'] = tempRequestId; - const subclientId = (message as any)['subclientId'] as string; + const subclientId = (message.action as any)['subclientId'] as string; this.requestSubclientMap.set(tempRequestId, { subclientId, originalRequestId }); } this.jsonrpcClient.sendActionMessage(message); @@ -206,4 +213,10 @@ export class LiveshareGlspClient implements GLSPClient { } return undefined; } + + protected getFullDocumentUri(relativeDocumentUri: string): string { + let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.toString(); + workspacePath = workspacePath?.endsWith('/') ? workspacePath : workspacePath + '/'; + return workspacePath + relativeDocumentUri; + } } From c9396e2c86e0d088c7be836c0640ddfe3d61e2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Sat, 15 Apr 2023 12:34:34 +0200 Subject: [PATCH 03/18] fix reopen diagram bug --- .../src/liveshare/liveshareService.ts | 15 ++- .../liveshare-glsp-client.ts | 110 ++++++------------ 2 files changed, 47 insertions(+), 78 deletions(-) diff --git a/packages/vscode-integration/src/liveshare/liveshareService.ts b/packages/vscode-integration/src/liveshare/liveshareService.ts index c41a6a7..cdfd806 100644 --- a/packages/vscode-integration/src/liveshare/liveshareService.ts +++ b/packages/vscode-integration/src/liveshare/liveshareService.ts @@ -14,7 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import {ActionMessage, DisposeClientSessionParameters, GLSPClient, InitializeClientSessionParameters, Operation } from '@eclipse-glsp/protocol'; +import { + ActionMessage, + DisposeClientSessionParameters, + GLSPClient, + InitializeClientSessionParameters, + Operation +} from '@eclipse-glsp/protocol'; import * as vscode from 'vscode'; import { LiveShare, Role, Session, SharedService, SharedServiceProxy, getApi } from 'vsls'; import { GlspVscodeServer } from '../types'; @@ -68,7 +74,6 @@ class LiveshareService { return this._service as SharedService; } - async liveshare(context: vscode.ExtensionContext): Promise { this.vsls = (await getApi())!; /* if (!this.vsls) { @@ -129,7 +134,7 @@ class LiveshareService { ...initializeClientSessionParams.args, subclientId }; - (await this._server.glspClient).initializeClientSession(initializeClientSessionParams); + await (await this._server.glspClient).initializeClientSession(initializeClientSessionParams); }); this._service!.onRequest('DISPOSE_CLIENT_SESSION', async params => { @@ -139,7 +144,7 @@ class LiveshareService { ...disposeClientSessionParams.args, subclientId }; - (await this._server.glspClient).disposeClientSession(disposeClientSessionParams); + await (await this._server.glspClient).disposeClientSession(disposeClientSessionParams); }); this._service!.onRequest('SEND_ACTION_MESSAGE', async params => { @@ -183,7 +188,7 @@ class LiveshareService { this._service!.onNotify('ON_ACTION_MESSAGE', (message: any) => { message = message as ActionMessage; // message is for this guest - if (e.session.peerNumber === +((message.action as any)['subclientId'])) { + if (e.session.peerNumber === +(message.action as any)['subclientId']) { this._server.onServerSendEmitter.fire(message); } }); diff --git a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts index 0ea4ebc..1195844 100644 --- a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts +++ b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts @@ -36,12 +36,9 @@ export class LiveshareGlspClient implements GLSPClient { protected registeredSubclientMap = new Map>(); // overwritten clientSessionIds for Server: Map protected serverClientIdMap = new Map(); - // Map - protected requestSubclientMap = new Map(); protected nextUniqueRequestId = 10000; - constructor(id: string, existingClient: GLSPClient) { this.id = id; @@ -68,13 +65,15 @@ export class LiveshareGlspClient implements GLSPClient { return; } // if a guest is initiating document firstly - params.clientSessionId += '_'+subclientId; + params.clientSessionId += '_' + subclientId; this.serverClientIdMap.set(relativeDocumentUri, params.clientSessionId); return this.jsonrpcClient.initializeClientSession(params); } else if (liveshareService.isGuest()) { - liveshareService.guestService.request('INITIALIZE_CLIENT_SESSION', [params, ''+liveshareService.session?.peerNumber, this.id]).then(res => { - console.log('INITIALIZE_CLIENT_SESSION', res); - }); + return liveshareService.guestService.request('INITIALIZE_CLIENT_SESSION', [ + params, + '' + liveshareService.session?.peerNumber, + this.id + ]); } } @@ -91,73 +90,46 @@ export class LiveshareGlspClient implements GLSPClient { this.serverClientIdMap.delete(relativeDocumentUri); return this.jsonrpcClient.disposeClientSession(params); } else if (liveshareService.isGuest()) { - liveshareService.guestService.request('DISPOSE_CLIENT_SESSION', [params, ''+liveshareService.session?.peerNumber, this.id]).then(res => { - console.log('DISPOSE_CLIENT_SESSION', res); - }); + return liveshareService.guestService.request('DISPOSE_CLIENT_SESSION', [ + params, + '' + liveshareService.session?.peerNumber, + this.id + ]); } } onActionMessage(handler: ActionMessageHandler): void { - this.jsonrpcClient.onActionMessage((message) => { + this.jsonrpcClient.onActionMessage(message => { if (!liveshareService.isConnectionOpen()) { const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; const subclientId = (message.action as any)['subclientId'] || 'H'; message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; - const tempRequestId = (message.action as any)['responseId']; - if (tempRequestId != null) { - const originalRequestId = this.requestSubclientMap.get(tempRequestId)?.originalRequestId || ''; - this.requestSubclientMap.delete(tempRequestId); - (message.action as any)['responseId'] = originalRequestId; - } handler(message); // only to host - } else if (liveshareService.isHost()){ - const tempRequestId = (message.action as any)['responseId']; - if (tempRequestId != null) { - const subclientId = this.requestSubclientMap.get(tempRequestId)?.subclientId || ''; - const originalRequestId = this.requestSubclientMap.get(tempRequestId)?.originalRequestId || ''; - this.requestSubclientMap.delete(tempRequestId); - (message.action as any)['responseId'] = originalRequestId; - (message.action as any)['subclientId'] = subclientId; - // find initial local clientId to action - const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; - message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; - if (subclientId === 'H') { - handler(message); // notify host - } else { - liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); // notify subclientId - } - } else if (message.action.kind === 'requestBounds') { - const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; - const subclientMap = this.registeredSubclientMap?.get(relativeDocumentUri) - if (subclientMap?.size! > 0) { - const subclientId = (message.action as any)['subclientId'] || 'H'; - message.clientId = subclientMap?.get(subclientId) || ''; - if (subclientId === 'H') { - handler(message); // host over handler - } else { - liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); - } - } - - } else /*if ( - message.action.kind === 'setModel' || - message.action.kind === 'updateModel' || - message.action.kind === 'setDirtyState' || - message.action.kind === 'serverMessage' || - message.action.kind === 'serverStatus')*/ { + } else if (liveshareService.isHost()) { + const subclientId2 = (message.action as any)['subclientId']; + if (subclientId2 == null || message.action.kind === 'setModel' || message.action.kind === 'updateModel') { const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); if (subclientMap) { for (const [subclientId, clientId] of subclientMap.entries()) { - (message.action as any)['subclientId'] = subclientId; - message.clientId = clientId; + const newMessage = JSON.parse(JSON.stringify(message)); + (newMessage.action as any)['subclientId'] = subclientId; + newMessage.clientId = clientId; if (subclientId === 'H') { - handler(message); // host over handler + handler(newMessage); // host over handler } else { - liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); + liveshareService.hostService.notify('ON_ACTION_MESSAGE', newMessage); } } } + } else { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; + message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId2) || ''; + if (subclientId2 === 'H') { + handler(message); // notify host + } else { + liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); // notify subclientId + } } } }); @@ -173,20 +145,15 @@ export class LiveshareGlspClient implements GLSPClient { requestModelAction.options = { ...requestModelAction.options, sourceUri: this.getFullDocumentUri(relativeDocumentUri) - } - } - const originalRequestId = (message.action as any)['requestId']; - if (originalRequestId != null) { - const tempRequestId = this.generateRequestId(); - (message.action as any)['requestId'] = tempRequestId; - const subclientId = (message.action as any)['subclientId'] as string; - this.requestSubclientMap.set(tempRequestId, { subclientId, originalRequestId }); + }; } this.jsonrpcClient.sendActionMessage(message); } else if (liveshareService.isGuest()) { - liveshareService.guestService.request('SEND_ACTION_MESSAGE', [message, ''+liveshareService.session?.peerNumber, this.id]).then(res => { - console.log('SEND_ACTION_MESSAGE', res); - }); + liveshareService.guestService + .request('SEND_ACTION_MESSAGE', [message, '' + liveshareService.session?.peerNumber, this.id]) + .then(res => { + console.log('SEND_ACTION_MESSAGE', res); + }); } } @@ -202,14 +169,11 @@ export class LiveshareGlspClient implements GLSPClient { return this.jsonrpcClient.currentState; } - private generateRequestId(): string { - return (this.nextUniqueRequestId++).toString(); - } - private getRelativeDocumentUriByServerClientId(serverClientId: string): string | undefined { - for (let [key, value] of this.serverClientIdMap.entries()) { - if (value === serverClientId) + for (const [key, value] of this.serverClientIdMap.entries()) { + if (value === serverClientId) { return key; + } } return undefined; } From 3efe51dc368f2cc757fba887f70b764b34c390e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Mon, 17 Apr 2023 23:57:13 +0200 Subject: [PATCH 04/18] generalize collaborative functionality --- .../extension/src/workflow-extension.ts | 7 +- .../vscode-integration/src/action-types.ts | 37 +++ .../src/glsp-vscode-connector.ts | 35 ++- .../src/liveshare/constants.ts | 19 -- .../vscode-integration/src/liveshare/index.ts | 2 +- .../liveshare-glsp-client-provider.ts | 121 ++++++++++ .../src/liveshare/liveshareService.ts | 225 ------------------ .../vscode-integration/src/liveshare/store.ts | 52 ---- .../src/liveshare/treeDataProvider.ts | 44 ---- .../collaborate-glsp-client-provider.ts | 12 + .../collaborate-glsp-client.ts | 201 ++++++++++++++++ .../liveshare-glsp-client.ts | 186 --------------- .../socket-glsp-vscode-server.ts | 25 +- packages/vscode-integration/src/types.ts | 2 - 14 files changed, 405 insertions(+), 563 deletions(-) create mode 100644 packages/vscode-integration/src/action-types.ts delete mode 100644 packages/vscode-integration/src/liveshare/constants.ts create mode 100644 packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts delete mode 100644 packages/vscode-integration/src/liveshare/liveshareService.ts delete mode 100644 packages/vscode-integration/src/liveshare/store.ts delete mode 100644 packages/vscode-integration/src/liveshare/treeDataProvider.ts create mode 100644 packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts create mode 100644 packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts delete mode 100644 packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 3e37657..7ac983c 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GlspVscodeConnector, NavigateAction, liveshareService } from '@eclipse-glsp/vscode-integration'; +import { GlspVscodeConnector, NavigateAction } from '@eclipse-glsp/vscode-integration'; import { GlspServerLauncher, configureDefaultCommands, @@ -47,8 +47,6 @@ export async function activate(context: vscode.ExtensionContext): Promise await serverProcess.start(); } - await liveshareService.liveshare(context); - // Wrap server with quickstart component const workflowServer = new SocketGlspVscodeServer({ clientId: 'glsp.workflow', @@ -56,9 +54,6 @@ export async function activate(context: vscode.ExtensionContext): Promise serverPort: JSON.parse(process.env.GLSP_SERVER_PORT || DEFAULT_SERVER_PORT) }); - liveshareService.registerServer(workflowServer); - - // Initialize GLSP-VSCode connector with server wrapper const glspVscodeConnector = new GlspVscodeConnector({ server: workflowServer, diff --git a/packages/vscode-integration/src/action-types.ts b/packages/vscode-integration/src/action-types.ts new file mode 100644 index 0000000..cf9c3fa --- /dev/null +++ b/packages/vscode-integration/src/action-types.ts @@ -0,0 +1,37 @@ +import { ActionMessage, Args, Action } from "@eclipse-glsp/protocol"; +import { hasOwnProperty } from 'sprotty-protocol'; +import * as sprotty from 'sprotty-protocol/lib/actions'; + +export function isActionMessage(message: unknown): message is ActionMessage { + return hasOwnProperty(message, 'action'); +}; +export interface _Action extends sprotty.Action { + /** + * Unique identifier specifying the kind of action to process. + */ + kind: string; + + /** + * Unique identifier specifying the subclient of the process. + */ + subclientId: string; +} + +export interface _ActionMessage extends sprotty.ActionMessage { + /** + * The unique client id + * */ + clientId: string; + + /** + * The action to execute. + */ + action: A; + + /** + * Additional custom arguments e.g. application specific parameters. + */ + args?: Args; +} + +export const SUBCLIENT_HOST_ID = 'H'; diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index eb8c59e..f8841ff 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -31,6 +31,7 @@ import { import * as fs from 'fs'; import * as vscode from 'vscode'; import { GlspVscodeClient, GlspVscodeConnectorOptions } from './types'; +import { isActionMessage, SUBCLIENT_HOST_ID } from './action-types'; // eslint-disable-next-line no-shadow export enum MessageOrigin { @@ -147,6 +148,8 @@ export class GlspVscodeConnector { if (this.options.logging) { @@ -166,8 +169,13 @@ export class GlspVscodeConnector { @@ -194,27 +202,21 @@ export class GlspVscodeConnector): string { - // FIXME %20 instead of blankspace - let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.toString(); - workspacePath = workspacePath?.endsWith('/') ? workspacePath : workspacePath + '/'; - return client.document.uri.toString().replace(workspacePath, ''); - } - protected async createInitializeClientSessionParams( client: GlspVscodeClient, + subclientId: string, relativeDocumentUri: string ): Promise { return { @@ -222,7 +224,7 @@ export class GlspVscodeConnector disposable.dispose()); } } + +function getRelativeDocumentUri(client: GlspVscodeClient): string { + let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.path; + workspacePath = workspacePath?.endsWith('/') ? workspacePath : `${workspacePath}/` + return client.document.uri.path.replace(workspacePath, ''); +} + diff --git a/packages/vscode-integration/src/liveshare/constants.ts b/packages/vscode-integration/src/liveshare/constants.ts deleted file mode 100644 index e5e50bd..0000000 --- a/packages/vscode-integration/src/liveshare/constants.ts +++ /dev/null @@ -1,19 +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 - ********************************************************************************/ -export const GET_COUNT_REQUEST = 'getCount'; -export const INCREMENT_COUNT_COMMAND = 'liveshare.incrementCount'; -export const INCREMENT_COUNT_NOTIFICATION = 'incrementCount'; -export const SERVICE_NAME = 'counter'; diff --git a/packages/vscode-integration/src/liveshare/index.ts b/packages/vscode-integration/src/liveshare/index.ts index 7ccf72e..e85b780 100644 --- a/packages/vscode-integration/src/liveshare/index.ts +++ b/packages/vscode-integration/src/liveshare/index.ts @@ -13,4 +13,4 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './liveshareService'; +export * from './liveshare-glsp-client-provider'; diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts new file mode 100644 index 0000000..b348922 --- /dev/null +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -0,0 +1,121 @@ +/******************************************************************************** + * 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 { + ActionMessage, + DisposeClientSessionParameters, + InitializeClientSessionParameters} from '@eclipse-glsp/protocol'; +import { CollaborateGlspClientProvider } from '../quickstart-components/collaborate-glsp-client-provider'; +import { LiveShare, Role, Session, SharedService, SharedServiceProxy, getApi } from 'vsls'; +import { GlspVscodeServer } from '../types'; + +export const INITIALIZE_CLIENT_SESSION = 'INITIALIZE_CLIENT_SESSION'; +export const DISPOSE_CLIENT_SESSION = 'DISPOSE_CLIENT_SESSION'; +export const SEND_ACTION_MESSAGE = 'SEND_ACTION_MESSAGE'; +export const ON_ACTION_MESSAGE = 'ON_ACTION_MESSAGE'; +export const SERVICE_NAME = 'GLSP-LIVESHARE-SERVICE'; + +export class LiveshareGlspClientProvider implements CollaborateGlspClientProvider { + protected session: Session | null; + protected vsls: LiveShare | null; + protected role: Role = Role.None; + protected service: SharedService | SharedServiceProxy | null; + protected server: GlspVscodeServer; + + protected get guestService(): SharedServiceProxy { + return this.service as SharedServiceProxy; + } + + protected get hostService(): SharedService { + return this.service as SharedService; + } + + async initialize(server: GlspVscodeServer): Promise { + this.server = server; + this.vsls = await getApi(); + + if (this.vsls) { + this.vsls.onDidChangeSession(async e => { + this.role = e.session.role; + this.session = e.session; + if (e.session.role === Role.Host) { + this.service = await this.vsls!.shareService(SERVICE_NAME); + if (!this.service) { + return; + } + + this.service.onRequest(INITIALIZE_CLIENT_SESSION, async params => { + await (await this.server.glspClient).initializeClientSession(params[1] as InitializeClientSessionParameters); + }); + + this.service.onRequest(DISPOSE_CLIENT_SESSION, async params => { + await (await this.server.glspClient).disposeClientSession(params[1] as DisposeClientSessionParameters); + }); + + this.service.onRequest(SEND_ACTION_MESSAGE, async params => { + (await this.server.glspClient).sendActionMessage(params[1] as ActionMessage); + }); + } else if (e.session.role === Role.Guest) { + this.service = await this.vsls!.getSharedService(SERVICE_NAME); + if (!this.service) { + return; + } + + this.service.onNotify(ON_ACTION_MESSAGE, (message: any) => { + const typedMessage = message as ActionMessage; + const subclientId = typedMessage.action.subclientId; + // checm ifmessage is adreeed to this guest + if (this.createSubclientIdFromSession() === subclientId) { + this.server.onServerSendEmitter.fire(message); + } + }); + } + }); + } + } + + isInCollaborateMode(): boolean { + return !!this.service && this.service.isServiceAvailable; + } + + isHost(): boolean { + return this.role === Role.Host; + } + + isGuest(): boolean { + return this.role === Role.Guest; + } + + initializeClientSession(params: InitializeClientSessionParameters): Promise { + return this.guestService.request(INITIALIZE_CLIENT_SESSION, [params]); + } + + disposeClientSession(params: DisposeClientSessionParameters): Promise { + return this.guestService.request(DISPOSE_CLIENT_SESSION, [params]); + } + + sendActionMessage(message: ActionMessage): void { + this.guestService.request(SEND_ACTION_MESSAGE, [message]) + } + + handleActionMessage(message: ActionMessage): void { + this.hostService.notify(ON_ACTION_MESSAGE, message); + } + + createSubclientIdFromSession(): string { + return `${this.session!.peerNumber}`; + } +} diff --git a/packages/vscode-integration/src/liveshare/liveshareService.ts b/packages/vscode-integration/src/liveshare/liveshareService.ts deleted file mode 100644 index cdfd806..0000000 --- a/packages/vscode-integration/src/liveshare/liveshareService.ts +++ /dev/null @@ -1,225 +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 { - ActionMessage, - DisposeClientSessionParameters, - GLSPClient, - InitializeClientSessionParameters, - Operation -} from '@eclipse-glsp/protocol'; -import * as vscode from 'vscode'; -import { LiveShare, Role, Session, SharedService, SharedServiceProxy, getApi } from 'vsls'; -import { GlspVscodeServer } from '../types'; -import { INCREMENT_COUNT_NOTIFICATION, SERVICE_NAME } from './constants'; - -class LiveshareService { - public session: Session | null; - public vsls: LiveShare; - public role: Role = Role.None; - private _service: SharedService | SharedServiceProxy | null; - private _glspClient: GLSPClient; - private _server: GlspVscodeServer; - protected readonly clients = new Map(); // relativeDocumentUri, clients - - // TODO DA clientIds are different, confusing - registerServer(server: GlspVscodeServer): void { - this._server = server; - } - - isConnectionOpen(): boolean { - return !!this._service && this._service.isServiceAvailable; - } - - isInVslsMode(): boolean { - return !!this.vsls; - } - - isHost(): boolean { - return this.role === Role.Host; - } - - isGuest(): boolean { - return this.role === Role.Guest; - } - - notify(action: Operation): void { - // only if liveshare is activated - if (this._service) { - this._service!.notify(INCREMENT_COUNT_NOTIFICATION, { - peerNumber: this.vsls.session.peerNumber, - action - }); - } - } - - get guestService(): SharedServiceProxy { - return this._service as SharedServiceProxy; - } - - get hostService(): SharedService { - return this._service as SharedService; - } - - async liveshare(context: vscode.ExtensionContext): Promise { - this.vsls = (await getApi())!; - /* if (!this.vsls) { - return; - } - - this.session = this.vsls.session; - if (!this.session) { - return; - } - - this.role = this.session.role; - if (this.role === Role.Host) { - this._service = await this.vsls.shareService(SERVICE_NAME); - - this._service!.onRequest('INITIALIZE_SERVER', () => this._server.initializeResult); - } else if (this.role === Role.Guest) { - this._service = await this.vsls.shareService(SERVICE_NAME); - }*/ - - /* - // Register the custom tree provider with Live Share, which - // allows you to augment it however you'd like to. - const treeDataProvider = new CountTreeDataProvider(store); - vsls.registerTreeDataProvider(View.Session, treeDataProvider); - - context.subscriptions.push( - vscode.commands.registerCommand(INCREMENT_COUNT_COMMAND, () => { - store.increment(); - service!.notify(INCREMENT_COUNT_NOTIFICATION, { - peerNumber: vsls.session.peerNumber - }); - }) - ); - */ - // This event will fire whenever an end-user joins - // or leaves a sessionn, either of the host or guest. - this.vsls.onDidChangeSession(async e => { - this.role = e.session.role; - this.session = e.session; - if (e.session.role === Role.Host) { - /* - store.count = 0; - */ - - // Expose a new custom RPC service, that allows - // guests in a Live Share session to retrieve and - // synchronize custom state with each other. In the - // case of this sample, the state being sychronized - // is simply a count, and the only action that - // can be taken on it is to increment the count. - this._service = await this.vsls.shareService(SERVICE_NAME); - - this._service!.onRequest('INITIALIZE_CLIENT_SESSION', async params => { - const initializeClientSessionParams = params[1] as InitializeClientSessionParameters; - const subclientId = params[2] as string; - initializeClientSessionParams.args = { - ...initializeClientSessionParams.args, - subclientId - }; - await (await this._server.glspClient).initializeClientSession(initializeClientSessionParams); - }); - - this._service!.onRequest('DISPOSE_CLIENT_SESSION', async params => { - const disposeClientSessionParams = params[1] as DisposeClientSessionParameters; - const subclientId = params[2] as string; - disposeClientSessionParams.args = { - ...disposeClientSessionParams.args, - subclientId - }; - await (await this._server.glspClient).disposeClientSession(disposeClientSessionParams); - }); - - this._service!.onRequest('SEND_ACTION_MESSAGE', async params => { - const actionMessage = params[1] as ActionMessage; - const subclientId = params[2] as string; - (actionMessage.action as any)['subclientId'] = subclientId; - (await this._server.glspClient).sendActionMessage(actionMessage); - }); - - // TODO DA here we could expose the file as GModel - // this._service!.onRequest(GET_COUNT_REQUEST, () => store.count); - - this._service!.onNotify(INCREMENT_COUNT_NOTIFICATION, (ev: any) => { - /* - store.increment(); - */ - const { action } = ev; - for (const key of this.clients.keys()) { - const message = { - clientId: key, - action: action, - liveshareState: 'broadcasted' - }; - this._glspClient.sendActionMessage(message); - } - - // Re-broadcast the notification to all other guests. - this._service!.notify(INCREMENT_COUNT_NOTIFICATION, ev); - }); - } else if (e.session.role === Role.Guest) { - // Attempt to grab a proxy reference to the custom - // counter service on the host. If this doesn't exist, - // then it means the host doesn't have this extension - // installed, and therefore, the extension should - // gracefully degrade. - this._service = await this.vsls.getSharedService(SERVICE_NAME); - if (!this._service) { - return; - } - - this._service!.onNotify('ON_ACTION_MESSAGE', (message: any) => { - message = message as ActionMessage; - // message is for this guest - if (e.session.peerNumber === +(message.action as any)['subclientId']) { - this._server.onServerSendEmitter.fire(message); - } - }); - - // Grab the current count from the host, who is the - // "source of truth" for the state store. - // TODO DA here we could expose the file as GModel - // store.count = await this._service.request(GET_COUNT_REQUEST, []); - - this._service.onNotify(INCREMENT_COUNT_NOTIFICATION, ({ peerNumber, action }: any) => { - // Ignore the notification if it originated with this user. - if (peerNumber === this.vsls.session.peerNumber) { - return; - } - - for (const key of this.clients.keys()) { - const message = { - clientId: key, - action: action, - liveshareState: 'broadcasted' - }; - this._glspClient.sendActionMessage(message); - } - - /* - store.increment(); - */ - }); - } - }); - } -} - -export const liveshareService = new LiveshareService(); diff --git a/packages/vscode-integration/src/liveshare/store.ts b/packages/vscode-integration/src/liveshare/store.ts deleted file mode 100644 index 44d13f1..0000000 --- a/packages/vscode-integration/src/liveshare/store.ts +++ /dev/null @@ -1,52 +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 - ********************************************************************************/ -export interface ICountStore { - count: number; - onChange(handler: () => void): void; -} - -// This is a very basic store, which allows the extension -// to centralize state management. For example, the custom -// tree provider can subscribe to count changes, without -// needing to worry about the various places the count -// can actually change. Instead of writing your -// own store implementation, you could also use a library -// like Redux, MobX, etc. -class CountStore implements ICountStore { - private _count = 0; - // eslint-disable-next-line @typescript-eslint/no-empty-function - private _handler: () => void = () => {}; - - get count(): number { - return this._count; - } - - set count(count: number) { - this._count = count; - this._handler(); - } - - increment(): void { - this._count++; - this._handler(); - } - - onChange(handler: () => void): void { - this._handler = handler; - } -} - -export const store = new CountStore(); diff --git a/packages/vscode-integration/src/liveshare/treeDataProvider.ts b/packages/vscode-integration/src/liveshare/treeDataProvider.ts deleted file mode 100644 index 5570b86..0000000 --- a/packages/vscode-integration/src/liveshare/treeDataProvider.ts +++ /dev/null @@ -1,44 +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 { Command, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem } from 'vscode'; -import { INCREMENT_COUNT_COMMAND } from './constants'; -import { ICountStore } from './store'; - -export class CountTreeDataProvider implements TreeDataProvider { - private _command: Command = { - command: INCREMENT_COUNT_COMMAND, - title: 'Increment Count' - }; - - private _onDidChangeTreeData = new EventEmitter(); - public readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - - constructor(private store: ICountStore) { - store.onChange(() => { - this._onDidChangeTreeData.fire(this._command); - }); - } - - getChildren(element?: Command): ProviderResult { - return Promise.resolve([this._command]); - } - - getTreeItem(element: Command): TreeItem { - const treeItem = new TreeItem(`Count: ${this.store.count}`); - treeItem.command = element; - return treeItem; - } -} diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts new file mode 100644 index 0000000..e7f90fa --- /dev/null +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts @@ -0,0 +1,12 @@ +import { ActionMessage, DisposeClientSessionParameters, InitializeClientSessionParameters } from '@eclipse-glsp/protocol'; + +export interface CollaborateGlspClientProvider { + isInCollaborateMode(): boolean; + isHost(): boolean; + isGuest(): boolean; + initializeClientSession(params: InitializeClientSessionParameters): Promise; + disposeClientSession(params: DisposeClientSessionParameters): Promise; + sendActionMessage(message: ActionMessage): void; + handleActionMessage(message: ActionMessage): void; + createSubclientIdFromSession(): string; +} diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts new file mode 100644 index 0000000..8678b5b --- /dev/null +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts @@ -0,0 +1,201 @@ +/******************************************************************************** + * Copyright (c) 2019-2022 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 { + ActionMessage, + ActionMessageHandler, + Args, + ClientState, + DisposeClientSessionParameters, + GLSPClient, + InitializeClientSessionParameters, + InitializeParameters, + InitializeResult, + RequestModelAction, + SetModelAction, + UpdateModelAction +} from '@eclipse-glsp/protocol'; +import { SUBCLIENT_HOST_ID } from '../action-types'; +import * as vscode from 'vscode'; +import { CollaborateGlspClientProvider } from './collaborate-glsp-client-provider'; + +export class CollaborateGlspClient implements GLSPClient { + protected readonly BROADCAST_ACTION_TYPES = [ + SetModelAction.KIND, + UpdateModelAction.KIND + ] + + readonly id: string; + + protected glspClient: GLSPClient; + + protected provider: CollaborateGlspClientProvider; + + // Map subclientId H = host + protected registeredSubclientMap = new Map>(); + // overwritten clientSessionIds for Server: Map + protected serverClientIdMap = new Map(); + + constructor(glspClient: GLSPClient, provider: CollaborateGlspClientProvider) { + this.id = glspClient.id; + + this.glspClient = glspClient; + + this.provider = provider; + } + + shutdownServer(): void { + this.glspClient.shutdownServer(); + } + + initializeServer(params: InitializeParameters): Promise { + return this.glspClient.initializeServer(params); + } + + async initializeClientSession(params: InitializeClientSessionParameters): Promise { + if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); + const subclientId = params.args?.subclientId as string; + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); + const initialized = subclientMap.size > 0; + subclientMap.set(subclientId, params.clientSessionId); // set local clientId + this.registeredSubclientMap.set(relativeDocumentUri, subclientMap); + if (initialized) { + return; + } + params.clientSessionId += `_${subclientId}`; // new unique clientSessionId for server + this.serverClientIdMap.set(relativeDocumentUri, params.clientSessionId); + return this.glspClient.initializeClientSession(params); + } else if (this.provider.isGuest()) { + params.args = { + ...params.args, + subclientId: this.provider.createSubclientIdFromSession() + }; + return this.provider.initializeClientSession(params); + } + } + + async disposeClientSession(params: DisposeClientSessionParameters): Promise { + if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); + const subclientId = params.args?.subclientId as string; + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); + subclientMap.delete(subclientId); + this.registeredSubclientMap.set(relativeDocumentUri, subclientMap); + if (subclientMap.size > 0) { + return; + } + this.serverClientIdMap.delete(relativeDocumentUri); + return this.glspClient.disposeClientSession(params); + } else if (this.provider.isGuest()) { + params.args = { + ...params.args, + subclientId: this.provider.createSubclientIdFromSession() + }; + return this.provider.disposeClientSession(params); + } + } + + onActionMessage(handler: ActionMessageHandler): void { + this.glspClient.onActionMessage(message => { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + if (!this.provider.isInCollaborateMode()) { + // send only to host + const localClientId = subclientMap?.get(SUBCLIENT_HOST_ID) || ''; + this.handleMessage(handler, SUBCLIENT_HOST_ID, message, localClientId); + } else if (this.provider.isHost()) { + const subclientId = message.action.subclientId; + if (subclientId == null || this.BROADCAST_ACTION_TYPES.includes(message.action.kind)) { + // braodcast to all subclients if subclientId is null or listed in BROADCAST_ACTION_TYPES + for (const [id, localClientId] of subclientMap?.entries() || []) { + this.handleMessage(handler, id, message, localClientId); + } + } else { + // send to adressed subclient + const localClientId = subclientMap?.get(subclientId) || ''; + this.handleMessage(handler, subclientId, message, localClientId); + } + } + }); + } + + private handleMessage(handler: ActionMessageHandler, subclientId: string, originalMessage: ActionMessage, clientId: string): void { + // clone message so at broadcasting original message won't be overwritten (would lead to problems at host since we use this message there) + const clonedMessage: ActionMessage = { + ...originalMessage, + action: { + ...originalMessage.action, + subclientId + }, + clientId + } + if (subclientId === SUBCLIENT_HOST_ID) { + handler(clonedMessage); // notify host + } else { + this.provider.handleActionMessage(clonedMessage); // notify subclientId + } + } + + sendActionMessage(message: ActionMessage): void { + if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); + message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; + // if requestModel action and originClient not host => change sourceUri + if (message.action.kind === RequestModelAction.KIND && message.action.subclientId !== SUBCLIENT_HOST_ID) { + const requestModelAction = message.action as RequestModelAction; + requestModelAction.options = { + ...requestModelAction.options, + sourceUri: getFullDocumentUri(relativeDocumentUri) + }; + } + this.glspClient.sendActionMessage(message); + } else if (this.provider.isGuest()) { + message.action.subclientId = this.provider.createSubclientIdFromSession(); + this.provider.sendActionMessage(message); + } + } + + start(): Promise { + return this.glspClient.start(); + } + + stop(): Promise { + return this.glspClient.stop(); + } + + get currentState(): ClientState { + return this.glspClient.currentState; + } + + private getRelativeDocumentUriByServerClientId(serverClientId: string): string { + for (const [key, value] of this.serverClientIdMap.entries()) { + if (value === serverClientId) { + return key; + } + } + return ''; + } + + private getRelativeDocumentUriByArgs(args: Args | undefined): string { + return (args?.relativeDocumentUri || '') as string; + } +} + +function getFullDocumentUri(relativeDocumentUri: string): string { + let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.toString(); + workspacePath = workspacePath?.endsWith('/') ? workspacePath : workspacePath + '/'; + return workspacePath + relativeDocumentUri; +} diff --git a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts deleted file mode 100644 index 1195844..0000000 --- a/packages/vscode-integration/src/quickstart-components/liveshare-glsp-client.ts +++ /dev/null @@ -1,186 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019-2022 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 { - ActionMessage, - ActionMessageHandler, - ClientState, - DisposeClientSessionParameters, - GLSPClient, - InitializeClientSessionParameters, - InitializeParameters, - InitializeResult, - RequestModelAction -} from '@eclipse-glsp/protocol'; -import * as vscode from 'vscode'; - -import { liveshareService } from '../liveshare'; - -export class LiveshareGlspClient implements GLSPClient { - readonly id: string; - - protected jsonrpcClient: GLSPClient; - // Map subclientId H = host - protected registeredSubclientMap = new Map>(); - // overwritten clientSessionIds for Server: Map - protected serverClientIdMap = new Map(); - - protected nextUniqueRequestId = 10000; - - constructor(id: string, existingClient: GLSPClient) { - this.id = id; - - this.jsonrpcClient = existingClient; - } - - shutdownServer(): void { - this.jsonrpcClient.shutdownServer(); - } - - initializeServer(params: InitializeParameters): Promise { - return this.jsonrpcClient.initializeServer(params); - } - - async initializeClientSession(params: InitializeClientSessionParameters): Promise { - const relativeDocumentUri = params.args?.relativeDocumentUri as string; - const subclientId = params.args?.subclientId as string; - if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); - const initialized = subclientMap.size > 0; - subclientMap.set(subclientId, params.clientSessionId); - this.registeredSubclientMap.set(relativeDocumentUri, subclientMap); - if (initialized) { - return; - } - // if a guest is initiating document firstly - params.clientSessionId += '_' + subclientId; - this.serverClientIdMap.set(relativeDocumentUri, params.clientSessionId); - return this.jsonrpcClient.initializeClientSession(params); - } else if (liveshareService.isGuest()) { - return liveshareService.guestService.request('INITIALIZE_CLIENT_SESSION', [ - params, - '' + liveshareService.session?.peerNumber, - this.id - ]); - } - } - - async disposeClientSession(params: DisposeClientSessionParameters): Promise { - const relativeDocumentUri = params.args?.relativeDocumentUri as string; - const subclientId = params.args?.subclientId as string; - if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); - subclientMap.delete(subclientId); - this.registeredSubclientMap.set(relativeDocumentUri, subclientMap); - if (subclientMap.size > 0) { - return; - } - this.serverClientIdMap.delete(relativeDocumentUri); - return this.jsonrpcClient.disposeClientSession(params); - } else if (liveshareService.isGuest()) { - return liveshareService.guestService.request('DISPOSE_CLIENT_SESSION', [ - params, - '' + liveshareService.session?.peerNumber, - this.id - ]); - } - } - - onActionMessage(handler: ActionMessageHandler): void { - this.jsonrpcClient.onActionMessage(message => { - if (!liveshareService.isConnectionOpen()) { - const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; - const subclientId = (message.action as any)['subclientId'] || 'H'; - message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId) || ''; - handler(message); // only to host - } else if (liveshareService.isHost()) { - const subclientId2 = (message.action as any)['subclientId']; - if (subclientId2 == null || message.action.kind === 'setModel' || message.action.kind === 'updateModel') { - const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - if (subclientMap) { - for (const [subclientId, clientId] of subclientMap.entries()) { - const newMessage = JSON.parse(JSON.stringify(message)); - (newMessage.action as any)['subclientId'] = subclientId; - newMessage.clientId = clientId; - if (subclientId === 'H') { - handler(newMessage); // host over handler - } else { - liveshareService.hostService.notify('ON_ACTION_MESSAGE', newMessage); - } - } - } - } else { - const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId) || ''; - message.clientId = this.registeredSubclientMap?.get(relativeDocumentUri)?.get(subclientId2) || ''; - if (subclientId2 === 'H') { - handler(message); // notify host - } else { - liveshareService.hostService.notify('ON_ACTION_MESSAGE', message); // notify subclientId - } - } - } - }); - } - - sendActionMessage(message: ActionMessage): void { - const relativeDocumentUri = (message as any)['relativeDocumentUri'] as string; - if (!liveshareService.isConnectionOpen() || liveshareService.isHost()) { - message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; - // if requestModel action and originClient not host => change sourceUri - if (message.action.kind === 'requestModel' && (message.action as any)['subclientId'] !== 'H') { - const requestModelAction = message.action as RequestModelAction; - requestModelAction.options = { - ...requestModelAction.options, - sourceUri: this.getFullDocumentUri(relativeDocumentUri) - }; - } - this.jsonrpcClient.sendActionMessage(message); - } else if (liveshareService.isGuest()) { - liveshareService.guestService - .request('SEND_ACTION_MESSAGE', [message, '' + liveshareService.session?.peerNumber, this.id]) - .then(res => { - console.log('SEND_ACTION_MESSAGE', res); - }); - } - } - - start(): Promise { - return this.jsonrpcClient.start(); - } - - stop(): Promise { - return this.jsonrpcClient.stop(); - } - - get currentState(): ClientState { - return this.jsonrpcClient.currentState; - } - - private getRelativeDocumentUriByServerClientId(serverClientId: string): string | undefined { - for (const [key, value] of this.serverClientIdMap.entries()) { - if (value === serverClientId) { - return key; - } - } - return undefined; - } - - protected getFullDocumentUri(relativeDocumentUri: string): string { - let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.toString(); - workspacePath = workspacePath?.endsWith('/') ? workspacePath : workspacePath + '/'; - return workspacePath + relativeDocumentUri; - } -} 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 index 4e6c303..fb27043 100644 --- a/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts +++ b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts @@ -22,11 +22,12 @@ import { InitializeResult } from '@eclipse-glsp/protocol'; import * as net from 'net'; +import { LiveshareGlspClientProvider } from '../liveshare'; import * as vscode from 'vscode'; import { createMessageConnection } from 'vscode-jsonrpc'; import { SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node'; import { GlspVscodeServer } from '../types'; -import { LiveshareGlspClient } from './liveshare-glsp-client'; +import { CollaborateGlspClient } from './collaborate-glsp-client'; interface SocketGlspVscodeServerOptions { /** Port of the running server. */ @@ -60,6 +61,7 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab protected readonly onReady: Promise; protected setReady: () => void; + protected liveshareGlspClientProvider: LiveshareGlspClientProvider; _initializeResult: InitializeResult; constructor(protected readonly options: SocketGlspVscodeServerOptions) { @@ -72,11 +74,13 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab const reader = new SocketMessageReader(this.socket); const writer = new SocketMessageWriter(this.socket); const connection = createMessageConnection(reader, writer); + + this.liveshareGlspClientProvider = new LiveshareGlspClientProvider(); - this._glspClient = new LiveshareGlspClient(options.clientId, new BaseJsonrpcGLSPClient({ + this._glspClient = new CollaborateGlspClient(new BaseJsonrpcGLSPClient({ id: options.clientId, connectionProvider: connection - })); + }), this.liveshareGlspClientProvider); this.onSendToServerEmitter.event(message => { this.onReady.then(() => { @@ -88,11 +92,13 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab } /** - * Starts up the JSON-RPC client and connects it to a running server. + * Starts up the JSON-RPC client, initializes liveshare, and connects it to a running server. */ async start(): Promise { this.socket.connect(this.options.serverPort); + await this.liveshareGlspClientProvider.initialize(this); + await this._glspClient.start(); const parameters = await this.createInitializeParameters(); this._initializeResult = await this._glspClient.initializeServer(parameters); @@ -133,15 +139,4 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab get glspClient(): Promise { return this.onReady.then(() => this._glspClient); } - - async initalizeNewGlspClient(glspClient: GLSPClient): Promise { - // await this._glspClient.stop(); // old client - - this._glspClient = glspClient; - await this._glspClient.start(); - - this._glspClient.onActionMessage(message => { - this.onServerSendEmitter.fire(message); - }); - } } diff --git a/packages/vscode-integration/src/types.ts b/packages/vscode-integration/src/types.ts index b076832..993565c 100644 --- a/packages/vscode-integration/src/types.ts +++ b/packages/vscode-integration/src/types.ts @@ -111,8 +111,6 @@ export interface GlspVscodeServer { * to call this method during the their initialization phase. */ readonly initializeResult: Promise; - - initalizeNewGlspClient(glspClient: GLSPClient): Promise; } interface InterceptorCallback { From 9deff8fc36bb7be76f62ba876940a7874b4f61db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Tue, 18 Apr 2023 00:31:12 +0200 Subject: [PATCH 05/18] fix open files from guests path bug --- packages/vscode-integration/src/glsp-vscode-connector.ts | 1 + .../src/quickstart-components/collaborate-glsp-client.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index f8841ff..1003f81 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -507,6 +507,7 @@ export class GlspVscodeConnector Date: Fri, 21 Apr 2023 11:50:20 +0200 Subject: [PATCH 06/18] implement reload logic for requestModelAction --- .../vscode-integration/src/action-types.ts | 21 ++++++++++++++++--- .../liveshare-glsp-client-provider.ts | 2 +- .../collaborate-glsp-client-provider.ts | 15 +++++++++++++ .../collaborate-glsp-client.ts | 12 ++++++++--- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/vscode-integration/src/action-types.ts b/packages/vscode-integration/src/action-types.ts index cf9c3fa..bd65c3f 100644 --- a/packages/vscode-integration/src/action-types.ts +++ b/packages/vscode-integration/src/action-types.ts @@ -1,16 +1,31 @@ -import { ActionMessage, Args, Action } from "@eclipse-glsp/protocol"; +/******************************************************************************** + * Copyright (c) 2023 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, ActionMessage, Args } from '@eclipse-glsp/protocol'; import { hasOwnProperty } from 'sprotty-protocol'; import * as sprotty from 'sprotty-protocol/lib/actions'; export function isActionMessage(message: unknown): message is ActionMessage { return hasOwnProperty(message, 'action'); -}; +} export interface _Action extends sprotty.Action { /** * Unique identifier specifying the kind of action to process. */ kind: string; - + /** * Unique identifier specifying the subclient of the process. */ diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index b348922..c30aea3 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -77,7 +77,7 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide this.service.onNotify(ON_ACTION_MESSAGE, (message: any) => { const typedMessage = message as ActionMessage; const subclientId = typedMessage.action.subclientId; - // checm ifmessage is adreeed to this guest + // check if message is adrseeed to this guest if (this.createSubclientIdFromSession() === subclientId) { this.server.onServerSendEmitter.fire(message); } diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts index e7f90fa..9fa64db 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts @@ -1,3 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2023 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 { ActionMessage, DisposeClientSessionParameters, InitializeClientSessionParameters } from '@eclipse-glsp/protocol'; export interface CollaborateGlspClientProvider { diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts index 1b5a116..4a4c221 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts @@ -153,13 +153,19 @@ export class CollaborateGlspClient implements GLSPClient { if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; - // if requestModel action and originClient not host => change sourceUri - if (message.action.kind === RequestModelAction.KIND && message.action.subclientId !== SUBCLIENT_HOST_ID) { + // if requestModel action => add disableReload and if originClient not host => change sourceUri + if (message.action.kind === RequestModelAction.KIND) { const requestModelAction = message.action as RequestModelAction; requestModelAction.options = { ...requestModelAction.options, - sourceUri: getFullDocumentUri(relativeDocumentUri) + disableReload: true }; + if (message.action.subclientId !== SUBCLIENT_HOST_ID) { + requestModelAction.options = { + ...requestModelAction.options, + sourceUri: getFullDocumentUri(relativeDocumentUri), + }; + } } this.glspClient.sendActionMessage(message); } else if (this.provider.isGuest()) { From d0c756643fd5e02384bd8175c1e0dd7714c02c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Fri, 28 Apr 2023 18:12:19 +0200 Subject: [PATCH 07/18] implement mouse pointer collaboration feature --- .../src/glsp-vscode-diagramserver.ts | 6 +- .../vscode-integration/src/action-types.ts | 8 +- .../src/glsp-vscode-connector.ts | 4 +- .../liveshare-glsp-client-provider.ts | 33 ++++- .../collaborate-glsp-client-provider.ts | 7 +- .../collaborate-glsp-client.ts | 115 ++++++++++++------ .../socket-glsp-vscode-server.ts | 2 +- 7 files changed, 121 insertions(+), 54 deletions(-) diff --git a/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts b/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts index 7c767e9..657aed3 100644 --- a/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts +++ b/packages/vscode-integration-webview/src/glsp-vscode-diagramserver.ts @@ -19,9 +19,10 @@ import { ActionMessage, ComputedBoundsAction, DeleteElementOperation, - registerDefaultGLSPServerActions, SetEditModeAction, - TYPES + TYPES, + registerCollaborationActions, + registerDefaultGLSPServerActions } from '@eclipse-glsp/client'; import { SelectionService } from '@eclipse-glsp/client/lib/features/select/selection-service'; import { inject } from 'inversify'; @@ -39,6 +40,7 @@ export class GLSPVscodeDiagramServer extends VscodeDiagramServer { override initialize(registry: ActionHandlerRegistry): void { registerDefaultGLSPServerActions(registry, this); + registerCollaborationActions(registry, this); this.clientId = this.viewerOptions.baseDiv; window.addEventListener('message', message => { diff --git a/packages/vscode-integration/src/action-types.ts b/packages/vscode-integration/src/action-types.ts index bd65c3f..94d687a 100644 --- a/packages/vscode-integration/src/action-types.ts +++ b/packages/vscode-integration/src/action-types.ts @@ -13,13 +13,9 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Action, ActionMessage, Args } from '@eclipse-glsp/protocol'; -import { hasOwnProperty } from 'sprotty-protocol'; +import { Action, Args } from '@eclipse-glsp/protocol'; import * as sprotty from 'sprotty-protocol/lib/actions'; -export function isActionMessage(message: unknown): message is ActionMessage { - return hasOwnProperty(message, 'action'); -} export interface _Action extends sprotty.Action { /** * Unique identifier specifying the kind of action to process. @@ -48,5 +44,3 @@ export interface _ActionMessage extends sprotty.Actio */ args?: Args; } - -export const SUBCLIENT_HOST_ID = 'H'; diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index 1003f81..0bf3878 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -30,8 +30,8 @@ import { } from '@eclipse-glsp/protocol'; import * as fs from 'fs'; import * as vscode from 'vscode'; +import { SUBCLIENT_HOST_ID } from './quickstart-components/collaborate-glsp-client-provider'; import { GlspVscodeClient, GlspVscodeConnectorOptions } from './types'; -import { isActionMessage, SUBCLIENT_HOST_ID } from './action-types'; // eslint-disable-next-line no-shadow export enum MessageOrigin { @@ -169,7 +169,7 @@ export class GlspVscodeConnector { this.role = e.session.role; this.session = e.session; + this.subclientId = e.session.role === Role.Host ? SUBCLIENT_HOST_ID : `${this.session!.peerNumber}`; + const colorId = e.session.role === Role.Host ? 0 : (e.session.peerNumber - 1); + this.subclientInfo = { + subclientId: this.subclientId, + name: this.session.user!.emailAddress || this.session.user!.displayName, + color: COLORS[colorId] || '#FFFFFF' + }; if (e.session.role === Role.Host) { this.service = await this.vsls!.shareService(SERVICE_NAME); if (!this.service) { @@ -78,7 +91,7 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide const typedMessage = message as ActionMessage; const subclientId = typedMessage.action.subclientId; // check if message is adrseeed to this guest - if (this.createSubclientIdFromSession() === subclientId) { + if (this.getSubclientIdFromSession() === subclientId) { this.server.onServerSendEmitter.fire(message); } }); @@ -108,14 +121,22 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide } sendActionMessage(message: ActionMessage): void { - this.guestService.request(SEND_ACTION_MESSAGE, [message]) + this.guestService.request(SEND_ACTION_MESSAGE, [message]); } handleActionMessage(message: ActionMessage): void { this.hostService.notify(ON_ACTION_MESSAGE, message); } - createSubclientIdFromSession(): string { - return `${this.session!.peerNumber}`; + getSubclientIdFromSession(): string { + return this.subclientId || ''; + } + + getSubclientInfoFromSession(): SubclientInfo { + return this.subclientInfo || { + subclientId: '', + name: '', + color: '' + }; } } diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts index 9fa64db..dc2f655 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts @@ -13,7 +13,9 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { ActionMessage, DisposeClientSessionParameters, InitializeClientSessionParameters } from '@eclipse-glsp/protocol'; +import { ActionMessage, DisposeClientSessionParameters, InitializeClientSessionParameters, SubclientInfo } from '@eclipse-glsp/protocol'; + +export const SUBCLIENT_HOST_ID = 'H'; export interface CollaborateGlspClientProvider { isInCollaborateMode(): boolean; @@ -23,5 +25,6 @@ export interface CollaborateGlspClientProvider { disposeClientSession(params: DisposeClientSessionParameters): Promise; sendActionMessage(message: ActionMessage): void; handleActionMessage(message: ActionMessage): void; - createSubclientIdFromSession(): string; + getSubclientIdFromSession(): string; + getSubclientInfoFromSession(): SubclientInfo; } diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts index 4a4c221..d2de80d 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts @@ -18,18 +18,19 @@ import { ActionMessageHandler, Args, ClientState, + CollaborationAction, DisposeClientSessionParameters, + DisposeSubclientAction, GLSPClient, InitializeClientSessionParameters, InitializeParameters, InitializeResult, RequestModelAction, SetModelAction, - UpdateModelAction + UpdateModelAction, } from '@eclipse-glsp/protocol'; -import { SUBCLIENT_HOST_ID } from '../action-types'; import * as vscode from 'vscode'; -import { CollaborateGlspClientProvider } from './collaborate-glsp-client-provider'; +import { CollaborateGlspClientProvider, SUBCLIENT_HOST_ID } from './collaborate-glsp-client-provider'; export class CollaborateGlspClient implements GLSPClient { protected readonly BROADCAST_ACTION_TYPES = [ @@ -40,7 +41,7 @@ export class CollaborateGlspClient implements GLSPClient { readonly id: string; protected glspClient: GLSPClient; - + protected provider: CollaborateGlspClientProvider; // Map subclientId H = host @@ -48,11 +49,13 @@ export class CollaborateGlspClient implements GLSPClient { // overwritten clientSessionIds for Server: Map protected serverClientIdMap = new Map(); + protected handlers: ActionMessageHandler[] = []; + constructor(glspClient: GLSPClient, provider: CollaborateGlspClientProvider) { this.id = glspClient.id; this.glspClient = glspClient; - + this.provider = provider; } @@ -81,13 +84,34 @@ export class CollaborateGlspClient implements GLSPClient { } else if (this.provider.isGuest()) { params.args = { ...params.args, - subclientId: this.provider.createSubclientIdFromSession() + subclientId: this.provider.getSubclientIdFromSession() }; return this.provider.initializeClientSession(params); } } async disposeClientSession(params: DisposeClientSessionParameters): Promise { + if (this.provider.isInCollaborateMode() && this.provider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + const subclientId = params.args?.subclientId as string; + const disposeSubclientMessage: ActionMessage = { + clientId: '', + action: DisposeSubclientAction.create() + }; + disposeSubclientMessage.action.initialSubclientInfo = { + name: '', + color: '', + subclientId + }; + for (const [id, localClientId] of subclientMap?.entries() || []) { + // only handle to other subclients + if (subclientId !== id) { + this.handleMessage(id, disposeSubclientMessage, localClientId); + } + } + } + if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); const subclientId = params.args?.subclientId as string; @@ -102,37 +126,17 @@ export class CollaborateGlspClient implements GLSPClient { } else if (this.provider.isGuest()) { params.args = { ...params.args, - subclientId: this.provider.createSubclientIdFromSession() + subclientId: this.provider.getSubclientIdFromSession() }; return this.provider.disposeClientSession(params); } } onActionMessage(handler: ActionMessageHandler): void { - this.glspClient.onActionMessage(message => { - const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId); - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - if (!this.provider.isInCollaborateMode()) { - // send only to host - const localClientId = subclientMap?.get(SUBCLIENT_HOST_ID) || ''; - this.handleMessage(handler, SUBCLIENT_HOST_ID, message, localClientId); - } else if (this.provider.isHost()) { - const subclientId = message.action.subclientId; - if (subclientId == null || this.BROADCAST_ACTION_TYPES.includes(message.action.kind)) { - // braodcast to all subclients if subclientId is null or listed in BROADCAST_ACTION_TYPES - for (const [id, localClientId] of subclientMap?.entries() || []) { - this.handleMessage(handler, id, message, localClientId); - } - } else { - // send to adressed subclient - const localClientId = subclientMap?.get(subclientId) || ''; - this.handleMessage(handler, subclientId, message, localClientId); - } - } - }); + this.handlers.push(handler); } - private handleMessage(handler: ActionMessageHandler, subclientId: string, originalMessage: ActionMessage, clientId: string): void { + private handleMessage(subclientId: string, originalMessage: ActionMessage, clientId: string): void { // clone message so at broadcasting original message won't be overwritten (would lead to problems at host since we use this message there) const clonedMessage: ActionMessage = { ...originalMessage, @@ -143,14 +147,35 @@ export class CollaborateGlspClient implements GLSPClient { clientId } if (subclientId === SUBCLIENT_HOST_ID) { - handler(clonedMessage); // notify host + this.handlers.forEach(handler => handler(clonedMessage)); // notify host } else { this.provider.handleActionMessage(clonedMessage); // notify subclientId } } sendActionMessage(message: ActionMessage): void { - if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + // send to all other subclients + if (CollaborationAction.is(message.action) && this.provider.isInCollaborateMode()) { + if (this.provider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + const subclientId = message.action.subclientId; + // set initialSubclientInfo if host dispatches actions (not set yet) + if (!message.action.initialSubclientInfo) { + message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); + } + for (const [id, localClientId] of subclientMap?.entries() || []) { + // only handle to other subclients + if (subclientId !== id) { + this.handleMessage(id, message, localClientId); + } + } + } else if (this.provider.isGuest()) { + message.action.subclientId = this.provider.getSubclientIdFromSession(); + message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); + this.provider.sendActionMessage(message); + } + } else if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; // if requestModel action => add disableReload and if originClient not host => change sourceUri @@ -169,13 +194,35 @@ export class CollaborateGlspClient implements GLSPClient { } this.glspClient.sendActionMessage(message); } else if (this.provider.isGuest()) { - message.action.subclientId = this.provider.createSubclientIdFromSession(); + message.action.subclientId = this.provider.getSubclientIdFromSession(); this.provider.sendActionMessage(message); } } - start(): Promise { - return this.glspClient.start(); + async start(): Promise { + await this.glspClient.start(); + + this.glspClient.onActionMessage((message: ActionMessage) => { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + if (!this.provider.isInCollaborateMode()) { + // send only to host + const localClientId = subclientMap?.get(SUBCLIENT_HOST_ID) || ''; + this.handleMessage(SUBCLIENT_HOST_ID, message, localClientId); + } else if (this.provider.isHost()) { + const subclientId = message.action.subclientId; + if (subclientId == null || this.BROADCAST_ACTION_TYPES.includes(message.action.kind)) { + // braodcast to all subclients if subclientId is null or listed in BROADCAST_ACTION_TYPES + for (const [id, localClientId] of subclientMap?.entries() || []) { + this.handleMessage(id, message, localClientId); + } + } else { + // send to adressed subclient + const localClientId = subclientMap?.get(subclientId) || ''; + this.handleMessage(subclientId, message, localClientId); + } + } + }); } stop(): Promise { 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 index fb27043..8b253d5 100644 --- a/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts +++ b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts @@ -74,7 +74,7 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab const reader = new SocketMessageReader(this.socket); const writer = new SocketMessageWriter(this.socket); const connection = createMessageConnection(reader, writer); - + this.liveshareGlspClientProvider = new LiveshareGlspClientProvider(); this._glspClient = new CollaborateGlspClient(new BaseJsonrpcGLSPClient({ From 5cc4f8572c37b95492f3f74305f4626947064761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 3 May 2023 01:16:03 +0200 Subject: [PATCH 08/18] refactor collaborate-glsp-client.ts --- .../src/glsp-vscode-connector.ts | 11 ++- .../liveshare-glsp-client-provider.ts | 5 +- .../collaborate-glsp-client.ts | 98 ++++++++++--------- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index 0bf3878..430cdda 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -16,6 +16,7 @@ import { Action, ActionMessage, + CollaborationAction, ExportSvgAction, InitializeClientSessionParameters, InitializeResult, @@ -112,7 +113,10 @@ export class GlspVscodeConnector { if (this.options.logging) { if (ActionMessage.is(message)) { - console.log(`Server (${message.clientId}): ${message.action.kind}`, message.action); + // don't log CollaborationActions + if (!CollaborationAction.is(message.action)) { + console.log(`Server (${message.clientId}): ${message.action.kind}`, message.action); + } } else { console.log('Server (no action message):', message); } @@ -154,7 +158,10 @@ export class GlspVscodeConnector { if (this.options.logging) { if (ActionMessage.is(message)) { - console.log(`Client (${message.clientId}): ${message.action.kind}`, message.action); + // don't log CollaborationActions + if (!CollaborationAction.is(message.action)) { + console.log(`Client (${message.clientId}): ${message.action.kind}`, message.action); + } } else { console.log('Client (no action message):', message); } diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index ac8a362..0780960 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -30,7 +30,8 @@ export const SEND_ACTION_MESSAGE = 'SEND_ACTION_MESSAGE'; export const ON_ACTION_MESSAGE = 'ON_ACTION_MESSAGE'; export const SERVICE_NAME = 'GLSP-LIVESHARE-SERVICE'; -const COLORS = ["#5C2D91", "#FFF100", "#E3008C", "#FF8C00"]; +const COLORS = ["#FFF100", "#5C2D91", "#E3008C", "#FF8C00"]; +const FALLBACK_COLOR = '#ABABAB'; export class LiveshareGlspClientProvider implements CollaborateGlspClientProvider { protected session: Session | null; @@ -62,7 +63,7 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide this.subclientInfo = { subclientId: this.subclientId, name: this.session.user!.emailAddress || this.session.user!.displayName, - color: COLORS[colorId] || '#FFFFFF' + color: COLORS[colorId] || FALLBACK_COLOR }; if (e.session.role === Role.Host) { this.service = await this.vsls!.shareService(SERVICE_NAME); diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts index d2de80d..08226b5 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts +++ b/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts @@ -27,16 +27,13 @@ import { InitializeResult, RequestModelAction, SetModelAction, - UpdateModelAction, + UpdateModelAction } from '@eclipse-glsp/protocol'; import * as vscode from 'vscode'; import { CollaborateGlspClientProvider, SUBCLIENT_HOST_ID } from './collaborate-glsp-client-provider'; export class CollaborateGlspClient implements GLSPClient { - protected readonly BROADCAST_ACTION_TYPES = [ - SetModelAction.KIND, - UpdateModelAction.KIND - ] + protected readonly BROADCAST_ACTION_TYPES = [SetModelAction.KIND, UpdateModelAction.KIND]; readonly id: string; @@ -92,24 +89,7 @@ export class CollaborateGlspClient implements GLSPClient { async disposeClientSession(params: DisposeClientSessionParameters): Promise { if (this.provider.isInCollaborateMode() && this.provider.isHost()) { - const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - const subclientId = params.args?.subclientId as string; - const disposeSubclientMessage: ActionMessage = { - clientId: '', - action: DisposeSubclientAction.create() - }; - disposeSubclientMessage.action.initialSubclientInfo = { - name: '', - color: '', - subclientId - }; - for (const [id, localClientId] of subclientMap?.entries() || []) { - // only handle to other subclients - if (subclientId !== id) { - this.handleMessage(id, disposeSubclientMessage, localClientId); - } - } + this.handleDisposeSubclientMessage(params); } if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { @@ -132,6 +112,27 @@ export class CollaborateGlspClient implements GLSPClient { } } + private handleDisposeSubclientMessage(params: DisposeClientSessionParameters): void { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + const subclientId = params.args?.subclientId as string; + const disposeSubclientMessage: ActionMessage = { + clientId: '', + action: DisposeSubclientAction.create() + }; + disposeSubclientMessage.action.initialSubclientInfo = { + name: '', + color: '', + subclientId + }; + for (const [id, localClientId] of subclientMap?.entries() || []) { + // only handle to other subclients + if (subclientId !== id) { + this.handleMessage(id, disposeSubclientMessage, localClientId); + } + } + } + onActionMessage(handler: ActionMessageHandler): void { this.handlers.push(handler); } @@ -145,7 +146,7 @@ export class CollaborateGlspClient implements GLSPClient { subclientId }, clientId - } + }; if (subclientId === SUBCLIENT_HOST_ID) { this.handlers.forEach(handler => handler(clonedMessage)); // notify host } else { @@ -154,27 +155,12 @@ export class CollaborateGlspClient implements GLSPClient { } sendActionMessage(message: ActionMessage): void { - // send to all other subclients - if (CollaborationAction.is(message.action) && this.provider.isInCollaborateMode()) { - if (this.provider.isHost()) { - const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - const subclientId = message.action.subclientId; - // set initialSubclientInfo if host dispatches actions (not set yet) - if (!message.action.initialSubclientInfo) { - message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); - } - for (const [id, localClientId] of subclientMap?.entries() || []) { - // only handle to other subclients - if (subclientId !== id) { - this.handleMessage(id, message, localClientId); - } - } - } else if (this.provider.isGuest()) { - message.action.subclientId = this.provider.getSubclientIdFromSession(); - message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); - this.provider.sendActionMessage(message); + if (CollaborationAction.is(message.action)) { + // handle collabofration action without sending to glsp-server + if (!this.provider.isInCollaborateMode()) { + return; } + this.handleCollaborationAction(message as ActionMessage); } else if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; @@ -188,7 +174,7 @@ export class CollaborateGlspClient implements GLSPClient { if (message.action.subclientId !== SUBCLIENT_HOST_ID) { requestModelAction.options = { ...requestModelAction.options, - sourceUri: getFullDocumentUri(relativeDocumentUri), + sourceUri: getFullDocumentUri(relativeDocumentUri) }; } } @@ -199,6 +185,28 @@ export class CollaborateGlspClient implements GLSPClient { } } + private handleCollaborationAction(message: ActionMessage): void { + if (this.provider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + const subclientId = message.action.subclientId; + // set initialSubclientInfo if host dispatches actions (not set yet) + if (!message.action.initialSubclientInfo) { + message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); + } + for (const [id, localClientId] of subclientMap?.entries() || []) { + // only handle to other subclients + if (subclientId !== id) { + this.handleMessage(id, message, localClientId); + } + } + } else if (this.provider.isGuest()) { + message.action.subclientId = this.provider.getSubclientIdFromSession(); + message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); + this.provider.sendActionMessage(message); + } + } + async start(): Promise { await this.glspClient.start(); From c2c63bb2468fe52b6d86c71cbd92a094429019c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Mon, 8 May 2023 14:10:24 +0200 Subject: [PATCH 09/18] implement collaboration feature toggle --- example/workflow/extension/package.json | 2 +- .../extension/src/workflow-extension.ts | 5 + example/workflow/workspace/example1.wf | 232 ++++++++++++++---- package.json | 1 + .../collaboration/collaboration-commands.ts | 74 ++++++ .../collaboration-feature-store.ts | 44 ++++ .../collaboration-glsp-client-provider.ts} | 22 +- .../collaboration-glsp-client.ts} | 227 ++++++++++------- .../src/collaboration/index.ts | 20 ++ .../src/glsp-vscode-connector.ts | 23 +- packages/vscode-integration/src/index.ts | 1 + .../vscode-integration/src/liveshare/index.ts | 1 + .../liveshare-glsp-client-provider.ts | 172 ++++++++----- .../toggle-feature-tree-data-provider.ts | 39 +++ .../socket-glsp-vscode-server.ts | 6 +- 15 files changed, 669 insertions(+), 200 deletions(-) create mode 100644 packages/vscode-integration/src/collaboration/collaboration-commands.ts create mode 100644 packages/vscode-integration/src/collaboration/collaboration-feature-store.ts rename packages/vscode-integration/src/{quickstart-components/collaborate-glsp-client-provider.ts => collaboration/collaboration-glsp-client-provider.ts} (61%) rename packages/vscode-integration/src/{quickstart-components/collaborate-glsp-client.ts => collaboration/collaboration-glsp-client.ts} (61%) create mode 100644 packages/vscode-integration/src/collaboration/index.ts create mode 100644 packages/vscode-integration/src/liveshare/toggle-feature-tree-data-provider.ts diff --git a/example/workflow/extension/package.json b/example/workflow/extension/package.json index 715506e..b748fec 100644 --- a/example/workflow/extension/package.json +++ b/example/workflow/extension/package.json @@ -29,7 +29,7 @@ }, "contributes": { "liveshare.spaces": [ - "vsls-workspace" + "workflow-vscode-example" ], "customEditors": [ { diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 7ac983c..6072300 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -19,6 +19,9 @@ import { configureDefaultCommands, SocketGlspVscodeServer } from '@eclipse-glsp/vscode-integration/lib/quickstart-components'; +import { + configureCollaborationCommands, +} from '@eclipse-glsp/vscode-integration/lib/collaboration'; import * as path from 'path'; import * as process from 'process'; import 'reflect-metadata'; @@ -75,6 +78,8 @@ export async function activate(context: vscode.ExtensionContext): Promise configureDefaultCommands({ extensionContext: context, connector: glspVscodeConnector, diagramPrefix: 'workflow' }); + configureCollaborationCommands({ extensionContext: context, connector: glspVscodeConnector }); + context.subscriptions.push( vscode.commands.registerCommand('workflow.goToNextNode', () => { glspVscodeConnector.sendActionToActiveClient(NavigateAction.create('next')); diff --git a/example/workflow/workspace/example1.wf b/example/workflow/workspace/example1.wf index 33fe061..57590fb 100644 --- a/example/workflow/workspace/example1.wf +++ b/example/workflow/workspace/example1.wf @@ -30,12 +30,12 @@ "y": 7.0 }, "size": { - "width": 31.9375, + "width": 31.9140625, "height": 16.0 }, "text": "Push", "alignment": { - "x": 15.96875, + "x": 15.95703125, "y": 13.0 } } @@ -46,15 +46,15 @@ "y": 140.0 }, "size": { - "width": 72.9375, + "width": 72.921875, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "paddingRight": 10.0, "prefWidth": 72.921875, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -91,7 +91,7 @@ "y": 7.0 }, "size": { - "width": 42.125, + "width": 42.0, "height": 16.0 }, "text": "ChkWt", @@ -107,15 +107,15 @@ "y": 90.0 }, "size": { - "width": 83.125, + "width": 83.0, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 75.6103515625, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -211,7 +211,17 @@ "id": "d34c37e0-e45e-4cfe-a76f-0e9274ed8e60", "type": "edge", "sourceId": "task0", - "targetId": "bb2709f5-0ff0-4438-8853-b7e934b506d7" + "targetId": "bb2709f5-0ff0-4438-8853-b7e934b506d7", + "args": { + "edgeSourcePoint": { + "x": 142.9375, + "y": 155.0 + }, + "edgeTargetPoint": { + "x": 169.5, + "y": 155.0 + } + } }, { "nodeType": "joinNode", @@ -233,13 +243,33 @@ "id": "7296b4a8-55c6-4c61-bddb-deacae77efa6", "type": "edge", "sourceId": "activityNode1", - "targetId": "bd94e44e-19f9-446e-89d4-97ca1a63c17b" + "targetId": "bd94e44e-19f9-446e-89d4-97ca1a63c17b", + "args": { + "edgeSourcePoint": { + "x": 524.3535533905932, + "y": 114.35355339059328 + }, + "edgeTargetPoint": { + "x": 560.4797799427402, + "y": 150.47977994274007 + } + } }, { "id": "0471cae4-c754-4e89-8337-96ed1546dd02", "type": "edge", "sourceId": "activityNode4", - "targetId": "bd94e44e-19f9-446e-89d4-97ca1a63c17b" + "targetId": "bd94e44e-19f9-446e-89d4-97ca1a63c17b", + "args": { + "edgeSourcePoint": { + "x": 524.1864130470989, + "y": 197.4794476448563 + }, + "edgeTargetPoint": { + "x": 560.5150734393876, + "y": 159.66798478757613 + } + } }, { "name": "AutomatedTask8", @@ -270,12 +300,12 @@ "y": 7.0 }, "size": { - "width": 38.0, + "width": 37.3359375, "height": 16.0 }, "text": "WtOK", "alignment": { - "x": 18.671875, + "x": 18.66796875, "y": 13.0 } } @@ -286,15 +316,15 @@ "y": 120.0 }, "size": { - "width": 79.0, + "width": 78.3359375, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 71.4931640625, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -309,7 +339,17 @@ ], "type": "edge:weighted", "sourceId": "b00fc494-0fa4-4448-8bf9-162c2c0865e4", - "targetId": "e47d5eba-612d-4c43-9aba-2c5502ff4f04" + "targetId": "e47d5eba-612d-4c43-9aba-2c5502ff4f04", + "args": { + "edgeSourcePoint": { + "x": 358.3478802143486, + "y": 110.28848534390552 + }, + "edgeTargetPoint": { + "x": 390.0, + "y": 121.28143712574851 + } + } }, { "id": "0a57ab51-c0b6-4a51-b42e-0192bf3767dc", @@ -317,7 +357,15 @@ "sourceId": "bb2709f5-0ff0-4438-8853-b7e934b506d7", "targetId": "task0_automated", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 179.91516327262408, + "y": 152.160916521228 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 235.59375, + "y": 120.0 + } } }, { @@ -326,7 +374,15 @@ "sourceId": "task0_automated", "targetId": "b00fc494-0fa4-4448-8bf9-162c2c0865e4", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 303.125, + "y": 105.49222797927462 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 329.6873064581633, + "y": 105.80680747840904 + } } }, { @@ -358,7 +414,7 @@ "y": 7.0 }, "size": { - "width": 34.328125, + "width": 34.21875, "height": 16.0 }, "text": "RflWt", @@ -374,15 +430,15 @@ "y": 70.0 }, "size": { - "width": 75.328125, + "width": 75.21875, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 67.82421875, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -397,7 +453,17 @@ ], "type": "edge:weighted", "sourceId": "b00fc494-0fa4-4448-8bf9-162c2c0865e4", - "targetId": "a63cfd87-da7c-4846-912b-29040b776bfb" + "targetId": "a63cfd87-da7c-4846-912b-29040b776bfb", + "args": { + "edgeSourcePoint": { + "x": 359.21143522013904, + "y": 102.6026654671641 + }, + "edgeTargetPoint": { + "x": 390.0, + "y": 94.68535348703722 + } + } }, { "id": "a36985a7-3e61-499c-9bdb-5be2b00cb75c", @@ -405,7 +471,15 @@ "sourceId": "a63cfd87-da7c-4846-912b-29040b776bfb", "targetId": "activityNode1", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 465.328125, + "y": 93.95383390819846 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 502.5866542972525, + "y": 102.81126087830678 + } } }, { @@ -414,7 +488,15 @@ "sourceId": "e47d5eba-612d-4c43-9aba-2c5502ff4f04", "targetId": "activityNode1", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 469.0, + "y": 121.75722543352602 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 503.5432491077811, + "y": 110.17625174421212 + } } }, { @@ -446,12 +528,12 @@ "y": 7.0 }, "size": { - "width": 31.90625, + "width": 31.8984375, "height": 16.0 }, "text": "Brew", "alignment": { - "x": 15.953125, + "x": 15.94921875, "y": 13.0 } } @@ -465,12 +547,12 @@ "width": 72.90625, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 72.90625, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -484,7 +566,15 @@ "sourceId": "bd94e44e-19f9-446e-89d4-97ca1a63c17b", "targetId": "7afd430b-5031-4082-8190-7e755c57cde9", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 570.5, + "y": 155.0 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 600.0, + "y": 155.0 + } } }, { @@ -516,12 +606,12 @@ "y": 7.0 }, "size": { - "width": 41.46875, + "width": 41.234375, "height": 16.0 }, "text": "ChkTp", "alignment": { - "x": 20.625, + "x": 20.6171875, "y": 13.0 } } @@ -532,15 +622,15 @@ "y": 170.0 }, "size": { - "width": 82.46875, + "width": 82.4482421875, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 82.4482421875, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -554,7 +644,15 @@ "sourceId": "bb2709f5-0ff0-4438-8853-b7e934b506d7", "targetId": "6c26f0a4-4354-45fa-9d9f-bc2b48adee17", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 180.14698081960435, + "y": 156.79057857830048 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 220.0, + "y": 170.65500996557347 + } } }, { @@ -563,7 +661,15 @@ "sourceId": "6c26f0a4-4354-45fa-9d9f-bc2b48adee17", "targetId": "activityNode2", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 302.46875, + "y": 195.21548387096774 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 332.69150756118734, + "y": 202.70292832483608 + } } }, { @@ -595,12 +701,12 @@ "y": 7.0 }, "size": { - "width": 49.265625, + "width": 49.0390625, "height": 16.0 }, "text": "KeepTp", "alignment": { - "x": 24.53125, + "x": 24.51953125, "y": 13.0 } } @@ -611,15 +717,15 @@ "y": 170.0 }, "size": { - "width": 90.265625, + "width": 90.0390625, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 82.748046875, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -656,12 +762,12 @@ "y": 7.0 }, "size": { - "width": 51.484375, + "width": 51.359375, "height": 16.0 }, "text": "PreHeat", "alignment": { - "x": 25.6875, + "x": 25.6796875, "y": 13.0 } } @@ -672,15 +778,15 @@ "y": 220.0 }, "size": { - "width": 92.484375, + "width": 92.359375, "height": 30.0 }, - "layout": "hbox", "layoutOptions": { "prefWidth": 84.96875, "paddingRight": 10.0, "prefHeight": 30.0 }, + "layout": "hbox", "args": { "radiusBottomLeft": 5.0, "radiusTopLeft": 5.0, @@ -695,7 +801,17 @@ ], "type": "edge:weighted", "sourceId": "activityNode2", - "targetId": "4b06ed95-c9be-47df-9941-98099259be4a" + "targetId": "4b06ed95-c9be-47df-9941-98099259be4a", + "args": { + "edgeSourcePoint": { + "x": 359.43581311150007, + "y": 202.8344757959758 + }, + "edgeTargetPoint": { + "x": 390.0, + "y": 195.63344727846436 + } + } }, { "id": "b9ef468f-2aaf-41b9-b408-29e2d08e490e", @@ -704,7 +820,17 @@ ], "type": "edge:weighted", "sourceId": "activityNode2", - "targetId": "80d1792f-9c7e-41c0-8eca-eeb0509397b6" + "targetId": "80d1792f-9c7e-41c0-8eca-eeb0509397b6", + "args": { + "edgeSourcePoint": { + "x": 358.58478388741327, + "y": 210.04421416241695 + }, + "edgeTargetPoint": { + "x": 390.0, + "y": 220.13972816206388 + } + } }, { "id": "58657bcf-6d32-4e7d-b73a-f84fd8d7158b", @@ -712,7 +838,15 @@ "sourceId": "4b06ed95-c9be-47df-9941-98099259be4a", "targetId": "activityNode4", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 480.265625, + "y": 196.72031687759636 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 502.81446410336747, + "y": 202.57591339096237 + } } }, { @@ -721,7 +855,15 @@ "sourceId": "80d1792f-9c7e-41c0-8eca-eeb0509397b6", "targetId": "activityNode4", "args": { - "edgePadding": 10.0 + "edgeSourcePoint": { + "x": 477.4962284482759, + "y": 220.0 + }, + "edgePadding": 10.0, + "edgeTargetPoint": { + "x": 503.79645808820464, + "y": 210.43721692394791 + } } } ], diff --git a/package.json b/package.json index bdcceed..ab9ee8c 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "publisher": "Eclipse-GLSP", "private": true, "engines": { "yarn": ">=1.7.0 <2.x.x", diff --git a/packages/vscode-integration/src/collaboration/collaboration-commands.ts b/packages/vscode-integration/src/collaboration/collaboration-commands.ts new file mode 100644 index 0000000..1bf02e9 --- /dev/null +++ b/packages/vscode-integration/src/collaboration/collaboration-commands.ts @@ -0,0 +1,74 @@ +import { + Command, + commands, + ExtensionContext +} from 'vscode'; +import { MouseMoveAction, ViewportBoundsChangeAction, SelectionChangeAction, ToggleCollaborationFeatureAction, CollaborationActionKinds } from '@eclipse-glsp/protocol'; +import { GlspVscodeConnector } from '../glsp-vscode-connector'; +import { collaborationFeatureStore } from './collaboration-feature-store'; + + +export const TOGGLE_MOUSE_POINTERS_COMMAND: Command = { + command: 'TOGGLE_MOUSE_POINTERS_COMMAND', + title: 'Mouse Pointers', + tooltip: 'Enable or disable other mouse pointers.', + arguments: [ + MouseMoveAction.KIND + ] +}; + +export const TOGGLE_VIEWPORTS_COMMAND: Command = { + command: 'TOGGLE_VIEWPORTS_COMMAND', + title: 'Viewports', + tooltip: 'Enable or disable other viewports.', + arguments: [ + ViewportBoundsChangeAction.KIND + ] +}; + +export const TOGGLE_SELECTIONS_COMMAND: Command = { + command: 'TOGGLE_SELECTIONS_COMMAND', + title: 'Selections', + tooltip: 'Enable or disable other selections.', + arguments: [ + SelectionChangeAction.KIND + ] +}; + +/** + * The `CommandContext` provides the necessary information to + * setup the default commands for a GLSP diagram extension. + */ +export interface CollaborationCommandContext { + /** + * The {@link vscode.ExtensionContext} of the GLSP diagram extension + */ + extensionContext: ExtensionContext; + + /** + * The {@link GlspVscodeConnector} of the GLSP diagram extension. + */ + connector: GlspVscodeConnector; +} + +export function configureCollaborationCommands(context: CollaborationCommandContext): void { + // keep track of diagram specific element selection. + const {extensionContext} = context; + + extensionContext.subscriptions.push( + commands.registerCommand(TOGGLE_MOUSE_POINTERS_COMMAND.command, (actionKind) => { + executeCollaborationCommand(context, actionKind); + }), + commands.registerCommand(TOGGLE_VIEWPORTS_COMMAND.command, (actionKind) => { + executeCollaborationCommand(context, actionKind); + }), + commands.registerCommand(TOGGLE_SELECTIONS_COMMAND.command, (actionKind) => { + executeCollaborationCommand(context, actionKind); + }) + ); +} + +function executeCollaborationCommand(context: CollaborationCommandContext, actionKind: CollaborationActionKinds): void { + collaborationFeatureStore.toggleFeature(actionKind); + context.connector.sendActionToAllClients(ToggleCollaborationFeatureAction.create({ actionKind })) +} diff --git a/packages/vscode-integration/src/collaboration/collaboration-feature-store.ts b/packages/vscode-integration/src/collaboration/collaboration-feature-store.ts new file mode 100644 index 0000000..d4540f1 --- /dev/null +++ b/packages/vscode-integration/src/collaboration/collaboration-feature-store.ts @@ -0,0 +1,44 @@ +import { MouseMoveAction, ViewportBoundsChangeAction, SelectionChangeAction, CollaborationActionKinds } from '@eclipse-glsp/protocol'; + + +export interface ICollaborationFeatureStore { + getFeature(kind: CollaborationActionKinds): boolean; + setFeature(kind: CollaborationActionKinds, value: boolean): void; + toggleFeature(kind: CollaborationActionKinds): boolean; + onChange(handler: CollaborationFeatureChangeHandler): void; +} + +type CollaborationFeatureChangeHandler = (ev: { kind: CollaborationActionKinds, value: boolean }) => void; + +class CollaborationFeatureStore implements ICollaborationFeatureStore { + + private handlers: CollaborationFeatureChangeHandler[] = []; + private featureMap = new Map(); + + constructor() { + this.featureMap.set(MouseMoveAction.KIND, true); + this.featureMap.set(ViewportBoundsChangeAction.KIND, true); + this.featureMap.set(SelectionChangeAction.KIND, true); + } + + getFeature(kind: CollaborationActionKinds): boolean { + return this.featureMap.get(kind)!; + } + + setFeature(kind: CollaborationActionKinds, value: boolean): void { + this.featureMap.set(kind, value); + this.handlers.forEach(handler => handler({ kind, value })); + } + + toggleFeature(kind: CollaborationActionKinds): boolean { + const newValue = !this.featureMap.get(kind) + this.setFeature(kind, newValue); + return newValue; + } + + onChange(handler: CollaborationFeatureChangeHandler) { + this.handlers.push(handler); + } +} + +export const collaborationFeatureStore = new CollaborationFeatureStore(); diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts similarity index 61% rename from packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts rename to packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts index dc2f655..ef29086 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client-provider.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts @@ -17,14 +17,24 @@ import { ActionMessage, DisposeClientSessionParameters, InitializeClientSessionP export const SUBCLIENT_HOST_ID = 'H'; -export interface CollaborateGlspClientProvider { - isInCollaborateMode(): boolean; +export type GuestsChangeHandler = (subclientIds: string[]) => void; + +export interface CommonCollaborationGlspClientProvider { + isInCollaborationMode(): boolean; isHost(): boolean; isGuest(): boolean; - initializeClientSession(params: InitializeClientSessionParameters): Promise; - disposeClientSession(params: DisposeClientSessionParameters): Promise; - sendActionMessage(message: ActionMessage): void; - handleActionMessage(message: ActionMessage): void; getSubclientIdFromSession(): string; getSubclientInfoFromSession(): SubclientInfo; } + +export interface HostCollaborationGlspClientProvider { + handleActionMessageForHost(message: ActionMessage): void; + handleMultipleActionMessagesForHost(messages: ActionMessage[]): void; + onGuestsChangeForHost(handler: GuestsChangeHandler): void; +} + +export interface GuestCollaborationGlspClientProvider { + initializeClientSessionForGuest(params: InitializeClientSessionParameters): Promise; + disposeClientSessionForGuest(params: DisposeClientSessionParameters): Promise; + sendActionMessageForGuest(message: ActionMessage): void; +} diff --git a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts similarity index 61% rename from packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts rename to packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts index 08226b5..a653f12 100644 --- a/packages/vscode-integration/src/quickstart-components/collaborate-glsp-client.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts @@ -30,30 +30,44 @@ import { UpdateModelAction } from '@eclipse-glsp/protocol'; import * as vscode from 'vscode'; -import { CollaborateGlspClientProvider, SUBCLIENT_HOST_ID } from './collaborate-glsp-client-provider'; +import { + CommonCollaborationGlspClientProvider, + GuestCollaborationGlspClientProvider, + HostCollaborationGlspClientProvider, + SUBCLIENT_HOST_ID +} from './collaboration-glsp-client-provider'; -export class CollaborateGlspClient implements GLSPClient { +export class CollaborationGlspClient implements GLSPClient { protected readonly BROADCAST_ACTION_TYPES = [SetModelAction.KIND, UpdateModelAction.KIND]; readonly id: string; protected glspClient: GLSPClient; - protected provider: CollaborateGlspClientProvider; + protected commonProvider: CommonCollaborationGlspClientProvider; + protected hostProvider: HostCollaborationGlspClientProvider; + protected guestProvider: GuestCollaborationGlspClientProvider; // Map subclientId H = host protected registeredSubclientMap = new Map>(); // overwritten clientSessionIds for Server: Map protected serverClientIdMap = new Map(); - protected handlers: ActionMessageHandler[] = []; + protected actionMessageHandlers: ActionMessageHandler[] = []; - constructor(glspClient: GLSPClient, provider: CollaborateGlspClientProvider) { + constructor( + glspClient: GLSPClient, + commonProvider: CommonCollaborationGlspClientProvider, + hostProvider: HostCollaborationGlspClientProvider, + guestProvider: GuestCollaborationGlspClientProvider + ) { this.id = glspClient.id; this.glspClient = glspClient; - this.provider = provider; + this.commonProvider = commonProvider; + this.hostProvider = hostProvider; + this.guestProvider = guestProvider; } shutdownServer(): void { @@ -65,7 +79,7 @@ export class CollaborateGlspClient implements GLSPClient { } async initializeClientSession(params: InitializeClientSessionParameters): Promise { - if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + if (!this.commonProvider.isInCollaborationMode() || this.commonProvider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); const subclientId = params.args?.subclientId as string; const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); @@ -78,21 +92,21 @@ export class CollaborateGlspClient implements GLSPClient { params.clientSessionId += `_${subclientId}`; // new unique clientSessionId for server this.serverClientIdMap.set(relativeDocumentUri, params.clientSessionId); return this.glspClient.initializeClientSession(params); - } else if (this.provider.isGuest()) { + } else if (this.commonProvider.isGuest()) { params.args = { ...params.args, - subclientId: this.provider.getSubclientIdFromSession() + subclientId: this.commonProvider.getSubclientIdFromSession() }; - return this.provider.initializeClientSession(params); + return this.guestProvider.initializeClientSessionForGuest(params); } } async disposeClientSession(params: DisposeClientSessionParameters): Promise { - if (this.provider.isInCollaborateMode() && this.provider.isHost()) { + if (this.commonProvider.isInCollaborationMode() && this.commonProvider.isHost()) { this.handleDisposeSubclientMessage(params); } - if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + if (!this.commonProvider.isInCollaborationMode() || this.commonProvider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); const subclientId = params.args?.subclientId as string; const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri) || new Map(); @@ -103,65 +117,27 @@ export class CollaborateGlspClient implements GLSPClient { } this.serverClientIdMap.delete(relativeDocumentUri); return this.glspClient.disposeClientSession(params); - } else if (this.provider.isGuest()) { + } else if (this.commonProvider.isGuest()) { params.args = { ...params.args, - subclientId: this.provider.getSubclientIdFromSession() + subclientId: this.commonProvider.getSubclientIdFromSession() }; - return this.provider.disposeClientSession(params); - } - } - - private handleDisposeSubclientMessage(params: DisposeClientSessionParameters): void { - const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - const subclientId = params.args?.subclientId as string; - const disposeSubclientMessage: ActionMessage = { - clientId: '', - action: DisposeSubclientAction.create() - }; - disposeSubclientMessage.action.initialSubclientInfo = { - name: '', - color: '', - subclientId - }; - for (const [id, localClientId] of subclientMap?.entries() || []) { - // only handle to other subclients - if (subclientId !== id) { - this.handleMessage(id, disposeSubclientMessage, localClientId); - } + return this.guestProvider.disposeClientSessionForGuest(params); } } onActionMessage(handler: ActionMessageHandler): void { - this.handlers.push(handler); - } - - private handleMessage(subclientId: string, originalMessage: ActionMessage, clientId: string): void { - // clone message so at broadcasting original message won't be overwritten (would lead to problems at host since we use this message there) - const clonedMessage: ActionMessage = { - ...originalMessage, - action: { - ...originalMessage.action, - subclientId - }, - clientId - }; - if (subclientId === SUBCLIENT_HOST_ID) { - this.handlers.forEach(handler => handler(clonedMessage)); // notify host - } else { - this.provider.handleActionMessage(clonedMessage); // notify subclientId - } + this.actionMessageHandlers.push(handler); } sendActionMessage(message: ActionMessage): void { if (CollaborationAction.is(message.action)) { // handle collabofration action without sending to glsp-server - if (!this.provider.isInCollaborateMode()) { + if (!this.commonProvider.isInCollaborationMode()) { return; } this.handleCollaborationAction(message as ActionMessage); - } else if (!this.provider.isInCollaborateMode() || this.provider.isHost()) { + } else if (!this.commonProvider.isInCollaborationMode() || this.commonProvider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); message.clientId = this.serverClientIdMap.get(relativeDocumentUri) || ''; // if requestModel action => add disableReload and if originClient not host => change sourceUri @@ -179,31 +155,9 @@ export class CollaborateGlspClient implements GLSPClient { } } this.glspClient.sendActionMessage(message); - } else if (this.provider.isGuest()) { - message.action.subclientId = this.provider.getSubclientIdFromSession(); - this.provider.sendActionMessage(message); - } - } - - private handleCollaborationAction(message: ActionMessage): void { - if (this.provider.isHost()) { - const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); - const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - const subclientId = message.action.subclientId; - // set initialSubclientInfo if host dispatches actions (not set yet) - if (!message.action.initialSubclientInfo) { - message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); - } - for (const [id, localClientId] of subclientMap?.entries() || []) { - // only handle to other subclients - if (subclientId !== id) { - this.handleMessage(id, message, localClientId); - } - } - } else if (this.provider.isGuest()) { - message.action.subclientId = this.provider.getSubclientIdFromSession(); - message.action.initialSubclientInfo = this.provider.getSubclientInfoFromSession(); - this.provider.sendActionMessage(message); + } else if (this.commonProvider.isGuest()) { + message.action.subclientId = this.commonProvider.getSubclientIdFromSession(); + this.guestProvider.sendActionMessageForGuest(message); } } @@ -213,24 +167,41 @@ export class CollaborateGlspClient implements GLSPClient { this.glspClient.onActionMessage((message: ActionMessage) => { const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId); const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); - if (!this.provider.isInCollaborateMode()) { + if (!subclientMap) { + return; + } + if (!this.commonProvider.isInCollaborationMode()) { // send only to host - const localClientId = subclientMap?.get(SUBCLIENT_HOST_ID) || ''; + const localClientId = subclientMap.get(SUBCLIENT_HOST_ID) || ''; this.handleMessage(SUBCLIENT_HOST_ID, message, localClientId); - } else if (this.provider.isHost()) { + } else if (this.commonProvider.isHost()) { const subclientId = message.action.subclientId; if (subclientId == null || this.BROADCAST_ACTION_TYPES.includes(message.action.kind)) { // braodcast to all subclients if subclientId is null or listed in BROADCAST_ACTION_TYPES - for (const [id, localClientId] of subclientMap?.entries() || []) { - this.handleMessage(id, message, localClientId); - } + this.handleMultipleMessages(subclientMap, message); } else { // send to adressed subclient - const localClientId = subclientMap?.get(subclientId) || ''; + const localClientId = subclientMap.get(subclientId) || ''; this.handleMessage(subclientId, message, localClientId); } } }); + + this.hostProvider.onGuestsChangeForHost((subclientIds: string[]) => { + for (const [relativeDocumentUri, subclientMap] of this.registeredSubclientMap.entries()) { + for (const [id, localClientId] of subclientMap.entries()) { + if (id !== SUBCLIENT_HOST_ID && !subclientIds.includes(id)) { + this.disposeClientSession({ + clientSessionId: localClientId, + args: { + relativeDocumentUri, + subclientId: id + } + }); + } + } + } + }); } stop(): Promise { @@ -241,6 +212,86 @@ export class CollaborateGlspClient implements GLSPClient { return this.glspClient.currentState; } + private handleCollaborationAction(message: ActionMessage): void { + if (this.commonProvider.isHost()) { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + if (!subclientMap) { + return; + } + const subclientId = message.action.subclientId; + // set initialSubclientInfo if host dispatches actions (not set yet) + if (!message.action.initialSubclientInfo) { + message.action.initialSubclientInfo = this.commonProvider.getSubclientInfoFromSession(); + } + this.handleMultipleMessages(subclientMap, message, actualSubclientId => actualSubclientId !== subclientId); + } else if (this.commonProvider.isGuest()) { + message.action.subclientId = this.commonProvider.getSubclientIdFromSession(); + message.action.initialSubclientInfo = this.commonProvider.getSubclientInfoFromSession(); + this.guestProvider.sendActionMessageForGuest(message); + } + } + + private handleDisposeSubclientMessage(params: DisposeClientSessionParameters): void { + const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + if (!subclientMap) { + return; + } + const subclientId = params.args?.subclientId as string; + const disposeSubclientMessage: ActionMessage = { + clientId: '', + action: DisposeSubclientAction.create({ initialSubclientId: subclientId}) + }; + this.handleMultipleMessages(subclientMap, disposeSubclientMessage, actualSubclientId => actualSubclientId !== subclientId); + } + + private handleMessage(subclientId: string, originalMessage: ActionMessage, clientId: string): void { + // clone message so at broadcasting original message won't be overwritten + // (would lead to problems at host since we use this message there) + const clonedMessage: ActionMessage = { + ...originalMessage, + action: { + ...originalMessage.action, + subclientId + }, + clientId + }; + if (subclientId === SUBCLIENT_HOST_ID) { + this.actionMessageHandlers.forEach(handler => handler(clonedMessage)); // notify host + } else { + this.hostProvider.handleActionMessageForHost(clonedMessage); // notify subclientId + } + } + + private handleMultipleMessages( + subclientMap: Map, + originalMessage: ActionMessage, + validate: (subclientId: string, localClientId: string) => boolean = () => true + ): void { + const messages: ActionMessage[] = []; + for (const [subclientId, localClientId] of subclientMap.entries()) { + if (validate(subclientId, localClientId)) { + const clonedMessage: ActionMessage = { + ...originalMessage, + action: { + ...originalMessage.action, + subclientId + }, + clientId: localClientId + }; + if (subclientId === SUBCLIENT_HOST_ID) { + this.actionMessageHandlers.forEach(handler => handler(clonedMessage)); // notify host + } else { + messages.push(clonedMessage); + } + } + } + if (messages.length > 0) { + this.hostProvider.handleMultipleActionMessagesForHost(messages); // notify all subclientIds + } + } + private getRelativeDocumentUriByServerClientId(serverClientId: string): string { for (const [key, value] of this.serverClientIdMap.entries()) { if (value === serverClientId) { diff --git a/packages/vscode-integration/src/collaboration/index.ts b/packages/vscode-integration/src/collaboration/index.ts new file mode 100644 index 0000000..8575219 --- /dev/null +++ b/packages/vscode-integration/src/collaboration/index.ts @@ -0,0 +1,20 @@ +/******************************************************************************** + * Copyright (c) 2023 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 + ********************************************************************************/ + +export * from './collaboration-commands'; +export * from './collaboration-feature-store'; +export * from './collaboration-glsp-client'; +export * from './collaboration-glsp-client-provider'; diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index 430cdda..2a3bc19 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -17,6 +17,7 @@ import { Action, ActionMessage, CollaborationAction, + CollaborationActionKinds, ExportSvgAction, InitializeClientSessionParameters, InitializeResult, @@ -31,7 +32,8 @@ import { } from '@eclipse-glsp/protocol'; import * as fs from 'fs'; import * as vscode from 'vscode'; -import { SUBCLIENT_HOST_ID } from './quickstart-components/collaborate-glsp-client-provider'; +import { collaborationFeatureStore } from './collaboration/collaboration-feature-store'; +import { SUBCLIENT_HOST_ID } from './collaboration/collaboration-glsp-client-provider'; import { GlspVscodeClient, GlspVscodeConnectorOptions } from './types'; // eslint-disable-next-line no-shadow @@ -131,6 +133,9 @@ export class GlspVscodeConnector { + client.onSendToClientEmitter.fire({ + clientId: client.clientId, + action: action, + __localDispatch: true + }); + }); + } + /** * Send message to registered client by id. * diff --git a/packages/vscode-integration/src/index.ts b/packages/vscode-integration/src/index.ts index ed5d873..0b708e6 100644 --- a/packages/vscode-integration/src/index.ts +++ b/packages/vscode-integration/src/index.ts @@ -16,5 +16,6 @@ export * from '@eclipse-glsp/protocol'; export * from './client-actions'; export * from './glsp-vscode-connector'; +export * from './collaboration'; export * from './liveshare'; export * from './types'; diff --git a/packages/vscode-integration/src/liveshare/index.ts b/packages/vscode-integration/src/liveshare/index.ts index e85b780..0cb08fb 100644 --- a/packages/vscode-integration/src/liveshare/index.ts +++ b/packages/vscode-integration/src/liveshare/index.ts @@ -14,3 +14,4 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ export * from './liveshare-glsp-client-provider'; +export * from './toggle-feature-tree-data-provider'; diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index 0780960..d8bcb6a 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -15,25 +15,36 @@ ********************************************************************************/ import { + Action, ActionMessage, DisposeClientSessionParameters, InitializeClientSessionParameters, SubclientInfo } from '@eclipse-glsp/protocol'; -import { LiveShare, Role, Session, SharedService, SharedServiceProxy, getApi } from 'vsls'; -import { CollaborateGlspClientProvider, SUBCLIENT_HOST_ID } from '../quickstart-components/collaborate-glsp-client-provider'; +import { LiveShare, Peer, Role, Session, SharedService, SharedServiceProxy, getApi, View } from 'vsls'; +import { + CommonCollaborationGlspClientProvider, + GuestCollaborationGlspClientProvider, + GuestsChangeHandler, + HostCollaborationGlspClientProvider, + SUBCLIENT_HOST_ID +} from '../collaboration/Collaboration-glsp-client-provider'; import { GlspVscodeServer } from '../types'; +import { ToggleFeatureTreeDataProvider } from './toggle-feature-tree-data-provider'; export const INITIALIZE_CLIENT_SESSION = 'INITIALIZE_CLIENT_SESSION'; export const DISPOSE_CLIENT_SESSION = 'DISPOSE_CLIENT_SESSION'; export const SEND_ACTION_MESSAGE = 'SEND_ACTION_MESSAGE'; export const ON_ACTION_MESSAGE = 'ON_ACTION_MESSAGE'; +export const ON_MULTIPLE_ACTION_MESSAGES = 'ON_MULTIPLE_ACTION_MESSAGES'; export const SERVICE_NAME = 'GLSP-LIVESHARE-SERVICE'; -const COLORS = ["#FFF100", "#5C2D91", "#E3008C", "#FF8C00"]; +const COLORS = ['#FFF100', '#5C2D91', '#E3008C', '#FF8C00', '#B4009E', '#107C10', '#FFB900', '#B4A0FF']; const FALLBACK_COLOR = '#ABABAB'; -export class LiveshareGlspClientProvider implements CollaborateGlspClientProvider { +export class LiveshareGlspClientProvider + implements CommonCollaborationGlspClientProvider, HostCollaborationGlspClientProvider, GuestCollaborationGlspClientProvider +{ protected session: Session | null; protected vsls: LiveShare | null; protected role: Role = Role.None; @@ -41,6 +52,7 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide protected server: GlspVscodeServer; protected subclientId: string | null; protected subclientInfo: SubclientInfo | null; + protected guestsChangeHandler: GuestsChangeHandler[] = []; protected get guestService(): SharedServiceProxy { return this.service as SharedServiceProxy; @@ -54,54 +66,67 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide this.server = server; this.vsls = await getApi(); - if (this.vsls) { - this.vsls.onDidChangeSession(async e => { - this.role = e.session.role; - this.session = e.session; - this.subclientId = e.session.role === Role.Host ? SUBCLIENT_HOST_ID : `${this.session!.peerNumber}`; - const colorId = e.session.role === Role.Host ? 0 : (e.session.peerNumber - 1); - this.subclientInfo = { - subclientId: this.subclientId, - name: this.session.user!.emailAddress || this.session.user!.displayName, - color: COLORS[colorId] || FALLBACK_COLOR - }; - if (e.session.role === Role.Host) { - this.service = await this.vsls!.shareService(SERVICE_NAME); - if (!this.service) { - return; - } - - this.service.onRequest(INITIALIZE_CLIENT_SESSION, async params => { - await (await this.server.glspClient).initializeClientSession(params[1] as InitializeClientSessionParameters); - }); + if (!this.vsls) { + return; + } - this.service.onRequest(DISPOSE_CLIENT_SESSION, async params => { - await (await this.server.glspClient).disposeClientSession(params[1] as DisposeClientSessionParameters); - }); + // Register the custom tree provider with Live Share + const treeDataProvider = new ToggleFeatureTreeDataProvider(); + this.vsls.registerTreeDataProvider(View.Session, treeDataProvider); + + this.vsls.onDidChangeSession(async e => { + this.role = e.session.role; + this.session = e.session; + this.subclientId = this.createSubclientIdFromSession(e.session); + this.subclientInfo = { + subclientId: this.subclientId, + name: this.createNameFromSession(e.session), + color: this.createColorFromSession(e.session) + }; + if (e.session.role === Role.Host) { + this.service = await this.vsls!.shareService(SERVICE_NAME); + if (!this.service) { + return; + } - this.service.onRequest(SEND_ACTION_MESSAGE, async params => { - (await this.server.glspClient).sendActionMessage(params[1] as ActionMessage); - }); - } else if (e.session.role === Role.Guest) { - this.service = await this.vsls!.getSharedService(SERVICE_NAME); - if (!this.service) { - return; - } - - this.service.onNotify(ON_ACTION_MESSAGE, (message: any) => { - const typedMessage = message as ActionMessage; - const subclientId = typedMessage.action.subclientId; - // check if message is adrseeed to this guest - if (this.getSubclientIdFromSession() === subclientId) { - this.server.onServerSendEmitter.fire(message); - } - }); + this.service.onRequest(INITIALIZE_CLIENT_SESSION, async params => { + await (await this.server.glspClient).initializeClientSession(params[1] as InitializeClientSessionParameters); + }); + + this.service.onRequest(DISPOSE_CLIENT_SESSION, async params => { + await (await this.server.glspClient).disposeClientSession(params[1] as DisposeClientSessionParameters); + }); + + this.service.onRequest(SEND_ACTION_MESSAGE, async params => { + (await this.server.glspClient).sendActionMessage(params[1] as ActionMessage); + }); + } else if (e.session.role === Role.Guest) { + this.service = await this.vsls!.getSharedService(SERVICE_NAME); + if (!this.service) { + return; } - }); - } + + this.service.onNotify(ON_ACTION_MESSAGE, (message: any) => { + this.checkActionMessageAndSendToClient(message); + }); + + this.service.onNotify(ON_MULTIPLE_ACTION_MESSAGES, (ev: any) => { + const typedMessages = ev.messages as ActionMessage[]; + typedMessages.forEach(typedMessage => { + this.checkActionMessageAndSendToClient(typedMessage); + }); + }); + } + }); + + this.vsls.onDidChangePeers(e => { + if (this.isInCollaborationMode() && this.isHost()) { + this.guestsChangeHandler.forEach(handler => handler(this.vsls!.peers.map(p => this.createSubclientIdFromPeer(p)))); + } + }); } - isInCollaborateMode(): boolean { + isInCollaborationMode(): boolean { return !!this.service && this.service.isServiceAvailable; } @@ -113,31 +138,66 @@ export class LiveshareGlspClientProvider implements CollaborateGlspClientProvide return this.role === Role.Guest; } - initializeClientSession(params: InitializeClientSessionParameters): Promise { + initializeClientSessionForGuest(params: InitializeClientSessionParameters): Promise { return this.guestService.request(INITIALIZE_CLIENT_SESSION, [params]); } - disposeClientSession(params: DisposeClientSessionParameters): Promise { + disposeClientSessionForGuest(params: DisposeClientSessionParameters): Promise { return this.guestService.request(DISPOSE_CLIENT_SESSION, [params]); } - sendActionMessage(message: ActionMessage): void { + sendActionMessageForGuest(message: ActionMessage): void { this.guestService.request(SEND_ACTION_MESSAGE, [message]); } - handleActionMessage(message: ActionMessage): void { + handleActionMessageForHost(message: ActionMessage): void { this.hostService.notify(ON_ACTION_MESSAGE, message); } + handleMultipleActionMessagesForHost(messages: ActionMessage[]): void { + this.hostService.notify(ON_MULTIPLE_ACTION_MESSAGES, { messages }); // sending arrays is not allowed + } + getSubclientIdFromSession(): string { return this.subclientId || ''; } getSubclientInfoFromSession(): SubclientInfo { - return this.subclientInfo || { - subclientId: '', - name: '', - color: '' - }; + return ( + this.subclientInfo || { + subclientId: '', + name: '', + color: '' + } + ); + } + + onGuestsChangeForHost(handler: GuestsChangeHandler): void { + this.guestsChangeHandler.push(handler); + } + + private checkActionMessageAndSendToClient(message: ActionMessage): void { + const subclientId = message.action.subclientId; + // check if message is adrseeed to this guest + if (this.getSubclientIdFromSession() === subclientId) { + this.server.onServerSendEmitter.fire(message); + } + } + + private createSubclientIdFromSession(session: Session): string { + return session.role === Role.Host ? SUBCLIENT_HOST_ID : `${session.peerNumber}`; + } + + private createSubclientIdFromPeer(peer: Peer): string { + return peer.role === Role.Host ? SUBCLIENT_HOST_ID : `${peer.peerNumber}`; + } + + private createColorFromSession(session: Session): string { + const colorId = session.role === Role.Host ? 0 : session.peerNumber - 1; + return COLORS[colorId % 8] || FALLBACK_COLOR; + } + + private createNameFromSession(session: Session): string { + return session.user!.emailAddress || session.user!.displayName; } } diff --git a/packages/vscode-integration/src/liveshare/toggle-feature-tree-data-provider.ts b/packages/vscode-integration/src/liveshare/toggle-feature-tree-data-provider.ts new file mode 100644 index 0000000..758af20 --- /dev/null +++ b/packages/vscode-integration/src/liveshare/toggle-feature-tree-data-provider.ts @@ -0,0 +1,39 @@ +import { collaborationFeatureStore } from '../collaboration/collaboration-feature-store'; +import { + Command, + Event, + EventEmitter, + ProviderResult, + TreeDataProvider, + TreeItem +} from 'vscode'; +import { TOGGLE_MOUSE_POINTERS_COMMAND, TOGGLE_VIEWPORTS_COMMAND, TOGGLE_SELECTIONS_COMMAND} from '../collaboration/collaboration-commands'; + +export class ToggleFeatureTreeDataProvider implements TreeDataProvider { + + private static readonly TOGGLE_FEATURE_COMMANDS = [TOGGLE_MOUSE_POINTERS_COMMAND, TOGGLE_VIEWPORTS_COMMAND, TOGGLE_SELECTIONS_COMMAND]; + + private onDidChangeTreeDataEmitter = new EventEmitter(); + public readonly onDidChangeTreeData: Event = this.onDidChangeTreeDataEmitter.event; + + constructor() { + collaborationFeatureStore.onChange(({ kind, value }) => { + const command = ToggleFeatureTreeDataProvider.TOGGLE_FEATURE_COMMANDS.find(command => command.arguments![0] === kind); + if (command) { + this.onDidChangeTreeDataEmitter.fire(command); + } + }); + } + + getChildren(element?: Command): ProviderResult { + return Promise.resolve(ToggleFeatureTreeDataProvider.TOGGLE_FEATURE_COMMANDS); + } + + getTreeItem(command: Command): TreeItem { + const enabled = collaborationFeatureStore.getFeature(command.arguments![0]); + const treeItem = new TreeItem((enabled ? 'Disable ' : 'Enable ' ) + command.title); + treeItem.tooltip = command.tooltip; + treeItem.command = command; + return treeItem; + } +} 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 index 8b253d5..97e251c 100644 --- a/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts +++ b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts @@ -27,7 +27,7 @@ import * as vscode from 'vscode'; import { createMessageConnection } from 'vscode-jsonrpc'; import { SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node'; import { GlspVscodeServer } from '../types'; -import { CollaborateGlspClient } from './collaborate-glsp-client'; +import { CollaborationGlspClient } from '../collaboration/collaboration-glsp-client'; interface SocketGlspVscodeServerOptions { /** Port of the running server. */ @@ -77,10 +77,10 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab this.liveshareGlspClientProvider = new LiveshareGlspClientProvider(); - this._glspClient = new CollaborateGlspClient(new BaseJsonrpcGLSPClient({ + this._glspClient = new CollaborationGlspClient(new BaseJsonrpcGLSPClient({ id: options.clientId, connectionProvider: connection - }), this.liveshareGlspClientProvider); + }), this.liveshareGlspClientProvider, this.liveshareGlspClientProvider, this.liveshareGlspClientProvider); this.onSendToServerEmitter.event(message => { this.onReady.then(() => { From 66902e7a6033c37058bf8eb99513f999bd8cadcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 10 May 2023 01:12:02 +0200 Subject: [PATCH 10/18] refactor collaboration-glsp-client so it's independent from extension if it's in collaboration-mode or not --- .../extension/src/workflow-extension.ts | 7 +- .../vscode-integration/src/action-types.ts | 46 -------- .../collaboration-glsp-client-provider.ts | 11 ++ .../collaboration-glsp-client.ts | 102 +++++++++++------- .../src/collaboration/collaboration-util.ts | 16 +++ .../src/collaboration/index.ts | 1 + .../src/glsp-vscode-connector.ts | 25 ++--- .../liveshare-glsp-client-provider.ts | 23 ++-- .../socket-glsp-vscode-server.ts | 23 ++-- 9 files changed, 130 insertions(+), 124 deletions(-) delete mode 100644 packages/vscode-integration/src/action-types.ts create mode 100644 packages/vscode-integration/src/collaboration/collaboration-util.ts diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 6072300..269beec 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GlspVscodeConnector, NavigateAction } from '@eclipse-glsp/vscode-integration'; +import { GlspVscodeConnector, LiveshareGlspClientProvider, NavigateAction } from '@eclipse-glsp/vscode-integration'; import { GlspServerLauncher, configureDefaultCommands, @@ -50,11 +50,14 @@ export async function activate(context: vscode.ExtensionContext): Promise await serverProcess.start(); } + const liveshareGlspClientProvider = new LiveshareGlspClientProvider(); + // 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) + serverPort: JSON.parse(process.env.GLSP_SERVER_PORT || DEFAULT_SERVER_PORT), + collaboration: liveshareGlspClientProvider }); // Initialize GLSP-VSCode connector with server wrapper diff --git a/packages/vscode-integration/src/action-types.ts b/packages/vscode-integration/src/action-types.ts deleted file mode 100644 index 94d687a..0000000 --- a/packages/vscode-integration/src/action-types.ts +++ /dev/null @@ -1,46 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 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, Args } from '@eclipse-glsp/protocol'; -import * as sprotty from 'sprotty-protocol/lib/actions'; - -export interface _Action extends sprotty.Action { - /** - * Unique identifier specifying the kind of action to process. - */ - kind: string; - - /** - * Unique identifier specifying the subclient of the process. - */ - subclientId: string; -} - -export interface _ActionMessage extends sprotty.ActionMessage { - /** - * The unique client id - * */ - clientId: string; - - /** - * The action to execute. - */ - action: A; - - /** - * Additional custom arguments e.g. application specific parameters. - */ - args?: Args; -} diff --git a/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts index ef29086..1e9d08e 100644 --- a/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts @@ -14,12 +14,23 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { ActionMessage, DisposeClientSessionParameters, InitializeClientSessionParameters, SubclientInfo } from '@eclipse-glsp/protocol'; +import { CollaborationGlspClient } from './collaboration-glsp-client'; export const SUBCLIENT_HOST_ID = 'H'; export type GuestsChangeHandler = (subclientIds: string[]) => void; +export interface CollaborationGlspClientProviderInitializeConfig { + collaborationGlspClient: CollaborationGlspClient; +} + +export type CollaborativeGlspClientProvider = + CommonCollaborationGlspClientProvider + & HostCollaborationGlspClientProvider + & GuestCollaborationGlspClientProvider; + export interface CommonCollaborationGlspClientProvider { + initialize(config: CollaborationGlspClientProviderInitializeConfig): Promise; isInCollaborationMode(): boolean; isHost(): boolean; isGuest(): boolean; diff --git a/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts index a653f12..5b9c2b6 100644 --- a/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { + Action, ActionMessage, ActionMessageHandler, Args, @@ -22,6 +23,7 @@ import { DisposeClientSessionParameters, DisposeSubclientAction, GLSPClient, + hasObjectProp, InitializeClientSessionParameters, InitializeParameters, InitializeResult, @@ -29,21 +31,32 @@ import { SetModelAction, UpdateModelAction } from '@eclipse-glsp/protocol'; -import * as vscode from 'vscode'; import { + CollaborativeGlspClientProvider, CommonCollaborationGlspClientProvider, GuestCollaborationGlspClientProvider, HostCollaborationGlspClientProvider, SUBCLIENT_HOST_ID } from './collaboration-glsp-client-provider'; +import { getFullDocumentUri } from './collaboration-util'; + +interface CollaborativeGlspClientDistributedConfig { + commonProvider: CommonCollaborationGlspClientProvider, + hostProvider: HostCollaborationGlspClientProvider, + guestProvider: GuestCollaborationGlspClientProvider +} + +function isDistributedConfig(config: any): config is CollaborativeGlspClientDistributedConfig { + return hasObjectProp(config, 'commonProvider') && hasObjectProp(config, 'hostProvider') && hasObjectProp(config, 'guestProvider') +} + +export type CollaborativeGlspClientConfig = CollaborativeGlspClientProvider | CollaborativeGlspClientDistributedConfig; export class CollaborationGlspClient implements GLSPClient { protected readonly BROADCAST_ACTION_TYPES = [SetModelAction.KIND, UpdateModelAction.KIND]; readonly id: string; - protected glspClient: GLSPClient; - protected commonProvider: CommonCollaborationGlspClientProvider; protected hostProvider: HostCollaborationGlspClientProvider; protected guestProvider: GuestCollaborationGlspClientProvider; @@ -56,18 +69,20 @@ export class CollaborationGlspClient implements GLSPClient { protected actionMessageHandlers: ActionMessageHandler[] = []; constructor( - glspClient: GLSPClient, - commonProvider: CommonCollaborationGlspClientProvider, - hostProvider: HostCollaborationGlspClientProvider, - guestProvider: GuestCollaborationGlspClientProvider + protected glspClient: GLSPClient, + config: CollaborativeGlspClientConfig ) { this.id = glspClient.id; - this.glspClient = glspClient; - - this.commonProvider = commonProvider; - this.hostProvider = hostProvider; - this.guestProvider = guestProvider; + if (isDistributedConfig(config)) { + this.commonProvider = config.commonProvider; + this.hostProvider = config.hostProvider; + this.guestProvider = config.guestProvider; + } else { + this.commonProvider = config; + this.hostProvider = config; + this.guestProvider = config; + } } shutdownServer(): void { @@ -79,6 +94,13 @@ export class CollaborationGlspClient implements GLSPClient { } async initializeClientSession(params: InitializeClientSessionParameters): Promise { + if (!params.args?.subclientId) { + params.args = { + ...params.args, + subclientId: this.commonProvider.getSubclientIdFromSession() + }; + } + if (!this.commonProvider.isInCollaborationMode() || this.commonProvider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(params.args); const subclientId = params.args?.subclientId as string; @@ -93,15 +115,18 @@ export class CollaborationGlspClient implements GLSPClient { this.serverClientIdMap.set(relativeDocumentUri, params.clientSessionId); return this.glspClient.initializeClientSession(params); } else if (this.commonProvider.isGuest()) { + return this.guestProvider.initializeClientSessionForGuest(params); + } + } + + async disposeClientSession(params: DisposeClientSessionParameters): Promise { + if (!params.args?.subclientId) { params.args = { ...params.args, subclientId: this.commonProvider.getSubclientIdFromSession() }; - return this.guestProvider.initializeClientSessionForGuest(params); } - } - async disposeClientSession(params: DisposeClientSessionParameters): Promise { if (this.commonProvider.isInCollaborationMode() && this.commonProvider.isHost()) { this.handleDisposeSubclientMessage(params); } @@ -118,10 +143,6 @@ export class CollaborationGlspClient implements GLSPClient { this.serverClientIdMap.delete(relativeDocumentUri); return this.glspClient.disposeClientSession(params); } else if (this.commonProvider.isGuest()) { - params.args = { - ...params.args, - subclientId: this.commonProvider.getSubclientIdFromSession() - }; return this.guestProvider.disposeClientSessionForGuest(params); } } @@ -131,11 +152,11 @@ export class CollaborationGlspClient implements GLSPClient { } sendActionMessage(message: ActionMessage): void { + if (!message.action.subclientId) { + message.action.subclientId = this.commonProvider.getSubclientIdFromSession(); + } + if (CollaborationAction.is(message.action)) { - // handle collabofration action without sending to glsp-server - if (!this.commonProvider.isInCollaborationMode()) { - return; - } this.handleCollaborationAction(message as ActionMessage); } else if (!this.commonProvider.isInCollaborationMode() || this.commonProvider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); @@ -156,12 +177,13 @@ export class CollaborationGlspClient implements GLSPClient { } this.glspClient.sendActionMessage(message); } else if (this.commonProvider.isGuest()) { - message.action.subclientId = this.commonProvider.getSubclientIdFromSession(); this.guestProvider.sendActionMessageForGuest(message); } } async start(): Promise { + await this.commonProvider.initialize({ collaborationGlspClient: this }); + await this.glspClient.start(); this.glspClient.onActionMessage((message: ActionMessage) => { @@ -212,7 +234,21 @@ export class CollaborationGlspClient implements GLSPClient { return this.glspClient.currentState; } + handleActionOnAllLocalHandlers(message: ActionMessage): void { + this.actionMessageHandlers.forEach(handler => handler(message)); + } + private handleCollaborationAction(message: ActionMessage): void { + // handle collabofration action without sending to glsp-server + if (!this.commonProvider.isInCollaborationMode()) { + return; + } + + // set initialSubclientInfo if not set yet + if (!message.action.initialSubclientInfo) { + message.action.initialSubclientInfo = this.commonProvider.getSubclientInfoFromSession(); + } + if (this.commonProvider.isHost()) { const relativeDocumentUri = this.getRelativeDocumentUriByArgs(message.args); const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); @@ -220,14 +256,8 @@ export class CollaborationGlspClient implements GLSPClient { return; } const subclientId = message.action.subclientId; - // set initialSubclientInfo if host dispatches actions (not set yet) - if (!message.action.initialSubclientInfo) { - message.action.initialSubclientInfo = this.commonProvider.getSubclientInfoFromSession(); - } this.handleMultipleMessages(subclientMap, message, actualSubclientId => actualSubclientId !== subclientId); } else if (this.commonProvider.isGuest()) { - message.action.subclientId = this.commonProvider.getSubclientIdFromSession(); - message.action.initialSubclientInfo = this.commonProvider.getSubclientInfoFromSession(); this.guestProvider.sendActionMessageForGuest(message); } } @@ -258,7 +288,7 @@ export class CollaborationGlspClient implements GLSPClient { clientId }; if (subclientId === SUBCLIENT_HOST_ID) { - this.actionMessageHandlers.forEach(handler => handler(clonedMessage)); // notify host + this.handleActionOnAllLocalHandlers(clonedMessage); // notify host } else { this.hostProvider.handleActionMessageForHost(clonedMessage); // notify subclientId } @@ -281,7 +311,7 @@ export class CollaborationGlspClient implements GLSPClient { clientId: localClientId }; if (subclientId === SUBCLIENT_HOST_ID) { - this.actionMessageHandlers.forEach(handler => handler(clonedMessage)); // notify host + this.handleActionOnAllLocalHandlers(clonedMessage); // notify host } else { messages.push(clonedMessage); } @@ -305,11 +335,3 @@ export class CollaborationGlspClient implements GLSPClient { return (args?.relativeDocumentUri || '') as string; } } - -function getFullDocumentUri(relativeDocumentUri: string): string { - let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.path || ''; - // FIXME test on microsoft - workspacePath = workspacePath.endsWith('/') ? workspacePath : `${workspacePath}/`; - workspacePath = workspacePath.startsWith('file://') ? workspacePath : `file://${workspacePath}`; - return workspacePath + relativeDocumentUri; -} diff --git a/packages/vscode-integration/src/collaboration/collaboration-util.ts b/packages/vscode-integration/src/collaboration/collaboration-util.ts new file mode 100644 index 0000000..5a3f32c --- /dev/null +++ b/packages/vscode-integration/src/collaboration/collaboration-util.ts @@ -0,0 +1,16 @@ +import * as vscode from 'vscode'; + +export function getRelativeDocumentUri(path: string): string { + let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.path; + // FIXME test on microsoft / windows + workspacePath = workspacePath?.endsWith('/') ? workspacePath : `${workspacePath}/` + return path.replace(workspacePath, ''); +} + +export function getFullDocumentUri(relativeDocumentUri: string): string { + let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.path || ''; + // FIXME test on microsoft / windows + workspacePath = workspacePath.endsWith('/') ? workspacePath : `${workspacePath}/`; + workspacePath = workspacePath.startsWith('file://') ? workspacePath : `file://${workspacePath}`; + return workspacePath + relativeDocumentUri; +} diff --git a/packages/vscode-integration/src/collaboration/index.ts b/packages/vscode-integration/src/collaboration/index.ts index 8575219..c1efefd 100644 --- a/packages/vscode-integration/src/collaboration/index.ts +++ b/packages/vscode-integration/src/collaboration/index.ts @@ -18,3 +18,4 @@ export * from './collaboration-commands'; export * from './collaboration-feature-store'; export * from './collaboration-glsp-client'; export * from './collaboration-glsp-client-provider'; +export * from './collaboration-util'; diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index 2a3bc19..d25f539 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -33,7 +33,7 @@ import { import * as fs from 'fs'; import * as vscode from 'vscode'; import { collaborationFeatureStore } from './collaboration/collaboration-feature-store'; -import { SUBCLIENT_HOST_ID } from './collaboration/collaboration-glsp-client-provider'; +import { getRelativeDocumentUri } from './collaboration/collaboration-util'; import { GlspVscodeClient, GlspVscodeConnectorOptions } from './types'; // eslint-disable-next-line no-shadow @@ -157,7 +157,7 @@ export class GlspVscodeConnector { @@ -186,7 +186,7 @@ export class GlspVscodeConnector, - subclientId: string, relativeDocumentUri: string ): Promise { return { clientSessionId: client.clientId, diagramType: client.diagramType, args: { - relativeDocumentUri, - subclientId + relativeDocumentUri + // subclientId } }; } @@ -532,11 +531,3 @@ export class GlspVscodeConnector disposable.dispose()); } } - -function getRelativeDocumentUri(client: GlspVscodeClient): string { - let workspacePath = vscode.workspace.workspaceFolders?.[0].uri.path; - // FIXME test on microsoft - workspacePath = workspacePath?.endsWith('/') ? workspacePath : `${workspacePath}/` - return client.document.uri.path.replace(workspacePath, ''); -} - diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index d8bcb6a..a7ee864 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -23,14 +23,15 @@ import { } from '@eclipse-glsp/protocol'; import { LiveShare, Peer, Role, Session, SharedService, SharedServiceProxy, getApi, View } from 'vsls'; import { + CollaborationGlspClientProviderInitializeConfig, CommonCollaborationGlspClientProvider, GuestCollaborationGlspClientProvider, GuestsChangeHandler, HostCollaborationGlspClientProvider, SUBCLIENT_HOST_ID } from '../collaboration/Collaboration-glsp-client-provider'; -import { GlspVscodeServer } from '../types'; import { ToggleFeatureTreeDataProvider } from './toggle-feature-tree-data-provider'; +import { CollaborationGlspClient } from '../collaboration/collaboration-glsp-client'; export const INITIALIZE_CLIENT_SESSION = 'INITIALIZE_CLIENT_SESSION'; export const DISPOSE_CLIENT_SESSION = 'DISPOSE_CLIENT_SESSION'; @@ -49,9 +50,12 @@ export class LiveshareGlspClientProvider protected vsls: LiveShare | null; protected role: Role = Role.None; protected service: SharedService | SharedServiceProxy | null; - protected server: GlspVscodeServer; + + protected collaborationGlspClient: CollaborationGlspClient; + protected subclientId: string | null; protected subclientInfo: SubclientInfo | null; + protected guestsChangeHandler: GuestsChangeHandler[] = []; protected get guestService(): SharedServiceProxy { @@ -62,8 +66,9 @@ export class LiveshareGlspClientProvider return this.service as SharedService; } - async initialize(server: GlspVscodeServer): Promise { - this.server = server; + async initialize(config: CollaborationGlspClientProviderInitializeConfig): Promise { + this.collaborationGlspClient = config.collaborationGlspClient; + this.vsls = await getApi(); if (!this.vsls) { @@ -90,15 +95,15 @@ export class LiveshareGlspClientProvider } this.service.onRequest(INITIALIZE_CLIENT_SESSION, async params => { - await (await this.server.glspClient).initializeClientSession(params[1] as InitializeClientSessionParameters); + await this.collaborationGlspClient.initializeClientSession(params[1] as InitializeClientSessionParameters); }); this.service.onRequest(DISPOSE_CLIENT_SESSION, async params => { - await (await this.server.glspClient).disposeClientSession(params[1] as DisposeClientSessionParameters); + await this.collaborationGlspClient.disposeClientSession(params[1] as DisposeClientSessionParameters); }); this.service.onRequest(SEND_ACTION_MESSAGE, async params => { - (await this.server.glspClient).sendActionMessage(params[1] as ActionMessage); + this.collaborationGlspClient.sendActionMessage(params[1] as ActionMessage); }); } else if (e.session.role === Role.Guest) { this.service = await this.vsls!.getSharedService(SERVICE_NAME); @@ -159,7 +164,7 @@ export class LiveshareGlspClientProvider } getSubclientIdFromSession(): string { - return this.subclientId || ''; + return this.subclientId || SUBCLIENT_HOST_ID; } getSubclientInfoFromSession(): SubclientInfo { @@ -180,7 +185,7 @@ export class LiveshareGlspClientProvider const subclientId = message.action.subclientId; // check if message is adrseeed to this guest if (this.getSubclientIdFromSession() === subclientId) { - this.server.onServerSendEmitter.fire(message); + this.collaborationGlspClient.handleActionOnAllLocalHandlers(message); } } 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 index 97e251c..8027437 100644 --- a/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts +++ b/packages/vscode-integration/src/quickstart-components/socket-glsp-vscode-server.ts @@ -22,12 +22,11 @@ import { InitializeResult } from '@eclipse-glsp/protocol'; import * as net from 'net'; -import { LiveshareGlspClientProvider } from '../liveshare'; import * as vscode from 'vscode'; import { createMessageConnection } from 'vscode-jsonrpc'; import { SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node'; import { GlspVscodeServer } from '../types'; -import { CollaborationGlspClient } from '../collaboration/collaboration-glsp-client'; +import { CollaborationGlspClient, CollaborativeGlspClientConfig } from '../collaboration/collaboration-glsp-client'; interface SocketGlspVscodeServerOptions { /** Port of the running server. */ @@ -36,6 +35,8 @@ interface SocketGlspVscodeServerOptions { readonly clientId: string; /** Name to register the client with on the server. */ readonly clientName: string; + /** Config to set if server should be in collaboration mode. */ + readonly collaboration?: CollaborativeGlspClientConfig; } /** @@ -61,7 +62,6 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab protected readonly onReady: Promise; protected setReady: () => void; - protected liveshareGlspClientProvider: LiveshareGlspClientProvider; _initializeResult: InitializeResult; constructor(protected readonly options: SocketGlspVscodeServerOptions) { @@ -75,12 +75,17 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab const writer = new SocketMessageWriter(this.socket); const connection = createMessageConnection(reader, writer); - this.liveshareGlspClientProvider = new LiveshareGlspClientProvider(); - - this._glspClient = new CollaborationGlspClient(new BaseJsonrpcGLSPClient({ + this._glspClient = new BaseJsonrpcGLSPClient({ id: options.clientId, connectionProvider: connection - }), this.liveshareGlspClientProvider, this.liveshareGlspClientProvider, this.liveshareGlspClientProvider); + }); + + if (options.collaboration != null) { + this._glspClient = new CollaborationGlspClient( + this._glspClient, + options.collaboration + ); + } this.onSendToServerEmitter.event(message => { this.onReady.then(() => { @@ -92,13 +97,11 @@ export class SocketGlspVscodeServer implements GlspVscodeServer, vscode.Disposab } /** - * Starts up the JSON-RPC client, initializes liveshare, and connects it to a running server. + * Starts up the glsp client and connects it to a running server. */ async start(): Promise { this.socket.connect(this.options.serverPort); - await this.liveshareGlspClientProvider.initialize(this); - await this._glspClient.start(); const parameters = await this.createInitializeParameters(); this._initializeResult = await this._glspClient.initializeServer(parameters); From 1373554a5e75ab96cd28e383c3966fbc2952d0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 10 May 2023 01:12:02 +0200 Subject: [PATCH 11/18] initalize liveshare session already at liveshare start --- .../collaboration-glsp-client-provider.ts | 2 +- .../collaboration-glsp-client.ts | 4 +- .../liveshare-glsp-client-provider.ts | 94 ++++++++++--------- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts index 1e9d08e..f574060 100644 --- a/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts @@ -24,7 +24,7 @@ export interface CollaborationGlspClientProviderInitializeConfig { collaborationGlspClient: CollaborationGlspClient; } -export type CollaborativeGlspClientProvider = +export type CollaborationGlspClientProvider = CommonCollaborationGlspClientProvider & HostCollaborationGlspClientProvider & GuestCollaborationGlspClientProvider; diff --git a/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts index 5b9c2b6..cc28e57 100644 --- a/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts @@ -32,7 +32,7 @@ import { UpdateModelAction } from '@eclipse-glsp/protocol'; import { - CollaborativeGlspClientProvider, + CollaborationGlspClientProvider, CommonCollaborationGlspClientProvider, GuestCollaborationGlspClientProvider, HostCollaborationGlspClientProvider, @@ -50,7 +50,7 @@ function isDistributedConfig(config: any): config is CollaborativeGlspClientDist return hasObjectProp(config, 'commonProvider') && hasObjectProp(config, 'hostProvider') && hasObjectProp(config, 'guestProvider') } -export type CollaborativeGlspClientConfig = CollaborativeGlspClientProvider | CollaborativeGlspClientDistributedConfig; +export type CollaborativeGlspClientConfig = CollaborationGlspClientProvider | CollaborativeGlspClientDistributedConfig; export class CollaborationGlspClient implements GLSPClient { protected readonly BROADCAST_ACTION_TYPES = [SetModelAction.KIND, UpdateModelAction.KIND]; diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index a7ee864..9160dc1 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -75,53 +75,16 @@ export class LiveshareGlspClientProvider return; } + if (this.vsls.session) { + await this.initializeSession(this.vsls.session); + } + // Register the custom tree provider with Live Share const treeDataProvider = new ToggleFeatureTreeDataProvider(); this.vsls.registerTreeDataProvider(View.Session, treeDataProvider); this.vsls.onDidChangeSession(async e => { - this.role = e.session.role; - this.session = e.session; - this.subclientId = this.createSubclientIdFromSession(e.session); - this.subclientInfo = { - subclientId: this.subclientId, - name: this.createNameFromSession(e.session), - color: this.createColorFromSession(e.session) - }; - if (e.session.role === Role.Host) { - this.service = await this.vsls!.shareService(SERVICE_NAME); - if (!this.service) { - return; - } - - this.service.onRequest(INITIALIZE_CLIENT_SESSION, async params => { - await this.collaborationGlspClient.initializeClientSession(params[1] as InitializeClientSessionParameters); - }); - - this.service.onRequest(DISPOSE_CLIENT_SESSION, async params => { - await this.collaborationGlspClient.disposeClientSession(params[1] as DisposeClientSessionParameters); - }); - - this.service.onRequest(SEND_ACTION_MESSAGE, async params => { - this.collaborationGlspClient.sendActionMessage(params[1] as ActionMessage); - }); - } else if (e.session.role === Role.Guest) { - this.service = await this.vsls!.getSharedService(SERVICE_NAME); - if (!this.service) { - return; - } - - this.service.onNotify(ON_ACTION_MESSAGE, (message: any) => { - this.checkActionMessageAndSendToClient(message); - }); - - this.service.onNotify(ON_MULTIPLE_ACTION_MESSAGES, (ev: any) => { - const typedMessages = ev.messages as ActionMessage[]; - typedMessages.forEach(typedMessage => { - this.checkActionMessageAndSendToClient(typedMessage); - }); - }); - } + await this.initializeSession(e.session); }); this.vsls.onDidChangePeers(e => { @@ -181,6 +144,51 @@ export class LiveshareGlspClientProvider this.guestsChangeHandler.push(handler); } + private async initializeSession(session: Session): Promise { + this.role = session.role; + this.session = session; + this.subclientId = session.role === Role.None ? null : this.createSubclientIdFromSession(session); + this.subclientInfo = session.role === Role.None ? null : { + subclientId: this.subclientId!, + name: this.createNameFromSession(session), + color: this.createColorFromSession(session) + }; + if (session.role === Role.Host) { + this.service = await this.vsls!.shareService(SERVICE_NAME); + if (!this.service) { + return; + } + + this.service.onRequest(INITIALIZE_CLIENT_SESSION, async params => { + await this.collaborationGlspClient.initializeClientSession(params[1] as InitializeClientSessionParameters); + }); + + this.service.onRequest(DISPOSE_CLIENT_SESSION, async params => { + await this.collaborationGlspClient.disposeClientSession(params[1] as DisposeClientSessionParameters); + }); + + this.service.onRequest(SEND_ACTION_MESSAGE, async params => { + this.collaborationGlspClient.sendActionMessage(params[1] as ActionMessage); + }); + } else if (session.role === Role.Guest) { + this.service = await this.vsls!.getSharedService(SERVICE_NAME); + if (!this.service) { + return; + } + + this.service.onNotify(ON_ACTION_MESSAGE, (message: any) => { + this.checkActionMessageAndSendToClient(message); + }); + + this.service.onNotify(ON_MULTIPLE_ACTION_MESSAGES, (ev: any) => { + const typedMessages = ev.messages as ActionMessage[]; + typedMessages.forEach(typedMessage => { + this.checkActionMessageAndSendToClient(typedMessage); + }); + }); + } + } + private checkActionMessageAndSendToClient(message: ActionMessage): void { const subclientId = message.action.subclientId; // check if message is adrseeed to this guest @@ -203,6 +211,6 @@ export class LiveshareGlspClientProvider } private createNameFromSession(session: Session): string { - return session.user!.emailAddress || session.user!.displayName; + return session.user?.emailAddress || session.user?.displayName || ''; } } From 703052724b536e215277f962d77559bf714e4d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 10 May 2023 21:54:44 +0200 Subject: [PATCH 12/18] change order of name creation for liveshare-glsp-client-provider --- .../src/liveshare/liveshare-glsp-client-provider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index 9160dc1..87169a9 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -211,6 +211,10 @@ export class LiveshareGlspClientProvider } private createNameFromSession(session: Session): string { - return session.user?.emailAddress || session.user?.displayName || ''; + const user = session.user; + if (!user) { + return ''; + } + return user.userName || user.displayName || user.emailAddress || ''; } } From f69a34b6026335e8b4111255573e9e0fb8d5cfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 17 May 2023 13:43:45 +0200 Subject: [PATCH 13/18] add init-liveshare-config --- .../src/glsp-vscode-connector.ts | 3 - .../vscode-integration/src/liveshare/index.ts | 1 + .../src/liveshare/init-liveshare-config.ts | 61 +++++++++++++++++++ .../liveshare-glsp-client-provider.ts | 2 +- 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 packages/vscode-integration/src/liveshare/init-liveshare-config.ts diff --git a/packages/vscode-integration/src/glsp-vscode-connector.ts b/packages/vscode-integration/src/glsp-vscode-connector.ts index d25f539..421ddfc 100644 --- a/packages/vscode-integration/src/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/glsp-vscode-connector.ts @@ -186,7 +186,6 @@ export class GlspVscodeConnector Date: Wed, 17 May 2023 14:39:07 +0200 Subject: [PATCH 14/18] add information message to init-liveshare-config --- .../vscode-integration/src/liveshare/init-liveshare-config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vscode-integration/src/liveshare/init-liveshare-config.ts b/packages/vscode-integration/src/liveshare/init-liveshare-config.ts index 75290d5..505b3d7 100644 --- a/packages/vscode-integration/src/liveshare/init-liveshare-config.ts +++ b/packages/vscode-integration/src/liveshare/init-liveshare-config.ts @@ -1,5 +1,6 @@ import * as os from 'os'; import * as fs from 'fs'; +import * as vscode from 'vscode'; const fileNotFoundCode = 'ENOENT'; const liveshareConfigFileName = '.vs-liveshare-settings.json'; @@ -53,6 +54,7 @@ export function writeExtensionPermissionsForLiveshare(publisher: string) { console.log('Content:'); console.log(jsonData); console.log('Please restart VS Code, so Liveshare can re-load content of config file.'); + vscode.window.showInformationMessage('Please restart VS Code, so Liveshare can re-load content of config file.'); } catch(err: any) { showError(publisher, err); } From 0229bb26b4970399556380fee38f6f969bc32c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 17 May 2023 17:18:04 +0200 Subject: [PATCH 15/18] add forced flag to init-liveshare-config --- .../src/liveshare/init-liveshare-config.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/vscode-integration/src/liveshare/init-liveshare-config.ts b/packages/vscode-integration/src/liveshare/init-liveshare-config.ts index 505b3d7..9d9493e 100644 --- a/packages/vscode-integration/src/liveshare/init-liveshare-config.ts +++ b/packages/vscode-integration/src/liveshare/init-liveshare-config.ts @@ -7,7 +7,7 @@ const liveshareConfigFileName = '.vs-liveshare-settings.json'; const homedir = os.homedir() + '/'; const liveshareConfigPath = homedir + liveshareConfigFileName; -function showError(publisher: string, err: Error) { +function showError(publisher: string, err: Error, forced: boolean = false) { const publisherKey = createPublisherKey(publisher); console.error(err); @@ -18,13 +18,17 @@ function showError(publisher: string, err: Error) { [publisherKey]: '*' } }); + + if (forced) { + vscode.window.showErrorMessage('Liveshare initializing failed!'); + } } function createPublisherKey(publisher: string) { return publisher + '.*'; } -export function writeExtensionPermissionsForLiveshare(publisher: string) { +export function writeExtensionPermissionsForLiveshare(publisher: string, forced: boolean = false) { const publisherKey = createPublisherKey(publisher); try { let data: string | null = null; @@ -43,6 +47,9 @@ export function writeExtensionPermissionsForLiveshare(publisher: string) { } if (jsonData.extensionPermissions[publisherKey] === '*') { + if (forced) { + vscode.window.showInformationMessage('Liveshare already initialized. Everything fine!'); + } return; } @@ -54,9 +61,11 @@ export function writeExtensionPermissionsForLiveshare(publisher: string) { console.log('Content:'); console.log(jsonData); console.log('Please restart VS Code, so Liveshare can re-load content of config file.'); - vscode.window.showInformationMessage('Please restart VS Code, so Liveshare can re-load content of config file.'); + vscode.window.showWarningMessage('Please restart VS Code, so Liveshare can re-load content of config file.', { + modal: true + }); } catch(err: any) { - showError(publisher, err); + showError(publisher, err, forced); } } From 6130a07f89a286dec0157b879f25dfd4800750e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Fri, 2 Feb 2024 12:15:17 +0100 Subject: [PATCH 16/18] Add liveshare init to workflow --- example/workflow/extension/package.json | 8 +++++++- example/workflow/extension/src/workflow-extension.ts | 12 +++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/example/workflow/extension/package.json b/example/workflow/extension/package.json index b748fec..a5ce604 100644 --- a/example/workflow/extension/package.json +++ b/example/workflow/extension/package.json @@ -84,6 +84,11 @@ "title": "Export as SVG", "category": "Workflow Diagram", "enablement": "activeCustomEditorId == 'workflow.glspDiagram'" + }, + { + "command": "workflow.liveshare.init", + "title": "Initialize Liveshare for Workflow", + "category": "Workflow Diagram" } ], "submenus": [ @@ -176,7 +181,8 @@ ] }, "activationEvents": [ - "*" + "*", + "onCommand:bigUML.liveshare.init" ], "files": [ "lib", diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 269beec..f6fea30 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -13,7 +13,12 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GlspVscodeConnector, LiveshareGlspClientProvider, NavigateAction } from '@eclipse-glsp/vscode-integration'; +import { + GlspVscodeConnector, + LiveshareGlspClientProvider, + NavigateAction, + writeExtensionPermissionsForLiveshare +} from '@eclipse-glsp/vscode-integration'; import { GlspServerLauncher, configureDefaultCommands, @@ -50,6 +55,8 @@ export async function activate(context: vscode.ExtensionContext): Promise await serverProcess.start(); } + writeExtensionPermissionsForLiveshare('Eclipse-GLSP'); + const liveshareGlspClientProvider = new LiveshareGlspClientProvider(); // Wrap server with quickstart component @@ -92,6 +99,9 @@ export async function activate(context: vscode.ExtensionContext): Promise }), vscode.commands.registerCommand('workflow.showDocumentation', () => { glspVscodeConnector.sendActionToActiveClient(NavigateAction.create('documentation')); + }), + vscode.commands.registerCommand('workflow.liveshare.init', () => { + writeExtensionPermissionsForLiveshare('Eclipse-GLSP', true); }) ); } From 1960f929e152d804effbd7a00e8e59f69751f3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Fri, 2 Feb 2024 11:55:49 +0100 Subject: [PATCH 17/18] fix ordering of functions in liveshare-glsp-client-provider.ts --- .../liveshare-glsp-client-provider.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts index 9a78b5f..d23d011 100644 --- a/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -106,6 +106,20 @@ export class LiveshareGlspClientProvider return this.role === Role.Guest; } + getSubclientIdFromSession(): string { + return this.subclientId || SUBCLIENT_HOST_ID; + } + + getSubclientInfoFromSession(): SubclientInfo { + return ( + this.subclientInfo || { + subclientId: '', + name: '', + color: '' + } + ); + } + initializeClientSessionForGuest(params: InitializeClientSessionParameters): Promise { return this.guestService.request(INITIALIZE_CLIENT_SESSION, [params]); } @@ -126,20 +140,6 @@ export class LiveshareGlspClientProvider this.hostService.notify(ON_MULTIPLE_ACTION_MESSAGES, { messages }); // sending arrays is not allowed } - getSubclientIdFromSession(): string { - return this.subclientId || SUBCLIENT_HOST_ID; - } - - getSubclientInfoFromSession(): SubclientInfo { - return ( - this.subclientInfo || { - subclientId: '', - name: '', - color: '' - } - ); - } - onGuestsChangeForHost(handler: GuestsChangeHandler): void { this.guestsChangeHandler.push(handler); } From 70342655b29b5183c957be3985e6baa3b9d00a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Hegedu=CC=88s?= Date: Wed, 28 Feb 2024 10:33:58 +0100 Subject: [PATCH 18/18] Fix feature to work for latest master branch. --- .../extension/src/workflow-extension.ts | 3 + .../collaboration/collaboration-commands.ts | 2 +- .../collaboration-glsp-client.ts | 26 ++++-- .../src/common/glsp-vscode-connector.ts | 69 +++++++++++---- .../vscode-integration/src/common/index.ts | 2 - .../base-glsp-vscode-server.ts | 11 ++- .../quickstart-components/webview-endpoint.ts | 17 +++- yarn.lock | 85 +++++++++++++++++-- 8 files changed, 178 insertions(+), 37 deletions(-) diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 07ae9d3..8d75d98 100644 --- a/example/workflow/extension/src/workflow-extension.ts +++ b/example/workflow/extension/src/workflow-extension.ts @@ -18,6 +18,8 @@ import 'reflect-metadata'; import { WorkflowDiagramModule, WorkflowLayoutConfigurator, WorkflowServerModule } from '@eclipse-glsp-examples/workflow-server/node'; import { configureELKLayoutModule } from '@eclipse-glsp/layout-elk'; import { GModelStorage, LogLevel, createAppModule } from '@eclipse-glsp/server/node'; +import { configureCollaborationCommands } from '@eclipse-glsp/vscode-integration/lib/collaboration'; +import { LiveshareGlspClientProvider, writeExtensionPermissionsForLiveshare } from '@eclipse-glsp/vscode-integration/lib/liveshare'; import { GlspSocketServerLauncher, GlspVscodeConnector, @@ -69,6 +71,7 @@ export async function activate(context: vscode.ExtensionContext): Promise clientId: 'glsp.workflow', clientName: 'workflow', serverModules: createServerModules() + // collaboration: liveshareGlspClientProvider TODO implement collaboration for Node-Server }) : new SocketGlspVscodeServer({ clientId: 'glsp.workflow', diff --git a/packages/vscode-integration/src/collaboration/collaboration-commands.ts b/packages/vscode-integration/src/collaboration/collaboration-commands.ts index 1bf02e9..2d3d223 100644 --- a/packages/vscode-integration/src/collaboration/collaboration-commands.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-commands.ts @@ -4,7 +4,7 @@ import { ExtensionContext } from 'vscode'; import { MouseMoveAction, ViewportBoundsChangeAction, SelectionChangeAction, ToggleCollaborationFeatureAction, CollaborationActionKinds } from '@eclipse-glsp/protocol'; -import { GlspVscodeConnector } from '../glsp-vscode-connector'; +import { GlspVscodeConnector } from '../common/glsp-vscode-connector'; import { collaborationFeatureStore } from './collaboration-feature-store'; diff --git a/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts index cc28e57..e557afb 100644 --- a/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts @@ -20,8 +20,10 @@ import { Args, ClientState, CollaborationAction, + Disposable, DisposeClientSessionParameters, DisposeSubclientAction, + Event, GLSPClient, hasObjectProp, InitializeClientSessionParameters, @@ -41,13 +43,13 @@ import { import { getFullDocumentUri } from './collaboration-util'; interface CollaborativeGlspClientDistributedConfig { - commonProvider: CommonCollaborationGlspClientProvider, - hostProvider: HostCollaborationGlspClientProvider, - guestProvider: GuestCollaborationGlspClientProvider + commonProvider: CommonCollaborationGlspClientProvider; + hostProvider: HostCollaborationGlspClientProvider; + guestProvider: GuestCollaborationGlspClientProvider; } function isDistributedConfig(config: any): config is CollaborativeGlspClientDistributedConfig { - return hasObjectProp(config, 'commonProvider') && hasObjectProp(config, 'hostProvider') && hasObjectProp(config, 'guestProvider') + return hasObjectProp(config, 'commonProvider') && hasObjectProp(config, 'hostProvider') && hasObjectProp(config, 'guestProvider'); } export type CollaborativeGlspClientConfig = CollaborationGlspClientProvider | CollaborativeGlspClientDistributedConfig; @@ -85,6 +87,14 @@ export class CollaborationGlspClient implements GLSPClient { } } + get initializeResult(): InitializeResult | undefined { + return this.glspClient.initializeResult; + } + + get onServerInitialized(): Event { + return this.glspClient.onServerInitialized; + } + shutdownServer(): void { this.glspClient.shutdownServer(); } @@ -147,8 +157,9 @@ export class CollaborationGlspClient implements GLSPClient { } } - onActionMessage(handler: ActionMessageHandler): void { + onActionMessage(handler: ActionMessageHandler): Disposable { this.actionMessageHandlers.push(handler); + return Disposable.empty(); } sendActionMessage(message: ActionMessage): void { @@ -182,6 +193,9 @@ export class CollaborationGlspClient implements GLSPClient { } async start(): Promise { + if (this.currentState === ClientState.Running) { + return; + } await this.commonProvider.initialize({ collaborationGlspClient: this }); await this.glspClient.start(); @@ -271,7 +285,7 @@ export class CollaborationGlspClient implements GLSPClient { const subclientId = params.args?.subclientId as string; const disposeSubclientMessage: ActionMessage = { clientId: '', - action: DisposeSubclientAction.create({ initialSubclientId: subclientId}) + action: DisposeSubclientAction.create({ initialSubclientId: subclientId }) }; this.handleMultipleMessages(subclientMap, disposeSubclientMessage, actualSubclientId => actualSubclientId !== subclientId); } diff --git a/packages/vscode-integration/src/common/glsp-vscode-connector.ts b/packages/vscode-integration/src/common/glsp-vscode-connector.ts index 6b7ed78..486fb3a 100644 --- a/packages/vscode-integration/src/common/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/common/glsp-vscode-connector.ts @@ -16,10 +16,10 @@ import { Action, ActionMessage, - Deferred, - EndProgressAction, CollaborationAction, CollaborationActionKinds, + Deferred, + EndProgressAction, ExportSvgAction, MessageAction, NavigateToExternalTargetAction, @@ -36,8 +36,8 @@ import { import * as vscode from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; import { Messenger } from 'vscode-messenger'; -import { collaborationFeatureStore } from './collaboration/collaboration-feature-store'; -import { getRelativeDocumentUri } from './collaboration/collaboration-util'; +import { collaborationFeatureStore } from '../collaboration/collaboration-feature-store'; +import { getRelativeDocumentUri } from '../collaboration/collaboration-util'; import { GlspVscodeClient, GlspVscodeConnectorOptions } from './types'; // eslint-disable-next-line no-shadow @@ -81,6 +81,8 @@ export class GlspVscodeConnector>(); /** Maps Documents to corresponding clientId. */ protected readonly documentMap = new Map(); + /** Maps Documents to corresponding relativeDocumentUri. */ + protected readonly relativeDocumentUriMap = new Map(); /** Maps clientId to selected elementIDs for that client. */ protected readonly clientSelectionMap = new Map(); @@ -152,7 +154,9 @@ export class GlspVscodeConnector): Promise { + const relativeDocumentUri = getRelativeDocumentUri(client.document.uri.path); const toDispose: Disposable[] = [ Disposable.create(() => { 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.relativeDocumentUriMap.delete(client.clientId); this.clientSelectionMap.delete(client.clientId); }) ]; @@ -193,17 +199,16 @@ export class GlspVscodeConnector { if (this.options.logging) { if (ActionMessage.is(message)) { - // don't log CollaborationActions - if (!CollaborationAction.is(message.action)) { - console.log(`Client (${message.clientId}): ${message.action.kind}`, message.action); - } + // don't log CollaborationActions + if (!CollaborationAction.is(message.action)) { + console.log(`Client (${message.clientId}): ${message.action.kind}`, message.action); + } } else { console.log('Client (no action message):', message); } @@ -218,12 +223,12 @@ export class GlspVscodeConnector glspClient.disposeClientSession({ clientSessionId: client.clientId }))); + toDispose.push(client.webviewEndpoint.initialize(glspClient, relativeDocumentUri)); + toDispose.unshift( + Disposable.create(() => + glspClient.disposeClientSession({ + clientSessionId: client.clientId, + args: { + relativeDocumentUri + } + }) + ) + ); } /** @@ -273,6 +287,18 @@ export class GlspVscodeConnector implements GlspVscodeServer, vscode.Disposable { readonly onSendToServerEmitter = new vscode.EventEmitter(); - protected readonly onServerSendEmitter = new vscode.EventEmitter(); + readonly onServerSendEmitter = new vscode.EventEmitter(); get onServerMessage(): vscode.Event { return this.onServerSendEmitter.event; } @@ -68,6 +71,12 @@ export abstract class BaseGlspVscodeServer im async start(): Promise { try { this._glspClient = await this.createGLSPClient(); + if (this.options.collaboration != null) { + this._glspClient = new CollaborationGlspClient( + this._glspClient, + this.options.collaboration + ) as unknown as C; // FIXME probably a better solution + } await this._glspClient.start(); const parameters = await this.createInitializeParameters(); this._initializeResult = await this._glspClient.initializeServer(parameters); diff --git a/packages/vscode-integration/src/common/quickstart-components/webview-endpoint.ts b/packages/vscode-integration/src/common/quickstart-components/webview-endpoint.ts index 4ea7523..c9b01fd 100644 --- a/packages/vscode-integration/src/common/quickstart-components/webview-endpoint.ts +++ b/packages/vscode-integration/src/common/quickstart-components/webview-endpoint.ts @@ -112,9 +112,10 @@ export class WebviewEndpoint implements Disposable { * The GLSP client is called remotely from the webview context via the `vscode-messenger` RPC * protocol. * @param glspClient The client that should be connected + * @param relativeDocumentUri The uri which is used to identify the document in a collaborative session * @returns A {@link Disposable} to dispose the remote connection and all attached listeners */ - initialize(glspClient: GLSPClient): Disposable { + initialize(glspClient: GLSPClient, relativeDocumentUri: string): Disposable { const toDispose = new DisposableCollection(); toDispose.push( this.messenger.onNotification( @@ -146,13 +147,23 @@ export class WebviewEndpoint implements Disposable { if (!this._clientActions) { this._clientActions = params.clientActionKinds; } - glspClient.initializeClientSession(params); + glspClient.initializeClientSession({ + ...params, + args: { + relativeDocumentUri + } + }); }, { sender: this.messageParticipant } ), - this.messenger.onRequest(DisposeClientSessionRequest, params => glspClient.disposeClientSession(params), { + this.messenger.onRequest(DisposeClientSessionRequest, params => glspClient.disposeClientSession({ + ...params, + args: { + relativeDocumentUri + } + }), { sender: this.messageParticipant }), this.messenger.onRequest(ShutdownServerNotification, () => glspClient.shutdownServer(), { diff --git a/yarn.lock b/yarn.lock index f3f163d..6f58cc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -620,6 +620,17 @@ yargs "16.2.0" yargs-parser "20.2.4" +"@microsoft/servicehub-framework@^2.6.74": + version "2.6.74" + resolved "https://registry.yarnpkg.com/@microsoft/servicehub-framework/-/servicehub-framework-2.6.74.tgz#7c45717adea4f6fe2bd0c8bbe572f42c64a13a83" + integrity sha512-QJ//zzvxffupIkzupnVbMYY5YDOP+g5FlG6x0Pl7svRyq8pAouiibckJJcZlMtsMypKWwAnVBKb9/sonEOsUxw== + dependencies: + await-semaphore "^0.1.3" + msgpack-lite "^0.1.26" + nerdbank-streams "2.5.60" + strict-event-emitter-types "^2.0.0" + vscode-jsonrpc "^4.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1749,6 +1760,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +await-semaphore@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" + integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q== + axios@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" @@ -1981,11 +1997,21 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +cancellationtoken@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cancellationtoken/-/cancellationtoken-2.2.0.tgz#a3d93cb8675f29dd1574a358b72adbde3da89aeb" + integrity sha512-uF4sHE5uh2VdEZtIRJKGoXAD9jm7bFY0tDRCzH4iLp262TOJ2lrtNHjMG2zc8H+GICOpELIpM7CGW5JeWnb3Hg== + caniuse-lite@^1.0.30001541: version "1.0.30001561" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz#752f21f56f96f1b1a52e97aae98c57c562d5d9da" integrity sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw== +caught@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/caught/-/caught-0.1.3.tgz#f63db0d65f1bacea7cb4852cd8f1d72166a2c8bf" + integrity sha512-DTWI84qfoqHEV5jHRpsKNnEisVCeuBDscXXaXyRLXC+4RD6rFftUNuTElcQ7LeO7w622pfzWkA1f6xu5qEAidw== + chai@^4.3.10: version "4.3.10" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" @@ -3097,6 +3123,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-lite@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d" + integrity sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw== + eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -3889,7 +3920,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.1.8: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -4006,6 +4037,11 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^6.0.1" +int64-buffer@^0.1.9: + version "0.1.10" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423" + integrity sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA== + internal-slot@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" @@ -4299,16 +4335,16 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5177,6 +5213,16 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpack-lite@^0.1.26: + version "0.1.26" + resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89" + integrity sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw== + dependencies: + event-lite "^0.1.1" + ieee754 "^1.1.8" + int64-buffer "^0.1.9" + isarray "^1.0.0" + multimatch@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" @@ -5228,6 +5274,16 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nerdbank-streams@2.5.60: + version "2.5.60" + resolved "https://registry.yarnpkg.com/nerdbank-streams/-/nerdbank-streams-2.5.60.tgz#455edb9a71070a0964a1b39eee5afb30ef826cd6" + integrity sha512-saQaMyTtVDAEc+S+BPXKM6K1AF3FyrorFSDzaCkdmtDe2kZzu1aYPQZNLmnxJhxbTcghYrEmYFFoaDxBDVadCw== + dependencies: + await-semaphore "^0.1.3" + cancellationtoken "^2.0.1" + caught "^0.1.3" + msgpack-lite "^0.1.26" + nise@^5.1.4: version "5.1.5" resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.5.tgz#f2aef9536280b6c18940e32ba1fbdc770b8964ee" @@ -6854,6 +6910,11 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== +strict-event-emitter-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" + integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== + "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -7530,6 +7591,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +vscode-jsonrpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz#a7bf74ef3254d0a0c272fab15c82128e378b3be9" + integrity sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg== + vscode-jsonrpc@^8.0.2: version "8.2.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" @@ -7554,6 +7620,13 @@ vscode-messenger@^0.4.5: dependencies: vscode-messenger-common "^0.4.5" +vsls@^1.0.4753: + version "1.0.4753" + resolved "https://registry.yarnpkg.com/vsls/-/vsls-1.0.4753.tgz#1b0957fc987fddd2b4d8c03925d086fb701d11fa" + integrity sha512-hmrsMbhjuLoU8GgtVfqhbV4ZkGvDpLV2AFmzx+cCOGNra2qk0Q36dYkfwENqy/vJVQ/2/lhxcn+69FYnKQRhgg== + dependencies: + "@microsoft/servicehub-framework" "^2.6.74" + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"