Skip to content

Commit

Permalink
Preliminary live share support (just a simple cell running mirrored o…
Browse files Browse the repository at this point in the history
…n guest) (#4325)

* First commit

* Webpanellistener abstraction and new general purpose live share
post office

* Logic in place

* Fix callbacks for non live share mode

* Get codelens to show up in liveshare guest

* Cells visible, but crashing in execution

* Remove the IJupyterExecutionFactory idea

* Fix local non shared case to work again

* Fix text documents being found on unopened docs

* New idea for jupyter server.

* Local mode working again

* Closer to having code lens work on guest

* Communication getting to point of waiting for results

* More communication in place

* Add getSysInfo to jupyterServer and make executeSilently return data

* Trying to get responses to parse

* Fix splicing

* Get single cell to run across live share

* Fix hygiene errors

* Abstracting liveshare api for tests

* Fix History Simple text to pass.

* Get notebook tests to pass

* Add the feature flag and fixup new loc strings

* Add news entry

* Fix code lens/watcher tests. Fix capitilization issue

* Rename JupyterExecutionBase and JupyterServerBase back

* Some review feedback

* More code review feedback. Fix send_info.

* Move regexes to the common location

* Fix compile-webviews
  • Loading branch information
rchiodo committed Feb 8, 2019
1 parent ede5b3d commit 7d702c3
Show file tree
Hide file tree
Showing 46 changed files with 2,060 additions and 325 deletions.
1 change: 1 addition & 0 deletions news/1 Enhancements/3581.md
@@ -0,0 +1 @@
Support live share in Python Interactive window
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Expand Up @@ -1157,6 +1157,12 @@
"description": "Regular expression used to identify markdown cells. All comments after this expression are considered part of the markdown. \nDefaults to '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)' if left blank",
"scope": "resource"
},
"python.dataScience.allowLiveShare": {
"type": "boolean",
"default": false,
"description": "Allow the Python Interactive window to be shared during a Live Share session (experimental)",
"scope": "resource"
},
"python.disableInstallationCheck": {
"type": "boolean",
"default": false,
Expand Down Expand Up @@ -2044,6 +2050,7 @@
"vscode-languageclient": "^4.4.0",
"vscode-languageserver": "^4.4.0",
"vscode-languageserver-protocol": "^3.10.3",
"vsls": "^0.3.967",
"winreg": "^1.2.4",
"xml2js": "^0.4.19"
},
Expand Down
7 changes: 7 additions & 0 deletions package.nls.json
Expand Up @@ -137,6 +137,13 @@
"DataScience.notebookVersionFormat": "Jupyter Notebook Version: {0}",
"DataScience.jupyterKernelNotSupportedOnActive": "Jupyter kernel cannot be started from '{0}'. Using closest match {1} instead.",
"DataScience.jupyterKernelSpecNotFound": "Cannot create a Jupyter kernel spec and none are available for use",
"DataScience.liveShareConnectFailure" : "Cannot connect to host Jupyter session. URI not found.",
"DataScience.liveShareCannotSpawnNotebooks" : "Spawning Jupyter notebooks is not supported over a live share connection",
"DataScience.liveShareCannotImportNotebooks" : "Importing notebooks is not currently supported over a live share connection",
"DataScience.liveShareHostFormat" : "{0} Jupyter Server",
"DataScience.liveShareSyncFailure" : "Synchronization failure during live share startup.",
"DataScience.liveShareServiceFailure" : "Failure starting '{0}' service during live share connection.",
"DataScience.documentMismatch": "Cannot run cells, duplicate documents for {0} found.",
"diagnostics.warnSourceMaps": "Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.",
"diagnostics.disableSourceMaps": "Disable Source Map Support",
"diagnostics.warnBeforeEnablingSourceMaps": "Enabling source map support in the Python Extension will adversely impact performance of the extension.",
Expand Down
11 changes: 10 additions & 1 deletion src/client/common/application/types.ts
Expand Up @@ -45,6 +45,9 @@ import {
WorkspaceFolderPickOptions,
WorkspaceFoldersChangeEvent
} from 'vscode';
import * as vsls from 'vsls/vscode';

import { IAsyncDisposable } from '../types';

