diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index 1fbff0f35d..9d62b7e30c 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -29,7 +29,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; -import { RunCellAction, DeleteCellAction, AddCellAction } from 'sql/parts/notebook/cellViews/codeActions'; +import { RunCellAction, DeleteCellAction, AddCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions'; import { CellTypes } from 'sql/parts/notebook/models/contracts'; @@ -97,7 +97,7 @@ export class CodeComponent extends AngularDisposable implements OnInit { get model(): NotebookModel { return this._model; } - + private createEditor(): void { let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()])); this._editor = instantiationService.createInstance(QueryTextEditor); @@ -130,12 +130,12 @@ export class CodeComponent extends AngularDisposable implements OnInit { } protected initActionBar() { - + let context = new CellContext(this.model, this.cellModel); let runCellAction = this._instantiationService.createInstance(RunCellAction); let taskbar = this.toolbarElement.nativeElement; this._actionBar = new Taskbar(taskbar, this.contextMenuService); - this._actionBar.context = this; + this._actionBar.context = context; this._actionBar.setContent([ { action: runCellAction } ]); @@ -145,13 +145,13 @@ export class CodeComponent extends AngularDisposable implements OnInit { this._moreActions.context = { target: moreActionsElement }; let actions: Action[] = []; - actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false, this.notificationService)); - actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true, this.notificationService)); - actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false, this.notificationService)); - actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true, this.notificationService)); - actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'), CellTypes.Code, false, this.notificationService)); + actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false)); + actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true)); + actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false)); + actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true)); + actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'))); - this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, this.model, this.contextMenuService), { icon: true, label: false }); + this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, context), { icon: true, label: false }); } private createUri(): URI { diff --git a/src/sql/parts/notebook/cellViews/code.css b/src/sql/parts/notebook/cellViews/code.css index 24a38c9085..99dc0b8292 100644 --- a/src/sql/parts/notebook/cellViews/code.css +++ b/src/sql/parts/notebook/cellViews/code.css @@ -25,6 +25,17 @@ code-component .toolbarIconRun { background-image: url('../media/dark/execute_cell_inverse.svg'); } +code-component .toolbarIconStop { + height: 20px; + background-image: url('../media/light/stop_cell.svg'); + padding-bottom: 10px; +} + +.vs-dark code-component .toolbarIconStop, +.hc-black code-component .toolbarIconStop { + background-image: url('../media/dark/stop_cell_inverse.svg'); +} + /* overview ruler */ code-component .monaco-editor .decorationsOverviewRuler { visibility: hidden; diff --git a/src/sql/parts/notebook/cellViews/codeActions.ts b/src/sql/parts/notebook/cellViews/codeActions.ts index 364ad449ad..4288cd6ace 100644 --- a/src/sql/parts/notebook/cellViews/codeActions.ts +++ b/src/sql/parts/notebook/cellViews/codeActions.ts @@ -1,7 +1,9 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { nb } from 'sqlops'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -10,97 +12,176 @@ import { CellType } from 'sql/parts/notebook/models/contracts'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; import { getErrorMessage } from 'sql/parts/notebook/notebookUtils'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { NOTFOUND } from 'dns'; -import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService'; +import { ICellModel, FutureInternal } from 'sql/parts/notebook/models/modelInterfaces'; +import { ToggleableAction } from 'sql/parts/notebook/notebookActions'; let notebookMoreActionMsg = localize('notebook.failed', "Please select active cell and try again"); -export class RunCellAction extends Action { - public static ID = 'jobaction.notebookRunCell'; + +function hasModelAndCell(context: CellContext, notificationService: INotificationService): boolean { + if (!context || !context.model) { + return false; + } + if (context.cell === undefined) { + notificationService.notify({ + severity: Severity.Error, + message: notebookMoreActionMsg + }); + return false; + } + return true; +} + +export class CellContext { + constructor(public model: NotebookModel, private _cell?: ICellModel) { + } + + public get cell(): ICellModel { + return this._cell ? this._cell : this.model.activeCell; + } +} + +abstract class CellActionBase extends Action { + + constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) { + super(id, label, icon); + } + + public run(context: CellContext): TPromise { + if (hasModelAndCell(context, this.notificationService)) { + return TPromise.wrap(this.runCellAction(context).then(() => true)); + } + return TPromise.as(true); + } + + abstract runCellAction(context: CellContext): Promise; +} + +export class RunCellAction extends ToggleableAction { + public static ID = 'notebook.runCell'; public static LABEL = 'Run cell'; - constructor( - ) { - super(RunCellAction.ID, '', 'toolbarIconRun'); - this.tooltip = localize('runCell', 'Run cell'); + constructor(@INotificationService private notificationService: INotificationService) { + super(RunCellAction.ID, { + shouldToggleTooltip: true, + toggleOnLabel: localize('runCell', 'Run cell'), + toggleOnClass: 'toolbarIconRun', + toggleOffLabel: localize('stopCell', 'Cancel execution'), + toggleOffClass: 'toolbarIconStop', + isOn: true + }); } - public run(context: any): TPromise { - return new TPromise((resolve, reject) => { - try { - resolve(true); - } catch (e) { - reject(e); + public run(context: CellContext): TPromise { + if (hasModelAndCell(context, this.notificationService)) { + return TPromise.wrap(this.runCellAction(context).then(() => true)); + } + return TPromise.as(true); + } + + public async runCellAction(context: CellContext): Promise { + try { + let model = context.model; + let cell = context.cell; + let kernel = await this.getOrStartKernel(model); + if (!kernel) { + return; } - }); + // If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt() + // This matches the same behavior as JupyterLab + if (cell.future && cell.future.inProgress) { + cell.future.inProgress = false; + await kernel.interrupt(); + } else { + // TODO update source based on editor component contents + let content = cell.source; + if (content) { + this.toggle(false); + let future = await kernel.requestExecute({ + code: content, + stop_on_error: true + }, false); + cell.setFuture(future as FutureInternal); + // For now, await future completion. Later we should just track and handle cancellation based on model notifications + let reply = await future.done; + } + } + } catch (error) { + let message = getErrorMessage(error); + this.notificationService.error(message); + } finally { + this.toggle(true); + } + } + + private async getOrStartKernel(model: NotebookModel): Promise { + let clientSession = model && model.clientSession; + if (!clientSession) { + this.notificationService.error(localize('notebookNotReady', 'The session for this notebook is not yet ready')); + return undefined; + } else if (!clientSession.isReady || clientSession.status === 'dead') { + this.notificationService.info(localize('sessionNotReady', 'The session for this notebook will start momentarily')); + await clientSession.kernelChangeCompleted; + } + if (!clientSession.kernel) { + let defaultKernel = model && model.defaultKernel && model.defaultKernel.name; + if (!defaultKernel) { + this.notificationService.error(localize('noDefaultKernel', 'No kernel is available for this notebook')); + return undefined; + } + await clientSession.changeKernel({ + name: defaultKernel + }); + } + return clientSession.kernel; + } +} + +export class AddCellAction extends CellActionBase { + constructor( + id: string, label: string, private cellType: CellType, private isAfter: boolean, + @INotificationService notificationService: INotificationService + ) { + super(id, label, undefined, notificationService); + } + + runCellAction(context: CellContext): Promise { + try { + let model = context.model; + let index = model.cells.findIndex((cell) => cell.id === context.cell.id); + if (index !== undefined && this.isAfter) { + index += 1; + } + model.addCell(this.cellType, index); + } catch (error) { + let message = getErrorMessage(error); + + this.notificationService.notify({ + severity: Severity.Error, + message: message + }); + } + return Promise.resolve(); } } -export class AddCellAction extends Action { - constructor( - id: string, label: string, private cellType: CellType, private isAfter: boolean, private notificationService: INotificationService +export class DeleteCellAction extends CellActionBase { + constructor(id: string, label: string, + @INotificationService notificationService: INotificationService ) { - super(id, label); + super(id, label, undefined, notificationService); } - public run(model: NotebookModel): TPromise { - return new TPromise((resolve, reject) => { - try { - if (!model) { - return; - } - if (model.activeCell === undefined) { - this.notificationService.notify({ - severity: Severity.Error, - message: notebookMoreActionMsg - }); - } - else { - let index = model.cells.findIndex((cell) => cell.id === model.activeCell.id); - if (index !== undefined && this.isAfter) { - index += 1; - } - model.addCell(this.cellType, index); - } - } catch (error) { - let message = getErrorMessage(error); - this.notificationService.notify({ - severity: Severity.Error, - message: message - }); - } - }); - } -} + runCellAction(context: CellContext): Promise { + try { + context.model.deleteCell(context.cell); + } catch (error) { + let message = getErrorMessage(error); -export class DeleteCellAction extends Action { - constructor( - id: string, label: string, private cellType: CellType, private isAfter: boolean, private notificationService: INotificationService - ) { - super(id, label); - } - public run(model: NotebookModel): TPromise { - return new TPromise((resolve, reject) => { - try { - if (!model) { - return; - } - if (model.activeCell === undefined) { - this.notificationService.notify({ - severity: Severity.Error, - message: notebookMoreActionMsg - }); - } - else { - model.deleteCell(model.activeCell); - } - } catch (error) { - let message = getErrorMessage(error); - - this.notificationService.notify({ - severity: Severity.Error, - message: message - }); - } - }); + this.notificationService.notify({ + severity: Severity.Error, + message: message + }); + } + return Promise.resolve(); } } \ No newline at end of file diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts index 9aeff66b02..fabb43e53e 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.ts +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -39,6 +39,11 @@ export class CodeCellComponent extends CellView implements OnInit { ngOnInit() { this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this.updateTheme(this.themeService.getColorTheme()); + if (this.cellModel) { + this.cellModel.onOutputsChanged(() => { + this._changeRef.detectChanges(); + }); + } } // Todo: implement layout diff --git a/src/sql/parts/notebook/cellViews/outputArea.component.ts b/src/sql/parts/notebook/cellViews/outputArea.component.ts index 461de0da21..f0cdfb9e26 100644 --- a/src/sql/parts/notebook/cellViews/outputArea.component.ts +++ b/src/sql/parts/notebook/cellViews/outputArea.component.ts @@ -3,9 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./code'; -import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; +import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef } from '@angular/core'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const OUTPUT_AREA_SELECTOR: string = 'output-area-component'; @@ -20,11 +19,15 @@ export class OutputAreaComponent extends AngularDisposable implements OnInit { private readonly _minimumHeight = 30; constructor( - @Inject(IModeService) private _modeService: IModeService + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, ) { super(); } ngOnInit(): void { - + if (this.cellModel) { + this.cellModel.onOutputsChanged(() => { + this._changeRef.detectChanges(); + }); + } } } diff --git a/src/sql/parts/notebook/media/dark/stop_cell_inverse.svg b/src/sql/parts/notebook/media/dark/stop_cell_inverse.svg new file mode 100755 index 0000000000..aa175704ce --- /dev/null +++ b/src/sql/parts/notebook/media/dark/stop_cell_inverse.svg @@ -0,0 +1 @@ +stop_cell_inverse \ No newline at end of file diff --git a/src/sql/parts/notebook/media/light/stop_cell.svg b/src/sql/parts/notebook/media/light/stop_cell.svg new file mode 100755 index 0000000000..790114755e --- /dev/null +++ b/src/sql/parts/notebook/media/light/stop_cell.svg @@ -0,0 +1 @@ +stop_cell \ No newline at end of file diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts index 0d277f9527..5370320a97 100644 --- a/src/sql/parts/notebook/models/cell.ts +++ b/src/sql/parts/notebook/models/cell.ts @@ -10,7 +10,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { nb } from 'sqlops'; -import { ICellModelOptions, IModelFactory } from './modelInterfaces'; +import { ICellModelOptions, IModelFactory, FutureInternal } from './modelInterfaces'; import * as notebookUtils from '../notebookUtils'; import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; @@ -24,7 +24,7 @@ export class CellModel implements ICellModel { private _cellType: nb.CellType; private _source: string; private _language: string; - private _future: nb.IFuture; + private _future: FutureInternal; private _outputs: nb.ICellOutput[] = []; private _isEditMode: boolean; private _onOutputsChanged = new Emitter>(); @@ -69,7 +69,7 @@ export class CellModel implements ICellModel { return this._isEditMode; } - public get future(): nb.IFuture { + public get future(): FutureInternal { return this._future; } @@ -137,7 +137,7 @@ export class CellModel implements ICellModel { * Sets the future which will be used to update the output * area for this cell */ - setFuture(future: nb.IFuture): void { + setFuture(future: FutureInternal): void { if (this._future === future) { // Nothing to do return; diff --git a/src/sql/parts/notebook/models/clientSession.ts b/src/sql/parts/notebook/models/clientSession.ts index 5b4154e4d5..77b7e4bed4 100644 --- a/src/sql/parts/notebook/models/clientSession.ts +++ b/src/sql/parts/notebook/models/clientSession.ts @@ -299,7 +299,7 @@ export class ClientSession implements IClientSession { public async shutdown(): Promise { // Always try to shut down session if (this._session && this._session.id) { - this.notebookManager.sessionManager.shutdown(this._session.id); + await this.notebookManager.sessionManager.shutdown(this._session.id); } let serverManager = this.notebookManager.serverManager; if (serverManager) { diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index b94cb3608a..662703fc44 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -338,10 +338,16 @@ export interface ICellModel { cellType: CellType; trustedMode: boolean; active: boolean; + readonly future: FutureInternal; readonly outputs: ReadonlyArray; + readonly onOutputsChanged: Event>; + setFuture(future: FutureInternal): void; equals(cellModel: ICellModel): boolean; toJSON(): nb.ICell; - onOutputsChanged: Event>; +} + +export interface FutureInternal extends nb.IFuture { + inProgress: boolean; } export interface IModelFactory { diff --git a/src/sql/parts/notebook/notebookActions.ts b/src/sql/parts/notebook/notebookActions.ts index 4e8184dafe..9406ebf6e7 100644 --- a/src/sql/parts/notebook/notebookActions.ts +++ b/src/sql/parts/notebook/notebookActions.ts @@ -43,41 +43,82 @@ export class AddCellAction extends Action { } } -export class TrustedAction extends Action { +export interface IToggleableState { + baseClass?: string; + shouldToggleTooltip?: boolean; + toggleOnClass: string; + toggleOnLabel: string; + toggleOffLabel: string; + toggleOffClass: string; + isOn: boolean; +} + +export abstract class ToggleableAction extends Action { + + constructor(id: string, protected state: IToggleableState) { + super(id, ''); + this.updateLabelAndIcon(); + } + + private updateLabelAndIcon() { + if (this.state.shouldToggleTooltip) { + this.tooltip = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel; + } else { + this.label = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel; + } + let classes = this.state.baseClass ? `${this.state.baseClass} ` : ''; + classes += this.state.isOn ? this.state.toggleOnClass : this.state.toggleOffClass; + this.class = classes; + } + + protected toggle(isOn: boolean): void { + this.state.isOn = isOn; + this.updateLabelAndIcon(); + } +} + +export class TrustedAction extends ToggleableAction { // Constants - private static readonly trustLabel = localize('trustLabel', 'Trusted'); - private static readonly notTrustLabel = localize('untrustLabel', 'Not Trusted'); + private static readonly trustedLabel = localize('trustLabel', 'Trusted'); + private static readonly notTrustedLabel = localize('untrustLabel', 'Not Trusted'); private static readonly alreadyTrustedMsg = localize('alreadyTrustedMsg', 'Notebook is already trusted.'); - private static readonly trustedCssClass = 'notebook-button icon-trusted'; - private static readonly notTrustedCssClass = 'notebook-button icon-notTrusted'; + private static readonly baseClass = 'notebook-button'; + private static readonly trustedCssClass = 'icon-trusted'; + private static readonly notTrustedCssClass = 'icon-notTrusted'; + // Properties - private _isTrusted: boolean = false; - public get trusted(): boolean { - return this._isTrusted; - } - public set trusted(value: boolean) { - this._isTrusted = value; - this._setClass(value ? TrustedAction.trustedCssClass : TrustedAction.notTrustedCssClass); - this._setLabel(value ? TrustedAction.trustLabel : TrustedAction.notTrustLabel); - } constructor( id: string, @INotificationService private _notificationService: INotificationService ) { - super(id, TrustedAction.notTrustLabel, TrustedAction.notTrustedCssClass); + super(id, { + baseClass: TrustedAction.baseClass, + toggleOnLabel: TrustedAction.trustedLabel, + toggleOnClass: TrustedAction.trustedCssClass, + toggleOffLabel: TrustedAction.notTrustedLabel, + toggleOffClass: TrustedAction.notTrustedCssClass, + isOn: false + }); + } + + public get trusted(): boolean { + return this.state.isOn; + } + public set trusted(value: boolean) { + this.toggle(value); } public run(context: NotebookComponent): TPromise { let self = this; return new TPromise((resolve, reject) => { try { - if (self._isTrusted) { + if (self.trusted) { const actions: INotificationActions = { primary: [] }; self._notificationService.notify({ severity: Severity.Info, message: TrustedAction.alreadyTrustedMsg, actions }); } else { - self.trusted = !self._isTrusted; + self.trusted = !self.trusted; context.updateModelTrustDetails(self.trusted); } resolve(true); diff --git a/src/sql/parts/notebook/outputs/common/outputProcessor.ts b/src/sql/parts/notebook/outputs/common/outputProcessor.ts index 11ad262d25..83eea21820 100644 --- a/src/sql/parts/notebook/outputs/common/outputProcessor.ts +++ b/src/sql/parts/notebook/outputs/common/outputProcessor.ts @@ -40,7 +40,7 @@ export function getData(output: nb.ICellOutput): JSONObject { bundle['application/vnd.jupyter.stdout'] = output.text; } } else if (nbformat.isError(output)) { - let traceback = output.traceback.join('\n'); + let traceback = output.traceback ? output.traceback.join('\n') : undefined; bundle['application/vnd.jupyter.stderr'] = traceback || `${output.ename}: ${output.evalue}`; } diff --git a/src/sql/services/notebook/sessionManager.ts b/src/sql/services/notebook/sessionManager.ts index dda6f17735..49fe3e9f37 100644 --- a/src/sql/services/notebook/sessionManager.ts +++ b/src/sql/services/notebook/sessionManager.ts @@ -2,8 +2,11 @@ import { nb } from 'sqlops'; import { localize } from 'vs/nls'; +import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces'; const noKernel: string = localize('noKernel', 'No Kernel'); +const runNotebookDisabled = localize('runNotebookDisabled', 'Cannot run cells as no kernel has been configured'); + let noKernelSpec: nb.IKernelSpec = ({ name: noKernel, language: 'python', @@ -130,7 +133,7 @@ class EmptyKernel implements nb.IKernel { } requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture { - throw new Error('Method not implemented.'); + return new EmptyFuture(); } requestComplete(content: nb.ICompleteRequest): Thenable { @@ -138,4 +141,72 @@ class EmptyKernel implements nb.IKernel { return Promise.resolve(response as nb.ICompleteReplyMsg); } + interrupt(): Thenable { + return Promise.resolve(undefined); + } +} + +class EmptyFuture implements FutureInternal { + + + get inProgress(): boolean { + return false; + } + + get msg(): nb.IMessage { + return undefined; + } + + get done(): Thenable { + let msg: nb.IShellMessage = { + channel: 'shell', + type: 'shell', + content: runNotebookDisabled, + header: undefined, + metadata: undefined, + parent_header: undefined + }; + + return Promise.resolve(msg); + } + + sendInputReply(content: nb.IInputReply): void { + // no-op + } + dispose() { + // No-op + } + + setReplyHandler(handler: nb.MessageHandler): void { + // no-op + } + setStdInHandler(handler: nb.MessageHandler): void { + // no-op + } + setIOPubHandler(handler: nb.MessageHandler): void { + setTimeout(() => { + let msg: nb.IIOPubMessage = { + channel: 'iopub', + type: 'iopub', + header: { + msg_id: '0', + msg_type: 'error' + }, + content: { + ename: localize('errorName', 'Error'), + evalue: runNotebookDisabled, + output_type: 'error' + }, + metadata: undefined, + parent_header: undefined + }; + handler.handle(msg); + }, 10); + } + registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable): void { + // no-op + } + removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable): void { + // no-op + } } \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 2054df7efe..3b10534757 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1698,6 +1698,20 @@ declare module 'sqlops' { */ requestComplete(content: ICompleteRequest): Thenable; + /** + * Interrupt a kernel. + * + * #### Notes + * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/kernels). + * + * The promise is fulfilled on a valid response and rejected otherwise. + * + * It is assumed that the API call does not mutate the kernel id or name. + * + * The promise will be rejected if the kernel status is `Dead` or if the + * request fails or the response is invalid. + */ + interrupt(): Thenable; } export interface IInfoReply { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 8d880e9886..43b5803269 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -416,4 +416,39 @@ export interface INotebookManagerDetails { handle: number; hasContentManager: boolean; hasServerManager: boolean; +} + +export interface INotebookSessionDetails { + readonly sessionId: number; + readonly canChangeKernels: boolean; + readonly id: string; + readonly path: string; + readonly name: string; + readonly type: string; + readonly status: string; + readonly kernelDetails: INotebookKernelDetails; +} + +export interface INotebookKernelDetails { + readonly kernelId: number; + readonly id: string; + readonly name: string; + readonly supportsIntellisense: boolean; + readonly info?: any; +} + +export interface INotebookFutureDetails { + readonly futureId: number; + readonly msg: any; +} + +export enum FutureMessageType { + Reply = 0, + StdIn = 1, + IOPub = 2 +} + +export interface INotebookFutureDone { + succeeded: boolean; + rejectReason: string; } \ No newline at end of file diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts index 9070ba3302..0ae03c6fe6 100644 --- a/src/sql/workbench/api/node/extHostNotebook.ts +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -6,24 +6,25 @@ import * as sqlops from 'sqlops'; import * as vscode from 'vscode'; + import { TPromise } from 'vs/base/common/winjs.base'; import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; import { Disposable } from 'vs/workbench/api/node/extHostTypes'; import { localize } from 'vs/nls'; - +import URI, { UriComponents } from 'vs/base/common/uri'; import { ExtHostNotebookShape, MainThreadNotebookShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType } from 'sql/workbench/api/common/sqlExtHostTypes'; + +type Adapter = sqlops.nb.NotebookProvider | sqlops.nb.NotebookManager | sqlops.nb.ISession | sqlops.nb.IKernel | sqlops.nb.IFuture; export class ExtHostNotebook implements ExtHostNotebookShape { private static _handlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private _providers = new Map(); + private _adapters = new Map(); // Notebook URI to manager lookup. - private _managers = new Map(); - constructor(private _mainContext: IMainContext) { + constructor(_mainContext: IMainContext) { this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook); } @@ -39,7 +40,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { } return { - handle: adapter.managerHandle, + handle: adapter.handle, hasContentManager: !!adapter.contentManager, hasServerManager: !!adapter.serverManager }; @@ -50,7 +51,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { let manager = this.findManagerForUri(uriString); if (manager) { manager.provider.handleNotebookClosed(uri); - this._managers.delete(manager.managerHandle); + // Note: deliberately not removing handle. } } @@ -70,6 +71,124 @@ export class ExtHostNotebook implements ExtHostNotebookShape { return this._withContentManager(managerHandle, (contentManager) => contentManager.save(URI.revive(notebookUri), notebook)); } + $refreshSpecs(managerHandle: number): Thenable { + return this._withSessionManager(managerHandle, async (sessionManager) => { + await sessionManager.ready; + return sessionManager.specs; + }); + } + + $startNewSession(managerHandle: number, options: sqlops.nb.ISessionOptions): Thenable { + return this._withSessionManager(managerHandle, async (sessionManager) => { + let session = await sessionManager.startNew(options); + let sessionId = this._addNewAdapter(session); + let kernelDetails: INotebookKernelDetails = undefined; + if (session.kernel) { + kernelDetails = this.saveKernel(session.kernel); + } + let details: INotebookSessionDetails = { + sessionId: sessionId, + id: session.id, + path: session.path, + name: session.name, + type: session.type, + status: session.status, + canChangeKernels: session.canChangeKernels, + kernelDetails: kernelDetails + }; + return details; + }); + } + + private saveKernel(kernel: sqlops.nb.IKernel): INotebookKernelDetails { + let kernelId = this._addNewAdapter(kernel); + let kernelDetails: INotebookKernelDetails = { + kernelId: kernelId, + id: kernel.id, + info: kernel.info, + name: kernel.name, + supportsIntellisense: kernel.supportsIntellisense + }; + return kernelDetails; + } + + $shutdownSession(managerHandle: number, sessionId: string): Thenable { + return this._withSessionManager(managerHandle, async (sessionManager) => { + return sessionManager.shutdown(sessionId); + }); + } + + $changeKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable { + let session = this._getAdapter(sessionId); + return session.changeKernel(kernelInfo).then(kernel => this.saveKernel(kernel)); + } + + $getKernelReadyStatus(kernelId: number): Thenable { + let kernel = this._getAdapter(kernelId); + return kernel.ready.then(success => kernel.info); + } + + $getKernelSpec(kernelId: number): Thenable { + let kernel = this._getAdapter(kernelId); + return kernel.getSpec(); + } + + $requestComplete(kernelId: number, content: sqlops.nb.ICompleteRequest): Thenable { + let kernel = this._getAdapter(kernelId); + return kernel.requestComplete(content); + } + + $requestExecute(kernelId: number, content: sqlops.nb.IExecuteRequest, disposeOnDone?: boolean): Thenable { + let kernel = this._getAdapter(kernelId); + let future = kernel.requestExecute(content, disposeOnDone); + let futureId = this._addNewAdapter(future); + this.hookFutureDone(futureId, future); + this.hookFutureMessages(futureId, future); + return Promise.resolve({ + futureId: futureId, + msg: future.msg + }); + } + + private hookFutureDone(futureId: number, future: sqlops.nb.IFuture): void { + future.done.then(success => { + return this._proxy.$onFutureDone(futureId, { succeeded: true, rejectReason: undefined }); + }, err => { + let rejectReason: string; + if (typeof err === 'string') { + rejectReason = err; + } + else if (err instanceof Error && typeof err.message === 'string') { + rejectReason = err.message; + } + else { + rejectReason = err; + } + return this._proxy.$onFutureDone(futureId, { succeeded: false, rejectReason: rejectReason }); + }); + } + + private hookFutureMessages(futureId: number, future: sqlops.nb.IFuture): void { + future.setReplyHandler({ handle: (msg) => this._proxy.$onFutureMessage(futureId, FutureMessageType.Reply, msg) }); + future.setStdInHandler({ handle: (msg) => this._proxy.$onFutureMessage(futureId, FutureMessageType.StdIn, msg) }); + future.setIOPubHandler({ handle: (msg) => this._proxy.$onFutureMessage(futureId, FutureMessageType.IOPub, msg) }); + } + + $interruptKernel(kernelId: number): Thenable { + let kernel = this._getAdapter(kernelId); + return kernel.interrupt(); + } + + $sendInputReply(futureId: number, content: sqlops.nb.IInputReply): void { + let future = this._getAdapter(futureId); + return future.sendInputReply(content); + } + + $disposeFuture(futureId: number): void { + let future = this._getAdapter(futureId); + future.dispose(); + } + //#endregion //#region APIs called by extensions @@ -77,7 +196,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { if (!provider || !provider.providerId) { throw new Error(localize('providerRequired', 'A NotebookProvider with valid providerId must be passed to this method')); } - const handle = this._addNewProvider(provider); + const handle = this._addNewAdapter(provider); this._proxy.$registerNotebookProvider(provider.providerId, handle); return this._createDisposable(handle); } @@ -86,8 +205,18 @@ export class ExtHostNotebook implements ExtHostNotebookShape { //#region private methods + private getAdapters(ctor: { new(...args: any[]): A }): A[] { + let matchingAdapters = []; + this._adapters.forEach(a => { + if (a instanceof ctor) { + matchingAdapters.push(a); + } + }); + return matchingAdapters; + } + private findManagerForUri(uriString: string): NotebookManagerAdapter { - for(let manager of Array.from(this._managers.values())) { + for(let manager of this.getAdapters(NotebookManagerAdapter)) { if (manager.uriString === uriString) { return manager; } @@ -98,15 +227,14 @@ export class ExtHostNotebook implements ExtHostNotebookShape { private async createManager(provider: sqlops.nb.NotebookProvider, notebookUri: URI): Promise { let manager = await provider.getNotebookManager(notebookUri); let uriString = notebookUri.toString(); - let handle = this._nextHandle(); - let adapter = new NotebookManagerAdapter(provider, handle, manager, uriString); - this._managers.set(handle, adapter); + let adapter = new NotebookManagerAdapter(provider, manager, uriString); + adapter.handle = this._addNewAdapter(adapter); return adapter; } private _createDisposable(handle: number): Disposable { return new Disposable(() => { - this._providers.delete(handle); + this._adapters.delete(handle); this._proxy.$unregisterNotebookProvider(handle); }); } @@ -116,7 +244,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { } private _withProvider(handle: number, callback: (provider: sqlops.nb.NotebookProvider) => R | PromiseLike): TPromise { - let provider = this._providers.get(handle); + let provider = this._adapters.get(handle) as sqlops.nb.NotebookProvider; if (provider === undefined) { return TPromise.wrapError(new Error(localize('errNoProvider', 'no notebook provider found'))); } @@ -124,7 +252,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { } private _withNotebookManager(handle: number, callback: (manager: NotebookManagerAdapter) => R | PromiseLike): TPromise { - let manager = this._managers.get(handle); + let manager = this._adapters.get(handle) as NotebookManagerAdapter; if (manager === undefined) { return TPromise.wrapError(new Error(localize('errNoManager', 'No Manager found'))); } @@ -161,19 +289,28 @@ export class ExtHostNotebook implements ExtHostNotebookShape { }); } - private _addNewProvider(adapter: sqlops.nb.NotebookProvider): number { + private _addNewAdapter(adapter: Adapter): number { const handle = this._nextHandle(); - this._providers.set(handle, adapter); + this._adapters.set(handle, adapter); return handle; } + + private _getAdapter(id: number): T { + let adapter = this._adapters.get(id); + if (adapter === undefined) { + throw new Error('No adapter found'); + } + return adapter; + } + //#endregion } class NotebookManagerAdapter implements sqlops.nb.NotebookManager { + public handle: number; constructor( public readonly provider: sqlops.nb.NotebookProvider, - public readonly managerHandle: number, private manager: sqlops.nb.NotebookManager, public readonly uriString: string ) { @@ -190,5 +327,4 @@ class NotebookManagerAdapter implements sqlops.nb.NotebookManager { public get serverManager(): sqlops.nb.ServerManager { return this.manager.serverManager; } - -} \ No newline at end of file +} diff --git a/src/sql/workbench/api/node/mainThreadNotebook.ts b/src/sql/workbench/api/node/mainThreadNotebook.ts index 8ecdc3a942..cff7da7c89 100644 --- a/src/sql/workbench/api/node/mainThreadNotebook.ts +++ b/src/sql/workbench/api/node/mainThreadNotebook.ts @@ -13,14 +13,17 @@ import { Event, Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { INotebookService, INotebookProvider, INotebookManager } from 'sql/services/notebook/notebookService'; -import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, FutureMessageType, INotebookFutureDetails, INotebookFutureDone } from 'sql/workbench/api/common/sqlExtHostTypes'; import { LocalContentManager } from 'sql/services/notebook/localContentManager'; +import { Deferred } from 'sql/base/common/promise'; +import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces'; @extHostNamedCustomer(SqlMainContext.MainThreadNotebook) export class MainThreadNotebook extends Disposable implements MainThreadNotebookShape { private _proxy: ExtHostNotebookShape; private _providers = new Map(); + private _futures = new Map(); constructor( extHostContext: IExtHostContext, @@ -32,9 +35,21 @@ export class MainThreadNotebook extends Disposable implements MainThreadNotebook } } + public addFuture(futureId: number, future: FutureWrapper): void { + this._futures.set(futureId, future); + } + + public disposeFuture(futureId: number): void { + this._futures.delete(futureId); + } + //#region Extension host callable methods public $registerNotebookProvider(providerId: string, handle: number): void { - let notebookProvider = new NotebookProviderWrapper(this._proxy, providerId, handle); + let proxy: Proxies = { + main: this, + ext: this._proxy + }; + let notebookProvider = new NotebookProviderWrapper(proxy, providerId, handle); this._providers.set(handle, notebookProvider); this.notebookService.registerProvider(providerId, notebookProvider); } @@ -48,14 +63,32 @@ export class MainThreadNotebook extends Disposable implements MainThreadNotebook } } - //#endregion + public $onFutureMessage(futureId: number, type: FutureMessageType, payload: sqlops.nb.IMessage): void { + let future = this._futures.get(futureId); + if (future) { + future.onMessage(type, payload); + } + } + public $onFutureDone(futureId: number, done: INotebookFutureDone): void { + let future = this._futures.get(futureId); + if (future) { + future.onDone(done); + } + + } + //#endregion +} + +interface Proxies { + main: MainThreadNotebook; + ext: ExtHostNotebookShape; } class NotebookProviderWrapper extends Disposable implements INotebookProvider { - private _managers = new Map(); + private _notebookUriToManagerMap = new Map(); - constructor(private _proxy: ExtHostNotebookShape, public readonly providerId, public readonly providerHandle: number) { + constructor(private _proxy: Proxies, public readonly providerId, public readonly providerHandle: number) { super(); } @@ -66,19 +99,19 @@ class NotebookProviderWrapper extends Disposable implements INotebookProvider { private async doGetNotebookManager(notebookUri: URI): Promise { let uriString = notebookUri.toString(); - let manager = this._managers.get(uriString); + let manager = this._notebookUriToManagerMap.get(uriString); if (!manager) { manager = new NotebookManagerWrapper(this._proxy, this.providerId, notebookUri); await manager.initialize(this.providerHandle); - this._managers.set(uriString, manager); + this._notebookUriToManagerMap.set(uriString, manager); } return manager; } handleNotebookClosed(notebookUri: URI): void { - this._proxy.$handleNotebookClosed(notebookUri); + this._notebookUriToManagerMap.delete(notebookUri.toString()); + this._proxy.ext.$handleNotebookClosed(notebookUri); } - } class NotebookManagerWrapper implements INotebookManager { @@ -87,13 +120,13 @@ class NotebookManagerWrapper implements INotebookManager { private _serverManager: sqlops.nb.ServerManager; private managerDetails: INotebookManagerDetails; - constructor(private _proxy: ExtHostNotebookShape, + constructor(private _proxy: Proxies, public readonly providerId, private notebookUri: URI ) { } public async initialize(providerHandle: number): Promise { - this.managerDetails = await this._proxy.$getNotebookManager(providerHandle, this.notebookUri); + this.managerDetails = await this._proxy.ext.$getNotebookManager(providerHandle, this.notebookUri); let managerHandle = this.managerDetails.handle; this._contentManager = this.managerDetails.hasContentManager ? new ContentManagerWrapper(managerHandle, this._proxy) : new LocalContentManager(); this._serverManager = this.managerDetails.hasServerManager ? new ServerManagerWrapper(managerHandle, this._proxy) : undefined; @@ -114,26 +147,25 @@ class NotebookManagerWrapper implements INotebookManager { public get managerHandle(): number { return this.managerDetails.handle; } - } class ContentManagerWrapper implements sqlops.nb.ContentManager { - constructor(private handle: number, private _proxy: ExtHostNotebookShape) { + constructor(private handle: number, private _proxy: Proxies) { } getNotebookContents(notebookUri: URI): Thenable { - return this._proxy.$getNotebookContents(this.handle, notebookUri); + return this._proxy.ext.$getNotebookContents(this.handle, notebookUri); } save(path: URI, notebook: sqlops.nb.INotebook): Thenable { - return this._proxy.$save(this.handle, path, notebook); + return this._proxy.ext.$save(this.handle, path, notebook); } } class ServerManagerWrapper implements sqlops.nb.ServerManager { - private onServerStartedEmitter: Emitter; + private onServerStartedEmitter = new Emitter(); private _isStarted: boolean; - constructor(private handle: number, private _proxy: ExtHostNotebookShape) { + constructor(private handle: number, private _proxy: Proxies) { this._isStarted = false; } @@ -150,7 +182,7 @@ class ServerManagerWrapper implements sqlops.nb.ServerManager { } private async doStartServer(): Promise { - await this._proxy.$doStartServer(this.handle); + await this._proxy.ext.$doStartServer(this.handle); this._isStarted = true; this.onServerStartedEmitter.fire(); } @@ -161,7 +193,7 @@ class ServerManagerWrapper implements sqlops.nb.ServerManager { private async doStopServer(): Promise { try { - await this._proxy.$doStopServer(this.handle); + await this._proxy.ext.$doStopServer(this.handle); } finally { // Always consider this a stopping event, even if a failure occurred. this._isStarted = false; @@ -170,30 +202,255 @@ class ServerManagerWrapper implements sqlops.nb.ServerManager { } class SessionManagerWrapper implements sqlops.nb.SessionManager { - constructor(private handle: number, private _proxy: ExtHostNotebookShape) { + private readyPromise: Promise; + private _isReady: boolean; + private _specs: sqlops.nb.IAllKernels; + constructor(private managerHandle: number, private _proxy: Proxies) { + this._isReady = false; + this.readyPromise = this.initializeSessionManager(); } get isReady(): boolean { - throw new Error('Method not implemented.'); - + return this._isReady; } get ready(): Thenable { - throw new Error('Method not implemented.'); - + return this.readyPromise; } - get specs(): sqlops.nb.IAllKernels { - throw new Error('Method not implemented.'); + get specs(): sqlops.nb.IAllKernels { + return this._specs; } startNew(options: sqlops.nb.ISessionOptions): Thenable { - throw new Error('Method not implemented.'); + return this.doStartNew(options); + } + + private async doStartNew(options: sqlops.nb.ISessionOptions): Promise { + let sessionDetails = await this._proxy.ext.$startNewSession(this.managerHandle, options); + return new SessionWrapper(this._proxy, sessionDetails); } shutdown(id: string): Thenable { - throw new Error('Method not implemented.'); + return this._proxy.ext.$shutdownSession(this.managerHandle, id); } + private async initializeSessionManager(): Promise { + await this.refreshSpecs(); + this._isReady = true; + } -} \ No newline at end of file + private async refreshSpecs(): Promise { + let specs = await this._proxy.ext.$refreshSpecs(this.managerHandle); + if (specs) { + this._specs = specs; + } + } +} + +class SessionWrapper implements sqlops.nb.ISession { + private _kernel: KernelWrapper; + constructor(private _proxy: Proxies, private sessionDetails: INotebookSessionDetails) { + if (sessionDetails && sessionDetails.kernelDetails) { + this._kernel = new KernelWrapper(_proxy, sessionDetails.kernelDetails); + } + } + + get canChangeKernels(): boolean { + return this.sessionDetails.canChangeKernels; + } + + get id(): string { + return this.sessionDetails.id; + } + + get path(): string { + return this.sessionDetails.path; + } + + get name(): string { + return this.sessionDetails.name; + } + + get type(): string { + return this.sessionDetails.type; + } + + get status(): sqlops.nb.KernelStatus { + return this.sessionDetails.status as sqlops.nb.KernelStatus; + } + + get kernel(): sqlops.nb.IKernel { + return this._kernel; + } + + changeKernel(kernelInfo: sqlops.nb.IKernelSpec): Thenable { + return this.doChangeKernel(kernelInfo); + } + + private async doChangeKernel(kernelInfo: sqlops.nb.IKernelSpec): Promise { + let kernelDetails = await this._proxy.ext.$changeKernel(this.sessionDetails.sessionId, kernelInfo); + this._kernel = new KernelWrapper(this._proxy, kernelDetails); + return this._kernel; + } +} + +class KernelWrapper implements sqlops.nb.IKernel { + private _isReady: boolean = false; + private _ready = new Deferred(); + private _info: sqlops.nb.IInfoReply; + constructor(private _proxy: Proxies, private kernelDetails: INotebookKernelDetails) { + this.initialize(kernelDetails); + } + + private async initialize(kernelDetails: INotebookKernelDetails): Promise { + try { + this._info = await this._proxy.ext.$getKernelReadyStatus(kernelDetails.kernelId); + this._isReady = true; + this._ready.resolve(); + } catch (error) { + this._isReady = false; + this._ready.reject(error); + } + } + + get isReady(): boolean { + return this._isReady; + } + get ready(): Thenable { + return this._ready.promise; + } + + get id(): string { + return this.kernelDetails.id; + } + + get name(): string { + return this.kernelDetails.name; + } + + get supportsIntellisense(): boolean { + return this.kernelDetails.supportsIntellisense; + } + + get info(): sqlops.nb.IInfoReply { + return this._info; + } + + getSpec(): Thenable { + return this._proxy.ext.$getKernelSpec(this.kernelDetails.kernelId); + } + + requestComplete(content: sqlops.nb.ICompleteRequest): Thenable { + return this._proxy.ext.$requestComplete(this.kernelDetails.kernelId, content); + } + + requestExecute(content: sqlops.nb.IExecuteRequest, disposeOnDone?: boolean): sqlops.nb.IFuture { + let future = new FutureWrapper(this._proxy); + this._proxy.ext.$requestExecute(this.kernelDetails.kernelId, content, disposeOnDone) + .then(details => { + future.setDetails(details); + // Save the future in the main thread notebook so extension can call through and reference it + this._proxy.main.addFuture(details.futureId, future); + }, error => future.setError(error)); + return future; + } + + interrupt(): Thenable { + return this._proxy.ext.$interruptKernel(this.kernelDetails.kernelId); + } +} + + +class FutureWrapper implements FutureInternal { + private _futureId: number; + private _done = new Deferred(); + private _messageHandlers = new Map>(); + private _msg: sqlops.nb.IMessage; + private _inProgress: boolean; + + constructor(private _proxy: Proxies) { + this._inProgress = true; + } + + public setDetails(details: INotebookFutureDetails): void { + this._futureId = details.futureId; + this._msg = details.msg; + } + + public setError(error: Error | string): void { + this._done.reject(error); + } + + public onMessage(type: FutureMessageType, payload: sqlops.nb.IMessage): void { + let handler = this._messageHandlers.get(type); + if (handler) { + try { + handler.handle(payload); + } catch (error) { + // TODO log errors from the handler + } + } + } + + public onDone(done: INotebookFutureDone): void { + this._inProgress = false; + if (done.succeeded) { + this._done.resolve(); + } else { + this._done.reject(new Error(done.rejectReason)); + } + } + + private addMessageHandler(type: FutureMessageType, handler: sqlops.nb.MessageHandler): void { + // Note: there can only be 1 message handler according to the Jupyter Notebook spec. + // You can use a message hook to override this / add additional side-processors + this._messageHandlers.set(type, handler); + } + + //#region Public APIs + get inProgress(): boolean { + return this._inProgress; + } + + set inProgress(value: boolean) { + this._inProgress = value; + } + + get msg(): sqlops.nb.IMessage { + return this._msg; + } + + get done(): Thenable { + return this._done.promise; + } + + setReplyHandler(handler: sqlops.nb.MessageHandler): void { + this.addMessageHandler(FutureMessageType.Reply, handler); + } + + setStdInHandler(handler: sqlops.nb.MessageHandler): void { + this.addMessageHandler(FutureMessageType.StdIn, handler); + } + + setIOPubHandler(handler: sqlops.nb.MessageHandler): void { + this.addMessageHandler(FutureMessageType.IOPub, handler); + } + + sendInputReply(content: sqlops.nb.IInputReply): void { + this._proxy.ext.$sendInputReply(this._futureId, content); + } + + dispose() { + this._proxy.main.disposeFuture(this._futureId); + this._proxy.ext.$disposeFuture(this._futureId); + } + + registerMessageHook(hook: (msg: sqlops.nb.IIOPubMessage) => boolean | Thenable): void { + throw new Error('Method not implemented.'); + } + removeMessageHook(hook: (msg: sqlops.nb.IIOPubMessage) => boolean | Thenable): void { + throw new Error('Method not implemented.'); + } + //#endregion +} diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 1502646b40..0d5d7267e0 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -21,7 +21,7 @@ import { ITreeComponentItem } from 'sql/workbench/common/views'; import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks'; import { IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, - IModelViewWizardDetails, IModelViewWizardPageDetails, INotebookManagerDetails + IModelViewWizardDetails, IModelViewWizardPageDetails, INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType, INotebookFutureDone } from 'sql/workbench/api/common/sqlExtHostTypes'; export abstract class ExtHostAccountManagementShape { @@ -717,15 +717,39 @@ export interface ExtHostNotebookShape { */ $getNotebookManager(providerHandle: number, notebookUri: UriComponents): Thenable; $handleNotebookClosed(notebookUri: UriComponents): void; + + // Server Manager APIs $doStartServer(managerHandle: number): Thenable; $doStopServer(managerHandle: number): Thenable; + + // Content Manager APIs $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable; $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable; + // Session Manager APIs + $refreshSpecs(managerHandle: number): Thenable; + $startNewSession(managerHandle: number, options: sqlops.nb.ISessionOptions): Thenable; + $shutdownSession(managerHandle: number, sessionId: string): Thenable; + + // Session APIs + $changeKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable; + + // Kernel APIs + $getKernelReadyStatus(kernelId: number): Thenable; + $getKernelSpec(kernelId: number): Thenable; + $requestComplete(kernelId: number, content: sqlops.nb.ICompleteRequest): Thenable; + $requestExecute(kernelId: number, content: sqlops.nb.IExecuteRequest, disposeOnDone?: boolean): Thenable; + $interruptKernel(kernelId: number): Thenable; + + // Future APIs + $sendInputReply(futureId: number, content: sqlops.nb.IInputReply): void; + $disposeFuture(futureId: number): void; } export interface MainThreadNotebookShape extends IDisposable { $registerNotebookProvider(providerId: string, handle: number): void; $unregisterNotebookProvider(handle: number): void; + $onFutureMessage(futureId: number, type: FutureMessageType, payload: sqlops.nb.IMessage): void; + $onFutureDone(futureId: number, done: INotebookFutureDone): void; } diff --git a/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts b/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts index 25a3809c83..f925634038 100644 --- a/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts +++ b/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts @@ -71,7 +71,7 @@ suite('auto OAuth dialog controller tests', () => { }); - test('Open auto OAuth when the flyout is already open, return an error', () => { + test('Open auto OAuth when the flyout is already open, return an error', (done) => { // If: Open auto OAuth dialog first time autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri); @@ -81,7 +81,8 @@ suite('auto OAuth dialog controller tests', () => { mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); // If: a oauth flyout is already open - autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri); + autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri) + .then(success => done('Failure: Expected error on 2nd dialog open'), error => done()); // Then: An error dialog should have been opened mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); diff --git a/src/sqltest/utils/testUtils.ts b/src/sqltest/utils/testUtils.ts index a929a20a45..ee2267566a 100644 --- a/src/sqltest/utils/testUtils.ts +++ b/src/sqltest/utils/testUtils.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; -export async function assertThrowsAsync(fn, regExp?: string): Promise { +export async function assertThrowsAsync(fn, regExp?: any): Promise { let f = () => { // Empty }; diff --git a/src/sqltest/workbench/api/extHostAccountManagement.test.ts b/src/sqltest/workbench/api/extHostAccountManagement.test.ts index 44991d2d4a..c6ffcaefa5 100644 --- a/src/sqltest/workbench/api/extHostAccountManagement.test.ts +++ b/src/sqltest/workbench/api/extHostAccountManagement.test.ts @@ -36,7 +36,7 @@ suite('ExtHostAccountManagement', () => { instantiationService.stub(IRPCProtocol, threadService); instantiationService.stub(IAccountManagementService, accountMgmtStub); - const accountMgmtService = instantiationService.createInstance(MainThreadAccountManagement); + const accountMgmtService = instantiationService.createInstance(MainThreadAccountManagement, undefined); threadService.set(SqlMainContext.MainThreadAccountManagement, accountMgmtService); mockAccountMetadata = { @@ -356,7 +356,7 @@ suite('ExtHostAccountManagement', () => { let mockAccountManagementService = getMockAccountManagementService(mockAccounts); instantiationService.stub(IAccountManagementService, mockAccountManagementService.object); - let accountManagementService = instantiationService.createInstance(MainThreadAccountManagement); + let accountManagementService = instantiationService.createInstance(MainThreadAccountManagement, undefined); threadService.set(SqlMainContext.MainThreadAccountManagement, accountManagementService); // Setup: Create ext host account management with registered account provider @@ -365,18 +365,10 @@ suite('ExtHostAccountManagement', () => { extHost.$getAllAccounts() .then((accounts) => { - // If: I get security token - extHost.$getSecurityToken(mockAccount1) - .then((securityToken) => { - // Then: The call should have been passed to the account management service - mockAccountManagementService.verify( - (obj) => obj.getSecurityToken(TypeMoq.It.isAny()), - TypeMoq.Times.once() - ); - } - ); + // If: I get security token it will not throw + return extHost.$getSecurityToken(mockAccount1); } - ).then(() => done(), (err) => done(err)); + ).then(() => done(), (err) => done(new Error(err))); }); test('GetSecurityToken - Account not found', (done) => { @@ -402,7 +394,7 @@ suite('ExtHostAccountManagement', () => { let mockAccountManagementService = getMockAccountManagementService(mockAccounts); instantiationService.stub(IAccountManagementService, mockAccountManagementService.object); - let accountManagementService = instantiationService.createInstance(MainThreadAccountManagement); + let accountManagementService = instantiationService.createInstance(MainThreadAccountManagement, undefined); threadService.set(SqlMainContext.MainThreadAccountManagement, accountManagementService); // Setup: Create ext host account management with registered account provider @@ -423,18 +415,16 @@ suite('ExtHostAccountManagement', () => { isStale: false }; - extHost.$getAllAccounts().then((accounts) => { - // If: I get security token for mockAccount2 - // Then: It should throw - assert.throws( - () => extHost.$getSecurityToken(mockAccount2), - (error) => { - return error.message === `Account ${mockAccount2.key.accountId} not found.`; - } - ); + extHost.$getAllAccounts() + .then(accounts => { + return extHost.$getSecurityToken(mockAccount2); + }) + .then((noError) => { + done(new Error('Expected getSecurityToken to throw')); + }, (err) => { + // Expected error caught + done(); }); - - done(); }); }); diff --git a/src/sqltest/workbench/api/extHostCredentialManagement.test.ts b/src/sqltest/workbench/api/extHostCredentialManagement.test.ts index b2c59960e8..87e9b37cf0 100644 --- a/src/sqltest/workbench/api/extHostCredentialManagement.test.ts +++ b/src/sqltest/workbench/api/extHostCredentialManagement.test.ts @@ -34,7 +34,7 @@ suite('ExtHostCredentialManagement', () => { instantiationService.stub(IRPCProtocol, threadService); instantiationService.stub(ICredentialsService, credentialServiceStub); - const credentialService = instantiationService.createInstance(MainThreadCredentialManagement); + const credentialService = instantiationService.createInstance(MainThreadCredentialManagement, undefined); threadService.set(SqlMainContext.MainThreadCredentialManagement, credentialService); }); diff --git a/src/sqltest/workbench/api/mainThreadNotebook.test.ts b/src/sqltest/workbench/api/mainThreadNotebook.test.ts index 0568f40e80..07ba48e4a1 100644 --- a/src/sqltest/workbench/api/mainThreadNotebook.test.ts +++ b/src/sqltest/workbench/api/mainThreadNotebook.test.ts @@ -6,18 +6,19 @@ import * as assert from 'assert'; import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; -import URI from 'vs/base/common/uri'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostNotebookShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { MainThreadNotebook } from 'sql/workbench/api/node/mainThreadNotebook'; import { NotebookService } from 'sql/services/notebook/notebookServiceImpl'; import { INotebookProvider } from 'sql/services/notebook/notebookService'; -import { INotebookManagerDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; import { LocalContentManager } from 'sql/services/notebook/localContentManager'; - suite('MainThreadNotebook Tests', () => { let mainThreadNotebook: MainThreadNotebook; @@ -26,14 +27,7 @@ suite('MainThreadNotebook Tests', () => { let mockNotebookService: TypeMoq.Mock; let providerId = 'TestProvider'; setup(() => { - mockProxy = TypeMoq.Mock.ofInstance( { - $getNotebookManager: (handle, uri) => undefined, - $handleNotebookClosed: (uri) => undefined, - $getNotebookContents: (handle, uri) => undefined, - $save: (handle, uri, notebook) => undefined, - $doStartServer: (handle) => undefined, - $doStopServer: (handle) => undefined - }); + mockProxy = TypeMoq.Mock.ofType(ExtHostNotebookStub); let extContext = { getProxy: proxyType => mockProxy.object }; @@ -85,6 +79,8 @@ suite('MainThreadNotebook Tests', () => { }); mainThreadNotebook.$registerNotebookProvider(providerId, 1); + // Always return empty specs in this test suite + mockProxy.setup(p => p.$refreshSpecs(TypeMoq.It.isAnyNumber())).returns(() => Promise.resolve(undefined)); }); test('should return manager with default content manager & undefined server manager if extension host has none', async () => { @@ -120,4 +116,58 @@ suite('MainThreadNotebook Tests', () => { }); }); -}); \ No newline at end of file +}); + +class ExtHostNotebookStub implements ExtHostNotebookShape { + $getNotebookManager(providerHandle: number, notebookUri: UriComponents): Thenable { + throw new Error('Method not implemented.'); + } + $handleNotebookClosed(notebookUri: UriComponents): void { + throw new Error('Method not implemented.'); + } + $doStartServer(managerHandle: number): Thenable { + throw new Error('Method not implemented.'); + } + $doStopServer(managerHandle: number): Thenable { + throw new Error('Method not implemented.'); + } + $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable { + throw new Error('Method not implemented.'); + } + $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable { + throw new Error('Method not implemented.'); + } + $refreshSpecs(managerHandle: number): Thenable { + throw new Error('Method not implemented.'); + } + $startNewSession(managerHandle: number, options: sqlops.nb.ISessionOptions): Thenable { + throw new Error('Method not implemented.'); + } + $shutdownSession(managerHandle: number, sessionId: string): Thenable { + throw new Error('Method not implemented.'); + } + $changeKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable { + throw new Error('Method not implemented.'); + } + $getKernelReadyStatus(kernelId: number): Thenable { + throw new Error('Method not implemented.'); + } + $getKernelSpec(kernelId: number): Thenable { + throw new Error('Method not implemented.'); + } + $requestComplete(kernelId: number, content: sqlops.nb.ICompleteRequest): Thenable { + throw new Error('Method not implemented.'); + } + $requestExecute(kernelId: number, content: sqlops.nb.IExecuteRequest, disposeOnDone?: boolean): Thenable { + throw new Error('Method not implemented.'); + } + $interruptKernel(kernelId: number): Thenable { + throw new Error('Method not implemented.'); + } + $sendInputReply(futureId: number, content: sqlops.nb.IInputReply): void { + throw new Error('Method not implemented.'); + } + $disposeFuture(futureId: number): void { + throw new Error('Method not implemented.'); + } +}