diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index d1736641da..dabad65934 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import * as path from 'vs/base/common/path'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -12,9 +11,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IExtHostContext, IUndoStopOptions } from 'vs/workbench/api/common/extHost.protocol'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; -import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import * as types from 'vs/base/common/types'; import { SqlMainContext, MainThreadNotebookDocumentsAndEditorsShape, SqlExtHostContext, ExtHostNotebookDocumentsAndEditorsShape, @@ -26,18 +23,10 @@ import { ISingleNotebookEditOperation, NotebookChangeKind } from 'sql/workbench/ import { disposed } from 'vs/base/common/errors'; import { ICellModel, NotebookContentChange, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookChangeType, CellTypes } from 'sql/workbench/services/notebook/common/contracts'; -import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { localize } from 'vs/nls'; import { IFileService } from 'vs/platform/files/common/files'; -import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/untitledNotebookInput'; -import { FileNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/fileNotebookInput'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor'; class MainThreadNotebookEditor extends Disposable { private _contentChangedEmitter = new Emitter(); @@ -328,11 +317,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements private _modelToDisposeMap = new Map(); constructor( extHostContext: IExtHostContext, - @IUntitledTextEditorService private _untitledEditorService: IUntitledTextEditorService, @IInstantiationService private _instantiationService: IInstantiationService, - @IEditorService private _editorService: IEditorService, - @IEditorGroupsService private _editorGroupService: IEditorGroupsService, - @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, @INotebookService private readonly _notebookService: INotebookService, @IFileService private readonly _fileService: IFileService, @ITextFileService private readonly _textFileService: ITextFileService @@ -455,47 +440,11 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements //#endregion private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise { - const uri = URI.revive(resource); - - const editorOptions: ITextEditorOptions = { - preserveFocus: options.preserveFocus, - pinned: !options.preview - }; - let isUntitled: boolean = uri.scheme === Schemas.untitled; - - let fileInput: UntitledTextEditorInput | FileEditorInput; - if (isUntitled && path.isAbsolute(uri.fsPath)) { - const model = this._untitledEditorService.create({ associatedResource: uri, mode: 'notebook', initialValue: options.initialContent }); - fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); - } else { - if (isUntitled) { - const model = this._untitledEditorService.create({ untitledResource: uri, mode: 'notebook', initialValue: options.initialContent }); - fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); - } else { - fileInput = this._editorService.createEditorInput({ forceFile: true, resource: uri, mode: 'notebook' }) as FileEditorInput; - } - } - let input: NotebookInput; - if (isUntitled) { - input = this._instantiationService.createInstance(UntitledNotebookInput, path.basename(uri.fsPath), uri, fileInput as UntitledTextEditorInput); - } else { - input = this._instantiationService.createInstance(FileNotebookInput, path.basename(uri.fsPath), uri, fileInput as FileEditorInput); - } - input.defaultKernel = options.defaultKernel; - input.connectionProfile = new ConnectionProfile(this._capabilitiesService, options.connectionProfile); - if (isUntitled) { - let untitledModel = await (input as UntitledNotebookInput).textInput.resolve(); - await untitledModel.load(); - input.untitledEditorModel = untitledModel; - if (options.initialDirtyState === false) { - (input.untitledEditorModel as UntitledTextEditorModel).setDirty(false); - } - } - let editor = await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position)); + const editor = await this._notebookService.openNotebook(resource, options); if (!editor) { return undefined; } - return this.waitOnEditor(input); + return this.waitOnEditor((editor as NotebookEditor).notebookInput); } private async waitOnEditor(input: NotebookInput): Promise { diff --git a/src/sql/workbench/contrib/notebook/common/constants.ts b/src/sql/workbench/common/constants.ts similarity index 97% rename from src/sql/workbench/contrib/notebook/common/constants.ts rename to src/sql/workbench/common/constants.ts index 9b2cfe7261..8e5e7d49e5 100644 --- a/src/sql/workbench/contrib/notebook/common/constants.ts +++ b/src/sql/workbench/common/constants.ts @@ -29,6 +29,10 @@ export const SearchViewFocusedKey = new RawContextKey('notebookSearchVi export const InputBoxFocusedKey = new RawContextKey('inputBoxFocus', false); export const SearchInputBoxFocusedKey = new RawContextKey('searchInputBoxFocus', false); +export const enum NotebookLanguage { + Notebook = 'notebook', + Ipynb = 'ipynb' +} export interface INotebookSearchConfigurationProperties { exclude: glob.IExpression; useRipgrep: boolean; diff --git a/src/sql/workbench/contrib/notebook/browser/models/nodebookInputFactory.ts b/src/sql/workbench/contrib/notebook/browser/models/nodebookInputFactory.ts index 7461201baf..94ab5e2017 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/nodebookInputFactory.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/nodebookInputFactory.ts @@ -13,6 +13,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { ILanguageAssociation } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; +import { NotebookLanguage } from 'sql/workbench/common/constants'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/diffNotebookInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -20,7 +21,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur const editorInputFactoryRegistry = Registry.as(EditorInputExtensions.EditorInputFactories); export class NotebookEditorInputAssociation implements ILanguageAssociation { - static readonly languages = ['notebook', 'ipynb']; + static readonly languages = [NotebookLanguage.Notebook, NotebookLanguage.Ipynb]; constructor(@IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService) { } diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index d854508b9c..cf279c4717 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -34,6 +34,7 @@ import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel' import { NotebookFindModel } from 'sql/workbench/contrib/notebook/browser/find/notebookFindModel'; import { onUnexpectedError } from 'vs/base/common/errors'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; +import { INotebookInput } from 'sql/workbench/services/notebook/browser/interface'; export type ModeViewSaveHandler = (handle: number) => Thenable; @@ -209,7 +210,7 @@ export class NotebookEditorModel extends EditorModel { type TextInput = ResourceEditorInput | UntitledTextEditorInput | FileEditorInput; -export abstract class NotebookInput extends EditorInput { +export abstract class NotebookInput extends EditorInput implements INotebookInput { private _providerId: string; private _providers: string[]; private _standardKernels: IStandardKernelWithProvider[]; diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 63a06c79ef..cddfb2c31b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import * as path from 'vs/base/common/path'; import { Action } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; @@ -361,14 +362,14 @@ export class RunParametersAction extends TooltipFromLabelAction { * (showNotebookDocument to be utilized in Notebook Service) **/ public async openParameterizedNotebook(uri: URI): Promise { - // const editor = this._notebookService.findNotebookEditor(uri); - // let modelContents = editor.model.toJSON(); - // let basename = path.basename(uri.fsPath); - // let untitledUri = uri.with({ authority: '', scheme: 'untitled', path: basename }); - // this._notebookService.showNotebookDocument(untitledUri, { - // initialContent: modelContents, - // preserveFocus: true - // }); + const editor = this._notebookService.findNotebookEditor(uri); + let modelContents = JSON.stringify(editor.model.toJSON()); + let basename = path.basename(uri.fsPath); + let untitledUri = uri.with({ authority: '', scheme: 'untitled', path: basename }); + this._notebookService.openNotebook(untitledUri, { + initialContent: modelContents, + preserveFocus: true + }); } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts index 83442d1285..d3a304c09f 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts @@ -27,7 +27,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ViewPaneContainer, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { NotebookSearchWidget, INotebookExplorerSearchOptions } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget'; -import * as Constants from 'sql/workbench/contrib/notebook/common/constants'; +import * as Constants from 'sql/workbench/common/constants'; import { IChangeEvent } from 'vs/workbench/contrib/search/common/searchModel'; import { Delayer } from 'vs/base/common/async'; import { ITextQuery, IPatternInfo } from 'vs/workbench/services/search/common/search'; diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts index 6c59c8ae19..161ddbb2cc 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts @@ -16,7 +16,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import * as Constants from 'sql/workbench/contrib/notebook/common/constants'; +import * as Constants from 'sql/workbench/common/constants'; import { IMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { appendKeyBindingLabel } from 'vs/workbench/contrib/search/browser/searchActions'; import { ContextScopedFindInput } from 'vs/platform/browser/contextScopedHistoryWidget'; 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 1e1db6feb8..40b1069024 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/cellToolbarActions.test.ts @@ -113,7 +113,10 @@ suite('CellToolbarActions', function (): void { undefined, undefined, new MockContextKeyService(), - instantiationService.get(IProductService) + instantiationService.get(IProductService), + undefined, + undefined, + undefined, ); instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(INotebookService, notebookService); diff --git a/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts b/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts index 8f3c310cb9..4628893cf6 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts @@ -67,7 +67,10 @@ suite('MarkdownTextTransformer', () => { undefined, undefined, new MockContextKeyService(), - instantiationService.get(IProductService) + instantiationService.get(IProductService), + undefined, + undefined, + undefined, ); mockNotebookService = TypeMoq.Mock.ofInstance(notebookService); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts index 8614836377..0b6adb8671 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts @@ -13,19 +13,20 @@ import { NotebookEditorStub } from 'sql/workbench/contrib/notebook/test/testComm import { ICellModel, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; import { INotebookEditor, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; -import { CellType } from 'sql/workbench/services/notebook/common/contracts'; +import { CellType, CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import * as TypeMoq from 'typemoq'; import { Emitter } from 'vs/base/common/event'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationChangeEvent, IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService'; import { MockQuickInputService } from 'sql/workbench/contrib/notebook/test/common/quickInputServiceMock'; +import { localize } from 'vs/nls'; class TestClientSession extends ClientSessionStub { private _errorState: boolean = false; @@ -259,22 +260,91 @@ suite('Notebook Actions', function (): void { assert.strictEqual(actualCmdId, NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID); }); - test.skip('Run with Parameters Action', async function (): Promise { + test('Run with Parameters Action', async function (): Promise { + const testContents: azdata.nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: ['x=2.0\n', 'y=5.0'], + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'python', + language: 'python', + display_name: 'Python 3' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + let mockNotification = TypeMoq.Mock.ofType(TestNotificationService); mockNotification.setup(n => n.notify(TypeMoq.It.isAny())); let quickInputService = new MockQuickInputService; + let mockNotebookModel = new NotebookModelStub(undefined, undefined, testContents); let action = new RunParametersAction('TestId', true, testUri, quickInputService, mockNotebookService.object, mockNotification.object); - // Normal use case const testCells = [{ isParameter: true, source: ['x=2.0\n', 'y=5.0'] }]; - + mockNotebookEditor.setup(x => x.model).returns(() => mockNotebookModel); mockNotebookEditor.setup(x => x.cells).returns(() => testCells); - assert.doesNotThrow(() => action.run(testUri)); + // Run Parameters Action + await action.run(testUri); + let testShowOptions = { + initialContent: JSON.stringify(mockNotebookModel.toJSON()), + preserveFocus: true + }; + assert.call(mockNotebookService.object.openNotebook(testUri, testShowOptions), 'Should Open Parameterized Notebook'); + }); + + test('Run with Parameters Action with no parameters in notebook', async function (): Promise { + const testContents: azdata.nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: [''], + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'python', + language: 'python', + display_name: 'Python 3' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + let expectedMsg: string = localize('noParametersCell', "This notebook cannot run with parameters until a parameter cell is added. [Learn more](https://docs.microsoft.com/sql/azure-data-studio/notebooks/notebooks-parameterization)."); + let notification = { + severity: Severity.Info, + message: expectedMsg, + }; + + let actualMsg: string; + let mockNotification = TypeMoq.Mock.ofType(TestNotificationService); + mockNotification.setup(n => n.notify(TypeMoq.It.isAny())).returns(notification => { + actualMsg = notification.message; + return undefined; + }); + let quickInputService = new MockQuickInputService; + let mockNotebookModel = new NotebookModelStub(undefined, undefined, testContents); + + let action = new RunParametersAction('TestId', true, testUri, quickInputService, mockNotebookService.object, mockNotification.object); + + mockNotebookEditor.setup(x => x.model).returns(() => mockNotebookModel); + + // Run Parameters Action + await action.run(testUri); + + assert.ok(actualMsg !== undefined, 'Should have received a notification'); + assert.call(mockNotification.object.notify(notification), 'Should notify user there is no parameter cell'); + assert.strictEqual(actualMsg, expectedMsg); }); suite('Kernels dropdown', async () => { diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index 60ef196213..daabe70867 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -697,7 +697,10 @@ function setupServices(arg: { workbenchThemeService?: WorkbenchThemeService, ins instantiationService.get(ILogService), queryManagementService, instantiationService.get(IContextKeyService), - instantiationService.get(IProductService) + instantiationService.get(IProductService), + undefined, + undefined, + undefined, ); instantiationService.stub(INotebookService, notebookService); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts index 2760a69fdb..3a5acb5544 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts @@ -30,6 +30,9 @@ import { ExtensionManagementService } from 'vs/workbench/services/extensionManag import { TestFileService, TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; /** * class to mock azdata.nb.ServerManager object @@ -109,6 +112,9 @@ suite.skip('NotebookService:', function (): void { let testNo = 0; let sandbox: sinon.SinonSandbox; let productService: IProductService; + let editorService: IEditorService; + let untitledTextEditorService: IUntitledTextEditorService; + let editorGroupsService: IEditorGroupsService; let installExtensionEmitter: Emitter, didInstallExtensionEmitter: Emitter, @@ -147,8 +153,13 @@ suite.skip('NotebookService:', function (): void { instantiationService.stub(IProductService, { quality: 'stable' }); productService = instantiationService.get(IProductService); + editorService = new IEditorService; + untitledTextEditorService = new IUntitledTextEditorService; + editorGroupsService = new IEditorGroupsService; - notebookService = new NotebookService(lifecycleService, storageService, extensionServiceMock.object, extensionManagementService, instantiationService, fileService, logServiceMock.object, queryManagementService, contextService, productService); + notebookService = new NotebookService(lifecycleService, storageService, extensionServiceMock.object, extensionManagementService, + instantiationService, fileService, logServiceMock.object, queryManagementService, contextService, productService, + editorService, untitledTextEditorService, editorGroupsService); sandbox = sinon.sandbox.create(); }); @@ -446,7 +457,7 @@ suite.skip('NotebookService:', function (): void { }; errorHandler.setUnexpectedErrorHandler(onUnexpectedErrorVerifier); // The following call throws an exception internally with queryManagementService parameter being undefined. - new NotebookService(lifecycleService, storageService, extensionService, extensionManagementService, instantiationService, fileService, logService, /* queryManagementService */ undefined, contextService, productService); + new NotebookService(lifecycleService, storageService, extensionService, extensionManagementService, instantiationService, fileService, logService, /* queryManagementService */ undefined, contextService, productService, editorService, untitledTextEditorService, editorGroupsService); await unexpectedErrorPromise; assert.strictEqual(unexpectedErrorCalled, true, `onUnexpectedError must be have been raised when queryManagementService is undefined when calling NotebookService constructor`); }); 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 e1f56f0941..a283e886f8 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 @@ -117,7 +117,10 @@ suite('Notebook Editor Model', function (): void { undefined, undefined, new MockContextKeyService(), - testinstantiationService.get(IProductService) + testinstantiationService.get(IProductService), + undefined, + undefined, + undefined, ); let mockNotebookService = TypeMoq.Mock.ofInstance(notebookService); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 2014cbcb6b..0c1d5d0b78 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -14,12 +14,14 @@ import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { NotebookFindMatch } from 'sql/workbench/contrib/notebook/browser/find/notebookFindDecorations'; import { RenderMimeRegistry } from 'sql/workbench/services/notebook/browser/outputs/registry'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol'; export class NotebookModelStub implements INotebookModel { - constructor(private _languageInfo?: nb.ILanguageInfo, private _cells?: ICellModel[]) { + constructor(private _languageInfo?: nb.ILanguageInfo, private _cells?: ICellModel[], private _testContents?: nb.INotebookContents) { } trustedMode: boolean; language: string; @@ -134,7 +136,7 @@ export class NotebookModelStub implements INotebookModel { throw new Error('method not implemented.'); } toJSON(): nb.INotebookContents { - throw new Error('Method not implemented.'); + return this._testContents; } serializationStateChanged(changeType: NotebookChangeType, cell?: ICellModel): void { throw new Error('Method not implemented.'); @@ -292,6 +294,9 @@ export class NotebookServiceStub implements INotebookService { navigateTo(notebookUri: URI, sectionId: string): void { throw new Error('Method not implemented.'); } + openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise { + throw new Error('Method not implemented.'); + } get onCodeCellExecutionStart(): vsEvent.Event { 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 new file mode 100644 index 0000000000..b886d1c2ef --- /dev/null +++ b/src/sql/workbench/services/notebook/browser/interface.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as azdata from 'azdata'; +import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; +import { IContentManager } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; + +export interface INotebookInput { + defaultKernel: azdata.nb.IKernelSpec, + connectionProfile: azdata.IConnectionProfile, + isDirty(): boolean; + setDirty(boolean); + readonly notebookUri: URI; + updateModel(): void; + readonly editorOpenedTimestamp: number; + readonly layoutChanged: Event; + readonly contentManager: IContentManager; + readonly standardKernels: IStandardKernelWithProvider[]; +} + +export function isINotebookInput(value: any): value is INotebookInput { + if (typeof value.defaultKernel === 'object' && + typeof value.connectionProfile === 'object' && + typeof value.isDirty === 'boolean' && + value.notebookUri instanceof URI && + typeof value.editorOpenedTimestamp === 'number' && + typeof value.layoutChanged === 'object' && + typeof value.contentManager === 'object' && + typeof value.standardKernels === 'object') { + return true; + } + return false; +} diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index b3d332fdb5..b3fdac39ee 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -7,17 +7,19 @@ import * as azdata from 'azdata'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { RenderMimeRegistry } from 'sql/workbench/services/notebook/browser/outputs/registry'; import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; -import { ICellModel, INotebookModel, IContentManager } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { ICellModel, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookChangeType, CellType } from 'sql/workbench/services/notebook/common/contracts'; import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { Range } from 'vs/editor/common/core/range'; -import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { INotebookInput } from 'sql/workbench/services/notebook/browser/interface'; +import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol'; export const SERVICE_ID = 'sqlNotebookService'; export const INotebookService = createDecorator(SERVICE_ID); @@ -139,6 +141,8 @@ export interface INotebookService { * Fires the onCodeCellExecutionStart event. */ notifyCellExecutionStarted(): void; + + openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise; } export interface INotebookProvider { @@ -159,17 +163,6 @@ export interface IProviderInfo { providers: string[]; } -export interface INotebookInput { - readonly notebookUri: URI; - updateModel(): void; - isDirty(): boolean; - readonly defaultKernel: azdata.nb.IKernelSpec; - readonly editorOpenedTimestamp: number; - readonly contentManager: IContentManager; - readonly standardKernels: IStandardKernelWithProvider[]; - readonly layoutChanged: Event; -} - export interface INotebookParams extends IBootstrapParams { notebookUri: URI; input: INotebookInput; diff --git a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts index 121a14cd02..24eb6d7690 100644 --- a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts @@ -5,7 +5,7 @@ import { nb } from 'azdata'; import { localize } from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; import { @@ -36,6 +36,23 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IProductService } from 'vs/platform/product/common/productService'; +import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { Extensions as LanguageAssociationExtensions, ILanguageAssociationRegistry } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; + +import * as path from 'vs/base/common/path'; + +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; + +import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; +import { isINotebookInput } from 'sql/workbench/services/notebook/browser/interface'; +import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol'; +import { NotebookLanguage } from 'sql/workbench/common/constants'; + +const languageAssociationRegistry = Registry.as(LanguageAssociationExtensions.LanguageAssociations); export interface NotebookProviderProperties { provider: string; @@ -120,7 +137,10 @@ export class NotebookService extends Disposable implements INotebookService { @ILogService private readonly _logService: ILogService, @IQueryManagementService private readonly _queryManagementService: IQueryManagementService, @IContextKeyService private contextKeyService: IContextKeyService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IEditorService private _editorService: IEditorService, + @IUntitledTextEditorService private _untitledEditorService: IUntitledTextEditorService, + @IEditorGroupsService private _editorGroupService: IEditorGroupsService ) { super(); this._providersMemento = new Memento('notebookProviders', this._storageService); @@ -163,8 +183,45 @@ export class NotebookService extends Disposable implements INotebookService { lifecycleService.onWillShutdown(() => this.shutdown()); } - public dispose(): void { - super.dispose(); + public async openNotebook(resource: UriComponents, options: INotebookShowOptions): Promise { + const uri = URI.revive(resource); + + const editorOptions: ITextEditorOptions = { + preserveFocus: options.preserveFocus, + pinned: !options.preview + }; + let isUntitled: boolean = uri.scheme === Schemas.untitled; + + let fileInput: IEditorInput; + if (isUntitled && path.isAbsolute(uri.fsPath)) { + const model = this._untitledEditorService.create({ associatedResource: uri, mode: 'notebook', initialValue: options.initialContent }); + fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); + } else { + if (isUntitled) { + const model = this._untitledEditorService.create({ untitledResource: uri, mode: 'notebook', initialValue: options.initialContent }); + fileInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); + } else { + fileInput = this._editorService.createEditorInput({ forceFile: true, resource: uri, mode: 'notebook' }); + } + } + // We only need to get the Notebook language association as such we only need to use ipynb + const inputCreator = languageAssociationRegistry.getAssociationForLanguage(NotebookLanguage.Ipynb); + if (inputCreator) { + fileInput = await inputCreator.convertInput(fileInput); + if (isINotebookInput(fileInput)) { + fileInput.defaultKernel = options.defaultKernel; + fileInput.connectionProfile = options.connectionProfile; + + if (isUntitled) { + let untitledModel = await fileInput.resolve(); + await untitledModel.load(); + if (options.initialDirtyState === false) { + fileInput.setDirty(false); + } + } + } + } + return await this._editorService.openEditor(fileInput, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position)); } private updateSQLRegistrationWithConnectionProviders() { @@ -350,7 +407,8 @@ export class NotebookService extends Disposable implements INotebookService { if (!notebookUri) { return undefined; } - let uriString = notebookUri.toString(); + // The NotebookEditor will not be found if there is query or fragments attached to the URI + let uriString = notebookUri.with({ query: '', fragment: '' }).toString(); let editor = this.listNotebookEditors().find(n => n.id === uriString); return editor; } diff --git a/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts b/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts index 0654f91e6b..72889683a1 100644 --- a/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts +++ b/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts @@ -46,7 +46,10 @@ suite('MainThreadNotebook Tests', () => { undefined, undefined, new MockContextKeyService(), - instantiationService.get(IProductService) + instantiationService.get(IProductService), + undefined, + undefined, + undefined, ); mockNotebookService = TypeMoq.Mock.ofInstance(notebookService); notebookUri = URI.parse('file:/user/default/my.ipynb');