diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 78cc3f454b..9531be9010 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -54,10 +54,8 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti uri = getNotebookEditorUri(input); if(uri){ //TODO: We need to pass in notebook data either through notebook input or notebook service - let notebookData: string = fs.readFileSync(uri.fsPath); let fileName: string = input? input.getName() : 'untitled'; - let filePath: string = uri.fsPath; - let notebookInputModel = new NotebookInputModel(filePath, undefined, undefined); + let notebookInputModel = new NotebookInputModel(uri, undefined, undefined); //TO DO: Second paramter has to be the content. let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); return notebookInput; diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index 582b7aabc7..462798818c 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -11,7 +11,6 @@ import { AngularDisposable } from 'sql/base/common/lifecycle'; import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; -import { ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; @@ -26,6 +25,7 @@ import { Schemas } from 'vs/base/common/network'; import * as DOM from 'vs/base/browser/dom'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const CODE_SELECTOR: string = 'code-component'; diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts index 522f71ca4b..5e02d19dc7 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.ts +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -7,10 +7,11 @@ import 'vs/css!./codeCell'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; -import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; +import { CellView } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const CODE_SELECTOR: string = 'code-cell-component'; diff --git a/src/sql/parts/notebook/cellViews/interfaces.ts b/src/sql/parts/notebook/cellViews/interfaces.ts index 6ffe170a98..dcf46d6887 100644 --- a/src/sql/parts/notebook/cellViews/interfaces.ts +++ b/src/sql/parts/notebook/cellViews/interfaces.ts @@ -5,7 +5,6 @@ import { OnDestroy } from '@angular/core'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; export abstract class CellView extends AngularDisposable implements OnDestroy { constructor() { @@ -14,20 +13,3 @@ export abstract class CellView extends AngularDisposable implements OnDestroy { public abstract layout(): void; } - -export interface ICellModel { - id: string; - language: string; - source: string; - cellType: CellType; - active: boolean; - cellUri?: URI; -} - -export type CellType = 'code' | 'markdown' | 'raw'; - -export class CellTypes { - public static readonly Code = 'code'; - public static readonly Markdown = 'markdown'; - public static readonly Raw = 'raw'; -} diff --git a/src/sql/parts/notebook/cellViews/textCell.component.ts b/src/sql/parts/notebook/cellViews/textCell.component.ts index 2f5e64be7f..7ff14e4e06 100644 --- a/src/sql/parts/notebook/cellViews/textCell.component.ts +++ b/src/sql/parts/notebook/cellViews/textCell.component.ts @@ -7,11 +7,12 @@ import 'vs/css!./textCell'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; -import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; +import { CellView } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const TEXT_SELECTOR: string = 'text-cell-component'; diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts new file mode 100644 index 0000000000..0d277f9527 --- /dev/null +++ b/src/sql/parts/notebook/models/cell.ts @@ -0,0 +1,321 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +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 * as notebookUtils from '../notebookUtils'; +import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; + +let modelId = 0; + + +export class CellModel implements ICellModel { + private static LanguageMapping: Map; + + private _cellType: nb.CellType; + private _source: string; + private _language: string; + private _future: nb.IFuture; + private _outputs: nb.ICellOutput[] = []; + private _isEditMode: boolean; + private _onOutputsChanged = new Emitter>(); + private _onCellModeChanged = new Emitter(); + public id: string; + private _isTrusted: boolean; + private _active: boolean; + private _cellUri: URI; + + constructor(private factory: IModelFactory, cellData?: nb.ICell, private _options?: ICellModelOptions) { + this.id = `${modelId++}`; + CellModel.CreateLanguageMappings(); + // Do nothing for now + if (cellData) { + this.fromJSON(cellData); + } else { + this._cellType = CellTypes.Code; + this._source = ''; + } + this._isEditMode = this._cellType !== CellTypes.Markdown; + this.setDefaultLanguage(); + if (_options && _options.isTrusted) { + this._isTrusted = true; + } else { + this._isTrusted = false; + } + } + + public equals(other: ICellModel) { + return other && other.id === this.id; + } + + public get onOutputsChanged(): Event> { + return this._onOutputsChanged.event; + } + + public get onCellModeChanged(): Event { + return this._onCellModeChanged.event; + } + + public get isEditMode(): boolean { + return this._isEditMode; + } + + public get future(): nb.IFuture { + return this._future; + } + + public set isEditMode(isEditMode: boolean) { + this._isEditMode = isEditMode; + this._onCellModeChanged.fire(this._isEditMode); + // Note: this does not require a notebook update as it does not change overall state + } + + public get trustedMode(): boolean { + return this._isTrusted; + } + + public set trustedMode(isTrusted: boolean) { + if (this._isTrusted !== isTrusted) { + this._isTrusted = isTrusted; + this._onOutputsChanged.fire(this._outputs); + } + } + + public get active(): boolean { + return this._active; + } + + public set active(value: boolean) { + this._active = value; + } + + public get cellUri(): URI { + return this._cellUri; + } + + public set cellUri(value: URI) { + this._cellUri = value; + } + + public get options(): ICellModelOptions { + return this._options; + } + + public get cellType(): CellType { + return this._cellType; + } + + public get source(): string { + return this._source; + } + + public set source(newSource: string) { + if (this._source !== newSource) { + this._source = newSource; + this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated); + } + } + + public get language(): string { + return this._language; + } + + public set language(newLanguage: string) { + this._language = newLanguage; + } + + /** + * Sets the future which will be used to update the output + * area for this cell + */ + setFuture(future: nb.IFuture): void { + if (this._future === future) { + // Nothing to do + return; + } + // Setting the future indicates the cell is running which enables trusted mode. + // See https://jupyter-notebook.readthedocs.io/en/stable/security.html + + this._isTrusted = true; + + if (this._future) { + this._future.dispose(); + } + this.clearOutputs(); + this._future = future; + future.setReplyHandler({ handle: (msg) => this.handleReply(msg) }); + future.setIOPubHandler({ handle: (msg) => this.handleIOPub(msg) }); + } + + private clearOutputs(): void { + this._outputs = []; + this.fireOutputsChanged(); + } + + private fireOutputsChanged(): void { + this._onOutputsChanged.fire(this.outputs); + this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated); + } + + private sendChangeToNotebook(change: NotebookChangeType): void { + if (this._options && this._options.notebook) { + this._options.notebook.onCellChange(this, change); + } + } + + public get outputs(): ReadonlyArray { + return this._outputs; + } + + private handleReply(msg: nb.IShellMessage): void { + // 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; + } + + private handleIOPub(msg: nb.IIOPubMessage): void { + let msgType = msg.header.msg_type; + let displayId = this.getDisplayId(msg); + let output: nb.ICellOutput; + switch (msgType) { + case 'execute_result': + case 'display_data': + case 'stream': + case 'error': + output = msg.content as nb.ICellOutput; + output.output_type = msgType; + break; + case 'clear_output': + // TODO wait until next message before clearing + // let wait = (msg as KernelMessage.IClearOutputMsg).content.wait; + this.clearOutputs(); + break; + case 'update_display_data': + output = msg.content as nb.ICellOutput; + output.output_type = 'display_data'; + // TODO #930 handle in-place update of displayed data + // targets = this._displayIdMap.get(displayId); + // if (targets) { + // for (let index of targets) { + // model.set(index, output); + // } + // } + break; + default: + break; + } + // TODO handle in-place update of displayed data + // if (displayId && msgType === 'display_data') { + // targets = this._displayIdMap.get(displayId) || []; + // targets.push(model.length - 1); + // this._displayIdMap.set(displayId, targets); + // } + if (output) { + this._outputs.push(output); + this.fireOutputsChanged(); + } + } + + private getDisplayId(msg: nb.IIOPubMessage): string | undefined { + let transient = (msg.content.transient || {}); + return transient['display_id'] as string; + } + + public toJSON(): nb.ICell { + let cellJson: Partial = { + cell_type: this._cellType, + source: this._source, + metadata: { + } + }; + if (this._cellType === CellTypes.Code) { + cellJson.metadata.language = this._language, + cellJson.outputs = this._outputs; + cellJson.execution_count = 1; // TODO: keep track of actual execution count + + } + return cellJson as nb.ICell; + } + + public fromJSON(cell: nb.ICell): void { + if (!cell) { + return; + } + this._cellType = cell.cell_type; + this._source = Array.isArray(cell.source) ? cell.source.join('') : cell.source; + this._language = (cell.metadata && cell.metadata.language) ? cell.metadata.language : 'python'; + if (cell.outputs) { + for (let output of cell.outputs) { + // For now, we're assuming it's OK to save these as-is with no modification + this.addOutput(output); + } + } + } + + private addOutput(output: nb.ICellOutput) { + this._normalize(output); + this._outputs.push(output); + } + + /** + * Normalize an output. + */ + private _normalize(value: nb.ICellOutput): void { + if (notebookUtils.isStream(value)) { + if (Array.isArray(value.text)) { + value.text = (value.text as string[]).join('\n'); + } + } + } + + private static CreateLanguageMappings(): void { + if (CellModel.LanguageMapping) { + return; + } + CellModel.LanguageMapping = new Map(); + CellModel.LanguageMapping['pyspark'] = 'python'; + CellModel.LanguageMapping['pyspark3'] = 'python'; + CellModel.LanguageMapping['python'] = 'python'; + CellModel.LanguageMapping['scala'] = 'scala'; + } + + private get languageInfo(): nb.ILanguageInfo { + if (this._options && this._options.notebook && this._options.notebook.languageInfo) { + return this._options.notebook.languageInfo; + } + return undefined; + } + + private setDefaultLanguage(): void { + this._language = 'python'; + // In languageInfo, set the language to the "name" property + // If the "name" property isn't defined, check the "mimeType" property + // Otherwise, default to python as the language + let languageInfo = this.languageInfo; + if (languageInfo) { + if (languageInfo.name) { + // check the LanguageMapping to determine if a mapping is necessary (example 'pyspark' -> 'python') + if (CellModel.LanguageMapping[languageInfo.name]) { + this._language = CellModel.LanguageMapping[languageInfo.name]; + } else { + this._language = languageInfo.name; + } + } else if (languageInfo.mimetype) { + this._language = languageInfo.mimetype; + } + } + let mimeTypePrefix = 'x-'; + if (this._language.includes(mimeTypePrefix)) { + this._language = this._language.replace(mimeTypePrefix, ''); + } + } +} diff --git a/src/sql/parts/notebook/models/clientSession.ts b/src/sql/parts/notebook/models/clientSession.ts new file mode 100644 index 0000000000..9869d62e01 --- /dev/null +++ b/src/sql/parts/notebook/models/clientSession.ts @@ -0,0 +1,360 @@ + +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx + +'use strict'; + +import { nb } from 'sqlops'; +import * as nls from 'vs/nls'; +import URI from 'vs/base/common/uri'; +import { Event, Emitter } from 'vs/base/common/event'; + +import { IClientSession, IKernelPreference, IClientSessionOptions } from './modelInterfaces'; +import { Deferred } from 'sql/base/common/promise'; + +import * as notebookUtils from '../notebookUtils'; +import * as sparkUtils from '../spark/sparkUtils'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; + +/** + * Implementation of a client session. This is a model over session operations, + * which may come from the session manager or a specific session. + */ +export class ClientSession implements IClientSession { + //#region private fields with public accessors + private _terminatedEmitter = new Emitter(); + private _kernelChangedEmitter = new Emitter(); + private _statusChangedEmitter = new Emitter(); + private _iopubMessageEmitter = new Emitter(); + private _unhandledMessageEmitter = new Emitter(); + private _propertyChangedEmitter = new Emitter<'path' | 'name' | 'type'>(); + private _path: string; + private _type: string; + private _name: string; + private _isReady: boolean; + private _ready: Deferred; + private _kernelChangeCompleted: Deferred; + private _kernelPreference: IKernelPreference; + private _kernelDisplayName: string; + private _errorMessage: string; + //#endregion + + private _serverLoadFinished: Promise; + private _session: nb.ISession; + private isServerStarted: boolean; + private notebookManager: INotebookManager; + private _connection: NotebookConnection; + private _kernelConfigActions: ((kernelName: string) => Promise)[] = []; + + constructor(private options: IClientSessionOptions) { + this._path = options.path; + this.notebookManager = options.notebookManager; + this._isReady = false; + this._ready = new Deferred(); + this._kernelChangeCompleted = new Deferred(); + } + + public async initialize(connection?: NotebookConnection): Promise { + try { + this._kernelConfigActions.push((kernelName: string) => { return this.runTasksBeforeSessionStart(kernelName); }); + this._connection = connection; + this._serverLoadFinished = this.startServer(); + await this._serverLoadFinished; + await this.initializeSession(); + } catch (err) { + this._errorMessage = notebookUtils.getErrorMessage(err); + } + // Always resolving for now. It's up to callers to check for error case + this._isReady = true; + this._ready.resolve(); + this._kernelChangeCompleted.resolve(); + } + + private async startServer(): Promise { + let serverManager = this.notebookManager.serverManager; + if (serverManager && !serverManager.isStarted) { + await serverManager.startServer(); + if (!serverManager.isStarted) { + throw new Error(nls.localize('ServerNotStarted', 'Server did not start for unknown reason')); + } + this.isServerStarted = serverManager.isStarted; + } else { + this.isServerStarted = true; + } + } + + private async initializeSession(): Promise { + await this._serverLoadFinished; + if (this.isServerStarted) { + if (!this.notebookManager.sessionManager.isReady) { + await this.notebookManager.sessionManager.ready; + } + if (this._kernelPreference && this._kernelPreference.shouldStart) { + await this.startSessionInstance(this._kernelPreference.name); + } + } + } + + private async startSessionInstance(kernelName: string): Promise { + let session: nb.ISession; + try { + session = await this.notebookManager.sessionManager.startNew({ + path: this.path, + kernelName: kernelName + // TODO add kernel name if saved in the document + }); + session.defaultKernelLoaded = true; + } catch (err) { + // TODO move registration + if (err && err.response && err.response.status === 501) { + this.options.notificationService.warn(nls.localize('sparkKernelRequiresConnection', 'Kernel {0} was not found. The default kernel will be used instead.', kernelName)); + session = await this.notebookManager.sessionManager.startNew({ + path: this.path, + kernelName: undefined + }); + } else { + throw err; + } + session.defaultKernelLoaded = false; + } + this._session = session; + await this.runKernelConfigActions(kernelName); + this._statusChangedEmitter.fire(session); + } + + private async runKernelConfigActions(kernelName: string): Promise { + for (let startAction of this._kernelConfigActions) { + await startAction(kernelName); + } + } + + public dispose(): void { + // No-op for now + } + + /** + * Indicates the server has finished loading. It may have failed to load in + * which case the view will be in an error state. + */ + public get serverLoadFinished(): Promise { + return this._serverLoadFinished; + } + + + //#region IClientSession Properties + public get terminated(): Event { + return this._terminatedEmitter.event; + } + public get kernelChanged(): Event { + return this._kernelChangedEmitter.event; + } + public get statusChanged(): Event { + return this._statusChangedEmitter.event; + } + public get iopubMessage(): Event { + return this._iopubMessageEmitter.event; + } + public get unhandledMessage(): Event { + return this._unhandledMessageEmitter.event; + } + public get propertyChanged(): Event<'path' | 'name' | 'type'> { + return this._propertyChangedEmitter.event; + } + public get kernel(): nb.IKernel | null { + return this._session ? this._session.kernel : undefined; + } + public get path(): string { + return this._path; + } + public get name(): string { + return this._name; + } + public get type(): string { + return this._type; + } + public get status(): nb.KernelStatus { + if (!this.isReady) { + return 'starting'; + } + return this._session ? this._session.status : 'dead'; + } + public get isReady(): boolean { + return this._isReady; + } + public get ready(): Promise { + return this._ready.promise; + } + public get kernelChangeCompleted(): Promise { + return this._kernelChangeCompleted.promise; + } + public get kernelPreference(): IKernelPreference { + return this._kernelPreference; + } + public set kernelPreference(value: IKernelPreference) { + this._kernelPreference = value; + } + public get kernelDisplayName(): string { + return this._kernelDisplayName; + } + public get errorMessage(): string { + return this._errorMessage; + } + public get isInErrorState(): boolean { + return !!this._errorMessage; + } + //#endregion + + //#region Not Yet Implemented + /** + * Change the current kernel associated with the document. + */ + async changeKernel(options: nb.IKernelSpec): Promise { + this._kernelChangeCompleted = new Deferred(); + this._isReady = false; + let oldKernel = this.kernel; + let newKernel = this.kernel; + + let kernel = await this.doChangeKernel(options); + try { + await kernel.ready; + } catch (error) { + // Cleanup some state before re-throwing + this._isReady = kernel.isReady; + this._kernelChangeCompleted.resolve(); + throw error; + } + newKernel = this._session ? kernel : this._session.kernel; + this._isReady = kernel.isReady; + // Send resolution events to listeners + this._kernelChangeCompleted.resolve(); + this._kernelChangedEmitter.fire({ + oldValue: oldKernel, + newValue: newKernel + }); + return kernel; + } + + /** + * Helper method to either call ChangeKernel on current session, or start a new session + * @param options + */ + private async doChangeKernel(options: nb.IKernelSpec): Promise { + let kernel: nb.IKernel; + if (this._session) { + kernel = await this._session.changeKernel(options); + await this.runKernelConfigActions(kernel.name); + } else { + kernel = await this.startSessionInstance(options.name).then(() => this.kernel); + } + return kernel; + } + + public async runTasksBeforeSessionStart(kernelName: string): Promise { + // TODO we should move all Spark-related code to SparkMagicContext + if (this._session && this._connection && this.isSparkKernel(kernelName)) { + // TODO may need to reenable a way to get the credential + // await this._connection.getCredential(); + // %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options, + // such as user/profile/host name/auth type + + let server = URI.parse(sparkUtils.getLivyUrl(this._connection.host, this._connection.knoxport)).toString(); + let doNotCallChangeEndpointParams = + `%_do_not_call_change_endpoint --username=${this._connection.user} --password=${this._connection.password} --server=${server} --auth=Basic_Access`; + let future = this._session.kernel.requestExecute({ + code: doNotCallChangeEndpointParams + }, true); + await future.done; + } + } + + public async updateConnection(connection: NotebookConnection): Promise { + if (!this.kernel) { + // TODO is there any case where skipping causes errors? Do far it seems like it gets called twice + return; + } + this._connection = (connection.connectionProfile.id !== '-1') ? connection : this._connection; + // if kernel is not set, don't run kernel config actions + // this should only occur when a cell is cancelled, which interrupts the kernel + if (this.kernel && this.kernel.name) { + await this.runKernelConfigActions(this.kernel.name); + } + } + + isSparkKernel(kernelName: string): any { + return kernelName && kernelName.toLowerCase().indexOf('spark') > -1; + } + + /** + * Kill the kernel and shutdown the session. + * + * @returns A promise that resolves when the session is shut down. + */ + public async shutdown(): Promise { + // Always try to shut down session + if (this._session && this._session.id) { + this.notebookManager.sessionManager.shutdown(this._session.id); + } + let serverManager = this.notebookManager.serverManager; + if (serverManager) { + await serverManager.stopServer(); + } + } + + /** + * Select a kernel for the session. + */ + selectKernel(): Promise { + throw new Error('Not implemented'); + } + + /** + * Restart the session. + * + * @returns A promise that resolves with whether the kernel has restarted. + * + * #### Notes + * If there is a running kernel, present a dialog. + * If there is no kernel, we start a kernel with the last run + * kernel name and resolves with `true`. If no kernel has been started, + * this is a no-op, and resolves with `false`. + */ + restart(): Promise { + throw new Error('Not implemented'); + } + + /** + * Change the session path. + * + * @param path - The new session path. + * + * @returns A promise that resolves when the session has renamed. + * + * #### Notes + * This uses the Jupyter REST API, and the response is validated. + * The promise is fulfilled on a valid response and rejected otherwise. + */ + setPath(path: string): Promise { + throw new Error('Not implemented'); + } + + /** + * Change the session name. + */ + setName(name: string): Promise { + throw new Error('Not implemented'); + } + + /** + * Change the session type. + */ + setType(type: string): Promise { + throw new Error('Not implemented'); + } + //#endregion +} diff --git a/src/sql/parts/notebook/models/contracts.ts b/src/sql/parts/notebook/models/contracts.ts new file mode 100644 index 0000000000..9a918d5730 --- /dev/null +++ b/src/sql/parts/notebook/models/contracts.ts @@ -0,0 +1,47 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export type CellType = 'code' | 'markdown' | 'raw'; + +export class CellTypes { + public static readonly Code = 'code'; + public static readonly Markdown = 'markdown'; + public static readonly Raw = 'raw'; +} + +// to do: add all mime types +export type MimeType = 'text/plain' | 'text/html'; + +// to do: add all mime types +export class MimeTypes { + public static readonly PlainText = 'text/plain'; + public static readonly HTML = 'text/html'; +} + +export type OutputType = + | 'execute_result' + | 'display_data' + | 'stream' + | 'error' + | 'update_display_data'; + +export class OutputTypes { + public static readonly ExecuteResult = 'execute_result'; + public static readonly DisplayData = 'display_data'; + public static readonly Stream = 'stream'; + public static readonly Error = 'error'; + public static readonly UpdateDisplayData = 'update_display_data'; +} + +export enum NotebookChangeType { + CellsAdded, + CellDeleted, + CellSourceUpdated, + CellOutputUpdated, + DirtyStateChanged +} diff --git a/src/sql/parts/notebook/models/modelFactory.ts b/src/sql/parts/notebook/models/modelFactory.ts new file mode 100644 index 0000000000..37dfca2639 --- /dev/null +++ b/src/sql/parts/notebook/models/modelFactory.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; + +import { CellModel } from './cell'; +import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from './modelInterfaces'; +import { ClientSession } from './clientSession'; + +export class ModelFactory implements IModelFactory { + + public createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel { + return new CellModel(this, cell, options); + } + + public createClientSession(options: IClientSessionOptions): IClientSession { + return new ClientSession(options); + } +} diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts new file mode 100644 index 0000000000..418413d290 --- /dev/null +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -0,0 +1,372 @@ + +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx + +'use strict'; + +import { nb } from 'sqlops'; +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { INotificationService } from 'vs/platform/notification/common/notification'; + +import { CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; + +export interface IClientSessionOptions { + path: string; + notebookManager: INotebookManager; + notificationService: INotificationService; +} + +/** + * The interface of client session object. + * + * The client session represents the link between + * a path and its kernel for the duration of the lifetime + * of the session object. The session can have no current + * kernel, and can start a new kernel at any time. + */ +export interface IClientSession extends IDisposable { + /** + * A signal emitted when the session is shut down. + */ + readonly terminated: Event; + + /** + * A signal emitted when the kernel changes. + */ + readonly kernelChanged: Event; + + /** + * A signal emitted when the kernel status changes. + */ + readonly statusChanged: Event; + + /** + * A signal emitted for a kernel messages. + */ + readonly iopubMessage: Event; + + /** + * A signal emitted for an unhandled kernel message. + */ + readonly unhandledMessage: Event; + + /** + * A signal emitted when a session property changes. + */ + readonly propertyChanged: Event<'path' | 'name' | 'type'>; + + /** + * The current kernel associated with the document. + */ + readonly kernel: nb.IKernel | null; + + /** + * The current path associated with the client session. + */ + readonly path: string; + + /** + * The current name associated with the client session. + */ + readonly name: string; + + /** + * The type of the client session. + */ + readonly type: string; + + /** + * The current status of the client session. + */ + readonly status: nb.KernelStatus; + + /** + * Whether the session is ready. + */ + readonly isReady: boolean; + + /** + * Whether the session is in an unusable state + */ + readonly isInErrorState: boolean; + /** + * The error information, if this session is in an error state + */ + readonly errorMessage: string; + + /** + * A promise that is fulfilled when the session is ready. + */ + readonly ready: Promise; + + /** + * A promise that is fulfilled when the session completes a kernel change. + */ + readonly kernelChangeCompleted: Promise; + + /** + * The kernel preference. + */ + kernelPreference: IKernelPreference; + + /** + * The display name of the kernel. + */ + readonly kernelDisplayName: string; + + /** + * Initializes the ClientSession, by starting the server and + * connecting to the SessionManager. + * This will optionally start a session if the kernel preferences + * indicate this is desired + */ + initialize(connection?: NotebookConnection): Promise; + + /** + * Change the current kernel associated with the document. + */ + changeKernel( + options: nb.IKernelSpec + ): Promise; + + /** + * Kill the kernel and shutdown the session. + * + * @returns A promise that resolves when the session is shut down. + */ + shutdown(): Promise; + + /** + * Select a kernel for the session. + */ + selectKernel(): Promise; + + /** + * Restart the session. + * + * @returns A promise that resolves with whether the kernel has restarted. + * + * #### Notes + * If there is a running kernel, present a dialog. + * If there is no kernel, we start a kernel with the last run + * kernel name and resolves with `true`. If no kernel has been started, + * this is a no-op, and resolves with `false`. + */ + restart(): Promise; + + /** + * Change the session path. + * + * @param path - The new session path. + * + * @returns A promise that resolves when the session has renamed. + * + * #### Notes + * This uses the Jupyter REST API, and the response is validated. + * The promise is fulfilled on a valid response and rejected otherwise. + */ + setPath(path: string): Promise; + + /** + * Change the session name. + */ + setName(name: string): Promise; + + /** + * Change the session type. + */ + setType(type: string): Promise; + + /** + * Updates the connection + */ + updateConnection(connection: NotebookConnection): void; +} + +export interface IDefaultConnection { + defaultConnection: IConnectionProfile; + otherConnections: IConnectionProfile[]; +} + +/** + * A kernel preference. + */ +export interface IKernelPreference { + /** + * The name of the kernel. + */ + readonly name?: string; + + /** + * The preferred kernel language. + */ + readonly language?: string; + + /** + * The id of an existing kernel. + */ + readonly id?: string; + + /** + * Whether to prefer starting a kernel. + */ + readonly shouldStart?: boolean; + + /** + * Whether a kernel can be started. + */ + readonly canStart?: boolean; + + /** + * Whether to auto-start the default kernel if no matching kernel is found. + */ + readonly autoStartDefault?: boolean; +} + +export interface INotebookModel { + /** + * Client Session in the notebook, used for sending requests to the notebook service + */ + readonly clientSession: IClientSession; + /** + * LanguageInfo saved in the query book + */ + readonly languageInfo: nb.ILanguageInfo; + + /** + * The notebook service used to call backend APIs + */ + readonly notebookManager: INotebookManager; + + /** + * Event fired on first initialization of the kernel and + * on subsequent change events + */ + readonly kernelChanged: Event; + + /** + * Event fired on first initialization of the kernels and + * on subsequent change events + */ + readonly kernelsChanged: Event; + + /** + * Default kernel + */ + defaultKernel?: nb.IKernelSpec; + + /** + * Event fired on first initialization of the contexts and + * on subsequent change events + */ + readonly contextsChanged: Event; + + /** + * The specs for available kernels, or undefined if these have + * not been loaded yet + */ + readonly specs: nb.IAllKernels | undefined; + + /** + * The specs for available contexts, or undefined if these have + * not been loaded yet + */ + readonly contexts: IDefaultConnection | undefined; + + /** + * The trusted mode of the NoteBook + */ + trustedMode: boolean; + + /** + * Change the current kernel from the Kernel dropdown + * @param displayName kernel name (as displayed in Kernel dropdown) + */ + changeKernel(displayName: string): void; + + /** + * Change the current context (if applicable) + */ + changeContext(host: string): void; + + /** + * Adds a cell to the end of the model + */ + addCell(cellType: CellType): void; + + /** + * Deletes a cell + */ + deleteCell(cellModel: ICellModel): void; + + /** + * Save the model to its backing content manager. + * Serializes the model and then calls through to save it + */ + saveModel(): Promise; + + /** + * Notifies the notebook of a change in the cell + */ + onCellChange(cell: ICellModel, change: NotebookChangeType): void; +} + +export interface ICellModelOptions { + notebook: INotebookModel; + isTrusted: boolean; +} + +export interface ICellModel { + cellUri: URI; + id: string; + language: string; + source: string; + cellType: CellType; + trustedMode: boolean; + active: boolean; + equals(cellModel: ICellModel): boolean; + toJSON(): nb.ICell; +} + +export interface IModelFactory { + + createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel; + createClientSession(options: IClientSessionOptions): IClientSession; +} + + +export interface INotebookModelOptions { + /** + * Path to the local or remote notebook + */ + path: string; + + /** + * Factory for creating cells and client sessions + */ + factory: IModelFactory; + + notebookManager: INotebookManager; + + notificationService: INotificationService; + connectionService: IConnectionManagementService; +} + +// TODO would like to move most of these constants to an extension +export namespace notebookConstants { + export const hadoopKnoxProviderName = 'HADOOP_KNOX'; + export const python3 = 'python3'; + export const python3DisplayName = 'Python 3'; + export const defaultSparkKernel = 'pyspark3kernel'; + +} \ No newline at end of file diff --git a/src/sql/parts/notebook/models/notebookConnection.ts b/src/sql/parts/notebook/models/notebookConnection.ts new file mode 100644 index 0000000000..507ee11622 --- /dev/null +++ b/src/sql/parts/notebook/models/notebookConnection.ts @@ -0,0 +1,94 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { localize } from 'vs/nls'; + +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; + +export namespace constants { + export const hostPropName = 'host'; + export const userPropName = 'user'; + export const knoxPortPropName = 'knoxport'; + export const clusterPropName = 'clustername'; + export const passwordPropName = 'password'; + export const defaultKnoxPort = '30443'; +} +/** + * This is a temporary connection definition, with known properties for Knox gateway connections. + * Long term this should be refactored to an extension contribution + * + * @export + * @class NotebookConnection + */ +export class NotebookConnection { + private _host: string; + private _knoxPort: string; + + constructor(private _connectionProfile: IConnectionProfile) { + if (!this._connectionProfile) { + throw new Error(localize('connectionInfoMissing', 'connectionInfo is required')); + } + } + + public get connectionProfile(): IConnectionProfile { + return this.connectionProfile; + } + + + public get host(): string { + if (!this._host) { + this.ensureHostAndPort(); + } + return this._host; + } + + /** + * Sets host and port values, using any ',' or ':' delimited port in the hostname in + * preference to the built in port. + */ + private ensureHostAndPort(): void { + this._host = this.connectionProfile.options[constants.hostPropName]; + this._knoxPort = NotebookConnection.getKnoxPortOrDefault(this.connectionProfile); + // determine whether the host has either a ',' or ':' in it + this.setHostAndPort(','); + this.setHostAndPort(':'); + } + + // set port and host correctly after we've identified that a delimiter exists in the host name + private setHostAndPort(delimeter: string): void { + let originalHost = this._host; + let index = originalHost.indexOf(delimeter); + if (index > -1) { + this._host = originalHost.slice(0, index); + this._knoxPort = originalHost.slice(index + 1); + } + } + + public get user(): string { + return this.connectionProfile.options[constants.userPropName]; + } + + public get password(): string { + return this.connectionProfile.options[constants.passwordPropName]; + } + + public get knoxport(): string { + if (!this._knoxPort) { + this.ensureHostAndPort(); + } + return this._knoxPort; + } + + private static getKnoxPortOrDefault(connectionProfile: IConnectionProfile): string { + let port = connectionProfile.options[constants.knoxPortPropName]; + if (!port) { + port = constants.defaultKnoxPort; + } + return port; + } +} diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts new file mode 100644 index 0000000000..b718266cf0 --- /dev/null +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -0,0 +1,474 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; + +import { localize } from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; + +import { CellModel } from './cell'; +import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants } from './modelInterfaces'; +import { NotebookChangeType, CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; +import { nbversion } from '../notebookConstants'; +import * as notebookUtils from '../notebookUtils'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; +import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; + +/* +* Used to control whether a message in a dialog/wizard is displayed as an error, +* warning, or informational message. Default is error. +*/ +export enum MessageLevel { + Error = 0, + Warning = 1, + Information = 2 +} + +export class ErrorInfo { + constructor(public readonly message: string, public readonly severity: MessageLevel) { + } +} +export interface NotebookContentChange { + /** + * What was the change that occurred? + */ + changeType: NotebookChangeType; + /** + * Optional cells that were changed + */ + cells?: ICellModel | ICellModel[]; + /** + * Optional index of the change, indicating the cell at which an insert or + * delete occurred + */ + cellIndex?: number; + /** + * Optional value indicating if the notebook is in a dirty or clean state after this change + * + * @type {boolean} + * @memberof NotebookContentChange + */ + isDirty?: boolean; +} + +export class NotebookModel extends Disposable implements INotebookModel { + private _contextsChangedEmitter = new Emitter(); + private _contentChangedEmitter = new Emitter(); + private _kernelsChangedEmitter = new Emitter(); + private _inErrorState: boolean = false; + private _clientSession: IClientSession; + private _sessionLoadFinished: Promise; + private _onClientSessionReady = new Emitter(); + private _activeContexts: IDefaultConnection; + private _trustedMode: boolean; + + private _cells: ICellModel[]; + private _defaultLanguageInfo: nb.ILanguageInfo; + private onErrorEmitter = new Emitter(); + private _savedKernelInfo: nb.IKernelInfo; + private readonly _nbformat: number = nbversion.MAJOR_VERSION; + private readonly _nbformatMinor: number = nbversion.MINOR_VERSION; + private _hadoopConnection: NotebookConnection; + private _defaultKernel: nb.IKernelSpec; + + constructor(private notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, private connectionProfile?: IConnectionProfile) { + super(); + if (!notebookOptions || !notebookOptions.path || !notebookOptions.notebookManager) { + throw new Error('path or notebook service not defined'); + } + if (startSessionImmediately) { + this.backgroundStartSession(); + } + this._trustedMode = false; + } + + public get notebookManager(): INotebookManager { + return this.notebookOptions.notebookManager; + } + + public get hasServerManager(): boolean { + // If the service has a server manager, then we can show the start button + return !!this.notebookManager.serverManager; + } + + public get contentChanged(): Event { + return this._contentChangedEmitter.event; + } + + public get isSessionReady(): boolean { + return !!this._clientSession; + } + + /** + * ClientSession object which handles management of a session instance, + * plus startup of the session manager which can return key metadata about the + * notebook environment + */ + public get clientSession(): IClientSession { + return this._clientSession; + } + + public get kernelChanged(): Event { + return this.clientSession.kernelChanged; + } + + public get kernelsChanged(): Event { + return this._kernelsChangedEmitter.event; + } + + public get defaultKernel(): nb.IKernelSpec { + return this._defaultKernel; + } + + public get contextsChanged(): Event { + return this._contextsChangedEmitter.event; + } + + public get cells(): ICellModel[] { + return this._cells; + } + + public get contexts(): IDefaultConnection { + return this._activeContexts; + } + + public get specs(): nb.IAllKernels | undefined { + return this.notebookManager.sessionManager.specs; + } + + public get inErrorState(): boolean { + return this._inErrorState; + } + + public get onError(): Event { + return this.onErrorEmitter.event; + } + + public get trustedMode(): boolean { + return this._trustedMode; + } + + public set trustedMode(isTrusted: boolean) { + this._trustedMode = isTrusted; + if (this._cells) { + this._cells.forEach(c => { + c.trustedMode = this._trustedMode; + }); + } + } + + /** + * Indicates the server has finished loading. It may have failed to load in + * which case the view will be in an error state. + */ + public get sessionLoadFinished(): Promise { + return this._sessionLoadFinished; + } + + /** + * Notifies when the client session is ready for use + */ + public get onClientSessionReady(): Event { + return this._onClientSessionReady.event; + } + + public async requestModelLoad(isTrusted: boolean = false): Promise { + try { + this._trustedMode = isTrusted; + let contents = await this.notebookManager.contentManager.getNotebookContents(this.notebookOptions.path); + let factory = this.notebookOptions.factory; + // if cells already exist, create them with language info (if it is saved) + this._cells = undefined; + if (contents) { + this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents); + this._savedKernelInfo = this.getSavedKernelInfo(contents); + if (contents.cells && contents.cells.length > 0) { + this._cells = contents.cells.map(c => factory.createCell(c, { notebook: this, isTrusted: isTrusted })); + } + } + if (!this._cells) { + this._cells = [this.createCell(CellTypes.Code)]; + } + } catch (error) { + this._inErrorState = true; + throw error; + } + } + + addCell(cellType: CellType): void { + if (this.inErrorState || !this._cells) { + return; + } + let cell = this.createCell(cellType); + this._cells.push(cell); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.CellsAdded, + cells: [cell] + }); + } + + private createCell(cellType: CellType): ICellModel { + let singleCell: nb.ICell = { + cell_type: cellType, + source: '', + metadata: {}, + execution_count: 1 + }; + return this.notebookOptions.factory.createCell(singleCell, { notebook: this, isTrusted: true }); + } + + deleteCell(cellModel: CellModel): void { + if (this.inErrorState || !this._cells) { + return; + } + let index = this._cells.findIndex((cell) => cell.equals(cellModel)); + if (index > -1) { + this._cells.splice(index, 1); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.CellDeleted, + cells: [cellModel], + cellIndex: index + }); + } else { + this.notifyError(localize('deleteCellFailed', 'Failed to delete cell.')); + } + } + + private notifyError(error: string): void { + this.onErrorEmitter.fire(new ErrorInfo(error, MessageLevel.Error)); + } + + public backgroundStartSession(): void { + this._clientSession = this.notebookOptions.factory.createClientSession({ + path: this.notebookOptions.path, + notebookManager: this.notebookManager, + notificationService: this.notebookOptions.notificationService + }); + let id: string = this.connectionProfile ? this.connectionProfile.id : undefined; + + this._hadoopConnection = this.connectionProfile ? new NotebookConnection(this.connectionProfile) : undefined; + this._clientSession.initialize(this._hadoopConnection); + this._sessionLoadFinished = this._clientSession.ready.then(async () => { + if (this._clientSession.isInErrorState) { + this.setErrorState(this._clientSession.errorMessage); + } else { + this._onClientSessionReady.fire(this._clientSession); + // Once session is loaded, can use the session manager to retrieve useful info + this.loadKernelInfo(); + await this.loadActiveContexts(undefined); + } + }); + } + + public get languageInfo(): nb.ILanguageInfo { + return this._defaultLanguageInfo; + } + + private updateLanguageInfo(info: nb.ILanguageInfo) { + if (info) { + this._defaultLanguageInfo = info; + } + } + + public changeKernel(displayName: string): void { + let spec = this.getSpecNameFromDisplayName(displayName); + this.doChangeKernel(spec); + } + + private doChangeKernel(kernelSpec: nb.IKernelSpec): void { + this._clientSession.changeKernel(kernelSpec) + .then((kernel) => { + kernel.ready.then(() => { + if (kernel.info) { + this.updateLanguageInfo(kernel.info.language_info); + } + }, err => undefined); + return this.updateKernelInfo(kernel); + }).catch((err) => { + this.notifyError(localize('changeKernelFailed', 'Failed to change kernel: {0}', notebookUtils.getErrorMessage(err))); + // TODO should revert kernels dropdown + }); + + } + + public changeContext(host: string): void { + try { + let newConnection: IConnectionProfile = this._activeContexts.otherConnections.find((connection) => connection.options['host'] === host); + if (!newConnection && this._activeContexts.defaultConnection.options['host'] === host) { + newConnection = this._activeContexts.defaultConnection; + } + if (newConnection) { + SparkMagicContexts.configureContext(newConnection, this.notebookOptions); + this._hadoopConnection = new NotebookConnection(newConnection); + this._clientSession.updateConnection(this._hadoopConnection); + } + } catch (err) { + let msg = notebookUtils.getErrorMessage(err); + this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg)); + } + } + + private loadKernelInfo(): void { + this.clientSession.kernelChanged(async (e) => { + await this.loadActiveContexts(e); + }); + try { + let sessionManager = this.notebookManager.sessionManager; + if (sessionManager) { + let defaultKernel = SparkMagicContexts.getDefaultKernel(sessionManager.specs, this.connectionProfile, this._savedKernelInfo, this.notebookOptions.notificationService); + this._defaultKernel = defaultKernel; + this._clientSession.statusChanged(async (session) => { + if (session && session.defaultKernelLoaded === true) { + this._kernelsChangedEmitter.fire(defaultKernel); + } else if (session && !session.defaultKernelLoaded) { + this._kernelsChangedEmitter.fire({ name: notebookConstants.python3, display_name: notebookConstants.python3DisplayName }); + } + }); + this.doChangeKernel(defaultKernel); + } + } catch (err) { + let msg = notebookUtils.getErrorMessage(err); + this.notifyError(localize('loadKernelFailed', 'Loading kernel info failed: {0}', msg)); + } + } + + // Get default language if saved in notebook file + // Otherwise, default to python + private getDefaultLanguageInfo(notebook: nb.INotebook): nb.ILanguageInfo { + return notebook!.metadata!.language_info || { + name: 'python', + version: '', + mimetype: 'x-python' + }; + } + + // Get default kernel info if saved in notebook file + private getSavedKernelInfo(notebook: nb.INotebook): nb.IKernelInfo { + return notebook!.metadata!.kernelspec; + } + + private getSpecNameFromDisplayName(displayName: string): nb.IKernelSpec { + displayName = this.sanitizeDisplayName(displayName); + let kernel: nb.IKernelSpec = this.specs.kernels.find(k => k.display_name.toLowerCase() === displayName.toLowerCase()); + if (!kernel) { + return undefined; // undefined is handled gracefully in the session to default to the default kernel + } else if (!kernel.name) { + kernel.name = this.specs.defaultKernel; + } + return kernel; + } + + private setErrorState(errMsg: string): void { + this._inErrorState = true; + let msg = localize('startSessionFailed', 'Could not start session: {0}', errMsg); + this.notifyError(msg); + + } + + public dispose(): void { + super.dispose(); + this.handleClosed(); + } + + public async handleClosed(): Promise { + try { + if (this._clientSession) { + await this._clientSession.shutdown(); + this._clientSession = undefined; + } + } catch (err) { + this.notifyError(localize('shutdownError', 'An error occurred when closing the notebook: {0}', err)); + } + } + + private async loadActiveContexts(kernelChangedArgs: nb.IKernelChangedArgs): Promise { + this._activeContexts = await SparkMagicContexts.getContextsForKernel(this.notebookOptions.connectionService, kernelChangedArgs, this.connectionProfile); + this._contextsChangedEmitter.fire(); + let defaultHadoopConnection = new NotebookConnection(this.contexts.defaultConnection); + this.changeContext(defaultHadoopConnection.host); + } + + /** + * Sanitizes display name to remove IP address in order to fairly compare kernels + * In some notebooks, display name is in the format () + * example: PySpark (25.23.32.4) + * @param displayName Display Name for the kernel + */ + public sanitizeDisplayName(displayName: string): string { + let name = displayName; + if (name) { + let index = name.indexOf('('); + name = (index > -1) ? name.substr(0, index - 1).trim() : name; + } + return name; + } + + public async saveModel(): Promise { + let notebook = this.toJSON(); + if (!notebook) { + return false; + } + await this.notebookManager.contentManager.save(this.notebookOptions.path, notebook); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.DirtyStateChanged, + isDirty: false + }); + return true; + } + + private async updateKernelInfo(kernel: nb.IKernel): Promise { + if (kernel) { + try { + let spec = await kernel.getSpec(); + this._savedKernelInfo = { + name: kernel.name, + display_name: spec.display_name, + language: spec.language + }; + } catch (err) { + // Don't worry about this for now. Just use saved values + } + } + } + /** + * Serialize the model to JSON. + */ + toJSON(): nb.INotebook { + let cells: nb.ICell[] = this.cells.map(c => c.toJSON()); + let metadata = Object.create(null) as nb.INotebookMetadata; + // TODO update language and kernel when these change + metadata.kernelspec = this._savedKernelInfo; + metadata.language_info = this.languageInfo; + return { + metadata, + nbformat_minor: this._nbformatMinor, + nbformat: this._nbformat, + cells + }; + } + + onCellChange(cell: CellModel, change: NotebookChangeType): void { + let changeInfo: NotebookContentChange = { + changeType: change, + cells: [cell] + }; + switch (change) { + case NotebookChangeType.CellOutputUpdated: + case NotebookChangeType.CellSourceUpdated: + changeInfo.changeType = NotebookChangeType.DirtyStateChanged; + changeInfo.isDirty = true; + break; + default: + // Do nothing for now + } + this._contentChangedEmitter.fire(changeInfo); + } + +} diff --git a/src/sql/parts/notebook/models/sparkMagicContexts.ts b/src/sql/parts/notebook/models/sparkMagicContexts.ts new file mode 100644 index 0000000000..2a9f55f694 --- /dev/null +++ b/src/sql/parts/notebook/models/sparkMagicContexts.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as path from 'path'; +import { nb } from 'sqlops'; + +import * as json from 'vs/base/common/json'; +import * as pfs from 'vs/base/node/pfs'; +import { localize } from 'vs/nls'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { IDefaultConnection, notebookConstants, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces'; +import * as notebookUtils from '../notebookUtils'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; + +export class SparkMagicContexts { + + public static get DefaultContext(): IDefaultConnection { + // TODO NOTEBOOK REFACTOR fix default connection handling + let defaultConnection: IConnectionProfile = { + providerName: notebookConstants.hadoopKnoxProviderName, + id: '-1', + options: + { + host: localize('selectConnection', 'Select Connection') + } + }; + + return { + // default context if no other contexts are applicable + defaultConnection: defaultConnection, + otherConnections: [defaultConnection] + }; + } + + /** + * Get all of the applicable contexts for a given kernel + * @param apiWrapper ApiWrapper + * @param kernelChangedArgs kernel changed args (both old and new kernel info) + * @param profile current connection profile + */ + public static async getContextsForKernel(connectionService: IConnectionManagementService, kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): Promise { + let connections: IDefaultConnection = this.DefaultContext; + if (!profile) { + if (!kernelChangedArgs || !kernelChangedArgs.newValue || + (kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) { + // nothing to do, kernels are the same or new kernel is undefined + return connections; + } + } + if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) { + switch (kernelChangedArgs.newValue.name) { + case (notebookConstants.python3): + // python3 case, use this.DefaultContext for the only connection + break; + //TO DO: Handle server connections based on kernel type. Right now, we call the same method for all kernel types. + default: + connections = await this.getActiveContexts(connectionService, profile); + } + } else { + connections = await this.getActiveContexts(connectionService, profile); + } + return connections; + } + + /** + * Get all active contexts and sort them + * @param apiWrapper ApiWrapper + * @param profile current connection profile + */ + public static async getActiveContexts(connectionService: IConnectionManagementService, profile: IConnectionProfile): Promise { + let defaultConnection: IConnectionProfile = SparkMagicContexts.DefaultContext.defaultConnection; + let activeConnections: IConnectionProfile[] = await connectionService.getActiveConnections(); + // If no connections exist, only show 'n/a' + if (activeConnections.length === 0) { + return SparkMagicContexts.DefaultContext; + } + activeConnections = activeConnections.filter(conn => conn.providerName === notebookConstants.hadoopKnoxProviderName); + + // If launched from the right click or server dashboard, connection profile data exists, so use that as default + if (profile && profile.options) { + let profileConnection = activeConnections.filter(conn => conn.options['host'] === profile.options['host']); + if (profileConnection) { + defaultConnection = profileConnection[0]; + } + } else { + if (activeConnections.length > 0) { + defaultConnection = activeConnections[0]; + } else { + // TODO NOTEBOOK REFACTOR change this so it's no longer incompatible with IConnectionProfile + defaultConnection = { + providerName: notebookConstants.hadoopKnoxProviderName, + id: '-1', + options: + { + host: localize('addConnection', 'Add new connection') + } + }; + activeConnections.push(defaultConnection); + } + } + return { + otherConnections: activeConnections, + defaultConnection: defaultConnection + }; + } + + public static async configureContext(connection: IConnectionProfile, options: INotebookModelOptions): Promise { + let sparkmagicConfDir = path.join(notebookUtils.getUserHome(), '.sparkmagic'); + // TODO NOTEBOOK REFACTOR re-enable this or move to extension. Requires config files to be available in order to work + // await notebookUtils.mkDir(sparkmagicConfDir); + + // let hadoopConnection = new Connection({ options: connection.options }, undefined, connection.connectionId); + // await hadoopConnection.getCredential(); + // // Default to localhost in config file. + // let creds: ICredentials = { + // 'url': 'http://localhost:8088' + // }; + + // let configPath = notebookUtils.getTemplatePath(options.extensionContext.extensionPath, path.join('jupyter_config', 'sparkmagic_config.json')); + // let fileBuffer: Buffer = await pfs.readFile(configPath); + // let fileContents: string = fileBuffer.toString(); + // let config: ISparkMagicConfig = json.parse(fileContents); + // SparkMagicContexts.updateConfig(config, creds, sparkmagicConfDir); + + // let configFilePath = path.join(sparkmagicConfDir, 'config.json'); + // await pfs.writeFile(configFilePath, JSON.stringify(config)); + + return {'SPARKMAGIC_CONF_DIR': sparkmagicConfDir}; + } + /** + * + * @param specs kernel specs (comes from session manager) + * @param connectionInfo connection profile + * @param savedKernelInfo kernel info loaded from + */ + public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo, notificationService: INotificationService): nb.IKernelSpec { + let defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel); + let profile = connectionInfo as IConnectionProfile; + if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) { + // set default kernel to default spark kernel if profile exists + // otherwise, set default to kernel info loaded from existing file + defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo; + } else { + // Handle kernels + if (savedKernelInfo && savedKernelInfo.name.toLowerCase().indexOf('spark') > -1) { + notificationService.warn(localize('sparkKernelRequiresConnection', 'Cannot use kernel {0} as no connection is active. The default kernel of Python3 will be used instead.', savedKernelInfo.display_name)); + } + } + + // If no default kernel specified (should never happen), default to python3 + if (!defaultKernel) { + defaultKernel = { + name: notebookConstants.python3, + display_name: notebookConstants.python3DisplayName + }; + } + return defaultKernel; + } + + private static updateConfig(config: ISparkMagicConfig, creds: ICredentials, homePath: string): void { + config.kernel_python_credentials = creds; + config.kernel_scala_credentials = creds; + config.kernel_r_credentials = creds; + config.logging_config.handlers.magicsHandler.home_path = homePath; + } +} + +interface ICredentials { + 'url': string; +} + +interface ISparkMagicConfig { + kernel_python_credentials: ICredentials; + kernel_scala_credentials: ICredentials; + kernel_r_credentials: ICredentials; + logging_config: { + handlers: { + magicsHandler: { + home_path: string; + } + } + }; + +} + +export interface IKernelJupyterID { + id: string; + jupyterId: string; +} diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 802666d8f2..ccaa9eb1ba 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -5,17 +5,44 @@ import './notebookStyles'; -import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, ViewChildren } from '@angular/core'; +import { nb } from 'sqlops'; + +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core'; + +import URI from 'vs/base/common/uri'; +import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import * as themeColors from 'vs/workbench/common/theme'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import * as themeColors from 'vs/workbench/common/theme'; -import { ICellModel, CellTypes } from 'sql/parts/notebook/cellViews/interfaces'; +import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; +import { INotebookService, INotebookParams } from 'sql/services/notebook/notebookService'; +import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; +class CellModelStub implements ICellModel { + public cellUri: URI; + constructor(public id: string, + public language: string, + public source: string, + public cellType: CellType, + public trustedMode: boolean = false, + public active: boolean = false + ) { } + + equals(cellModel: ICellModel): boolean { + throw new Error('Method not implemented.'); + } + toJSON(): nb.ICell { + throw new Error('Method not implemented.'); + } +} + @Component({ selector: NOTEBOOK_SELECTOR, templateUrl: decodeURI(require.toUrl('./notebook.component.html')) @@ -27,20 +54,18 @@ export class NotebookComponent extends AngularDisposable implements OnInit { constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, - @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService, + @Inject(INotificationService) private notificationService: INotificationService, + @Inject(INotebookService) private notebookService: INotebookService, + @Inject(IBootstrapParams) private notebookParams: INotebookParams ) { super(); - // Todo: This is mock data for cells. Will remove this code when we have a service - let cell1: ICellModel = { - id: '1', language: 'sql', source: 'select * from sys.tables', cellType: CellTypes.Code, active: false - }; - let cell2: ICellModel = { - id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code, active: false - }; - let cell3: ICellModel = { - id: '3', language: 'markdown', source: '## This is test!', cellType: CellTypes.Markdown, active: false - }; + // TODO NOTEBOOK REFACTOR: This is mock data for cells. Will remove this code when we have a service + let cell1 : ICellModel = new CellModelStub ('1', 'sql', 'select * from sys.tables', CellTypes.Code); + let cell2 : ICellModel = new CellModelStub ('2', 'sql', 'select 1', CellTypes.Code); + let cell3 : ICellModel = new CellModelStub ('3', 'markdown', '## This is test!', CellTypes.Markdown); this.cells.push(cell1, cell2, cell3); } diff --git a/src/sql/parts/notebook/notebook.contribution.ts b/src/sql/parts/notebook/notebook.contribution.ts index 69546dd9f3..86fa63417e 100644 --- a/src/sql/parts/notebook/notebook.contribution.ts +++ b/src/sql/parts/notebook/notebook.contribution.ts @@ -11,9 +11,14 @@ import { Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; +import { Schemas } from 'vs/base/common/network'; import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput'; import { NotebookEditor } from 'sql/parts/notebook/notebookEditor'; +import URI from 'vs/base/common/uri'; + + +let counter = 0; /** * todo: Will remove this code. @@ -34,7 +39,8 @@ export class OpenNotebookAction extends Action { public run(): TPromise { return new TPromise((resolve, reject) => { - let model = new NotebookInputModel('modelViewId', undefined, undefined); + let untitledUri = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter++}`}); + let model = new NotebookInputModel(untitledUri, undefined, undefined); let input = new NotebookInput('modelViewId', model); this._editorService.openEditor(input, { pinned: true }); }); diff --git a/src/sql/parts/notebook/notebookConstants.ts b/src/sql/parts/notebook/notebookConstants.ts new file mode 100644 index 0000000000..54ff5d1527 --- /dev/null +++ b/src/sql/parts/notebook/notebookConstants.ts @@ -0,0 +1,19 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export namespace nbversion { + /** + * The major version of the notebook format. + */ + export const MAJOR_VERSION: number = 4; + + /** + * The minor version of the notebook format. + */ + export const MINOR_VERSION: number = 2; +} \ No newline at end of file diff --git a/src/sql/parts/notebook/notebookEditor.ts b/src/sql/parts/notebook/notebookEditor.ts index eb89f73fcf..660362d310 100644 --- a/src/sql/parts/notebook/notebookEditor.ts +++ b/src/sql/parts/notebook/notebookEditor.ts @@ -16,6 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookInput } from 'sql/parts/notebook/notebookInput'; import { NotebookModule } from 'sql/parts/notebook/notebook.module'; import { NOTEBOOK_SELECTOR } from 'sql/parts/notebook/notebook.component'; +import { INotebookParams, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; export class NotebookEditor extends BaseEditor { @@ -83,12 +84,16 @@ export class NotebookEditor extends BaseEditor { private bootstrapAngular(input: NotebookInput): void { // Get the bootstrap params and perform the bootstrap input.hasBootstrapped = true; + let params: INotebookParams = { + notebookUri: input.notebookUri, + providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER + }; bootstrapAngular(this.instantiationService, NotebookModule, this._notebookContainer, NOTEBOOK_SELECTOR, - undefined, - undefined + params, + input ); } } diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index a2d6b4dc87..788bd73eff 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -1,21 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor'; - import { Emitter, Event } from 'vs/base/common/event'; +import URI from 'vs/base/common/uri'; export type ModeViewSaveHandler = (handle: number) => Thenable; export class NotebookInputModel extends EditorModel { private dirty: boolean; private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); - get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } - - constructor(public readonly modelViewId, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { + private _providerId: string; + constructor(public readonly notebookUri: URI, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { super(); this.dirty = false; } + public get providerId(): string { + return this._providerId; + } + + public set providerId(value: string) { + this._providerId = value; + } + + get onDidChangeDirty(): Event { + return this._onDidChangeDirty.event; + } + get isDirty(): boolean { return this.dirty; } @@ -55,8 +73,12 @@ export class NotebookInput extends EditorInput { return this._title; } - public get modelViewId(): string { - return this._model.modelViewId; + public get notebookUri(): URI { + return this._model.notebookUri; + } + + public get providerId(): string { + return this._model.providerId; } public getTypeId(): string { diff --git a/src/sql/parts/notebook/notebookUtils.ts b/src/sql/parts/notebook/notebookUtils.ts new file mode 100644 index 0000000000..d250f5c4bb --- /dev/null +++ b/src/sql/parts/notebook/notebookUtils.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; +import * as os from 'os'; +import * as pfs from 'vs/base/node/pfs'; +import { localize } from 'vs/nls'; +import { IOutputChannel } from 'vs/workbench/parts/output/common/output'; + + +/** + * Test whether an output is from a stream. + */ +export function isStream(output: nb.ICellOutput): output is nb.IStreamResult { + return output.output_type === 'stream'; +} + +export function getErrorMessage(error: Error | string): string { + return (error instanceof Error) ? error.message : error; +} + +export function getUserHome(): string { + return process.env.HOME || process.env.USERPROFILE; +} + +export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Promise { + let exists = await pfs.dirExists(dirPath); + if (!exists) { + if (outputChannel) { + outputChannel.append(localize('mkdirOutputMsg', '... Creating {0}', dirPath) + os.EOL); + } + await pfs.mkdirp(dirPath); + } +} diff --git a/src/sql/parts/notebook/spark/sparkUtils.ts b/src/sql/parts/notebook/spark/sparkUtils.ts new file mode 100644 index 0000000000..b0c4b59a46 --- /dev/null +++ b/src/sql/parts/notebook/spark/sparkUtils.ts @@ -0,0 +1,16 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +// TODO: The content of this file should be refactored to an extension +export function getKnoxUrl(host: string, port: string): string { + return `https://${host}:${port}/gateway`; +} + +export function getLivyUrl(serverName: string, port: string): string { + return getKnoxUrl(serverName, port) + '/default/livy/v1/'; +} \ No newline at end of file diff --git a/src/sql/services/notebook/notebookService.ts b/src/sql/services/notebook/notebookService.ts new file mode 100644 index 0000000000..02a26008db --- /dev/null +++ b/src/sql/services/notebook/notebookService.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as sqlops from 'sqlops'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import URI from 'vs/base/common/uri'; +import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; + +export const SERVICE_ID = 'notebookService'; +export const INotebookService = createDecorator(SERVICE_ID); + +export const DEFAULT_NOTEBOOK_PROVIDER = 'builtin'; + +export interface INotebookService { + _serviceBrand: any; + + /** + * Register a metadata provider + */ + registerProvider(providerId: string, provider: INotebookProvider): void; + + /** + * Register a metadata provider + */ + unregisterProvider(providerId: string): void; + + /** + * Initializes and returns a Notebook manager that can handle all important calls to open, display, and + * run cells in a notebook. + * @param providerId ID for the provider to be used to instantiate a backend notebook service + * @param uri URI for a notebook that is to be opened. Based on this an existing manager may be used, or + * a new one may need to be created + */ + getOrCreateNotebookManager(providerId: string, uri: URI): Thenable; + + shutdown(): void; +} + +export interface INotebookProvider { + readonly providerId: string; + getNotebookManager(notebookUri: URI): Thenable; + handleNotebookClosed(notebookUri: URI): void; +} + +export interface INotebookManager { + providerId: string; + readonly contentManager: sqlops.nb.ContentManager; + readonly sessionManager: sqlops.nb.SessionManager; + readonly serverManager: sqlops.nb.ServerManager; +} + +export interface INotebookParams extends IBootstrapParams { + notebookUri: URI; + providerId: string; +} \ No newline at end of file diff --git a/src/sql/services/notebook/notebookServiceImpl.ts b/src/sql/services/notebook/notebookServiceImpl.ts new file mode 100644 index 0000000000..629c96b044 --- /dev/null +++ b/src/sql/services/notebook/notebookServiceImpl.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; +import * as nls from 'vs/nls'; +import { INotebookService, INotebookManager, INotebookProvider } from 'sql/services/notebook/notebookService'; +import URI from 'vs/base/common/uri'; + +export class NotebookService implements INotebookService { + _serviceBrand: any; + + private _providers: Map = new Map(); + private _managers: Map = new Map(); + + registerProvider(providerId: string, provider: INotebookProvider): void { + this._providers.set(providerId, provider); + } + + unregisterProvider(providerId: string): void { + this._providers.delete(providerId); + } + + public shutdown(): void { + this._managers.forEach(manager => { + if (manager.serverManager) { + // TODO should this thenable be awaited? + manager.serverManager.stopServer(); + } + }); + } + + async getOrCreateNotebookManager(providerId: string, uri: URI): Promise { + if (!uri) { + throw new Error(nls.localize('notebookUriNotDefined', 'No URI was passed when creating a notebook manager')); + } + let manager = this._managers.get(uri); + if (!manager) { + manager = await this.doWithProvider(providerId, (provider) => provider.getNotebookManager(uri)); + if (manager) { + this._managers.set(uri, manager); + } + } + return manager; + } + + // PRIVATE HELPERS ///////////////////////////////////////////////////// + private doWithProvider(providerId: string, op: (provider: INotebookProvider) => Thenable): Thenable { + // Make sure the provider exists before attempting to retrieve accounts + let provider = this._providers.get(providerId); + if (!provider) { + return Promise.reject(new Error(nls.localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then(); + } + + return op(provider); + } +} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 16214c70dc..cafe5d5422 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -52,8 +52,8 @@ declare module 'sqlops' { } export interface TreeComponentView extends vscode.Disposable { - onNodeCheckedChanged: vscode.Event>; - onDidChangeSelection: vscode.Event>; + onNodeCheckedChanged: vscode.Event>; + onDidChangeSelection: vscode.Event>; } export class TreeComponentItem extends vscode.TreeItem { @@ -1365,4 +1365,620 @@ declare module 'sqlops' { */ export function openConnectionDialog(providers?: string[], initialConnectionProfile?: IConnectionProfile, connectionCompletionOptions?: IConnectionCompletionOptions): Thenable; } + + export namespace nb { + export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable; + + export interface NotebookProvider { + handle: number; + readonly providerId: string; + getNotebookManager(notebookUri: vscode.Uri): Thenable; + handleNotebookClosed(notebookUri: vscode.Uri): void; + } + + export interface NotebookManager { + /** + * Manages reading and writing contents to/from files. + * Files may be local or remote, with this manager giving them a chance to convert and migrate + * from specific notebook file types to and from a standard type for this UI + */ + readonly contentManager: ContentManager; + /** + * A SessionManager that handles starting, stopping and handling notifications around sessions. + * Each notebook has 1 session associated with it, and the session is responsible + * for kernel management + */ + readonly sessionManager: SessionManager; + /** + * (Optional) ServerManager to handle server lifetime management operations. + * Depending on the implementation this may not be needed. + */ + readonly serverManager?: ServerManager; + } + + /** + * Defines the contracts needed to manage the lifetime of a notebook server. + */ + export interface ServerManager { + /** + * Indicates if the server is started at the current time + */ + readonly isStarted: boolean; + + /** + * Event sent when the server has started. This can be used to query + * the manager for server settings + */ + readonly onServerStarted: vscode.Event; + + /** + * Starts the server. Some server types may not support or require this. + * Should no-op if server is already started + */ + startServer(): Thenable; + + /** + * Stops the server. Some server types may not support or require this + */ + stopServer(): Thenable; + } + + //#region Content APIs + /** + * Handles interacting with file and folder contents + */ + export interface ContentManager { + /* Reads contents from a Uri representing a local or remote notebook and returns a + * JSON object containing the cells and metadata about the notebook + */ + getNotebookContents(path: string): Thenable; + + /** + * Save a file. + * + * @param path - The desired file path. + * + * @param notebook - notebook to be saved. + * + * @returns A thenable which resolves with the file content model when the + * file is saved. + */ + save(path: string, notebook: INotebook): Thenable; + } + + export interface INotebook { + + readonly cells: ICell[]; + readonly metadata: INotebookMetadata; + readonly nbformat: number; + readonly nbformat_minor: number; + } + + export interface INotebookMetadata { + kernelspec: IKernelInfo; + language_info?: ILanguageInfo; + } + + export interface IKernelInfo { + name: string; + language?: string; + display_name?: string; + } + + export interface ILanguageInfo { + name: string; + version: string; + mimetype?: string; + codemirror_mode?: string | ICodeMirrorMode; + } + + export interface ICodeMirrorMode { + name: string; + version: string; + } + + export interface ICell { + cell_type: CellType; + source: string | string[]; + metadata: { + language?: string; + }; + execution_count: number; + outputs?: ICellOutput[]; + } + + export type CellType = 'code' | 'markdown' | 'raw'; + + export interface ICellOutput { + output_type: OutputType; + } + export interface IStreamResult extends ICellOutput { + /** + * Stream output field defining the stream name, for example stdout + */ + name: string; + /** + * Stream output field defining the multiline stream text + */ + text: string | Buffer; + } + export interface IDisplayResult extends ICellOutput { + /** + * Mime bundle expected to contain mime type -> contents mappings. + * This is dynamic and is controlled by kernels, so cannot be more specific + */ + data: {}; + /** + * Optional metadata, also a mime bundle + */ + metadata?: {}; + } + export interface IExecuteResult extends IDisplayResult { + /** + * Number of times the cell was executed + */ + executionCount: number; + } + export interface IErrorResult extends ICellOutput { + /** + * Exception name + */ + ename: string; + /** + * Exception value + */ + evalue: string; + /** + * Stacktrace equivalent + */ + traceback?: string[]; + } + + export type OutputType = + | 'execute_result' + | 'display_data' + | 'stream' + | 'error' + | 'update_display_data'; + + //#endregion + + //#region Session APIs + export interface SessionManager { + /** + * Indicates whether the manager is ready. + */ + readonly isReady: boolean; + + /** + * A Thenable that is fulfilled when the manager is ready. + */ + readonly ready: Thenable; + + readonly specs: IAllKernels | undefined; + + startNew(options: ISessionOptions): Thenable; + + shutdown(id: string): Thenable; + } + + export interface ISession { + /** + * Is change of kernels supported for this session? + */ + canChangeKernels: boolean; + /* + * Unique id of the session. + */ + readonly id: string; + + /** + * The current path associated with the session. + */ + readonly path: string; + + /** + * The current name associated with the session. + */ + readonly name: string; + + /** + * The type of the session. + */ + readonly type: string; + + /** + * The status indicates if the kernel is healthy, dead, starting, etc. + */ + readonly status: KernelStatus; + + /** + * The kernel. + * + * #### Notes + * This is a read-only property, and can be altered by [changeKernel]. + */ + readonly kernel: IKernel; + + /** + * Tracks whether the default kernel failed to load + * This could be for a reason such as the kernel name not being recognized as a valid kernel; + */ + defaultKernelLoaded?: boolean; + + changeKernel(kernelInfo: IKernelSpec): Thenable; + } + + export interface ISessionOptions { + /** + * The path (not including name) to the session. + */ + path: string; + /** + * The name of the session. + */ + name?: string; + /** + * The type of the session. + */ + type?: string; + /** + * The type of kernel (e.g. python3). + */ + kernelName?: string; + /** + * The id of an existing kernel. + */ + kernelId?: string; + } + + export interface IKernel { + readonly id: string; + readonly name: string; + readonly supportsIntellisense: boolean; + /** + * Test whether the kernel is ready. + */ + readonly isReady: boolean; + + /** + * A Thenable that is fulfilled when the kernel is ready. + */ + readonly ready: Thenable; + + /** + * The cached kernel info. + * + * #### Notes + * This value will be null until the kernel is ready. + */ + readonly info: IInfoReply | null; + + /** + * Gets the full specification for this kernel, which can be serialized to + * a noteobok file + */ + getSpec(): Thenable; + + /** + * Send an `execute_request` message. + * + * @param content - The content of the request. + * + * @param disposeOnDone - Whether to dispose of the future when done. + * + * @returns A kernel future. + * + * #### Notes + * See [Messaging in + * Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#execute). + * + * This method returns a kernel future, rather than a Thenable, since execution may + * have many response messages (for example, many iopub display messages). + * + * Future `onReply` is called with the `execute_reply` content when the + * shell reply is received and validated. + * + * **See also:** [[IExecuteReply]] + */ + requestExecute(content: IExecuteRequest, disposeOnDone?: boolean): IFuture; + + + /** + * Send a `complete_request` message. + * + * @param content - The content of the request. + * + * @returns A Thenable that resolves with the response message. + * + * #### Notes + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion). + * + * Fulfills with the `complete_reply` content when the shell reply is + * received and validated. + */ + requestComplete(content: ICompleteRequest): Thenable; + + } + + export interface IInfoReply { + protocol_version: string; + implementation: string; + implementation_version: string; + language_info: ILanguageInfo; + banner: string; + help_links: { + text: string; + url: string; + }[]; + } + + /** + * The contents of a requestExecute message sent to the server. + */ + export interface IExecuteRequest extends IExecuteOptions { + code: string; + } + + /** + * The options used to configure an execute request. + */ + export interface IExecuteOptions { + /** + * Whether to execute the code as quietly as possible. + * The default is `false`. + */ + silent?: boolean; + + /** + * Whether to store history of the execution. + * The default `true` if silent is False. + * It is forced to `false ` if silent is `true`. + */ + store_history?: boolean; + + /** + * A mapping of names to expressions to be evaluated in the + * kernel's interactive namespace. + */ + user_expressions?: {}; + + /** + * Whether to allow stdin requests. + * The default is `true`. + */ + allow_stdin?: boolean; + + /** + * Whether to the abort execution queue on an error. + * The default is `false`. + */ + stop_on_error?: boolean; + } + + /** + * The content of a `'complete_request'` message. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion). + * + * **See also:** [[ICompleteReply]], [[IKernel.complete]] + */ + export interface ICompleteRequest { + code: string; + cursor_pos: number; + } + + export interface ICompletionContent { + matches: string[]; + cursor_start: number; + cursor_end: number; + metadata: any; + status: 'ok' | 'error'; + } + /** + * A `'complete_reply'` message on the `'stream'` channel. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion). + * + * **See also:** [[ICompleteRequest]], [[IKernel.complete]] + */ + export interface ICompleteReplyMsg extends IShellMessage { + content: ICompletionContent; + } + + /** + * The valid Kernel status states. + */ + export type KernelStatus = + | 'unknown' + | 'starting' + | 'reconnecting' + | 'idle' + | 'busy' + | 'restarting' + | 'dead' + | 'connected'; + + /** + * An arguments object for the kernel changed event. + */ + export interface IKernelChangedArgs { + oldValue: IKernel | null; + newValue: IKernel | null; + } + + /// -------- JSON objects, and objects primarily intended not to have methods ----------- + export interface IAllKernels { + defaultKernel: string; + kernels: IKernelSpec[]; + } + export interface IKernelSpec { + name: string; + language?: string; + display_name?: string; + } + + export interface MessageHandler { + handle(message: T): void | Thenable; + } + + + /** + * A Future interface for responses from the kernel. + * + * When a message is sent to a kernel, a Future is created to handle any + * responses that may come from the kernel. + */ + export interface IFuture extends vscode.Disposable { + + /** + * The original outgoing message. + */ + readonly msg: IMessage; + + /** + * A Thenable that resolves when the future is done. + * + * #### Notes + * The future is done when there are no more responses expected from the + * kernel. + * + * The `done` Thenable resolves to the reply message if there is one, + * otherwise it resolves to `undefined`. + */ + readonly done: Thenable; + + /** + * Set the reply handler for the kernel future. + * + * #### Notes + * If the handler returns a Thenable, all kernel message processing pauses + * until the Thenable is resolved. If there is a reply message, the future + * `done` Thenable also resolves to the reply message after this handler has + * been called. + */ + setReplyHandler(handler: MessageHandler): void; + + /** + * Sets the stdin handler for the kernel future. + * + * #### Notes + * If the handler returns a Thenable, all kernel message processing pauses + * until the Thenable is resolved. + */ + setStdInHandler(handler: MessageHandler): void; + + /** + * Sets the iopub handler for the kernel future. + * + * #### Notes + * If the handler returns a Thenable, all kernel message processing pauses + * until the Thenable is resolved. + */ + setIOPubHandler(handler: MessageHandler): void; + + /** + * Register hook for IOPub messages. + * + * @param hook - The callback invoked for an IOPub message. + * + * #### Notes + * The IOPub hook system allows you to preempt the handlers for IOPub + * messages handled by the future. + * + * The most recently registered hook is run first. A hook can return a + * boolean or a Thenable to a boolean, in which case all kernel message + * processing pauses until the Thenable is fulfilled. If a hook return value + * resolves to false, any later hooks will not run and the function will + * return a Thenable resolving to false. If a hook throws an error, the error + * is logged to the console and the next hook is run. If a hook is + * registered during the hook processing, it will not run until the next + * message. If a hook is removed during the hook processing, it will be + * deactivated immediately. + */ + registerMessageHook( + hook: (msg: IIOPubMessage) => boolean | Thenable + ): void; + + /** + * Remove a hook for IOPub messages. + * + * @param hook - The hook to remove. + * + * #### Notes + * If a hook is removed during the hook processing, it will be deactivated immediately. + */ + removeMessageHook( + hook: (msg: IIOPubMessage) => boolean | Thenable + ): void; + + /** + * Send an `input_reply` message. + */ + sendInputReply(content: IInputReply): void; + } + + /** + * The valid channel names. + */ + export type Channel = 'shell' | 'iopub' | 'stdin'; + + /** + * Kernel message header content. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#general-message-format). + * + * **See also:** [[IMessage]] + */ + export interface IHeader { + username: string; + version: string; + session: string; + msg_id: string; + msg_type: string; + } + + /** + * A kernel message + */ + export interface IMessage { + type: Channel; + header: IHeader; + parent_header: IHeader | {}; + metadata: {}; + content: any; + } + + /** + * A kernel message on the `'shell'` channel. + */ + export interface IShellMessage extends IMessage { + channel: 'shell'; + } + + /** + * A kernel message on the `'iopub'` channel. + */ + export interface IIOPubMessage extends IMessage { + channel: 'iopub'; + } + + /** + * A kernel message on the `'stdin'` channel. + */ + export interface IStdinMessage extends IMessage { + channel: 'stdin'; + } + + /** + * The content of an `'input_reply'` message. + */ + export interface IInputReply { + value: string; + } + + //#endregion + + } + } diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts new file mode 100644 index 0000000000..33560101d4 --- /dev/null +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +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 { ExtHostNotebookShape, MainThreadNotebookShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; + +export class ExtHostNotebook implements ExtHostNotebookShape { + private static _handlePool: number = 0; + + private readonly _proxy: MainThreadNotebookShape; + private _providers = new Map(); + + constructor(private _mainContext: IMainContext) { + this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook); + } + + //#region APIs called by main thread + getNotebookManager(notebookUri: vscode.Uri): Thenable { + throw new Error('Not implemented'); + } + handleNotebookClosed(notebookUri: vscode.Uri): void { + throw new Error('Not implemented'); + } + + //#endregion + + //#region APIs called by extensions + registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable { + 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); + this._proxy.$registerNotebookProvider(provider.providerId, handle); + return this._createDisposable(handle); + } + //#endregion + + + //#region private methods + private _createDisposable(handle: number): Disposable { + return new Disposable(() => { + this._providers.delete(handle); + this._proxy.$unregisterNotebookProvider(handle); + }); + } + + private _nextHandle(): number { + return ExtHostNotebook._handlePool++; + } + + private _withProvider(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => TPromise): TPromise { + let provider = this._providers.get(handle); + if (!(provider instanceof ctor)) { + return TPromise.wrapError(new Error('no adapter found')); + } + return callback(provider); + } + + private _addNewProvider(adapter: sqlops.nb.NotebookProvider): number { + const handle = this._nextHandle(); + this._providers.set(handle, adapter); + return handle; + } + //#endregion +} + diff --git a/src/sql/workbench/api/node/mainThreadNotebook.ts b/src/sql/workbench/api/node/mainThreadNotebook.ts new file mode 100644 index 0000000000..b510f57122 --- /dev/null +++ b/src/sql/workbench/api/node/mainThreadNotebook.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as sqlops from 'sqlops'; +import { SqlExtHostContext, SqlMainContext, ExtHostNotebookShape, MainThreadNotebookShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { INotebookService, INotebookProvider, INotebookManager } from 'sql/services/notebook/notebookService'; +import URI from 'vs/base/common/uri'; + +@extHostNamedCustomer(SqlMainContext.MainThreadNotebook) +export class MainThreadNotebook extends Disposable implements MainThreadNotebookShape { + + private _proxy: ExtHostNotebookShape; + private _registrations: { [handle: number]: NotebookProviderWrapper } = Object.create(null); + + constructor( + extHostContext: IExtHostContext, + @INotebookService private notebookService: INotebookService + ) { + super(); + if (extHostContext) { + this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostNotebook); + } + } + + //#region Extension host callable methods + public $registerNotebookProvider(providerId: string, handle: number): void { + let notebookProvider = new NotebookProviderWrapper(providerId, handle); + this._registrations[providerId] = notebookProvider; + this.notebookService.registerProvider(providerId, notebookProvider); + } + + public $unregisterNotebookProvider(handle: number): void { + let registration = this._registrations[handle]; + if (registration) { + this.notebookService.unregisterProvider(registration.providerId); + registration.dispose(); + delete this._registrations[handle]; + } + } + + //#endregion + +} + +class NotebookProviderWrapper extends Disposable implements INotebookProvider { + + constructor(public readonly providerId, public readonly handle: number) { + super(); + } + + getNotebookManager(notebookUri: URI): Thenable { + // TODO must call through to setup in the extension host + return Promise.resolve(new NotebookManagerWrapper(this.providerId)); + } + + handleNotebookClosed(notebookUri: URI): void { + // TODO implement call through to extension host + } + + +} + +class NotebookManagerWrapper implements INotebookManager { + constructor(public readonly providerId) { + + } + sessionManager: sqlops.nb.SessionManager; + contentManager: sqlops.nb.ContentManager; + serverManager: sqlops.nb.ServerManager; + +} diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 3c99d11f94..4b80847baa 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -37,6 +37,7 @@ import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewD import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelViewTree'; import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement'; +import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription): typeof vscode; @@ -73,6 +74,7 @@ export function createApiFactory( const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol)); const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement)); const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol)); + const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol)); return { @@ -402,6 +404,12 @@ export function createApiFactory( } }; + const nb = { + registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable { + return extHostNotebook.registerNotebookProvider(provider); + } + }; + return { accounts, connection, @@ -437,7 +445,8 @@ export function createApiFactory( CardType: sqlExtHostTypes.CardType, Orientation: sqlExtHostTypes.Orientation, SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon, - TreeComponentItem: sqlExtHostTypes.TreeComponentItem + TreeComponentItem: sqlExtHostTypes.TreeComponentItem, + nb: nb }; } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts index c5bacce74a..f8baed3939 100644 --- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts +++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts @@ -23,7 +23,8 @@ import 'sql/workbench/api/node/mainThreadDashboardWebview'; import 'sql/workbench/api/node/mainThreadQueryEditor'; import 'sql/workbench/api/node/mainThreadModelView'; import 'sql/workbench/api/node/mainThreadModelViewDialog'; -import './mainThreadAccountManagement'; +import 'sql/workbench/api/node/mainThreadNotebook'; +import 'sql/workbench/api/node/mainThreadAccountManagement'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; export class SqlExtHostContribution implements IWorkbenchContribution { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 6694a5e502..82819fffee 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -545,6 +545,7 @@ export const SqlMainContext = { MainThreadDashboard: createMainId('MainThreadDashboard'), MainThreadModelViewDialog: createMainId('MainThreadModelViewDialog'), MainThreadQueryEditor: createMainId('MainThreadQueryEditor'), + MainThreadNotebook: createMainId('MainThreadNotebook') }; export const SqlExtHostContext = { @@ -563,7 +564,8 @@ export const SqlExtHostContext = { ExtHostModelViewTreeViews: createExtId('ExtHostModelViewTreeViews'), ExtHostDashboard: createExtId('ExtHostDashboard'), ExtHostModelViewDialog: createExtId('ExtHostModelViewDialog'), - ExtHostQueryEditor: createExtId('ExtHostQueryEditor') + ExtHostQueryEditor: createExtId('ExtHostQueryEditor'), + ExtHostNotebook: createExtId('ExtHostNotebook') }; export interface MainThreadDashboardShape extends IDisposable { @@ -703,4 +705,21 @@ export interface ExtHostQueryEditorShape { export interface MainThreadQueryEditorShape extends IDisposable { $connect(fileUri: string, connectionId: string): Thenable; $runQuery(fileUri: string): void; +} + +export interface ExtHostNotebookShape { + + /** + * Looks up a notebook manager for a given notebook URI + * @param {vscode.Uri} notebookUri + * @returns {Thenable} handle of the manager to be used when sending + */ + getNotebookManager(notebookUri: vscode.Uri): Thenable; + handleNotebookClosed(notebookUri: vscode.Uri): void; + +} + +export interface MainThreadNotebookShape extends IDisposable { + $registerNotebookProvider(providerId: string, handle: number): void; + $unregisterNotebookProvider(handle: number): void; } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 87e4a81d9c..bf7947d84b 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -172,6 +172,8 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay'; +import { NotebookService } from 'sql/services/notebook/notebookServiceImpl'; +import { INotebookService } from 'sql/services/notebook/notebookService'; interface WorkbenchParams { configuration: IWindowConfiguration; @@ -573,11 +575,14 @@ export class Workbench extends Disposable implements IPartService { serviceCollection.set(IInsightsDialogService, this.instantiationService.createInstance(InsightsDialogService)); let accountManagementService = this.instantiationService.createInstance(AccountManagementService, undefined); serviceCollection.set(IAccountManagementService, accountManagementService); + let notebookService = this.instantiationService.createInstance(NotebookService); + serviceCollection.set(INotebookService, notebookService); serviceCollection.set(IAccountPickerService, this.instantiationService.createInstance(AccountPickerService)); serviceCollection.set(IProfilerService, this.instantiationService.createInstance(ProfilerService)); this._register(toDisposable(() => connectionManagementService.shutdown())); this._register(toDisposable(() => accountManagementService.shutdown())); + this._register(toDisposable(() => notebookService.shutdown())); this._register(toDisposable(() => capabilitiesService.shutdown())); }