diff --git a/.vscode/launch.json b/.vscode/launch.json index 028c7ff..b823f47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,7 +30,7 @@ } }, { - "name": "Workflow GLSP Example Extension (External GLSP Server)", + "name": "Workflow GLSP Example Extension (External GLSP Server) 5007", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", @@ -53,6 +53,25 @@ "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" + } + }, { "name": "Workflow GLSP Example Extension (Integrated Node GLSP Server)", "type": "extensionHost", diff --git a/example/workflow/extension/package.json b/example/workflow/extension/package.json index b0ed6a5..f6d2dae 100644 --- a/example/workflow/extension/package.json +++ b/example/workflow/extension/package.json @@ -40,6 +40,9 @@ "watch": "tsc -w" }, "contributes": { + "liveshare.spaces": [ + "workflow-vscode-example" + ], "commands": [ { "command": "workflow.fit", @@ -82,6 +85,11 @@ "title": "Export as SVG", "category": "Workflow Diagram", "enablement": "activeCustomEditorId == 'workflow.glspDiagram'" + }, + { + "command": "workflow.liveshare.init", + "title": "Initialize Liveshare for Workflow", + "category": "Workflow Diagram" } ], "customEditors": [ @@ -191,8 +199,12 @@ ] }, "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onCommand:bigUML.liveshare.init" ], + "dependencies": { + "vsls": "^1.0.4753" + }, "devDependencies": { "@eclipse-glsp-examples/workflow-server": "next", "@eclipse-glsp-examples/workflow-server-bundled": "next", @@ -205,6 +217,9 @@ "webpack-merge": "^5.9.0", "workflow-glsp-webview": "2.2.0-next" }, + "extensionDependencies": [ + "ms-vsliveshare.vsliveshare" + ], "engines": { "vscode": "^1.54.0" } diff --git a/example/workflow/extension/src/workflow-extension.ts b/example/workflow/extension/src/workflow-extension.ts index 253044d..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, @@ -58,12 +60,18 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(serverProcess); await serverProcess.start(); } + + writeExtensionPermissionsForLiveshare('Eclipse-GLSP'); + + const liveshareGlspClientProvider = new LiveshareGlspClientProvider(); + // Wrap server with quickstart component const workflowServer = useIntegratedServer ? new NodeGlspVscodeServer({ clientId: 'glsp.workflow', clientName: 'workflow', serverModules: createServerModules() + // collaboration: liveshareGlspClientProvider TODO implement collaboration for Node-Server }) : new SocketGlspVscodeServer({ clientId: 'glsp.workflow', @@ -71,7 +79,8 @@ export async function activate(context: vscode.ExtensionContext): Promise connectionOptions: { port: serverProcess?.getPort() || JSON.parse(process.env.GLSP_SERVER_PORT || DEFAULT_SERVER_PORT), path: process.env.GLSP_WEBSOCKET_PATH - } + }, + collaboration: liveshareGlspClientProvider }); // Initialize GLSP-VSCode connector with server wrapper const glspVscodeConnector = new GlspVscodeConnector({ @@ -89,10 +98,13 @@ export async function activate(context: vscode.ExtensionContext): Promise ); context.subscriptions.push(workflowServer, glspVscodeConnector, customEditorProvider); + workflowServer.start(); configureDefaultCommands({ extensionContext: context, connector: glspVscodeConnector, diagramPrefix: 'workflow' }); + configureCollaborationCommands({ extensionContext: context, connector: glspVscodeConnector }); + context.subscriptions.push( vscode.commands.registerCommand('workflow.goToNextNode', () => { glspVscodeConnector.dispatchAction(NavigateAction.create('next')); @@ -102,6 +114,9 @@ export async function activate(context: vscode.ExtensionContext): Promise }), vscode.commands.registerCommand('workflow.showDocumentation', () => { glspVscodeConnector.dispatchAction(NavigateAction.create('documentation')); + }), + vscode.commands.registerCommand('workflow.liveshare.init', () => { + writeExtensionPermissionsForLiveshare('Eclipse-GLSP', true); }) ); } 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 a5c2fb6..a47298f 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "publisher": "Eclipse-GLSP", "private": true, "workspaces": [ "packages/*", diff --git a/packages/vscode-integration/package.json b/packages/vscode-integration/package.json index 2975c58..5daa15a 100644 --- a/packages/vscode-integration/package.json +++ b/packages/vscode-integration/package.json @@ -50,7 +50,8 @@ "@eclipse-glsp/protocol": "next", "vscode-jsonrpc": "^8.0.2", "vscode-messenger": "^0.4.5", - "ws": "^8.13.0" + "ws": "^8.13.0", + "vsls": "^1.0.4753" }, "devDependencies": { "@types/vscode": "^1.54.0", 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..2d3d223 --- /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 '../common/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/collaboration/collaboration-glsp-client-provider.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts new file mode 100644 index 0000000..f574060 --- /dev/null +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client-provider.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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, 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 CollaborationGlspClientProvider = + CommonCollaborationGlspClientProvider + & HostCollaborationGlspClientProvider + & GuestCollaborationGlspClientProvider; + +export interface CommonCollaborationGlspClientProvider { + initialize(config: CollaborationGlspClientProviderInitializeConfig): Promise; + isInCollaborationMode(): boolean; + isHost(): boolean; + isGuest(): boolean; + 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/collaboration/collaboration-glsp-client.ts b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts new file mode 100644 index 0000000..e557afb --- /dev/null +++ b/packages/vscode-integration/src/collaboration/collaboration-glsp-client.ts @@ -0,0 +1,351 @@ +/******************************************************************************** + * 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 { + Action, + ActionMessage, + ActionMessageHandler, + Args, + ClientState, + CollaborationAction, + Disposable, + DisposeClientSessionParameters, + DisposeSubclientAction, + Event, + GLSPClient, + hasObjectProp, + InitializeClientSessionParameters, + InitializeParameters, + InitializeResult, + RequestModelAction, + SetModelAction, + UpdateModelAction +} from '@eclipse-glsp/protocol'; +import { + CollaborationGlspClientProvider, + 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 = CollaborationGlspClientProvider | CollaborativeGlspClientDistributedConfig; + +export class CollaborationGlspClient implements GLSPClient { + protected readonly BROADCAST_ACTION_TYPES = [SetModelAction.KIND, UpdateModelAction.KIND]; + + readonly id: string; + + 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 actionMessageHandlers: ActionMessageHandler[] = []; + + constructor( + protected glspClient: GLSPClient, + config: CollaborativeGlspClientConfig + ) { + this.id = glspClient.id; + + 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; + } + } + + get initializeResult(): InitializeResult | undefined { + return this.glspClient.initializeResult; + } + + get onServerInitialized(): Event { + return this.glspClient.onServerInitialized; + } + + shutdownServer(): void { + this.glspClient.shutdownServer(); + } + + initializeServer(params: InitializeParameters): Promise { + return this.glspClient.initializeServer(params); + } + + 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; + 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.commonProvider.isGuest()) { + return this.guestProvider.initializeClientSessionForGuest(params); + } + } + + async disposeClientSession(params: DisposeClientSessionParameters): Promise { + if (!params.args?.subclientId) { + params.args = { + ...params.args, + subclientId: this.commonProvider.getSubclientIdFromSession() + }; + } + + if (this.commonProvider.isInCollaborationMode() && this.commonProvider.isHost()) { + this.handleDisposeSubclientMessage(params); + } + + 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(); + 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.commonProvider.isGuest()) { + return this.guestProvider.disposeClientSessionForGuest(params); + } + } + + onActionMessage(handler: ActionMessageHandler): Disposable { + this.actionMessageHandlers.push(handler); + return Disposable.empty(); + } + + sendActionMessage(message: ActionMessage): void { + if (!message.action.subclientId) { + message.action.subclientId = this.commonProvider.getSubclientIdFromSession(); + } + + if (CollaborationAction.is(message.action)) { + this.handleCollaborationAction(message as ActionMessage); + } 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 + if (message.action.kind === RequestModelAction.KIND) { + const requestModelAction = message.action as RequestModelAction; + requestModelAction.options = { + ...requestModelAction.options, + disableReload: true + }; + if (message.action.subclientId !== SUBCLIENT_HOST_ID) { + requestModelAction.options = { + ...requestModelAction.options, + sourceUri: getFullDocumentUri(relativeDocumentUri) + }; + } + } + this.glspClient.sendActionMessage(message); + } else if (this.commonProvider.isGuest()) { + this.guestProvider.sendActionMessageForGuest(message); + } + } + + async start(): Promise { + if (this.currentState === ClientState.Running) { + return; + } + await this.commonProvider.initialize({ collaborationGlspClient: this }); + + await this.glspClient.start(); + + this.glspClient.onActionMessage((message: ActionMessage) => { + const relativeDocumentUri = this.getRelativeDocumentUriByServerClientId(message.clientId); + const subclientMap = this.registeredSubclientMap.get(relativeDocumentUri); + if (!subclientMap) { + return; + } + if (!this.commonProvider.isInCollaborationMode()) { + // send only to host + const localClientId = subclientMap.get(SUBCLIENT_HOST_ID) || ''; + this.handleMessage(SUBCLIENT_HOST_ID, message, localClientId); + } 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 + this.handleMultipleMessages(subclientMap, message); + } else { + // send to adressed subclient + 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 { + return this.glspClient.stop(); + } + + get currentState(): ClientState { + 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); + if (!subclientMap) { + return; + } + const subclientId = message.action.subclientId; + this.handleMultipleMessages(subclientMap, message, actualSubclientId => actualSubclientId !== subclientId); + } else if (this.commonProvider.isGuest()) { + 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.handleActionOnAllLocalHandlers(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.handleActionOnAllLocalHandlers(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) { + return key; + } + } + return ''; + } + + private getRelativeDocumentUriByArgs(args: Args | undefined): string { + return (args?.relativeDocumentUri || '') as string; + } +} 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 new file mode 100644 index 0000000..c1efefd --- /dev/null +++ b/packages/vscode-integration/src/collaboration/index.ts @@ -0,0 +1,21 @@ +/******************************************************************************** + * 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'; +export * from './collaboration-util'; diff --git a/packages/vscode-integration/src/common/glsp-vscode-connector.ts b/packages/vscode-integration/src/common/glsp-vscode-connector.ts index e60e7f3..486fb3a 100644 --- a/packages/vscode-integration/src/common/glsp-vscode-connector.ts +++ b/packages/vscode-integration/src/common/glsp-vscode-connector.ts @@ -16,6 +16,8 @@ import { Action, ActionMessage, + CollaborationAction, + CollaborationActionKinds, Deferred, EndProgressAction, ExportSvgAction, @@ -34,6 +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 { GlspVscodeClient, GlspVscodeConnectorOptions } from './types'; // eslint-disable-next-line no-shadow @@ -77,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(); @@ -129,7 +135,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); } @@ -144,6 +153,11 @@ 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); }) ]; @@ -173,16 +189,26 @@ export class GlspVscodeConnector { toDispose.forEach(disposable => disposable.dispose()); panelOnDisposeListener.dispose(); + await glspClient.disposeClientSession({ + clientSessionId: client.clientId, + args: { + relativeDocumentUri + } + }); }); this.clientMap.set(client.clientId, client); this.documentMap.set(client.document, client.clientId); + this.relativeDocumentUriMap.set(client.clientId, relativeDocumentUri); toDispose.push( client.webviewEndpoint.onActionMessage(message => { 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); } @@ -197,6 +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 + } + }) + ) + ); } /** @@ -246,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/packages/vscode-integration/src/common/types.ts b/packages/vscode-integration/src/common/types.ts index c9f924d..adc71bf 100644 --- a/packages/vscode-integration/src/common/types.ts +++ b/packages/vscode-integration/src/common/types.ts @@ -60,7 +60,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 diff --git a/packages/vscode-integration/src/liveshare/index.ts b/packages/vscode-integration/src/liveshare/index.ts new file mode 100644 index 0000000..8ef0b16 --- /dev/null +++ b/packages/vscode-integration/src/liveshare/index.ts @@ -0,0 +1,18 @@ +/******************************************************************************** + * 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 './liveshare-glsp-client-provider'; +export * from './toggle-feature-tree-data-provider'; +export * from './init-liveshare-config'; diff --git a/packages/vscode-integration/src/liveshare/init-liveshare-config.ts b/packages/vscode-integration/src/liveshare/init-liveshare-config.ts new file mode 100644 index 0000000..9d9493e --- /dev/null +++ b/packages/vscode-integration/src/liveshare/init-liveshare-config.ts @@ -0,0 +1,72 @@ +import * as os from 'os'; +import * as fs from 'fs'; +import * as vscode from 'vscode'; + +const fileNotFoundCode = 'ENOENT'; +const liveshareConfigFileName = '.vs-liveshare-settings.json'; +const homedir = os.homedir() + '/'; +const liveshareConfigPath = homedir + liveshareConfigFileName; + +function showError(publisher: string, err: Error, forced: boolean = false) { + const publisherKey = createPublisherKey(publisher); + + console.error(err); + + console.log('Writing liveshare config was not possible. Please do on your own. Create a file with filename "' + liveshareConfigFileName + '" in your home directory (' + homedir + '). And add following content:'); + console.log({ + extensionPermissions: { + [publisherKey]: '*' + } + }); + + if (forced) { + vscode.window.showErrorMessage('Liveshare initializing failed!'); + } +} + +function createPublisherKey(publisher: string) { + return publisher + '.*'; +} + +export function writeExtensionPermissionsForLiveshare(publisher: string, forced: boolean = false) { + const publisherKey = createPublisherKey(publisher); + try { + let data: string | null = null; + try { + data = fs.readFileSync(liveshareConfigPath, 'utf-8'); + } catch (readErr: any) { + if (readErr.code !== fileNotFoundCode) { + throw readErr; + } + } + + const jsonData = data == null ? {} : JSON.parse(data); + + if (!jsonData.extensionPermissions) { + jsonData.extensionPermissions = {}; + } + + if (jsonData.extensionPermissions[publisherKey] === '*') { + if (forced) { + vscode.window.showInformationMessage('Liveshare already initialized. Everything fine!'); + } + return; + } + + jsonData.extensionPermissions[publisherKey] = '*'; + + fs.writeFileSync(liveshareConfigPath, JSON.stringify(jsonData)); + + console.log('File: ' + liveshareConfigPath); + console.log('Content:'); + console.log(jsonData); + console.log('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, forced); + } +} + + 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..d23d011 --- /dev/null +++ b/packages/vscode-integration/src/liveshare/liveshare-glsp-client-provider.ts @@ -0,0 +1,220 @@ +/******************************************************************************** + * Copyright (c) 2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { + Action, + ActionMessage, + DisposeClientSessionParameters, + InitializeClientSessionParameters, + SubclientInfo +} 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 { 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'; +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', '#B4009E', '#107C10', '#FFB900', '#B4A0FF']; +const FALLBACK_COLOR = '#ABABAB'; + +export class LiveshareGlspClientProvider + implements CommonCollaborationGlspClientProvider, HostCollaborationGlspClientProvider, GuestCollaborationGlspClientProvider +{ + protected session: Session | null; + protected vsls: LiveShare | null; + protected role: Role = Role.None; + protected service: SharedService | SharedServiceProxy | null; + + protected collaborationGlspClient: CollaborationGlspClient; + + protected subclientId: string | null; + protected subclientInfo: SubclientInfo | null; + + protected guestsChangeHandler: GuestsChangeHandler[] = []; + + protected get guestService(): SharedServiceProxy { + return this.service as SharedServiceProxy; + } + + protected get hostService(): SharedService { + return this.service as SharedService; + } + + async initialize(config: CollaborationGlspClientProviderInitializeConfig): Promise { + this.collaborationGlspClient = config.collaborationGlspClient; + + this.vsls = await getApi(); + + if (!this.vsls) { + 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 => { + await this.initializeSession(e.session); + }); + + this.vsls.onDidChangePeers(e => { + if (this.isInCollaborationMode() && this.isHost()) { + this.guestsChangeHandler.forEach(handler => handler(this.vsls!.peers.map(p => this.createSubclientIdFromPeer(p)))); + } + }); + } + + isInCollaborationMode(): boolean { + return !!this.service && this.service.isServiceAvailable; + } + + isHost(): boolean { + return this.role === Role.Host; + } + + isGuest(): boolean { + 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]); + } + + disposeClientSessionForGuest(params: DisposeClientSessionParameters): Promise { + return this.guestService.request(DISPOSE_CLIENT_SESSION, [params]); + } + + sendActionMessageForGuest(message: ActionMessage): void { + this.guestService.request(SEND_ACTION_MESSAGE, [message]); + } + + 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 + } + + onGuestsChangeForHost(handler: GuestsChangeHandler): void { + 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 + if (this.getSubclientIdFromSession() === subclientId) { + this.collaborationGlspClient.handleActionOnAllLocalHandlers(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 { + const user = session.user; + if (!user) { + return ''; + } + return user.userName || user.displayName || user.emailAddress || ''; + } +} 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/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"