diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 55a5ed61a8..0942d08ae4 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -17,7 +17,7 @@ import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService'; import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput'; import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput'; import { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/services/notebook/notebookService'; -import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils'; +import { getProvidersForFileName } from 'sql/parts/notebook/notebookUtils'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; const fs = require('fs'); @@ -63,13 +63,14 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti if (uri && notebookValidator.isNotebookEnabled()) { return withService(instantiationService, INotebookService, notebookService => { let fileName: string = 'untitled'; - let providerId: string = DEFAULT_NOTEBOOK_PROVIDER; + let providerIds: string[] = [DEFAULT_NOTEBOOK_PROVIDER]; if (input) { fileName = input.getName(); - providerId = getProviderForFileName(fileName, notebookService); + providerIds = getProvidersForFileName(fileName, notebookService); } let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined); - notebookInputModel.providerId = providerId; + notebookInputModel.providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0]; + notebookInputModel.providers = providerIds; let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); return notebookInput; }); diff --git a/src/sql/parts/connection/common/utils.ts b/src/sql/parts/connection/common/utils.ts index 02117d347d..cb05a5f5f4 100644 --- a/src/sql/parts/connection/common/utils.ts +++ b/src/sql/parts/connection/common/utils.ts @@ -88,7 +88,7 @@ export function parseNumAsTimeString(value: number, includeFraction: boolean = t return tempVal > 0 && includeFraction ? rs + '.' + mss : rs; } -export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string { +export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection' | 'notebook'): string { let prefix = purpose ? uriPrefixes[purpose] : uriPrefixes.default; let uri = generateUriWithPrefix(connection, prefix); diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts index cf9414ced5..053beda761 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.ts +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -38,9 +38,9 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { ngOnInit() { if (this.cellModel) { - this.cellModel.onOutputsChanged(() => { + this._register(this.cellModel.onOutputsChanged(() => { this._changeRef.detectChanges(); - }); + })); } } diff --git a/src/sql/parts/notebook/cellViews/outputArea.component.ts b/src/sql/parts/notebook/cellViews/outputArea.component.ts index 117cad2952..152b807ab6 100644 --- a/src/sql/parts/notebook/cellViews/outputArea.component.ts +++ b/src/sql/parts/notebook/cellViews/outputArea.component.ts @@ -33,9 +33,9 @@ export class OutputAreaComponent extends AngularDisposable implements OnInit { this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this.updateTheme(this.themeService.getColorTheme()); if (this.cellModel) { - this.cellModel.onOutputsChanged(() => { + this._register(this.cellModel.onOutputsChanged(() => { this._changeRef.detectChanges(); - }); + })); } } diff --git a/src/sql/parts/notebook/cellViews/textCell.component.ts b/src/sql/parts/notebook/cellViews/textCell.component.ts index 18719a25f4..385169b359 100644 --- a/src/sql/parts/notebook/cellViews/textCell.component.ts +++ b/src/sql/parts/notebook/cellViews/textCell.component.ts @@ -91,9 +91,9 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { this.setLoading(false); this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this.updateTheme(this.themeService.getColorTheme()); - this.cellModel.onOutputsChanged(e => { + this._register(this.cellModel.onOutputsChanged(e => { this.updatePreview(); - }); + })); } ngOnChanges(changes: { [propKey: string]: SimpleChange }) { diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts index 1795e3076a..3d275bd8d2 100644 --- a/src/sql/parts/notebook/models/cell.ts +++ b/src/sql/parts/notebook/models/cell.ts @@ -226,6 +226,9 @@ export class CellModel implements ICellModel { this._outputs.push(this.rewriteOutputUrls(output)); this.fireOutputsChanged(); } + if (!this._future.inProgress) { + this._future.dispose(); + } } private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput { @@ -327,6 +330,7 @@ export class CellModel implements ICellModel { CellModel.LanguageMapping['pyspark3'] = 'python'; CellModel.LanguageMapping['python'] = 'python'; CellModel.LanguageMapping['scala'] = 'scala'; + CellModel.LanguageMapping['sql'] = 'sql'; } private get languageInfo(): nb.ILanguageInfo { diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index 7d6868ede2..0f664a8e77 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -249,9 +249,9 @@ export interface INotebookModel { readonly languageInfo: nb.ILanguageInfo; /** - * The notebook service used to call backend APIs + * All notebook managers applicable for a given notebook */ - readonly notebookManager: INotebookManager; + readonly notebookManagers: INotebookManager[]; /** * Event fired on first initialization of the kernel and @@ -299,6 +299,11 @@ export interface INotebookModel { */ trustedMode: boolean; + /** + * Current notebook provider id + */ + providerId: string; + /** * Change the current kernel from the Kernel dropdown * @param displayName kernel name (as displayed in Kernel dropdown) @@ -411,7 +416,8 @@ export interface INotebookModelOptions { */ factory: IModelFactory; - notebookManager: INotebookManager; + notebookManagers: INotebookManager[]; + providerId: string; notificationService: INotificationService; connectionService: IConnectionManagementService; diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts index 959141ca27..2c7064c3ea 100644 --- a/src/sql/parts/notebook/models/notebookModel.ts +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -16,7 +16,7 @@ import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptio 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 { INotebookManager, SQL_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_PROVIDER } 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'; @@ -45,7 +45,8 @@ export class NotebookModel extends Disposable implements INotebookModel { private _contentChangedEmitter = new Emitter(); private _kernelsChangedEmitter = new Emitter(); private _inErrorState: boolean = false; - private _clientSession: IClientSession; + private _clientSessions: IClientSession[] = []; + private _activeClientSession: IClientSession; private _sessionLoadFinished: Promise; private _onClientSessionReady = new Emitter(); private _activeContexts: IDefaultConnection; @@ -60,20 +61,26 @@ export class NotebookModel extends Disposable implements INotebookModel { private _hadoopConnection: NotebookConnection; private _defaultKernel: nb.IKernelSpec; private _activeCell: ICellModel; + private _providerId: string; constructor(private notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, private connectionProfile?: IConnectionProfile) { super(); - if (!notebookOptions || !notebookOptions.notebookUri || !notebookOptions.notebookManager) { + if (!notebookOptions || !notebookOptions.notebookUri || !notebookOptions.notebookManagers) { throw new Error('path or notebook service not defined'); } if (startSessionImmediately) { this.backgroundStartSession(); } this._trustedMode = false; + this._providerId = notebookOptions.providerId; + } + + public get notebookManagers(): INotebookManager[] { + return this.notebookOptions.notebookManagers.filter(manager => manager.providerId !== DEFAULT_NOTEBOOK_PROVIDER); } public get notebookManager(): INotebookManager { - return this.notebookOptions.notebookManager; + return this.notebookManagers.find(manager => manager.providerId === this._providerId); } public get notebookUri() : URI { @@ -93,7 +100,7 @@ export class NotebookModel extends Disposable implements INotebookModel { } public get isSessionReady(): boolean { - return !!this._clientSession; + return !!this._activeClientSession; } /** @@ -102,11 +109,11 @@ export class NotebookModel extends Disposable implements INotebookModel { * notebook environment */ public get clientSession(): IClientSession { - return this._clientSession; + return this._activeClientSession; } public get kernelChanged(): Event { - return this.clientSession.kernelChanged; + return this._activeClientSession.kernelChanged; } public get kernelsChanged(): Event { @@ -130,7 +137,21 @@ export class NotebookModel extends Disposable implements INotebookModel { } public get specs(): nb.IAllKernels | undefined { - return this.notebookManager.sessionManager.specs; + let specs: nb.IAllKernels = { + defaultKernel: undefined, + kernels: [] + }; + this.notebookManagers.forEach(manager => { + if (manager.sessionManager && manager.sessionManager.specs && manager.sessionManager.specs.kernels) { + manager.sessionManager.specs.kernels.forEach(kernel => { + specs.kernels.push(kernel); + }); + if (!specs.defaultKernel) { + specs.defaultKernel = manager.sessionManager.specs.defaultKernel; + } + } + }); + return specs; } public get inErrorState(): boolean { @@ -145,6 +166,10 @@ export class NotebookModel extends Disposable implements INotebookModel { return this._trustedMode; } + public get providerId(): string { + return this._providerId; + } + public set trustedMode(isTrusted: boolean) { this._trustedMode = isTrusted; if (this._cells) { @@ -178,11 +203,16 @@ export class NotebookModel extends Disposable implements INotebookModel { this._trustedMode = isTrusted; let contents = null; if (this.notebookOptions.notebookUri.scheme !== Schemas.untitled) { - contents = await this.notebookManager.contentManager.getNotebookContents(this.notebookOptions.notebookUri); + // TODO: separate ContentManager from NotebookManager + contents = await this.notebookManagers[0].contentManager.getNotebookContents(this.notebookOptions.notebookUri); } let factory = this.notebookOptions.factory; // if cells already exist, create them with language info (if it is saved) this._cells = undefined; + this._defaultLanguageInfo = { + name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python', + version: '' + }; if (contents) { this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents); this._savedKernelInfo = this.getSavedKernelInfo(contents); @@ -288,29 +318,36 @@ export class NotebookModel extends Disposable implements INotebookModel { } public backgroundStartSession(): void { - this._clientSession = this.notebookOptions.factory.createClientSession({ - notebookUri: this.notebookOptions.notebookUri, - notebookManager: this.notebookManager, - notificationService: this.notebookOptions.notificationService - }); - let profile = this.connectionProfile as IConnectionProfile; - - if (this.isValidKnoxConnection(profile)) { - this._hadoopConnection = new NotebookConnection(this.connectionProfile); - } else { - this._hadoopConnection = 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); + // TODO: only one session should be active at a time, depending on the current provider + this.notebookManagers.forEach(manager => { + let clientSession = this.notebookOptions.factory.createClientSession({ + notebookUri: this.notebookOptions.notebookUri, + notebookManager: manager, + notificationService: this.notebookOptions.notificationService + }); + this._clientSessions.push(clientSession); + if (!this._activeClientSession) { + this._activeClientSession = clientSession; } + let profile = this.connectionProfile as IConnectionProfile; + + if (this.isValidKnoxConnection(profile)) { + this._hadoopConnection = new NotebookConnection(this.connectionProfile); + } else { + this._hadoopConnection = undefined; + } + + clientSession.initialize(this._hadoopConnection); + this._sessionLoadFinished = clientSession.ready.then(async () => { + if (clientSession.isInErrorState) { + this.setErrorState(clientSession.errorMessage); + } else { + this._onClientSessionReady.fire(clientSession); + // Once session is loaded, can use the session manager to retrieve useful info + this.loadKernelInfo(); + await this.loadActiveContexts(undefined); + } + }); }); } @@ -334,7 +371,8 @@ export class NotebookModel extends Disposable implements INotebookModel { } public doChangeKernel(kernelSpec: nb.IKernelSpec): Promise { - return this._clientSession.changeKernel(kernelSpec) + this.findProviderIdForKernel(kernelSpec); + return this._activeClientSession.changeKernel(kernelSpec) .then((kernel) => { kernel.ready.then(() => { if (kernel.info) { @@ -359,7 +397,7 @@ export class NotebookModel extends Disposable implements INotebookModel { SparkMagicContexts.configureContext(this.notebookOptions); this._hadoopConnection = new NotebookConnection(newConnection); this.refreshConnections(newConnection); - this._clientSession.updateConnection(this._hadoopConnection); + this._activeClientSession.updateConnection(this._hadoopConnection); } catch (err) { let msg = notebookUtils.getErrorMessage(err); this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg)); @@ -381,20 +419,24 @@ export class NotebookModel extends Disposable implements INotebookModel { } private loadKernelInfo(): void { - this.clientSession.kernelChanged(async (e) => { - await this.loadActiveContexts(e); + this._clientSessions.forEach(clientSession => { + 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._clientSessions.forEach(clientSession => { + 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); } @@ -408,9 +450,9 @@ export class NotebookModel extends Disposable implements INotebookModel { // Otherwise, default to python private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo { return notebook!.metadata!.language_info || { - name: 'python', + name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python', version: '', - mimetype: 'x-python' + mimetype: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'x-sql' : 'x-python' }; } @@ -444,9 +486,10 @@ export class NotebookModel extends Disposable implements INotebookModel { public async handleClosed(): Promise { try { - if (this._clientSession) { - await this._clientSession.shutdown(); - this._clientSession = undefined; + if (this._activeClientSession) { + await this._activeClientSession.shutdown(); + this._clientSessions = undefined; + this._activeClientSession = undefined; } } catch (err) { this.notifyError(localize('shutdownError', 'An error occurred when closing the notebook: {0}', err)); @@ -482,7 +525,8 @@ export class NotebookModel extends Disposable implements INotebookModel { if (!notebook) { return false; } - await this.notebookManager.contentManager.save(this.notebookOptions.notebookUri, notebook); + // TODO: refactor ContentManager out from NotebookManager + await this.notebookManagers[0].contentManager.save(this.notebookOptions.notebookUri, notebook); this._contentChangedEmitter.fire({ changeType: NotebookChangeType.DirtyStateChanged, isDirty: false @@ -504,6 +548,23 @@ export class NotebookModel extends Disposable implements INotebookModel { } } } + + /** + * Set _providerId and _activeClientSession based on a kernelSpec representing new kernel + * @param kernelSpec KernelSpec for new kernel + */ + private findProviderIdForKernel(kernelSpec: nb.IKernelSpec): void { + for (let i = 0; i < this.notebookManagers.length; i++) { + if (this.notebookManagers[i].sessionManager && this.notebookManagers[i].sessionManager.specs && this.notebookManagers[i].sessionManager.specs.kernels) { + let index = this.notebookManagers[i].sessionManager.specs.kernels.findIndex(kernel => kernel.name === kernelSpec.name); + if (index >= 0) { + this._activeClientSession = this._clientSessions[i]; + this._providerId = this.notebookManagers[i].providerId; + break; + } + } + } + } /** * Serialize the model to JSON. */ diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index fc01cdfeb2..a76597e96a 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -34,7 +34,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle'; import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; import { ICellModel, IModelFactory, notebookConstants, INotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces'; import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement'; -import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; +import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; @@ -64,7 +64,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe private _errorMessage: string; protected _actionBar: Taskbar; protected isLoading: boolean; - private notebookManager: INotebookManager; + private notebookManagers: INotebookManager[] = []; private _modelReadyDeferred = new Deferred(); private _modelRegisteredDeferred = new Deferred(); private profile: IConnectionProfile; @@ -231,13 +231,17 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe private async loadModel(): Promise { await this.awaitNonDefaultProvider(); - this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri); + for (let providerId of this._notebookParams.providers) { + let notebookManager = await this.notebookService.getOrCreateNotebookManager(providerId, this._notebookParams.notebookUri); + this.notebookManagers.push(notebookManager); + } let model = new NotebookModel({ factory: this.modelFactory, notebookUri: this._notebookParams.notebookUri, connectionService: this.connectionManagementService, notificationService: this.notificationService, - notebookManager: this.notebookManager + notebookManagers: this.notebookManagers, + providerId: notebookUtils.sqlNotebooksEnabled() ? 'sql' : 'jupyter' // this is tricky; really should also depend on the connection profile }, false, this.profile); model.onError((errInfo: INotification) => this.handleModelError(errInfo)); await model.requestModelLoad(this._notebookParams.isTrusted); @@ -258,7 +262,9 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe await this.notebookService.registrationComplete; // Refresh the provider if we had been using default if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) { - this._notebookParams.providerId = notebookUtils.getProviderForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService); + let providers= notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService); + let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER); + this._notebookParams.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0]; } if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) { // If it's still the default, warn them they should install an extension diff --git a/src/sql/parts/notebook/notebookEditor.ts b/src/sql/parts/notebook/notebookEditor.ts index ba262f892c..013ca227e8 100644 --- a/src/sql/parts/notebook/notebookEditor.ts +++ b/src/sql/parts/notebook/notebookEditor.ts @@ -88,6 +88,7 @@ export class NotebookEditor extends BaseEditor { notebookUri: input.notebookUri, input: input, providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER, + providers: input.providers ? input.providers : [DEFAULT_NOTEBOOK_PROVIDER], isTrusted: input.isTrusted }; bootstrapAngular(this.instantiationService, diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index fff390bd6d..3b622fd50b 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -24,9 +24,10 @@ export class NotebookInputModel extends EditorModel { private dirty: boolean; private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); private _providerId: string; - constructor(public readonly notebookUri: URI, private readonly handle: number, private _isTrusted: boolean = false, private saveHandler?: ModeViewSaveHandler) { + constructor(public readonly notebookUri: URI, private readonly handle: number, private _isTrusted: boolean = false, private saveHandler?: ModeViewSaveHandler, provider?: string, private _providers?: string[]) { super(); this.dirty = false; + this._providerId = provider; } public get providerId(): string { @@ -37,6 +38,14 @@ export class NotebookInputModel extends EditorModel { this._providerId = value; } + public get providers(): string[] { + return this._providers; + } + + public set providers(value: string[]) { + this._providers = value; + } + get isTrusted(): boolean { return this._isTrusted; } @@ -99,6 +108,10 @@ export class NotebookInput extends EditorInput { return this._model.providerId; } + public get providers(): string[] { + return this._model.providers; + } + public getTypeId(): string { return NotebookInput.ID; } diff --git a/src/sql/parts/notebook/notebookUtils.ts b/src/sql/parts/notebook/notebookUtils.ts index db90b3b920..b1bef9eb66 100644 --- a/src/sql/parts/notebook/notebookUtils.ts +++ b/src/sql/parts/notebook/notebookUtils.ts @@ -39,21 +39,26 @@ export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Pr } } -export function getProviderForFileName(fileName: string, notebookService: INotebookService): string { +export function getProvidersForFileName(fileName: string, notebookService: INotebookService): string[] { let fileExt = path.extname(fileName); - let provider: string; + let providers: string[]; // First try to get provider for actual file type if (fileExt && fileExt.startsWith('.')) { fileExt = fileExt.slice(1,fileExt.length); - provider = notebookService.getProviderForFileType(fileExt); + providers = notebookService.getProvidersForFileType(fileExt); } // Fallback to provider for default file type (assume this is a global handler) - if (!provider) { - provider = notebookService.getProviderForFileType(DEFAULT_NOTEBOOK_FILETYPE); + if (!providers) { + providers = notebookService.getProvidersForFileType(DEFAULT_NOTEBOOK_FILETYPE); } // Finally if all else fails, use the built-in handler - if (!provider) { - provider = DEFAULT_NOTEBOOK_PROVIDER; + if (!providers) { + providers = [DEFAULT_NOTEBOOK_PROVIDER]; } - return provider; + return providers; +} + +// Private feature flag to enable Sql Notebook experience +export function sqlNotebooksEnabled() { + return process.env['SQLOPS_SQL_NOTEBOOK'] !== undefined; } diff --git a/src/sql/services/notebook/notebookRegistry.ts b/src/sql/services/notebook/notebookRegistry.ts index 9af02ae34b..c065c4a909 100644 --- a/src/sql/services/notebook/notebookRegistry.ts +++ b/src/sql/services/notebook/notebookRegistry.ts @@ -18,11 +18,12 @@ export const Extensions = { export interface NotebookProviderRegistration { provider: string; fileExtensions: string | string[]; + standardKernels: string | string[]; } let notebookProviderType: IJSONSchema = { type: 'object', - default: { provider: '', fileExtensions: [] }, + default: { provider: '', fileExtensions: [], standardKernels: [] }, properties: { provider: { description: localize('carbon.extension.contributes.notebook.provider', 'Identifier of the notebook provider.'), @@ -39,6 +40,18 @@ let notebookProviderType: IJSONSchema = { } } ] + }, + standardKernels: { + description: localize('carbon.extension.contributes.notebook.standardKernels', 'What kernels should be standard with this notebook provider'), + oneOf: [ + { type: 'string' }, + { + type: 'array', + items: { + type: 'string' + } + } + ] } } }; diff --git a/src/sql/services/notebook/notebookService.ts b/src/sql/services/notebook/notebookService.ts index 58cc5dc44a..7dc380f240 100644 --- a/src/sql/services/notebook/notebookService.ts +++ b/src/sql/services/notebook/notebookService.ts @@ -23,6 +23,7 @@ export const INotebookService = createDecorator(SERVICE_ID); export const DEFAULT_NOTEBOOK_PROVIDER = 'builtin'; export const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB'; +export const SQL_NOTEBOOK_PROVIDER = 'sql'; export interface INotebookService { _serviceBrand: any; @@ -45,7 +46,7 @@ export interface INotebookService { getSupportedFileExtensions(): string[]; - getProviderForFileType(fileType: string): string; + getProvidersForFileType(fileType: string): string[]; /** * Initializes and returns a Notebook manager that can handle all important calls to open, display, and @@ -86,6 +87,7 @@ export interface INotebookParams extends IBootstrapParams { notebookUri: URI; input: NotebookInput; providerId: string; + providers: string[]; isTrusted: boolean; profile?: IConnectionProfile; modelFactory?: ModelFactory; diff --git a/src/sql/services/notebook/notebookServiceImpl.ts b/src/sql/services/notebook/notebookServiceImpl.ts index 55905963ad..ccf0a99485 100644 --- a/src/sql/services/notebook/notebookServiceImpl.ts +++ b/src/sql/services/notebook/notebookServiceImpl.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER, - DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor + DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor, SQL_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry'; import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories'; @@ -27,6 +27,9 @@ import { IExtensionManagementService, IExtensionIdentifier } from 'vs/platform/e import { Disposable } from 'vs/base/common/lifecycle'; import { getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Deferred } from 'sql/base/common/promise'; +import { SqlSessionManager } from 'sql/services/notebook/sqlSessionManager'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { sqlNotebooksEnabled } from 'sql/parts/notebook/notebookUtils'; export interface NotebookProviderProperties { provider: string; @@ -70,24 +73,25 @@ export class NotebookService extends Disposable implements INotebookService { private _memento = new Memento('notebookProviders'); private _mimeRegistry: RenderMimeRegistry; private _providers: Map = new Map(); - private _managers: Map = new Map(); + private _managersMap: Map = new Map(); private _onNotebookEditorAdd = new Emitter(); private _onNotebookEditorRemove = new Emitter(); private _onCellChanged = new Emitter(); private _onNotebookEditorRename = new Emitter(); private _editors = new Map(); - private _fileToProviders = new Map(); + private _fileToProviders = new Map(); private _registrationComplete = new Deferred(); private _isRegistrationComplete = false; constructor( @IStorageService private _storageService: IStorageService, @IExtensionService extensionService: IExtensionService, - @IExtensionManagementService extensionManagementService: IExtensionManagementService + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IInstantiationService private _instantiationService: IInstantiationService ) { super(); this._register(notebookRegistry.onNewRegistration(this.updateRegisteredProviders, this)); - this.registerDefaultProvider(); + this.registerBuiltInProvider(); if (extensionService) { extensionService.whenInstalledExtensionsRegistered().then(() => { @@ -142,25 +146,33 @@ export class NotebookService extends Disposable implements INotebookService { } private addFileProvider(fileType: string, provider: NotebookProviderRegistration) { - this._fileToProviders.set(fileType.toUpperCase(), provider); + let providers = this._fileToProviders.get(fileType.toUpperCase()); + if (!providers) { + providers = []; + } + providers.push(provider); + this._fileToProviders.set(fileType.toUpperCase(), providers); } getSupportedFileExtensions(): string[] { return Array.from(this._fileToProviders.keys()); } - getProviderForFileType(fileType: string): string { + getProvidersForFileType(fileType: string): string[] { fileType = fileType.toUpperCase(); - let provider = this._fileToProviders.get(fileType); - return provider ? provider.provider : undefined; + let providers = this._fileToProviders.get(fileType); + + return providers ? providers.map(provider => provider.provider) : undefined; } public shutdown(): void { - this._managers.forEach(manager => { - if (manager.serverManager) { - // TODO should this thenable be awaited? - manager.serverManager.stopServer(); - } + this._managersMap.forEach(manager => { + manager.forEach(m => { + if (m.serverManager) { + // TODO should this thenable be awaited? + m.serverManager.stopServer(); + } + }); }); } @@ -169,14 +181,20 @@ export class NotebookService extends Disposable implements INotebookService { throw new Error(localize('notebookUriNotDefined', 'No URI was passed when creating a notebook manager')); } let uriString = uri.toString(); - let manager = this._managers.get(uriString); - if (!manager) { - manager = await this.doWithProvider(providerId, (provider) => provider.getNotebookManager(uri)); - if (manager) { - this._managers.set(uriString, manager); + let managers: INotebookManager[] = this._managersMap.get(uriString); + // If manager already exists for a given notebook, return it + if (managers) { + let index = managers.findIndex(m => m.providerId === providerId); + if (index && index >= 0) { + return managers[index]; } } - return manager; + let newManager = await this.doWithProvider(providerId, (provider) => provider.getNotebookManager(uri)); + + managers = managers || []; + managers.push(newManager); + this._managersMap.set(uriString, managers); + return newManager; } get onNotebookEditorAdd(): Event { @@ -226,12 +244,14 @@ export class NotebookService extends Disposable implements INotebookService { private sendNotebookCloseToProvider(editor: INotebookEditor): void { let notebookUri = editor.notebookParams.notebookUri; let uriString = notebookUri.toString(); - let manager = this._managers.get(uriString); + let manager = this._managersMap.get(uriString); if (manager) { // As we have a manager, we can assume provider is ready - this._managers.delete(uriString); - let provider = this._providers.get(manager.providerId); - provider.instance.handleNotebookClosed(notebookUri); + this._managersMap.delete(uriString); + manager.forEach(m => { + let provider = this._providers.get(m.providerId); + provider.instance.handleNotebookClosed(notebookUri); + }); } } @@ -303,13 +323,24 @@ export class NotebookService extends Disposable implements INotebookService { } } - private registerDefaultProvider() { - let defaultProvider = new BuiltinProvider(); - this.registerProvider(defaultProvider.providerId, defaultProvider); - notebookRegistry.registerNotebookProvider({ - provider: defaultProvider.providerId, - fileExtensions: DEFAULT_NOTEBOOK_FILETYPE - }); + private registerBuiltInProvider() { + if (!sqlNotebooksEnabled()) { + let defaultProvider = new BuiltinProvider(); + this.registerProvider(defaultProvider.providerId, defaultProvider); + notebookRegistry.registerNotebookProvider({ + provider: defaultProvider.providerId, + fileExtensions: DEFAULT_NOTEBOOK_FILETYPE, + standardKernels: [] + }); + } else { + let sqlProvider = new SqlNotebookProvider(this._instantiationService); + this.registerProvider(sqlProvider.providerId, sqlProvider); + notebookRegistry.registerNotebookProvider({ + provider: sqlProvider.providerId, + fileExtensions: DEFAULT_NOTEBOOK_FILETYPE, + standardKernels: ['SQL'] + }); + } } private removeContributedProvidersFromCache(identifier: IExtensionIdentifier, extensionService: IExtensionService) { @@ -330,6 +361,7 @@ export class BuiltinProvider implements INotebookProvider { constructor() { this.manager = new BuiltInNotebookManager(); } + public get providerId(): string { return DEFAULT_NOTEBOOK_PROVIDER; } @@ -350,6 +382,7 @@ export class BuiltInNotebookManager implements INotebookManager { this._contentManager = new LocalContentManager(); this._sessionManager = new SessionManager(); } + public get providerId(): string { return DEFAULT_NOTEBOOK_PROVIDER; } @@ -367,3 +400,50 @@ export class BuiltInNotebookManager implements INotebookManager { } } + +export class SqlNotebookProvider implements INotebookProvider { + private manager: SqlNotebookManager; + + constructor(private _instantiationService: IInstantiationService) { + this.manager = new SqlNotebookManager(this._instantiationService); + } + + public get providerId(): string { + return SQL_NOTEBOOK_PROVIDER; + } + + getNotebookManager(notebookUri: URI): Thenable { + return Promise.resolve(this.manager); + } + + handleNotebookClosed(notebookUri: URI): void { + // No-op + } +} + +export class SqlNotebookManager implements INotebookManager { + private _contentManager: nb.ContentManager; + private _sessionManager: nb.SessionManager; + + constructor(private _instantiationService: IInstantiationService) { + this._contentManager = new LocalContentManager(); + this._sessionManager = new SqlSessionManager(this._instantiationService); + } + + public get providerId(): string { + return SQL_NOTEBOOK_PROVIDER; + } + + public get contentManager(): nb.ContentManager { + return this._contentManager; + } + + public get serverManager(): nb.ServerManager { + return undefined; + } + + public get sessionManager(): nb.SessionManager { + return this._sessionManager; + } + +} \ No newline at end of file diff --git a/src/sql/services/notebook/sessionManager.ts b/src/sql/services/notebook/sessionManager.ts index fb78d91817..b177ef448d 100644 --- a/src/sql/services/notebook/sessionManager.ts +++ b/src/sql/services/notebook/sessionManager.ts @@ -1,3 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; diff --git a/src/sql/services/notebook/sqlSessionManager.ts b/src/sql/services/notebook/sqlSessionManager.ts new file mode 100644 index 0000000000..8444eacc4e --- /dev/null +++ b/src/sql/services/notebook/sqlSessionManager.ts @@ -0,0 +1,325 @@ +/*--------------------------------------------------------------------------------------------- + * 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, QueryExecuteSubsetResult, IDbColumn, DbCellValue } from 'sqlops'; +import { localize } from 'vs/nls'; +import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces'; +import QueryRunner, { EventType } from 'sql/parts/query/execution/queryRunner'; +import { IConnectionManagementService, IErrorMessageService } from 'sql/parts/connection/common/connectionManagement'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import Severity from 'vs/base/common/severity'; +import * as Utils from 'sql/parts/connection/common/utils'; +import { Deferred } from 'sql/base/common/promise'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { mssqlProviderName } from 'sql/parts/connection/common/constants'; + +export const sqlKernel: string = localize('sqlKernel', 'SQL'); +export const sqlKernelError: string = localize("sqlKernelError", "SQL kernel error"); + +let sqlKernelSpec: nb.IKernelSpec = ({ + name: sqlKernel, + language: 'sql', + display_name: sqlKernel +}); + +export interface SQLData { + columns: Array; + rows: Array>; +} + +export class SqlSessionManager implements nb.SessionManager { + constructor(private _instantiationService: IInstantiationService) {} + + public get isReady(): boolean { + return true; + } + + public get ready(): Thenable { + return Promise.resolve(); + } + + public get specs(): nb.IAllKernels { + let allKernels: nb.IAllKernels = { + defaultKernel: sqlKernel, + kernels: [sqlKernelSpec] + }; + return allKernels; + } + + startNew(options: nb.ISessionOptions): Thenable { + let session = new SqlSession(options, this._instantiationService); + return Promise.resolve(session); + } + + shutdown(id: string): Thenable { + return Promise.resolve(); + } +} + +export class SqlSession implements nb.ISession { + private _kernel: SqlKernel; + private _defaultKernelLoaded = false; + + public set defaultKernelLoaded(value) { + this._defaultKernelLoaded = value; + } + + public get defaultKernelLoaded(): boolean { + return this._defaultKernelLoaded; + } + + constructor(private options: nb.ISessionOptions, private _instantiationService: IInstantiationService) { + this._kernel = this._instantiationService.createInstance(SqlKernel); + } + + public get canChangeKernels(): boolean { + return true; + } + + public get id(): string { + return this.options.kernelId || ''; + } + + public get path(): string { + return this.options.path; + } + + public get name(): string { + return this.options.name || ''; + } + + public get type(): string { + return this.options.type || ''; + } + + public get status(): nb.KernelStatus { + return 'connected'; + } + + public get kernel(): nb.IKernel { + return this._kernel; + } + + changeKernel(kernelInfo: nb.IKernelSpec): Thenable { + return Promise.resolve(this.kernel); + } +} + +class SqlKernel extends Disposable implements nb.IKernel { + private _queryRunner: QueryRunner; + private _columns: IDbColumn[]; + private _rows: DbCellValue[][]; + + constructor(@IConnectionManagementService private _connectionManagementService: IConnectionManagementService, + @IInstantiationService private _instantiationService: IInstantiationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService) { + super(); + } + + public get id(): string { + return '-1'; + } + + public get name(): string { + return sqlKernel; + } + + public get supportsIntellisense(): boolean { + return true; + } + + public get isReady(): boolean { + // should we be checking on the tools service status here? + return true; + } + + public get ready(): Thenable { + return Promise.resolve(); + } + + public get info(): nb.IInfoReply { + let info: nb.IInfoReply = { + protocol_version: '', + implementation: '', + implementation_version: '', + language_info: { + name: 'sql', + version: '', + }, + banner: '', + help_links: [{ + text: '', + url: '' + }] + }; + + return info; + } + getSpec(): Thenable { + return Promise.resolve(sqlKernelSpec); + } + + requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture { + if (this._queryRunner) { + this._queryRunner.runQuery(content.code); + } else { + let connections = this._connectionManagementService.getActiveConnections(); + let connectionProfile = connections.find(connection => connection.providerName === mssqlProviderName); + let connectionUri = Utils.generateUri(connectionProfile, 'notebook'); + this._queryRunner = this._instantiationService.createInstance(QueryRunner, connectionUri, undefined); + this._connectionManagementService.connect(connectionProfile, connectionUri).then((result) => + { + this.addQueryEventListeners(this._queryRunner); + this._queryRunner.runQuery(content.code); + }); + } + + return new SQLFuture(this._queryRunner); + } + + requestComplete(content: nb.ICompleteRequest): Thenable { + let response: Partial = { }; + return Promise.resolve(response as nb.ICompleteReplyMsg); + } + + interrupt(): Thenable { + return Promise.resolve(undefined); + } + + private addQueryEventListeners(queryRunner: QueryRunner): void { + this._register(queryRunner.addListener(EventType.COMPLETE, () => { + this.queryComplete().catch(error => { + this._errorMessageService.showDialog(Severity.Error, sqlKernelError, error); + }); + })); + this._register(queryRunner.addListener(EventType.MESSAGE, message => { + if (message.isError) { + this._errorMessageService.showDialog(Severity.Error, sqlKernelError, message.message); + } + })); + } + + private async queryComplete(): Promise { + let batches = this._queryRunner.batchSets; + // currently only support 1 batch set 1 resultset + if (batches.length > 0) { + let batch = batches[0]; + if (batch.resultSetSummaries.length > 0 + && batch.resultSetSummaries[0].rowCount > 0 + ) { + let resultset = batch.resultSetSummaries[0]; + this._columns = resultset.columnInfo; + let rows: QueryExecuteSubsetResult; + try { + rows = await this._queryRunner.getQueryRows(0, resultset.rowCount, batch.id, resultset.id); + } catch (e) { + return Promise.reject(e); + } + this._rows = rows.resultSubset.rows; + } + } + // TODO issue #2746 should ideally show a warning inside the dialog if have no data + } +} + +export class SQLFuture extends Disposable implements FutureInternal { + private _msg: nb.IMessage = undefined; + + constructor(private _queryRunner: QueryRunner) { + super(); + } + get inProgress(): boolean { + return !this._queryRunner.hasCompleted; + } + + get msg(): nb.IMessage { + return this._msg; + } + + get done(): Thenable { + let deferred = new Deferred (); + try { + this._register(this._queryRunner.onBatchEnd(e => { + let msg: nb.IShellMessage = { + channel: 'shell', + type: 'execute_reply', + content: { status: 'ok' }, + header: undefined, + metadata: {}, + parent_header: undefined + }; + this._msg = msg; + deferred.resolve(msg); + })); + } catch { + return Promise.resolve(undefined); + } + return deferred.promise; + } + + sendInputReply(content: nb.IInputReply): void { + // no-op + } + + setReplyHandler(handler: nb.MessageHandler): void { + // no-op + } + setStdInHandler(handler: nb.MessageHandler): void { + // no-op + } + setIOPubHandler(handler: nb.MessageHandler): void { + this._register(this._queryRunner.onBatchEnd(batch => { + this._queryRunner.getQueryRows(0, batch.resultSetSummaries[0].rowCount, 0, 0).then(d => { + let data:SQLData = { + columns: batch.resultSetSummaries[0].columnInfo.map(c => c.columnName), + rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue)) + }; + let table: HTMLTableElement = document.createElement('table'); + table.createTHead(); + table.createTBody(); + let hrow = table.insertRow(); + // headers + for (let column of data.columns) { + var cell = hrow.insertCell(); + cell.innerHTML = column; + } + + for (let row in data.rows) { + let hrow = table.insertRow(); + for (let column in data.columns) { + var cell = hrow.insertCell(); + cell.innerHTML = data.rows[row][column]; + } + } + let tableHtml = '' + table.innerHTML + '
'; + + let msg: nb.IIOPubMessage = { + channel: 'iopub', + type: 'iopub', + header: { + msg_id: undefined, + msg_type: 'execute_result' + }, + content: { + output_type: 'execute_result', + metadata: {}, + execution_count: 0, + data: { 'text/html' : tableHtml}, + }, + metadata: undefined, + parent_header: undefined + }; + handler.handle(msg); + }); + })); + } + registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable): void { + // no-op + } + removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable): void { + // no-op + } +} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 23da9370ed..093e44a0dc 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1655,6 +1655,7 @@ declare module 'sqlops' { export interface NotebookProvider { readonly providerId: string; + readonly standardKernels: string[]; getNotebookManager(notebookUri: vscode.Uri): Thenable; handleNotebookClosed(notebookUri: vscode.Uri): void; } @@ -2259,7 +2260,7 @@ declare module 'sqlops' { /** * The valid channel names. */ - export type Channel = 'shell' | 'iopub' | 'stdin'; + export type Channel = 'shell' | 'iopub' | 'stdin' | 'execute_reply'; /** * Kernel message header content. diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts index aef785599f..8d51ad54be 100644 --- a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -23,9 +23,9 @@ import { INotebookDocumentsAndEditorsDelta, INotebookEditorAddData, INotebookShowOptions, INotebookModelAddedData, INotebookModelChangedData } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { NotebookInputModel, NotebookInput } from 'sql/parts/notebook/notebookInput'; -import { INotebookService, INotebookEditor } from 'sql/services/notebook/notebookService'; +import { INotebookService, INotebookEditor, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; import { TPromise } from 'vs/base/common/winjs.base'; -import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils'; +import { getProvidersForFileName } from 'sql/parts/notebook/notebookUtils'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; import { disposed } from 'vs/base/common/errors'; import { ICellModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces'; @@ -57,6 +57,10 @@ class MainThreadNotebookEditor extends Disposable { return this.editor.notebookParams.providerId; } + public get providers(): string[] { + return this.editor.notebookParams.providers; + } + public get cells(): ICellModel[] { return this.editor.cells; } @@ -316,11 +320,20 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements let trusted = uri.scheme === Schemas.untitled; let model = new NotebookInputModel(uri, undefined, trusted, undefined); let providerId = options.providerId; - if (!providerId) { + let providers: string[] = undefined; + if (!providerId) + { // Ensure there is always a sensible provider ID for this file type - providerId = getProviderForFileName(uri.fsPath, this._notebookService); + providers = getProvidersForFileName(uri.fsPath, this._notebookService); + // Try to use a non-builtin provider first + if (providers) { + providerId = providers.find(p => p !== DEFAULT_NOTEBOOK_PROVIDER); + if (!providerId) { + providerId = model.providerId; + } + } } - + model.providers = providers; model.providerId = providerId; let input = this._instantiationService.createInstance(NotebookInput, undefined, model); @@ -452,6 +465,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements uri: editor.uri, isDirty: editor.isDirty, providerId: editor.providerId, + providers: editor.providers, cells: this.convertCellModelToNotebookCell(editor.cells) }; return addData; @@ -463,6 +477,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements cells: this.convertCellModelToNotebookCell(editor.cells), isDirty: e.isDirty, providerId: editor.providerId, + providers: editor.providers, uri: editor.uri }; return changeData; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 23aab8334a..25276a9ffb 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -796,6 +796,7 @@ export interface INotebookDocumentsAndEditorsDelta { export interface INotebookModelAddedData { uri: UriComponents; providerId: string; + providers: string[]; isDirty: boolean; cells: sqlops.nb.NotebookCell[]; } @@ -803,6 +804,7 @@ export interface INotebookModelAddedData { export interface INotebookModelChangedData { uri: UriComponents; providerId: string; + providers: string[]; isDirty: boolean; cells: sqlops.nb.NotebookCell[]; } diff --git a/src/sqltest/parts/notebook/common.ts b/src/sqltest/parts/notebook/common.ts index 8302593538..4069b85706 100644 --- a/src/sqltest/parts/notebook/common.ts +++ b/src/sqltest/parts/notebook/common.ts @@ -30,7 +30,7 @@ export class NotebookModelStub implements INotebookModel { get clientSession(): IClientSession { throw new Error('method not implemented.'); } - get notebookManager(): INotebookManager { + get notebookManagers(): INotebookManager[] { throw new Error('method not implemented.'); } get kernelChanged(): Event { @@ -53,6 +53,9 @@ export class NotebookModelStub implements INotebookModel { get contexts(): IDefaultConnection { throw new Error('method not implemented.'); } + get providerId(): string { + throw new Error('method not implemented.'); + } changeKernel(displayName: string): void { throw new Error('Method not implemented.'); } diff --git a/src/sqltest/parts/notebook/model/notebookModel.test.ts b/src/sqltest/parts/notebook/model/notebookModel.test.ts index 1f5915cadf..f91d50c343 100644 --- a/src/sqltest/parts/notebook/model/notebookModel.test.ts +++ b/src/sqltest/parts/notebook/model/notebookModel.test.ts @@ -73,7 +73,7 @@ let mockModelFactory: TypeMoq.Mock; let notificationService: TypeMoq.Mock; describe('notebook model', function(): void { - let notebookManager = new NotebookManagerStub(); + let notebookManagers = [new NotebookManagerStub()]; let memento: TypeMoq.Mock; let queryConnectionService: TypeMoq.Mock; let defaultModelOptions: INotebookModelOptions; @@ -87,9 +87,10 @@ describe('notebook model', function(): void { defaultModelOptions = { notebookUri: defaultUri, factory: new ModelFactory(), - notebookManager, + notebookManagers, notificationService: notificationService.object, - connectionService: queryConnectionService.object }; + connectionService: queryConnectionService.object, + providerId: 'jupyter' }; mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); mockClientSession.setup(c => c.initialize(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(); @@ -118,7 +119,7 @@ describe('notebook model', function(): void { let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(emptyNotebook)); - notebookManager.contentManager = mockContentManager.object; + notebookManagers[0].contentManager = mockContentManager.object; // When I initialize the model let model = new NotebookModel(defaultModelOptions); @@ -134,7 +135,7 @@ describe('notebook model', function(): void { let error = new Error('File not found'); let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).throws(error); - notebookManager.contentManager = mockContentManager.object; + notebookManagers[0].contentManager = mockContentManager.object; // When I initalize the model // Then it should throw @@ -148,7 +149,7 @@ describe('notebook model', function(): void { // Given a notebook with 2 cells let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContent)); - notebookManager.contentManager = mockContentManager.object; + notebookManagers[0].contentManager = mockContentManager.object; // When I initalize the model let model = new NotebookModel(defaultModelOptions); @@ -163,7 +164,7 @@ describe('notebook model', function(): void { it('Should load contents but then go to error state if client session startup fails', async function(): Promise { let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); - notebookManager.contentManager = mockContentManager.object; + notebookManagers[0].contentManager = mockContentManager.object; // Given I have a session that fails to start mockClientSession.setup(c => c.isInErrorState).returns(() => true); @@ -192,7 +193,7 @@ describe('notebook model', function(): void { it('Should not be in error state if client session initialization succeeds', async function(): Promise { let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); - notebookManager.contentManager = mockContentManager.object; + notebookManagers[0].contentManager = mockContentManager.object; let kernelChangedEmitter: Emitter = new Emitter(); mockClientSession.setup(c => c.isInErrorState).returns(() => false); diff --git a/src/sqltest/workbench/api/exthostNotebook.test.ts b/src/sqltest/workbench/api/exthostNotebook.test.ts index 5f73578353..5343a9bd32 100644 --- a/src/sqltest/workbench/api/exthostNotebook.test.ts +++ b/src/sqltest/workbench/api/exthostNotebook.test.ts @@ -122,6 +122,7 @@ suite('ExtHostNotebook Tests', () => { class NotebookProviderStub implements sqlops.nb.NotebookProvider { providerId: string = 'TestProvider'; + standardKernels: string[] = ['fakeKernel']; getNotebookManager(notebookUri: vscode.Uri): Thenable { throw new Error('Method not implemented.');