diff --git a/src/sql/parts/notebook/cellToggleMoreActions.ts b/src/sql/parts/notebook/cellToggleMoreActions.ts index c7c293af2a..ed6eda1bf4 100644 --- a/src/sql/parts/notebook/cellToggleMoreActions.ts +++ b/src/sql/parts/notebook/cellToggleMoreActions.ts @@ -61,7 +61,7 @@ export class AddCellFromContextAction extends CellActionBase { super(id, label, undefined, notificationService); } - runCellAction(context: CellContext): Promise { + doRun(context: CellContext): Promise { try { let model = context.model; let index = model.cells.findIndex((cell) => cell.id === context.cell.id); @@ -88,7 +88,7 @@ export class DeleteCellAction extends CellActionBase { super(id, label, undefined, notificationService); } - runCellAction(context: CellContext): Promise { + doRun(context: CellContext): Promise { try { context.model.deleteCell(context.cell); } catch (error) { @@ -110,7 +110,7 @@ export class ClearCellOutputAction extends CellActionBase { super(id, label, undefined, notificationService); } - runCellAction(context: CellContext): Promise { + doRun(context: CellContext): Promise { try { (context.model.activeCell as CellModel).clearOutputs(); } catch (error) { diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index 62108121c0..2c6a111790 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -153,7 +153,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange protected initActionBar() { let context = new CellContext(this.model, this.cellModel); - let runCellAction = this._instantiationService.createInstance(RunCellAction); + let runCellAction = this._instantiationService.createInstance(RunCellAction, context); let taskbar = this.toolbarElement.nativeElement; this._actionBar = new Taskbar(taskbar, this.contextMenuService); diff --git a/src/sql/parts/notebook/cellViews/codeActions.ts b/src/sql/parts/notebook/cellViews/codeActions.ts index 52f319b98a..b1f3b45940 100644 --- a/src/sql/parts/notebook/cellViews/codeActions.ts +++ b/src/sql/parts/notebook/cellViews/codeActions.ts @@ -8,12 +8,12 @@ import { nb } from 'sqlops'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; -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 { ICellModel, FutureInternal } from 'sql/parts/notebook/models/modelInterfaces'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { ToggleableAction } from 'sql/parts/notebook/notebookActions'; +import { IDisposable } from 'vs/base/common/lifecycle'; let notebookMoreActionMsg = localize('notebook.failed', "Please select active cell and try again"); @@ -48,90 +48,62 @@ export abstract class CellActionBase extends Action { public run(context: CellContext): TPromise { if (hasModelAndCell(context, this.notificationService)) { - return TPromise.wrap(this.runCellAction(context).then(() => true)); + return TPromise.wrap(this.doRun(context).then(() => true)); } return TPromise.as(true); } - abstract runCellAction(context: CellContext): Promise; + abstract doRun(context: CellContext): Promise; } export class RunCellAction extends ToggleableAction { public static ID = 'notebook.runCell'; public static LABEL = 'Run cell'; - - constructor(@INotificationService private notificationService: INotificationService) { + private _executionChangedDisposable: IDisposable; + private _context: CellContext; + constructor(context: CellContext, @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 + toggleOffLabel: localize('runCell', 'Run cell'), + toggleOffClass: 'toolbarIconRun', + toggleOnLabel: localize('stopCell', 'Cancel execution'), + toggleOnClass: 'toolbarIconStop', + // On == running + isOn: false }); + this.ensureContextIsUpdated(context); } - public run(context: CellContext): TPromise { - if (hasModelAndCell(context, this.notificationService)) { - return TPromise.wrap(this.runCellAction(context).then(() => true)); + private _handleExecutionStateChange(running: boolean): void { + this.toggle(running); + } + + public run(context?: CellContext): TPromise { + return TPromise.wrap(this.doRun(context).then(() => true)); + } + + public async doRun(context: CellContext): Promise { + this.ensureContextIsUpdated(context); + if (!this._context) { + // TODO should we error? + return; + } + try { + await this._context.cell.runCell(this.notificationService); + } catch (error) { + let message = getErrorMessage(error); + this.notificationService.error(message); } - 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; + private ensureContextIsUpdated(context: CellContext) { + if (context && context !== this._context) { + if (this._executionChangedDisposable) { + this._executionChangedDisposable.dispose(); } - // 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); + this._context = context; + this.toggle(context.cell.isRunning); + this._executionChangedDisposable = this._context.cell.onExecutionStateChange(this._handleExecutionStateChange, this); } - } - - 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; - } + } } diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts index 0d9a37eec5..f3577707df 100644 --- a/src/sql/parts/notebook/models/cell.ts +++ b/src/sql/parts/notebook/models/cell.ts @@ -6,16 +6,18 @@ 'use strict'; +import { nb } from 'sqlops'; + import { Event, Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; -import { nb } from 'sqlops'; 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'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; - +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; let modelId = 0; @@ -28,6 +30,7 @@ export class CellModel implements ICellModel { private _isEditMode: boolean; private _onOutputsChanged = new Emitter>(); private _onCellModeChanged = new Emitter(); + private _onExecutionStateChanged = new Emitter(); public id: string; private _isTrusted: boolean; private _active: boolean; @@ -131,6 +134,89 @@ export class CellModel implements ICellModel { this._language = newLanguage; } + public get onExecutionStateChange(): Event { + return this._onExecutionStateChanged.event; + } + + public get isRunning(): boolean { + return !!(this._future && this._future.inProgress); + } + + public async runCell(notificationService?: INotificationService): Promise { + try { + if (this.cellType !== CellTypes.Code) { + // TODO should change hidden state to false if we add support + // for this property + return false; + } + let kernel = await this.getOrStartKernel(notificationService); + if (!kernel) { + return false; + } + // If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt() + // This matches the same behavior as JupyterLab + if (this.future && this.future.inProgress) { + this.future.inProgress = false; + await kernel.interrupt(); + } else { + // TODO update source based on editor component contents + let content = this.source; + if (content) { + this._onExecutionStateChanged.fire(true); + let future = await kernel.requestExecute({ + code: content, + stop_on_error: true + }, false); + this.setFuture(future as FutureInternal); + // For now, await future completion. Later we should just track and handle cancellation based on model notifications + let result: nb.IExecuteReplyMsg = await future.done; + return result && result.content.status === 'ok' ? true : false; + } + } + } catch (error) { + if (error.message === 'Canceled') { + // swallow the error + } + let message = notebookUtils.getErrorMessage(error); + this.sendNotification(notificationService, Severity.Error, message); + throw error; + } finally { + this._onExecutionStateChanged.fire(false); + } + + return true; + } + + private async getOrStartKernel(notificationService: INotificationService): Promise { + let model = this.options.notebook; + let clientSession = model && model.clientSession; + if (!clientSession) { + this.sendNotification(notificationService, Severity.Error, localize('notebookNotReady', 'The session for this notebook is not yet ready')); + return undefined; + } else if (!clientSession.isReady || clientSession.status === 'dead') { + + this.sendNotification(notificationService, Severity.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.sendNotification(notificationService, Severity.Error, localize('noDefaultKernel', 'No kernel is available for this notebook')); + return undefined; + } + await clientSession.changeKernel({ + name: defaultKernel + }); + } + return clientSession.kernel; + } + + private sendNotification(notificationService: INotificationService, severity: Severity, message: string): void { + if (notificationService) { + notificationService.notify({ severity: severity, message: message}); + } + } + /** * Sets the future which will be used to update the output * area for this cell @@ -178,6 +264,11 @@ export class CellModel implements ICellModel { // TODO #931 we should process this. There can be a payload attached which should be added to outputs. // In all other cases, it is a no-op let output: nb.ICellOutput = msg.content as nb.ICellOutput; + + if (!this._future.inProgress) { + this._future.dispose(); + this._future = undefined; + } } private handleIOPub(msg: nb.IIOPubMessage): void { @@ -223,9 +314,6 @@ export class CellModel implements ICellModel { this._outputs.push(this.rewriteOutputUrls(output)); this.fireOutputsChanged(); } - if (!this._future.inProgress) { - this._future.dispose(); - } } private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput { diff --git a/src/sql/parts/notebook/models/clientSession.ts b/src/sql/parts/notebook/models/clientSession.ts index 082f47b497..6a63ebceb6 100644 --- a/src/sql/parts/notebook/models/clientSession.ts +++ b/src/sql/parts/notebook/models/clientSession.ts @@ -20,6 +20,7 @@ import * as notebookUtils from '../notebookUtils'; import { INotebookManager } from 'sql/workbench/services/notebook/common/notebookService'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +type KernelChangeHandler = (kernel: nb.IKernelChangedArgs) => Promise; /** * Implementation of a client session. This is a model over session operations, * which may come from the session manager or a specific session. @@ -41,6 +42,9 @@ export class ClientSession implements IClientSession { private _kernelPreference: IKernelPreference; private _kernelDisplayName: string; private _errorMessage: string; + private _cachedKernelSpec: nb.IKernelSpec; + private _kernelChangeHandlers: KernelChangeHandler[] = []; + //#endregion private _serverLoadFinished: Promise; @@ -62,6 +66,7 @@ export class ClientSession implements IClientSession { this._serverLoadFinished = this.startServer(); await this._serverLoadFinished; await this.initializeSession(); + await this.updateCachedKernelSpec(); } catch (err) { this._errorMessage = notebookUtils.getErrorMessage(err); } @@ -150,6 +155,12 @@ export class ClientSession implements IClientSession { public get kernelChanged(): Event { return this._kernelChangedEmitter.event; } + + public onKernelChanging(changeHandler: (kernel: nb.IKernelChangedArgs) => Promise): void { + if (changeHandler) { + this._kernelChangeHandlers.push(changeHandler); + } + } public get statusChanged(): Event { return this._statusChangedEmitter.event; } @@ -204,6 +215,10 @@ export class ClientSession implements IClientSession { public get isInErrorState(): boolean { return !!this._errorMessage; } + + public get cachedKernelSpec(): nb.IKernelSpec { + return this._cachedKernelSpec; + } //#endregion //#region Not Yet Implemented @@ -227,15 +242,28 @@ export class ClientSession implements IClientSession { } newKernel = this._session ? kernel : this._session.kernel; this._isReady = kernel.isReady; + await this.updateCachedKernelSpec(); // Send resolution events to listeners - this._kernelChangeCompleted.resolve(); - this._kernelChangedEmitter.fire({ + let changeArgs: nb.IKernelChangedArgs = { oldValue: oldKernel, newValue: newKernel - }); + }; + let changePromises = this._kernelChangeHandlers.map(handler => handler(changeArgs)); + await Promise.all(changePromises); + // Wait on connection configuration to complete before resolving full kernel change + this._kernelChangeCompleted.resolve(); + this._kernelChangedEmitter.fire(changeArgs); return kernel; } + private async updateCachedKernelSpec(): Promise { + this._cachedKernelSpec = undefined; + let kernel = this.kernel; + if (kernel && kernel.isReady) { + this._cachedKernelSpec = await this.kernel.getSpec(); + } + } + /** * Helper method to either call ChangeKernel on current session, or start a new session * @param options diff --git a/src/sql/parts/notebook/models/contracts.ts b/src/sql/parts/notebook/models/contracts.ts index 9a918d5730..eac52e4de8 100644 --- a/src/sql/parts/notebook/models/contracts.ts +++ b/src/sql/parts/notebook/models/contracts.ts @@ -43,5 +43,6 @@ export enum NotebookChangeType { CellDeleted, CellSourceUpdated, CellOutputUpdated, - DirtyStateChanged + DirtyStateChanged, + KernelChanged } diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index 3283c0f881..07c2a9cb4e 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -127,6 +127,8 @@ export interface IClientSession extends IDisposable { */ readonly kernelDisplayName: string; + readonly cachedKernelSpec: nb.IKernelSpec; + /** * Initializes the ClientSession, by starting the server and * connecting to the SessionManager. @@ -200,7 +202,13 @@ export interface IClientSession extends IDisposable { /** * Updates the connection */ - updateConnection(connection: IConnectionProfile): void; + updateConnection(connection: IConnectionProfile): Promise; + + /** + * Supports registering a handler to run during kernel change and implement any calls needed to configure + * the kernel before actions such as run should be allowed + */ + onKernelChanging(changeHandler: ((kernel: nb.IKernelChangedArgs) => Promise)): void; } export interface IDefaultConnection { @@ -322,7 +330,7 @@ export interface INotebookModel { /** * Change the current context (if applicable) */ - changeContext(host: string, connection?: IConnectionProfile): void; + changeContext(host: string, connection?: IConnectionProfile): Promise; /** * Find a cell's index given its model @@ -400,7 +408,10 @@ export interface ICellModel { readonly future: FutureInternal; readonly outputs: ReadonlyArray; readonly onOutputsChanged: Event>; + readonly onExecutionStateChange: Event; setFuture(future: FutureInternal): void; + readonly isRunning: boolean; + runCell(notificationService?: INotificationService): Promise; equals(cellModel: ICellModel): boolean; toJSON(): nb.ICellContents; } diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts index 1842b09d1c..85667c2fff 100644 --- a/src/sql/parts/notebook/models/notebookModel.ts +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -255,7 +255,7 @@ export class NotebookModel extends Disposable implements INotebookModel { } } - public findCellIndex(cellModel: CellModel): number { + public findCellIndex(cellModel: ICellModel): number { return this._cells.findIndex((cell) => cell.equals(cellModel)); } @@ -420,7 +420,7 @@ export class NotebookModel extends Disposable implements INotebookModel { return Promise.resolve(); } - public changeContext(server: string, newConnection?: IConnectionProfile): void { + public async changeContext(server: string, newConnection?: IConnectionProfile): Promise { try { if (!newConnection) { newConnection = this._activeContexts.otherConnections.find((connection) => connection.serverName === server); @@ -431,7 +431,7 @@ export class NotebookModel extends Disposable implements INotebookModel { let newConnectionProfile = new ConnectionProfile(this.notebookOptions.capabilitiesService, newConnection); this._activeConnection = newConnectionProfile; this.refreshConnections(newConnectionProfile); - this._activeClientSession.updateConnection(this._activeConnection); + await this._activeClientSession.updateConnection(this._activeConnection); } catch (err) { let msg = notebookUtils.getErrorMessage(err); this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg)); @@ -454,7 +454,7 @@ export class NotebookModel extends Disposable implements INotebookModel { private loadKernelInfo(): void { this._clientSessions.forEach(clientSession => { - clientSession.kernelChanged(async (e) => { + clientSession.onKernelChanging(async (e) => { await this.loadActiveContexts(e); }); }); @@ -550,7 +550,7 @@ export class NotebookModel extends Disposable implements INotebookModel { this._activeContexts = await NotebookContexts.getContextsForKernel(this.notebookOptions.connectionService, this.getApplicableConnectionProviderIds(kernelDisplayName), kernelChangedArgs, this.connectionProfile); this._contextsChangedEmitter.fire(); if (this.contexts.defaultConnection !== undefined && this.contexts.defaultConnection.serverName !== undefined) { - this.changeContext(this.contexts.defaultConnection.serverName); + await this.changeContext(this.contexts.defaultConnection.serverName); } } } diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index af00049e83..19d22e6768 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -123,7 +123,6 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe ngOnInit() { this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this.updateTheme(this.themeService.getColorTheme()); - this.notebookService.addNotebookEditor(this); this.initActionBar(); this.doLoad(); } @@ -135,7 +134,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } } - public get model(): NotebookModel { + public get model(): NotebookModel | null { return this._model; } @@ -222,6 +221,9 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe this.setViewInErrorState(localize('displayFailed', 'Could not display contents: {0}', error)); this.setLoading(false); this._modelReadyDeferred.reject(error); + } finally { + // Always add the editor for now to close loop, even if loading contents failed + this.notebookService.addNotebookEditor(this); } } @@ -538,4 +540,15 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe this._model.pushEditOperations(edits); return true; } + + public async runCell(cell: ICellModel): Promise { + await this.modelReady; + let uriString = cell.cellUri.toString(); + if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) { + return cell.runCell(this.notificationService); + } else { + return Promise.reject(new Error(localize('cellNotFound', 'cell with URI {0} was not found in this model', uriString))); + } + } + } diff --git a/src/sql/parts/notebook/notebookActions.ts b/src/sql/parts/notebook/notebookActions.ts index 4560414260..a2f13b9222 100644 --- a/src/sql/parts/notebook/notebookActions.ts +++ b/src/sql/parts/notebook/notebookActions.ts @@ -230,7 +230,8 @@ export class AttachToDropdown extends SelectBox { this.model = model; model.contextsChanged(() => { if (this.model.clientSession.kernel && this.model.clientSession.kernel.name) { - let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name === this.model.clientSession.kernel.name); + let nameLower = this.model.clientSession.kernel.name.toLowerCase(); + let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name && kernel.name.toLowerCase() === nameLower); this.loadAttachToDropdown(this.model, currentKernelSpec.display_name); } }); @@ -282,7 +283,7 @@ export class AttachToDropdown extends SelectBox { if (this.value === msgAddNewConnection) { this.openConnectionDialog(); } else { - this.model.changeContext(this.value, connection); + this.model.changeContext(this.value, connection).then(ok => undefined, err => this._notificationService.error(getErrorMessage(err))); } } diff --git a/src/sql/platform/connection/common/providerConnectionInfo.ts b/src/sql/platform/connection/common/providerConnectionInfo.ts index 133effdf8d..4ded24f95a 100644 --- a/src/sql/platform/connection/common/providerConnectionInfo.ts +++ b/src/sql/platform/connection/common/providerConnectionInfo.ts @@ -14,6 +14,8 @@ import * as Constants from 'sql/platform/connection/common/constants'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { ConnectionProviderProperties } from 'sql/workbench/parts/connection/common/connectionProviderExtension'; +type SettableProperty = 'serverName' | 'authenticationType' | 'databaseName' | 'password' | 'connectionName' | 'userName'; + export class ProviderConnectionInfo extends Disposable implements sqlops.ConnectionInfo { options: { [name: string]: any } = {}; @@ -39,16 +41,30 @@ export class ProviderConnectionInfo extends Disposable implements sqlops.Connect this.options[option.name] = value; }); } - this.serverName = model.serverName; - this.authenticationType = model.authenticationType; - this.databaseName = model.databaseName; - this.password = model.password; - this.userName = model.userName; - this.connectionName = model.connectionName; + + this.updateSpecialValueType('serverName', model); + this.updateSpecialValueType('authenticationType', model); + this.updateSpecialValueType('databaseName', model); + this.updateSpecialValueType('password', model); + this.updateSpecialValueType('userName', model); + this.updateSpecialValueType('connectionName', model); } } } + + /** + * Updates one of the special value types (serverName, authenticationType, etc.) if this doesn't already + * have a value in the options map. + * + * This handles the case where someone hasn't passed in a valid property bag, but doesn't cause errors when + */ + private updateSpecialValueType(typeName: SettableProperty, model: sqlops.IConnectionProfile): void { + if (!this[typeName]) { + this[typeName] = model[typeName]; + } + } + public get providerName(): string { return this._providerName; } diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 4add87dc42..822940d91a 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1488,6 +1488,12 @@ declare module 'sqlops' { */ readonly cells: NotebookCell[]; + /** + * The spec for current kernel, if applicable. This will be undefined + * until a kernel has been started + */ + readonly kernelSpec: IKernelSpec; + /** * Save the underlying file. * @@ -1558,6 +1564,15 @@ declare module 'sqlops' { * @return A promise that resolves with a value indicating if the edits could be applied. */ edit(callback: (editBuilder: NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + + /** + * Kicks off execution of a cell. Thenable will resolve only once the full execution is completed. + * + * + * @param cell A cell in this notebook which should be executed + * @return A promise that resolves with a value indicating if the cell was run or not. + */ + runCell(cell: NotebookCell): Thenable; } export interface NotebookCell { @@ -1801,7 +1816,7 @@ declare module 'sqlops' { export interface ICellContents { cell_type: CellType; source: string | string[]; - metadata: { + metadata?: { language?: string; }; execution_count?: number; @@ -2189,7 +2204,6 @@ declare module 'sqlops' { handle(message: T): void | Thenable; } - /** * A Future interface for responses from the kernel. * @@ -2285,6 +2299,20 @@ declare module 'sqlops' { sendInputReply(content: IInputReply): void; } + export interface IExecuteReplyMsg extends IShellMessage { + content: IExecuteReply; + } + + /** + * The content of an `execute-reply` message. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#execution-results). + */ + export interface IExecuteReply { + status: 'ok' | 'error' | 'abort'; + execution_count: number | null; + } + /** * The valid channel names. */ diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 63485a607d..c7597cbba4 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -470,6 +470,7 @@ export enum FutureMessageType { export interface INotebookFutureDone { succeeded: boolean; rejectReason: string; + message: nb.IShellMessage; } export interface ICellRange { diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts index 7907ddb4bc..aec281fe81 100644 --- a/src/sql/workbench/api/node/extHostNotebook.ts +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -164,7 +164,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { private hookFutureDone(futureId: number, future: sqlops.nb.IFuture): void { future.done.then(success => { - return this._proxy.$onFutureDone(futureId, { succeeded: true, rejectReason: undefined }); + return this._proxy.$onFutureDone(futureId, { succeeded: true, message: success, rejectReason: undefined }); }, err => { let rejectReason: string; if (typeof err === 'string') { @@ -176,7 +176,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape { else { rejectReason = err; } - return this._proxy.$onFutureDone(futureId, { succeeded: false, rejectReason: rejectReason }); + return this._proxy.$onFutureDone(futureId, { succeeded: false, message: undefined, rejectReason: rejectReason }); }); } diff --git a/src/sql/workbench/api/node/extHostNotebookDocumentData.ts b/src/sql/workbench/api/node/extHostNotebookDocumentData.ts index db7c9b7d6e..39c9446bae 100644 --- a/src/sql/workbench/api/node/extHostNotebookDocumentData.ts +++ b/src/sql/workbench/api/node/extHostNotebookDocumentData.ts @@ -19,6 +19,7 @@ import { CellRange } from 'sql/workbench/api/common/sqlExtHostTypes'; export class ExtHostNotebookDocumentData implements IDisposable { private _document: sqlops.nb.NotebookDocument; private _isDisposed: boolean = false; + private _kernelSpec: sqlops.nb.IKernelSpec; constructor(private readonly _proxy: MainThreadNotebookDocumentsAndEditorsShape, private readonly _uri: URI, @@ -49,6 +50,7 @@ export class ExtHostNotebookDocumentData implements IDisposable { get isClosed() { return data._isDisposed; }, get isDirty() { return data._isDirty; }, get cells() { return data._cells; }, + get kernelSpec() { return data._kernelSpec; }, save() { return data._save(); }, validateCellRange(range) { return data._validateRange(range); }, }; @@ -69,6 +71,7 @@ export class ExtHostNotebookDocumentData implements IDisposable { this._isDirty = data.isDirty; this._cells = data.cells; this._providerId = data.providerId; + this._kernelSpec = data.kernelSpec; } } diff --git a/src/sql/workbench/api/node/extHostNotebookEditor.ts b/src/sql/workbench/api/node/extHostNotebookEditor.ts index ac9abd1995..fb06b27830 100644 --- a/src/sql/workbench/api/node/extHostNotebookEditor.ts +++ b/src/sql/workbench/api/node/extHostNotebookEditor.ts @@ -152,7 +152,11 @@ export class ExtHostNotebookEditor implements sqlops.nb.NotebookEditor, IDisposa return this._id; } - edit(callback: (editBuilder: sqlops.nb.NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable { + public runCell(cell: sqlops.nb.NotebookCell): Thenable { + return this._proxy.$runCell(this._id, cell.uri); + } + + public edit(callback: (editBuilder: sqlops.nb.NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable { if (this._disposed) { return TPromise.wrapError(new Error('NotebookEditor#edit not possible on closed editors')); } diff --git a/src/sql/workbench/api/node/mainThreadNotebook.ts b/src/sql/workbench/api/node/mainThreadNotebook.ts index d444d820bd..6e74e89000 100644 --- a/src/sql/workbench/api/node/mainThreadNotebook.ts +++ b/src/sql/workbench/api/node/mainThreadNotebook.ts @@ -415,7 +415,7 @@ class FutureWrapper implements FutureInternal { public onDone(done: INotebookFutureDone): void { this._inProgress = false; if (done.succeeded) { - this._done.resolve(); + this._done.resolve(done.message); } else { this._done.reject(new Error(done.rejectReason)); } diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts index d888be3404..4a51ac9231 100644 --- a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -28,7 +28,8 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { getProvidersForFileName, getStandardKernelsForProvider } from 'sql/parts/notebook/notebookUtils'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; import { disposed } from 'vs/base/common/errors'; -import { ICellModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces'; +import { ICellModel, NotebookContentChange, INotebookModel } from 'sql/parts/notebook/models/modelInterfaces'; +import { NotebookChangeType } from 'sql/parts/notebook/models/contracts'; class MainThreadNotebookEditor extends Disposable { private _contentChangedEmitter = new Emitter(); @@ -38,6 +39,12 @@ class MainThreadNotebookEditor extends Disposable { super(); editor.modelReady.then(model => { this._register(model.contentChanged((e) => this._contentChangedEmitter.fire(e))); + this._register(model.kernelChanged((e) => { + let changeEvent: NotebookContentChange = { + changeType: NotebookChangeType.KernelChanged + }; + this._contentChangedEmitter.fire(changeEvent); + })); }); } @@ -65,6 +72,10 @@ class MainThreadNotebookEditor extends Disposable { return this.editor.cells; } + public get model(): INotebookModel | null { + return this.editor.model; + } + public save(): Thenable { return this.editor.save(); } @@ -100,6 +111,14 @@ class MainThreadNotebookEditor extends Disposable { // } return true; } + + public runCell(cell: ICellModel): Promise { + if (!this.editor) { + return Promise.resolve(false); + } + + return this.editor.runCell(cell); + } } function wait(timeMs: number): Promise { @@ -266,7 +285,6 @@ class MainThreadNotebookDocumentAndEditorStateComputer extends Disposable { @extHostNamedCustomer(SqlMainContext.MainThreadNotebookDocumentsAndEditors) export class MainThreadNotebookDocumentsAndEditors extends Disposable implements MainThreadNotebookDocumentsAndEditorsShape { - private _proxy: ExtHostNotebookDocumentsAndEditorsShape; private _notebookEditors = new Map(); private _modelToDisposeMap = new Map(); @@ -308,6 +326,22 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements } return TPromise.as(editor.applyEdits(modelVersionId, edits, opts)); } + + $runCell(id: string, cellUri: UriComponents): TPromise { + // Requires an editor and the matching cell in that editor + let editor = this.getEditor(id); + if (!editor) { + return TPromise.wrapError(disposed(`TextEditor(${id})`)); + } + let uriString = URI.revive(cellUri).toString(); + let cell = editor.cells.find(c => c.cellUri.toString() === uriString); + if (!cell) { + return TPromise.wrapError(disposed(`TextEditorCell(${uriString})`)); + } + + return TPromise.wrap(editor.runCell(cell)); + } + //#endregion private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise { @@ -480,11 +514,17 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements isDirty: e.isDirty, providerId: editor.providerId, providers: editor.providers, - uri: editor.uri + uri: editor.uri, + kernelSpec: this.getKernelSpec(editor) }; return changeData; } + private getKernelSpec(editor: MainThreadNotebookEditor): sqlops.nb.IKernelSpec { + let spec = editor && editor.model && editor.model.clientSession ? editor.model.clientSession.cachedKernelSpec : undefined; + return spec; + } + private convertCellModelToNotebookCell(cells: ICellModel | ICellModel[]): sqlops.nb.NotebookCell[] { let notebookCells: sqlops.nb.NotebookCell[] = []; if (Array.isArray(cells)) { @@ -497,8 +537,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements metadata: { language: cell.language }, - source: undefined - + source: undefined, + outputs: [...cell.outputs] } }); } diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index d3fb541d9b..9617c2839e 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -830,6 +830,7 @@ export interface INotebookModelChangedData { providers: string[]; isDirty: boolean; cells: sqlops.nb.NotebookCell[]; + kernelSpec: sqlops.nb.IKernelSpec; } export interface INotebookEditorAddData { @@ -856,6 +857,7 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable $trySaveDocument(uri: UriComponents): Thenable; $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): TPromise; $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): TPromise; + $runCell(id: string, cellUri: UriComponents): TPromise; } export interface ExtHostExtensionManagementShape { diff --git a/src/sql/workbench/services/notebook/common/notebookService.ts b/src/sql/workbench/services/notebook/common/notebookService.ts index e2e6dfe43d..351d23036f 100644 --- a/src/sql/workbench/services/notebook/common/notebookService.ts +++ b/src/sql/workbench/services/notebook/common/notebookService.ts @@ -100,9 +100,11 @@ export interface INotebookEditor { readonly id: string; readonly cells?: ICellModel[]; readonly modelReady: Promise; + readonly model: INotebookModel | null; isDirty(): boolean; isActive(): boolean; isVisible(): boolean; save(): Promise; executeEdits(edits: ISingleNotebookEditOperation[]): boolean; + runCell(cell: ICellModel): Promise; } \ No newline at end of file diff --git a/src/sqltest/parts/notebook/common.ts b/src/sqltest/parts/notebook/common.ts index caaeaddaee..be189ab7fe 100644 --- a/src/sqltest/parts/notebook/common.ts +++ b/src/sqltest/parts/notebook/common.ts @@ -62,7 +62,7 @@ export class NotebookModelStub implements INotebookModel { changeKernel(displayName: string): void { throw new Error('Method not implemented.'); } - changeContext(host: string, connection?: IConnectionProfile): void { + changeContext(host: string, connection?: IConnectionProfile): Promise { throw new Error('Method not implemented.'); } findCellIndex(cellModel: ICellModel): number {