diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index d3af5637d3..bd831891bb 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -27,7 +27,6 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import { UntitledTextEditorModel, IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { AbstractResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { NotebookFindModel } from 'sql/workbench/contrib/notebook/browser/find/notebookFindModel'; @@ -216,7 +215,7 @@ export class NotebookEditorModel extends EditorModel { } } -type TextInput = AbstractResourceEditorInput | UntitledTextEditorInput | FileEditorInput; +type TextInput = UntitledTextEditorInput | FileEditorInput; export abstract class NotebookInput extends EditorInput implements INotebookInput { private _providerId: string; @@ -259,6 +258,10 @@ export abstract class NotebookInput extends EditorInput implements INotebookInpu } } + public get languageMode(): string { + return this._textInput.getMode(); + } + public get textInput(): TextInput { return this._textInput; } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 350062d79b..03bcbc1aec 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -597,6 +597,9 @@ export class KernelsDropdown extends SelectBox { this._register(this.model.kernelChanged((changedArgs: azdata.nb.IKernelChangedArgs) => { this.updateKernel(changedArgs.newValue, changedArgs.nbKernelAlias); })); + this._register(this.model.kernelsChanged(kernel => { + this.updateKernel(kernel); + })); let kernel = this.model.clientSession && this.model.clientSession.kernel; this.updateKernel(kernel); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts index c3b68d45a4..38052f1f5f 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts @@ -12,8 +12,6 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con import { CellMagicMapper } from 'sql/workbench/contrib/notebook/browser/models/cellMagicMapper'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; -import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; -import { ILogService } from 'vs/platform/log/common/log'; import { IModelFactory, ViewMode, NotebookContentChange, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -23,12 +21,10 @@ import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; import { Deferred } from 'sql/base/common/promise'; import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { localize } from 'vs/nls'; import * as path from 'vs/base/common/path'; @@ -53,19 +49,15 @@ export class NotebookEditorComponent extends AngularDisposable { public ViewMode = ViewMode; //For use of the enum in the template constructor( - @Inject(ILogService) private readonly logService: ILogService, @Inject(IBootstrapParams) private _notebookParams: INotebookParams, @Inject(INotebookService) private notebookService: INotebookService, @Inject(ICapabilitiesService) private capabilitiesService: ICapabilitiesService, @Inject(IContextKeyService) private contextKeyService: IContextKeyService, @Inject(IMenuService) private menuService: IMenuService, @Inject(INotificationService) private notificationService: INotificationService, - @Inject(IAdsTelemetryService) private adstelemetryService: IAdsTelemetryService, @Inject(IInstantiationService) private instantiationService: IInstantiationService, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, - @Inject(IConfigurationService) private _configurationService: IConfigurationService, @Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService, - @Inject(IUndoRedoService) private _undoService: IUndoRedoService, ) { super(); this.updateProfile(); @@ -128,7 +120,7 @@ export class NotebookEditorComponent extends AngularDisposable { private async createModelAndLoadContents(): Promise { let providerInfo = await this._notebookParams.providerInfo; - let model = new NotebookModel({ + let model = this.instantiationService.createInstance(NotebookModel, { factory: this.modelFactory, notebookUri: this._notebookParams.notebookUri, connectionService: this.connectionManagementService, @@ -141,8 +133,9 @@ export class NotebookEditorComponent extends AngularDisposable { defaultKernel: this._notebookParams.input.defaultKernel, layoutChanged: this._notebookParams.input.layoutChanged, capabilitiesService: this.capabilitiesService, - editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp - }, this.profile, this.logService, this.notificationService, this.adstelemetryService, this.connectionManagementService, this._configurationService, this._undoService, this.capabilitiesService); + editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp, + getInputLanguageMode: () => this._notebookParams.input.languageMode // Can't pass in languageMode directly since it can change after the editor loads + }, this.profile); let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty()); this.model = this._register(model); diff --git a/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts index 42cd97f97e..3d5e9f89d4 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts @@ -31,10 +31,12 @@ import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import { nb } from 'azdata'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ExecuteManagerStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { ExecuteManagerStub, NotebookServiceStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; +import { Emitter } from 'vs/base/common/event'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; suite('CellToolbarActions', function (): void { suite('removeDuplicatedAndStartingSeparators', function (): void { @@ -236,7 +238,11 @@ export async function createandLoadNotebookModel(codeContent?: nb.INotebookConte cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: undefined + capabilitiesService: undefined, + getInputLanguageMode: () => undefined }; - return new NotebookModel(defaultModelOptions, undefined, undefined, undefined, new NullAdsTelemetryService(), undefined, undefined, undoRedoService); + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); + + return new NotebookModel(defaultModelOptions, undefined, undefined, undefined, new NullAdsTelemetryService(), undefined, undefined, undoRedoService, mockNotebookService.object, undefined, undefined); } diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index b8ece2d471..9f8a29cb71 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -11,7 +11,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { URI } from 'vs/base/common/uri'; -import { ExecuteManagerStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { ExecuteManagerStub, NotebookServiceStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory'; import { INotebookModelOptions } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; @@ -34,6 +34,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { NotebookViewModel } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel'; import { SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; +import { Emitter } from 'vs/base/common/event'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; let initialNotebookContent: nb.INotebookContents = { cells: [{ @@ -239,7 +241,8 @@ suite('NotebookViewModel', function (): void { cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: capabilitiesService.object + capabilitiesService: capabilitiesService.object, + getInputLanguageMode: () => undefined }; } @@ -247,8 +250,10 @@ suite('NotebookViewModel', function (): void { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(contents)); defaultModelOptions.contentLoader = mockContentManager.object; + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undefined); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undefined, mockNotebookService.object, undefined, undefined); await model.loadContents(); await model.requestModelLoad(); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts index 9f487c06f2..05fc2d7862 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts @@ -11,7 +11,7 @@ import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTeleme import { NotebookEditorContentLoader } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; import { DeleteViewAction, InsertCellAction } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsActions'; import { SessionManager } from 'sql/workbench/contrib/notebook/test/emptySessionClasses'; -import { ExecuteManagerStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { ExecuteManagerStub, NotebookServiceStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory'; import { ICellModel, INotebookModelOptions, ViewMode } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; @@ -35,6 +35,8 @@ import { InsertCellsModal } from 'sql/workbench/contrib/notebook/browser/noteboo import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; +import { Emitter } from 'vs/base/common/event'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; let initialNotebookContent: nb.INotebookContents = { cells: [{ @@ -190,7 +192,8 @@ suite('Notebook Views Actions', function (): void { cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: capabilitiesService.object + capabilitiesService: capabilitiesService.object, + getInputLanguageMode: () => undefined }; } @@ -198,8 +201,10 @@ suite('Notebook Views Actions', function (): void { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(contents)); defaultModelOptions.contentLoader = mockContentManager.object; + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undefined); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undefined, mockNotebookService.object, undefined, undefined); await model.loadContents(); await model.requestModelLoad(); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts index 21fadf00fb..525c55d1d5 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts @@ -11,7 +11,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { URI } from 'vs/base/common/uri'; -import { ExecuteManagerStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { ExecuteManagerStub, NotebookServiceStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory'; import { INotebookModelOptions } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; @@ -37,6 +37,8 @@ import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; +import { Emitter } from 'vs/base/common/event'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; let initialNotebookContent: nb.INotebookContents = { cells: [{ @@ -171,7 +173,8 @@ suite('NotebookViews', function (): void { cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: capabilitiesService.object + capabilitiesService: capabilitiesService.object, + getInputLanguageMode: () => undefined }; } @@ -179,8 +182,10 @@ suite('NotebookViews', function (): void { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(initialNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, undefined); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, mockNotebookService.object, undefined, undefined); await model.loadContents(); await model.requestModelLoad(); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts index 07863f1736..0010607ed6 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts @@ -42,6 +42,8 @@ import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTeleme import { IProductService } from 'vs/platform/product/common/productService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; class ServiceAccessor { @@ -175,7 +177,8 @@ suite('Notebook Editor Model', function (): void { cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: capabilitiesService.object + capabilitiesService: capabilitiesService.object, + getInputLanguageMode: () => undefined }; }); @@ -983,7 +986,10 @@ suite('Notebook Editor Model', function (): void { let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, >{ factory: mockModelFactory.object }); - notebookModel = new NotebookModel(options, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, undefined); + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); + + notebookModel = new NotebookModel(options, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, mockNotebookService.object, undefined, undefined); await notebookModel.loadContents(); } diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts index c25a5c80a1..942075979c 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts @@ -7,7 +7,7 @@ import { nb } from 'azdata'; import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ExecuteManagerStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { ExecuteManagerStub, NotebookServiceStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import { IClientSession, INotebookModelOptions } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; @@ -34,6 +34,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; import { SessionManager } from 'sql/workbench/contrib/notebook/test/emptySessionClasses'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; +import { Emitter } from 'vs/base/common/event'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; let expectedNotebookContent: nb.INotebookContents = { cells: [{ @@ -106,7 +108,8 @@ suite('Notebook Find Model', function (): void { cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: capabilitiesService.object + capabilitiesService: capabilitiesService.object, + getInputLanguageMode: () => undefined }; mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); mockClientSession.setup(c => c.initialize()).returns(() => { @@ -603,8 +606,11 @@ suite('Notebook Find Model', function (): void { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(contents)); defaultModelOptions.contentLoader = mockContentManager.object; + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); + // Initialize the model - model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undefined); + model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undefined, mockNotebookService.object, undefined, undefined); await model.loadContents(); await model.requestModelLoad(); } diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts index 395ad9e6fe..d508dcb92d 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts @@ -14,7 +14,7 @@ import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogSer import { URI } from 'vs/base/common/uri'; -import { ExecuteManagerStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { ExecuteManagerStub, NotebookServiceStub, SerializationManagerStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { NotebookModel, SplitCell } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory'; import { IClientSession, INotebookModelOptions, NotebookContentChange, IClientSessionOptions, ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; @@ -42,8 +42,10 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; -import { SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; +import { DEFAULT_NOTEBOOK_FILETYPE, IExecuteManager, INotebookService, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants'; +import { Emitter } from 'vs/base/common/event'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; let expectedNotebookContent: nb.INotebookContents = { cells: [{ @@ -145,6 +147,7 @@ let undoRedoService: IUndoRedoService; let capabilitiesService: ICapabilitiesService; let instantiationService: IInstantiationService; let configurationService: IConfigurationService; +let notebookService: INotebookService; suite('notebook model', function (): void { let serializationManagers = [new SerializationManagerStub()]; @@ -163,6 +166,9 @@ suite('notebook model', function (): void { dialogService = TypeMoq.Mock.ofType(TestDialogService, TypeMoq.MockBehavior.Loose); undoRedoService = new UndoRedoService(dialogService.object, notificationService.object); capabilitiesService = new TestCapabilitiesService(); + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => new Emitter().event); + notebookService = mockNotebookService.object; memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => void 0); queryConnectionService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService()); @@ -182,7 +188,8 @@ suite('notebook model', function (): void { cellMagicMapper: undefined, defaultKernel: undefined, layoutChanged: undefined, - capabilitiesService: capabilitiesService + capabilitiesService: capabilitiesService, + getInputLanguageMode: () => 'notebook' }; clientSessionOptions = { executeManager: defaultModelOptions.executeManagers[0], @@ -218,7 +225,7 @@ suite('notebook model', function (): void { mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(emptyNotebook)); defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // Then I expect to have 0 code cell as the contents @@ -234,7 +241,7 @@ suite('notebook model', function (): void { mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(true); await model.requestModelLoad(); @@ -251,7 +258,7 @@ suite('notebook model', function (): void { // When I initalize the model // Then it should throw - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); assert.strictEqual(model.inErrorState, false); await assert.rejects(async () => { await model.loadContents(); }); assert.strictEqual(model.inErrorState, true); @@ -264,7 +271,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initalize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // Then I expect all cells to be in the model @@ -292,7 +299,7 @@ suite('notebook model', function (): void { defaultModelOptions.providerId = 'jupyter'; // When I initalize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // I expect the default provider to be jupyter @@ -302,7 +309,7 @@ suite('notebook model', function (): void { defaultModelOptions.providerId = 'SQL'; // When I initalize the model - model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // I expect the default provider to be SQL @@ -327,7 +334,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initalize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let activeCellChangeCount = 0; @@ -384,7 +391,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); assert.strictEqual(model.notebookUri, defaultModelOptions.notebookUri, 'Notebook model has incorrect URI'); @@ -412,7 +419,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); assert.strictEqual(model.notebookUri, defaultModelOptions.notebookUri, 'Notebook model has incorrect URI'); @@ -436,7 +443,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); assert.strictEqual(model.notebookUri, defaultModelOptions.notebookUri, 'Notebook model has incorrect URI'); @@ -457,7 +464,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); assert.strictEqual(model.notebookUri, defaultModelOptions.notebookUri, 'Notebook model has incorrect URI'); @@ -476,7 +483,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); assert.strictEqual(model.notebookUri, defaultModelOptions.notebookUri, 'Notebook model has incorrect URI'); @@ -496,7 +503,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); assert.strictEqual(model.notebookUri, defaultModelOptions.notebookUri, 'Notebook model has incorrect URI'); @@ -517,7 +524,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initalize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // Count number of times onError event is fired @@ -569,7 +576,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initalize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // Then I expect all cells to be in the model @@ -591,7 +598,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let firstCell = model.cells[0]; @@ -637,7 +644,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let splitCells: SplitCell[] = [ @@ -657,7 +664,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initalize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let notebookContentChange: NotebookContentChange; @@ -673,7 +680,7 @@ suite('notebook model', function (): void { mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let newCell: ICellModel; @@ -705,7 +712,7 @@ suite('notebook model', function (): void { sessionReady.resolve(); let sessionFired = false; - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); model.onClientSessionReady((session) => sessionFired = true); await model.loadContents(); await model.requestModelLoad(); @@ -735,7 +742,7 @@ suite('notebook model', function (): void { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.requestModelLoad(); let actualChanged: NotebookContentChange; @@ -804,7 +811,7 @@ suite('notebook model', function (): void { mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, undefined, queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, undefined, queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let output = model.toJSON(); @@ -937,7 +944,7 @@ suite('notebook model', function (): void { sinon.stub(configurationService, 'getValue').returns(true); // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // I expect the saved connection name to be read @@ -966,7 +973,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentLoader = mockContentManager.object; // When I initialize the model - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); // I expect multiConnectionMode to be set to true @@ -997,7 +1004,7 @@ suite('notebook model', function (): void { // Given I have a session that fails to start sessionReady.resolve(); - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); await model.requestModelLoad(); @@ -1011,7 +1018,7 @@ suite('notebook model', function (): void { mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); const newLanguage = 'CustomCellLanguage'; @@ -1025,7 +1032,7 @@ suite('notebook model', function (): void { mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); defaultModelOptions.contentLoader = mockContentManager.object; - let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService); + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, undefined, undefined); await model.loadContents(); let cell = model.addCell(CellTypes.Code); @@ -1033,6 +1040,81 @@ suite('notebook model', function (): void { assert.strictEqual(cell.language, expectedNotebookContent.metadata.language_info.name); }); + test('Should update kernels list when new kernels are installed', async function () { + let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); + mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(expectedNotebookContent)); + defaultModelOptions.contentLoader = mockContentManager.object; + + let kernelsAddedEmitter = new Emitter(); + let mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); + mockNotebookService.setup(s => s.onNotebookKernelsAdded).returns(() => kernelsAddedEmitter.event); + let mockExecuteManager = TypeMoq.Mock.ofType(ExecuteManagerStub); + mockNotebookService.setup(s => s.getOrCreateExecuteManager(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockExecuteManager.object)); + + let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, mockNotebookService.object, undefined, undefined); + model.standardKernels = [{ + name: 'SQL', + displayName: 'SQL', + connectionProviderIds: [], + notebookProvider: 'sql', + supportedLanguages: ['sql'], + supportedFileExtensions: ['.ipynb'] + }]; + await model.loadContents(); + + assert.strictEqual(model.standardKernels.length, 1, 'Should start with only 1 kernel in the notebook model.'); + assert.strictEqual(model.executeManagers.length, 1, 'Should start with only 1 execute manager in the notebook model.'); + + let expectedKernel: IStandardKernelWithProvider = { + name: 'csharp-test', + displayName: 'CSharpTest', + connectionProviderIds: undefined, + notebookProvider: 'csharp-test', + supportedLanguages: ['csharp'], + supportedFileExtensions: [DEFAULT_NOTEBOOK_FILETYPE] + }; + let kernelsAddedPromise = new Promise(resolve => { + model.kernelsChanged(kernel => { + resolve(); + }); + }); + let timeoutPromise = new Promise((resolve, reject) => setTimeout(() => { + reject('KernelsAdded event failed to fire within expected time.'); + }, 4000)); + + kernelsAddedEmitter.fire([expectedKernel]); + await Promise.race([kernelsAddedPromise, timeoutPromise]); + + assert.strictEqual(model.standardKernels.length, 2, 'New kernel was not registered.'); + assert.strictEqual(model.executeManagers.length, 2, 'Should create another execute manager when adding a new provider\'s kernel.'); + assert.deepStrictEqual(model.standardKernels[1], expectedKernel, 'Did not add expected kernel.'); + + // Shouldn't add kernel if it's for a different file extension + let invalidKernel = { + name: 'html-test', + displayName: 'HtmlTest', + connectionProviderIds: undefined, + notebookProvider: 'html-test', + supportedLanguages: ['html'], + supportedFileExtensions: ['.html'] + }; + kernelsAddedPromise = new Promise((resolve, reject) => { + model.kernelsChanged(kernel => { + reject('Should not have added a new kernel'); + }); + }); + timeoutPromise = new Promise((resolve, reject) => setTimeout(() => { + resolve(); + }, 1000)); + + kernelsAddedEmitter.fire([invalidKernel]); + await Promise.race([kernelsAddedPromise, timeoutPromise]); + + assert.strictEqual(model.standardKernels.length, 2, 'Should not have registered invalid kernel.'); + assert.strictEqual(model.executeManagers.length, 2, 'Should not have created another execute manager for invalid kernel.'); + assert.deepStrictEqual(model.standardKernels[1], expectedKernel, 'Did not keep old kernel.'); + }); + async function loadModelAndStartClientSession(notebookContent: nb.INotebookContents): Promise { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentLoader); mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(notebookContent)); @@ -1046,7 +1128,7 @@ suite('notebook model', function (): void { let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, >{ factory: mockModelFactory.object }); - let model = new NotebookModel(options, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, capabilitiesService); + let model = new NotebookModel(options, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, undoRedoService, notebookService, capabilitiesService, undefined); model.onClientSessionReady((session) => actualSession = session); await model.requestModelLoad(); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index fafe6c1ad7..7de00fa018 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -236,6 +236,9 @@ export class ServerManagerStub implements nb.ServerManager { } export class NotebookServiceStub implements INotebookService { + get onNotebookKernelsAdded(): vsEvent.Event { + throw new Error('Method not implemented.'); + } getNotebookURIForCell(cellUri: URI): URI { throw new Error('Method not implemented.'); } diff --git a/src/sql/workbench/services/notebook/browser/interface.ts b/src/sql/workbench/services/notebook/browser/interface.ts index e1244e7d59..e493aad940 100644 --- a/src/sql/workbench/services/notebook/browser/interface.ts +++ b/src/sql/workbench/services/notebook/browser/interface.ts @@ -23,6 +23,7 @@ export abstract class INotebookInput extends EditorInput { readonly standardKernels: IStandardKernelWithProvider[]; readonly providersLoaded: Promise; readonly showActions: boolean; + readonly languageMode: string; } export function isINotebookInput(value: any): value is INotebookInput { diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index 99fc54a990..d9b5d40cb8 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -622,6 +622,7 @@ export interface INotebookModelOptions { notificationService: INotificationService; connectionService: IConnectionManagementService; capabilitiesService: ICapabilitiesService; + getInputLanguageMode: () => string; editorLoadedTimestamp?: number; } diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index e10a84847a..c97d25fd4c 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -23,7 +23,7 @@ import { INotebookEditOperation, NotebookEditOperationType } from 'sql/workbench import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { uriPrefixes } from 'sql/platform/connection/common/utils'; import { ILogService } from 'vs/platform/log/common/log'; -import { getErrorMessage } from 'vs/base/common/errors'; +import { getErrorMessage, onUnexpectedError } from 'vs/base/common/errors'; import { notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces'; import { IAdsTelemetryService, ITelemetryEvent, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; import { Deferred } from 'sql/base/common/promise'; @@ -40,6 +40,8 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { deepClone } from 'vs/base/common/objects'; import { DotnetInteractiveDisplayName } from 'sql/workbench/api/common/notebooks/notebookUtils'; import { IPYKERNEL_DISPLAY_NAME } from 'sql/workbench/common/constants'; +import * as path from 'vs/base/common/path'; +import { IModeService } from 'vs/editor/common/services/modeService'; /* * Used to control whether a message in a dialog/wizard is displayed as an error, @@ -134,7 +136,9 @@ export class NotebookModel extends Disposable implements INotebookModel { @IConnectionManagementService private connectionManagementService: IConnectionManagementService, @IConfigurationService private configurationService: IConfigurationService, @IUndoRedoService private undoService: IUndoRedoService, - @ICapabilitiesService private _capabilitiesService?: ICapabilitiesService, + @INotebookService private _notebookService: INotebookService, + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, + @IModeService private _modeService: IModeService, ) { super(); if (!_notebookOptions || !_notebookOptions.notebookUri || !_notebookOptions.executeManagers) { @@ -147,6 +151,45 @@ export class NotebookModel extends Disposable implements INotebookModel { this._notebookOptions.layoutChanged(() => this._layoutChanged.fire()); } this._defaultKernel = _notebookOptions.defaultKernel; + + this._register(this._notebookService.onNotebookKernelsAdded(async kernels => this.handleNewKernelsAdded(kernels).catch(error => onUnexpectedError(error)))); + } + + // Add new kernels to the model's list as they're registered so that we don't + // need to restart the notebook to select them in the kernel dropdown. + private async handleNewKernelsAdded(kernels: notebookUtils.IStandardKernelWithProvider[]): Promise { + // Kernels are file-specific, so we need to check the file extension + // to see if the kernel is supported for this notebook. + let extensions: string[]; + let fileExt = path.extname(this._notebookOptions.notebookUri.path); + if (!fileExt) { + let languageMode = this._notebookOptions.getInputLanguageMode(); + if (languageMode) { + let languageName = this._modeService.getLanguageName(languageMode); + let fileExtensions = this._modeService.getExtensions(languageName); + if (fileExtensions?.length > 0) { + extensions = fileExtensions; + } else { + this.logService.warn(`Could not retrieve file extensions for language mode '${languageMode}' in notebook '${this._notebookOptions.notebookUri.toString()}'`); + } + } else { + this.logService.warn(`Could not determine language mode for notebook '${this._notebookOptions.notebookUri.toString()}'`); + } + } else { + extensions = [fileExt]; + } + // All kernels from the same provider share the same supported file extensions, + // so we only need to check the first one here. + if (extensions?.some(ext => kernels[0]?.supportedFileExtensions?.includes(ext))) { + this._standardKernels.push(...kernels); + this.setDisplayNameMapsForKernels(kernels); + + // Also add corresponding execute manager so that we can change to the new kernels + let manager = await this._notebookService.getOrCreateExecuteManager(kernels[0].notebookProvider, this.notebookUri); + this._notebookOptions.executeManagers.push(manager); + + this._kernelsChangedEmitter.fire(this._activeClientSession?.kernel); + } } private get serializationManagers(): ISerializationManager[] { @@ -401,7 +444,7 @@ export class NotebookModel extends Disposable implements INotebookModel { public set standardKernels(kernels: notebookUtils.IStandardKernelWithProvider[]) { this._standardKernels = kernels; - this.setKernelDisplayNameMapsWithStandardKernels(); + this.setDisplayNameMapsForKernels(kernels); } public getApplicableConnectionProviderIds(kernelDisplayName: string): string[] { @@ -1567,8 +1610,8 @@ export class NotebookModel extends Disposable implements INotebookModel { * Set maps with values to have a way to determine the connection * provider and notebook provider ids from a kernel display name */ - private setKernelDisplayNameMapsWithStandardKernels(): void { - this._standardKernels.forEach(kernel => { + private setDisplayNameMapsForKernels(kernels: notebookUtils.IStandardKernelWithProvider[]): void { + kernels.forEach(kernel => { let displayName = kernel.displayName; if (!displayName) { displayName = kernel.name; diff --git a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts index 77ffbe46c3..a9cc664ac2 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts @@ -65,6 +65,7 @@ export interface IStandardKernelWithProvider { readonly connectionProviderIds: string[]; readonly notebookProvider: string; readonly supportedLanguages: string[]; + readonly supportedFileExtensions?: string[]; } export interface IEndpoint { diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index 3dc4ee61d3..33c3ff6861 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -23,6 +23,7 @@ import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protoc import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; export const SERVICE_ID = 'sqlNotebookService'; export const INotebookService = createDecorator(SERVICE_ID); @@ -55,6 +56,7 @@ export interface INotebookService { readonly onNotebookEditorAdd: Event; readonly onNotebookEditorRemove: Event; onNotebookEditorRename: Event; + readonly onNotebookKernelsAdded: Event; readonly isRegistrationComplete: boolean; readonly registrationComplete: Promise; diff --git a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts index 2cf8544b02..186a6330d3 100644 --- a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts @@ -54,6 +54,7 @@ import { DEFAULT_NB_LANGUAGE_MODE, INTERACTIVE_LANGUAGE_MODE, INTERACTIVE_PROVID import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SqlSerializationProvider } from 'sql/workbench/services/notebook/browser/sql/sqlSerializationProvider'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; const languageAssociationRegistry = Registry.as(LanguageAssociationExtensions.LanguageAssociations); @@ -178,6 +179,7 @@ export class NotebookService extends Disposable implements INotebookService { private _onNotebookEditorAdd = new Emitter(); private _onNotebookEditorRemove = new Emitter(); private _onNotebookEditorRename = new Emitter(); + private _onNotebookKernelsAdded = new Emitter(); private _editors = new Map(); private _fileToProviderDescriptions = new Map(); private _providerToStandardKernels = new Map(); // Note: providerId key here should be in upper case @@ -426,7 +428,7 @@ export class NotebookService extends Disposable implements INotebookService { if (!this._executeProviders.has(p.id)) { this._executeProviders.set(p.id, new ExecuteProviderDescriptor(p.id)); } - this.addStandardKernels(registration); + this.addStandardKernels(registration, registration.fileExtensions); } else { // Standard kernels might get registered later for VSCode notebooks, so add a descriptor to wait on if (!this._providerToStandardKernels.has(p.id)) { @@ -506,7 +508,7 @@ export class NotebookService extends Disposable implements INotebookService { // in the kernels dropdown list before a SessionManager has been started; this way, // every NotebookProvider doesn't need to have an active SessionManager in order to contribute // kernels to the dropdown - private addStandardKernels(provider: ProviderDescriptionRegistration) { + private addStandardKernels(provider: ProviderDescriptionRegistration, supportedFileExtensions?: string[]) { let providerUpperCase = provider.provider.toUpperCase(); let descriptor = this._providerToStandardKernels.get(providerUpperCase); if (!descriptor) { @@ -526,6 +528,20 @@ export class NotebookService extends Disposable implements INotebookService { } descriptor.instance = standardKernels; this._providerToStandardKernels.set(providerUpperCase, descriptor); + + // Emit update event if the provider is not one of the default options + if (provider.provider !== SQL_NOTEBOOK_PROVIDER && provider.provider !== JUPYTER_PROVIDER_ID && standardKernels.length > 0) { + this._onNotebookKernelsAdded.fire(standardKernels.map(kernel => { + return { + name: kernel.name, + displayName: kernel.displayName, + connectionProviderIds: kernel.connectionProviderIds, + notebookProvider: provider.provider, + supportedLanguages: kernel.supportedLanguages, + supportedFileExtensions: supportedFileExtensions + }; + })); + } } getSupportedFileExtensions(): string[] { @@ -632,6 +648,10 @@ export class NotebookService extends Disposable implements INotebookService { return this._onNotebookEditorRename.event; } + get onNotebookKernelsAdded(): Event { + return this._onNotebookKernelsAdded.event; + } + addNotebookEditor(editor: INotebookEditor): void { this._editors.set(editor.id, editor); this._onNotebookEditorAdd.fire(editor);