// tslint:disable:no-any unified-signatures

Expand Down Expand Up @@ -808,7 +811,7 @@ export interface IApplicationEnvironment {
}

export const IWebPanelMessageListener = Symbol('IWebPanelMessageListener');
export interface IWebPanelMessageListener extends Disposable {
export interface IWebPanelMessageListener extends IAsyncDisposable {
/**
* Listens to web panel messages
* @param message: the message being sent
Expand Down Expand Up @@ -862,3 +865,9 @@ export interface IWebPanelProvider {
*/
create(listener: IWebPanelMessageListener, title: string, mainScriptPath: string, embeddedCss?: string, settings?: any): IWebPanel;
}

// Wraps the vsls liveshare API
export const ILiveShareApi = Symbol('ILiveShareApi');
export interface ILiveShareApi {
getApi(): Promise<vsls.LiveShare | null>;
}
46 changes: 46 additions & 0 deletions src/client/common/liveshare/liveshare.ts
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { inject, injectable } from 'inversify';
import * as vsls from 'vsls/vscode';

import { ILiveShareApi, IWorkspaceService } from '../application/types';
import { IConfigurationService, IDisposableRegistry } from '../types';

// tslint:disable:no-any unified-signatures

@injectable()
export class LiveShareApi implements ILiveShareApi {

private supported : boolean = false;
private apiPromise : Promise<vsls.LiveShare | null> | undefined;

constructor(
@inject(IDisposableRegistry) disposableRegistry : IDisposableRegistry,
@inject(IWorkspaceService) workspace : IWorkspaceService,
@inject(IConfigurationService) private configService : IConfigurationService
) {
const disposable = workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('python.dataScience', undefined)) {
// When config changes happen, recreate our commands.
this.onSettingsChanged();
}
});
disposableRegistry.push(disposable);
this.onSettingsChanged();
}

public getApi(): Promise<vsls.LiveShare | null> {
return this.apiPromise!;
}

private onSettingsChanged() {
const supported = this.configService.getSettings().datascience.allowLiveShare;
if (supported !== this.supported) {
this.supported = supported ? true : false;
this.apiPromise = supported ? vsls.getApi() : Promise.resolve(null);
} else if (!this.apiPromise) {
this.apiPromise = Promise.resolve(null);
}
}
}
3 changes: 3 additions & 0 deletions src/client/common/serviceRegistry.ts
Expand Up @@ -15,6 +15,7 @@ import {
ICommandManager,
IDebugService,
IDocumentManager,
ILiveShareApi,
ITerminalManager,
IWorkspaceService
} from './application/types';
Expand All @@ -24,6 +25,7 @@ import { ConfigurationService } from './configuration/service';
import { EditorUtils } from './editor';
import { FeatureDeprecationManager } from './featureDeprecationManager';
import { ProductInstaller } from './installer/productInstaller';
import { LiveShareApi } from './liveshare/liveshare';
import { Logger } from './logger';
import { BrowserService } from './net/browser';
import { HttpClient } from './net/httpClient';
Expand Down Expand Up @@ -93,6 +95,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<INugetService>(INugetService, NugetService);
serviceManager.addSingleton<ITerminalActivator>(ITerminalActivator, TerminalActivator);
serviceManager.addSingleton<ITerminalActivationHandler>(ITerminalActivationHandler, PowershellTerminalActivationFailedHandler);
serviceManager.addSingleton<ILiveShareApi>(ILiveShareApi, LiveShareApi);

serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
serviceManager.addSingleton<ITerminalActivationCommandProvider>(
Expand Down
1 change: 1 addition & 0 deletions src/client/common/types.ts
Expand Up @@ -297,6 +297,7 @@ export interface IDataScienceSettings {
sendSelectionToInteractiveWindow: boolean;
markdownRegularExpression: string;
codeRegularExpression: string;
allowLiveShare? : boolean;
}

export const IConfigurationService = Symbol('IConfigurationService');
Expand Down
7 changes: 7 additions & 0 deletions src/client/common/utils/localize.ts
Expand Up @@ -125,6 +125,13 @@ export namespace DataScience {
export const sysInfoURILabel = localize('DataScience.sysInfoURILabel', 'Jupyter Server URI: ');
export const executingCodeFailure = localize('DataScience.executingCodeFailure', 'Executing code failed : {0}');
export const inputWatermark = localize('DataScience.inputWatermark', 'Shift-enter to run');
export const liveShareConnectFailure = localize('DataScience.liveShareConnectFailure', 'Cannot connect to host jupyter session. URI not found.');
export const liveShareCannotSpawnNotebooks = localize('DataScience.liveShareCannotSpawnNotebooks', 'Spawning jupyter notebooks is not supported over a live share connection');
export const liveShareCannotImportNotebooks = localize('DataScience.liveShareCannotImportNotebooks', 'Importing notebooks is not currently supported over a live share connection');
export const liveShareHostFormat = localize('DataScience.liveShareHostFormat', '{0} Jupyter Server');
export const liveShareSyncFailure = localize('DataScience.liveShareSyncFailure', 'Synchronization failure during live share startup.');
export const liveShareServiceFailure = localize('DataScience.liveShareServiceFailure', 'Failure starting \'{0}\' service during live share connection.');
export const documentMismatch = localize('DataScience.documentMismatch', 'Cannot run cells, duplicate documents for {0} found.');
}

export namespace DebugConfigurationPrompts {
Expand Down
18 changes: 17 additions & 1 deletion src/client/common/utils/misc.ts
@@ -1,7 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';
import { IAsyncDisposable, IDisposable } from '../types';

// tslint:disable-next-line:no-empty
export function noop() { }

export function using<T extends IDisposable>(disposable: T, func: (obj: T) => void) {
try {
func(disposable);
} finally {
disposable.dispose();
}
}

export async function usingAsync<T extends IAsyncDisposable, R>(disposable: T, func: (obj: T) => Promise<R>) : Promise<R> {
try {
return await func(disposable);
} finally {
await disposable.dispose();
}
}
81 changes: 81 additions & 0 deletions src/client/datascience/commandBroker.ts
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { inject, injectable } from 'inversify';
import { Disposable, TextEditor, TextEditorEdit } from 'vscode';
import * as vsls from 'vsls/vscode';

import { ICommandManager, ILiveShareApi } from '../common/application/types';
import { LiveShare } from './constants';
import { PostOffice } from './liveshare/postOffice';
import { ICommandBroker } from './types';

// tslint:disable:no-any

// This class acts as a broker between the VSCode command manager and a potential live share session
// It works like so:
// -- If not connected to any live share session, then just register commands as normal
// -- If a host, register commands as normal (as they will be listened to), but when they are hit, post them to all guests
// -- If a guest, register commands as normal (as they will be ignored), but also register for notifications from the host.
@injectable()
export class CommandBroker implements ICommandBroker {

private postOffice : PostOffice;
constructor(
@inject(ILiveShareApi) liveShare: ILiveShareApi,
@inject(ICommandManager) private commandManager: ICommandManager) {
this.postOffice = new PostOffice(LiveShare.CommandBrokerService, liveShare);
}

public registerCommand(command: string, callback: (...args: any[]) => void, thisArg?: any): Disposable {
// Modify the callback such that it sends the command to our service
const disposable = this.commandManager.registerCommand(command, (...args: any[]) => this.wrapCallback(command, callback, ...args), thisArg);

// Register it for lookup
this.register(command, callback, thisArg).ignoreErrors();

return disposable;
}
public registerTextEditorCommand(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, thisArg?: any): Disposable {
// Modify the callback such that it sends the command to our service
const disposable = this.commandManager.registerCommand(
command,
(textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => this.wrapTextEditorCallback(command, callback, textEditor, edit, ...args), thisArg);

// Register it for lookup
this.register(command, callback, thisArg).ignoreErrors();

return disposable;
}
public executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined> {
// Execute the command but potentially also send to our service too
this.postCommand<T>(command, ...rest).ignoreErrors();
return this.commandManager.executeCommand(command, ...rest);
}
public getCommands(filterInternal?: boolean): Thenable<string[]> {
// This does not go across to the other side. Just return the command registered locally
return this.commandManager.getCommands(filterInternal);
}

private async register(command: string, callback: (...args: any[]) => void, thisArg?: any) : Promise<void> {
return this.postOffice.registerCallback(command, callback, thisArg);
}

private wrapCallback(command: string, callback: (...args: any[]) => void, ...args: any[]) {
// Have the post office handle it.
this.postCommand(command, ...args).ignoreErrors();
}

private wrapTextEditorCallback(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, ...args: any[]) {
// Not really supported at the moment as we don't have a special case for the textEditor. But not using it.
this.postCommand(command, ...args).ignoreErrors();
}

private async postCommand<T>(command: string, ...rest: any[]): Promise<void> {
// Make sure we're the host (or none). Guest shouldn't be sending
if (this.postOffice.role() !== vsls.Role.Guest) {
// This means we should send this across to the other side.
return this.postOffice.postCommand(command, ...rest);
}
}
}
45 changes: 45 additions & 0 deletions src/client/datascience/constants.ts
Expand Up @@ -3,6 +3,8 @@

'use strict';

import { IS_WINDOWS } from '../common/platform/constants';

export namespace Commands {
export const RunAllCells = 'python.datascience.runallcells';
export const RunCell = 'python.datascience.runcell';
Expand Down Expand Up @@ -36,6 +38,15 @@ export namespace EditorContexts {
export namespace RegExpValues {
export const PythonCellMarker = /^(#\s*%%|#\s*\<codecell\>|#\s*In\[\d*?\]|#\s*In\[ \])/;
export const PythonMarkdownCellMarker = /^(#\s*%%\s*\[markdown\]|#\s*\<markdowncell\>)/;
export const CheckJupyterRegEx = IS_WINDOWS ? /^jupyter?\.exe$/ : /^jupyter?$/;
export const PyKernelOutputRegEx = /.*\s+(.+)$/m;
export const KernelSpecOutputRegEx = /^\s*(\S+)\s+(\S+)$/;
export const UrlPatternRegEx = /(https?:\/\/[^\s]+)/ ;
export const HttpPattern = /https?:\/\//;
export const ExtractPortRegex = /https?:\/\/[^\s]+:(\d+)[^\s]+/;
export const ConvertToRemoteUri = /(https?:\/\/)([^\s])+(:\d+[^\s]*)/;
export const ParamsExractorRegEx = /\S+\((.*)\)\s*{/;
export const ArgsSplitterRegEx = /([^\s,]+)/g;
}

export namespace HistoryMessages {
Expand Down Expand Up @@ -107,3 +118,37 @@ export namespace Identifiers {
export const EmptyFileName = '2DB9B899-6519-4E1B-88B0-FA728A274115';
export const GeneratedThemeName = 'ipython-theme'; // This needs to be all lower class and a valid class name.
}

export namespace JupyterCommands {
export const NotebookCommand = 'notebook';
export const ConvertCommand = 'nbconvert';
export const KernelSpecCommand = 'kernelspec';
export const KernelCreateCommand = 'ipykernel';

}

export namespace LiveShare {
export const None = 'none';
export const Host = 'host';
export const Guest = 'guest';
export const JupyterExecutionService = 'jupyterExecutionService';
export const JupyterServerSharedService = 'jupyterServerSharedService';
export const CommandBrokerService = 'commmandBrokerService';
export const WebPanelMessageService = 'webPanelMessageService';
export const LiveShareBroadcastRequest = 'broadcastRequest';
export const ResponseLifetime = 15000;
export const ResponseRange = 1000; // Range of time alloted to check if a response matches or not
}

export namespace LiveShareCommands {
export const isNotebookSupported = 'isNotebookSupported';
export const isImportSupported = 'isImportSupported';
export const isKernelCreateSupported = 'isKernelCreateSupported';
export const isKernelSpecSupported = 'isKernelSpecSupported';
export const connectToNotebookServer = 'connectToNotebookServer';
export const getUsableJupyterPython = 'getUsableJupyterPython';
export const getSysInfo = 'getSysInfo';
export const serverResponse = 'serverResponse';
export const catchupRequest = 'catchupRequest';
export const syncRequest = 'synchRequest';
}

0 comments on commit 7d702c3

Please sign in to comment.