From cac8cc99e1bf916e13ae0b77b1e7a1af22a2cc8a Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Mon, 3 Dec 2018 18:50:44 -0800 Subject: [PATCH] Notebook extensibility: Move `New Notebook` and configuration to an extension (#3382) initial support for Notebook extensibility. Fixes #3148 , Fixes #3382. ## Design notes The extensibility patterns are modeled after the VSCode Document and Editor APIs but need to be different since core editor concepts are different - for example Notebooks have cells, and cells have contents rather than editors which have text lines. Most importantly, a lot of the code is based on the MainThreadDocumentsAndEditors class, with some related classes (the MainThreadDocuments, and MainThreadEditors) brought in too. Given our current limitations I felt moving to add 3 full sets of extension host API classes was overkill so am currently using one. Will see if we need to change this in the future based on what we add in the additional APIs ## Limitations The current implementation is limited to visible editors, rather than all documents in the workspace. We are not following the `openDocument` -> `showDocument` pattern, but instead just supporting `showDocument` directly. ## Changes in this PR - Renamed existing APIs to make clear that they were about notebook contents, not about notebook behavior - Added new APIs for querying notebook documents and editors - Added new API for opening a notebook - Moved `New Notebook` command to an extension, and added an `Open Notebook` command too - Moved notebook feature flag to the extension ## Not covered in this PR - Need to actually implement support for defining the provider and connection IDs for a notebook. this will be important to support New Notebook from a big data connection in Object Explorer - Need to add APIs for adding cells, to support - Need to implement the metadata for getting full notebook contents. I've only implemented to key APIs needed to make this all work. --- extensions/notebook/README.md | 17 + extensions/notebook/package.json | 71 ++++ extensions/notebook/package.nls.json | 8 + .../resources/dark/new_notebook_inverse.svg | 1 + .../resources/dark/open_notebook_inverse.svg | 1 + .../notebook/resources/light/new_notebook.svg | 1 + .../resources/light/open_notebook.svg | 1 + extensions/notebook/src/extension.ts | 50 +++ extensions/notebook/src/typings/refs.d.ts | 9 + extensions/notebook/tsconfig.json | 22 + extensions/notebook/yarn.lock | 13 + src/sql/parts/common/customInputConverter.ts | 12 +- src/sql/parts/notebook/models/cell.ts | 10 +- src/sql/parts/notebook/models/modelFactory.ts | 2 +- .../parts/notebook/models/modelInterfaces.ts | 4 +- .../parts/notebook/models/notebookModel.ts | 10 +- src/sql/parts/notebook/notebook.component.ts | 47 ++- .../parts/notebook/notebook.contribution.ts | 80 +--- src/sql/parts/notebook/notebookEditor.ts | 1 + src/sql/parts/notebook/notebookInput.ts | 14 +- src/sql/parts/notebook/notebookUtils.ts | 24 ++ .../services/notebook/localContentManager.ts | 7 +- src/sql/services/notebook/notebookService.ts | 23 +- .../services/notebook/notebookServiceImpl.ts | 41 +- src/sql/sqlops.proposed.d.ts | 218 +++++++++- src/sql/workbench/api/node/extHostNotebook.ts | 11 +- .../extHostNotebookDocumentsAndEditors.ts | 281 +++++++++++++ .../workbench/api/node/mainThreadNotebook.ts | 4 +- .../mainThreadNotebookDocumentsAndEditors.ts | 378 ++++++++++++++++++ .../workbench/api/node/sqlExtHost.api.impl.ts | 20 + .../api/node/sqlExtHost.contribution.ts | 1 + .../workbench/api/node/sqlExtHost.protocol.ts | 48 ++- .../workbench/api/mainThreadNotebook.test.ts | 4 +- 33 files changed, 1286 insertions(+), 148 deletions(-) create mode 100644 extensions/notebook/README.md create mode 100644 extensions/notebook/package.json create mode 100644 extensions/notebook/package.nls.json create mode 100755 extensions/notebook/resources/dark/new_notebook_inverse.svg create mode 100755 extensions/notebook/resources/dark/open_notebook_inverse.svg create mode 100755 extensions/notebook/resources/light/new_notebook.svg create mode 100755 extensions/notebook/resources/light/open_notebook.svg create mode 100644 extensions/notebook/src/extension.ts create mode 100644 extensions/notebook/src/typings/refs.d.ts create mode 100644 extensions/notebook/tsconfig.json create mode 100644 extensions/notebook/yarn.lock create mode 100644 src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts create mode 100644 src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts diff --git a/extensions/notebook/README.md b/extensions/notebook/README.md new file mode 100644 index 0000000000..67d15dad7d --- /dev/null +++ b/extensions/notebook/README.md @@ -0,0 +1,17 @@ +# Notebook extension for Azure Data Studio + +Welcome to the Notebook extension for Azure Data Studio! This extension supports core notebook functionality including configuration settings, actions such as New / Open Notebook, and more. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json new file mode 100644 index 0000000000..835624dab7 --- /dev/null +++ b/extensions/notebook/package.json @@ -0,0 +1,71 @@ +{ + "name": "notebook", + "displayName": "%displayName%", + "description": "%description%", + "version": "0.1.0", + "publisher": "Microsoft", + "engines": { + "vscode": "*", + "sqlops": "*" + }, + "main": "./out/extension", + "activationEvents": [ + "*" + ], + "contributes": { + "configuration": { + "type": "object", + "title": "%notebook.configuration.title%", + "properties": { + "notebook.enabled": { + "type": "boolean", + "default": true, + "description": "%notebook.enabled.description%" + } + } + }, + "commands": [ + { + "command": "notebook.command.new", + "title": "%notebook.command.new%", + "icon": { + "dark": "resources/dark/new_notebook_inverse.svg", + "light": "resources/light/new_notebook.svg" + } + }, + { + "command": "notebook.command.open", + "title": "%notebook.command.open%", + "icon": { + "dark": "resources/dark/open_notebook_inverse.svg", + "light": "resources/light/open_notebook.svg" + } + } + ], + "menus": { + "commandPalette": [ + { + "command": "notebook.command.new", + "when": "config.notebook.enabled" + }, + { + "command": "notebook.command.open", + "when": "config.notebook.enabled" + } + ] + }, + "keybindings": [ + { + "command": "notebook.command.new", + "key": "Ctrl+Shift+N", + "when": "config.notebook.enabled" + } + ] + }, + "dependencies": { + "vscode-nls": "^4.0.0" + }, + "devDependencies": { + "@types/node": "8.0.33" + } +} diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json new file mode 100644 index 0000000000..5113ee3aab --- /dev/null +++ b/extensions/notebook/package.nls.json @@ -0,0 +1,8 @@ +{ + "displayName": "Notebook Core Extensions", + "description": "Defines the Data-procotol based Notebook contribution and many Notebook commands and contributions.", + "notebook.configuration.title": "Notebook configuration", + "notebook.enabled.description": "Enable viewing notebook files using built-in notebook editor.", + "notebook.command.new": "New Notebook", + "notebook.command.open": "Open Notebook" +} \ No newline at end of file diff --git a/extensions/notebook/resources/dark/new_notebook_inverse.svg b/extensions/notebook/resources/dark/new_notebook_inverse.svg new file mode 100755 index 0000000000..e0072afee1 --- /dev/null +++ b/extensions/notebook/resources/dark/new_notebook_inverse.svg @@ -0,0 +1 @@ +new_notebook_inverse \ No newline at end of file diff --git a/extensions/notebook/resources/dark/open_notebook_inverse.svg b/extensions/notebook/resources/dark/open_notebook_inverse.svg new file mode 100755 index 0000000000..a95750c49f --- /dev/null +++ b/extensions/notebook/resources/dark/open_notebook_inverse.svg @@ -0,0 +1 @@ +open_notebook_inverse \ No newline at end of file diff --git a/extensions/notebook/resources/light/new_notebook.svg b/extensions/notebook/resources/light/new_notebook.svg new file mode 100755 index 0000000000..9618487568 --- /dev/null +++ b/extensions/notebook/resources/light/new_notebook.svg @@ -0,0 +1 @@ +new_notebook \ No newline at end of file diff --git a/extensions/notebook/resources/light/open_notebook.svg b/extensions/notebook/resources/light/open_notebook.svg new file mode 100755 index 0000000000..0041ae9b21 --- /dev/null +++ b/extensions/notebook/resources/light/open_notebook.svg @@ -0,0 +1 @@ +open_notebook \ No newline at end of file diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts new file mode 100644 index 0000000000..7ff0c3e292 --- /dev/null +++ b/extensions/notebook/src/extension.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +let counter = 0; + +export function activate(extensionContext: vscode.ExtensionContext) { + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', () => { + let title = `Untitled-${counter++}`; + let untitledUri = vscode.Uri.parse(`untitled:${title}`); + sqlops.nb.showNotebookDocument(untitledUri).then(success => { + + }, (err: Error) => { + vscode.window.showErrorMessage(err.message); + }); + })); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', () => { + openNotebook(); + })); + +} + +async function openNotebook(): Promise { + try { + let filter = {}; + // TODO support querying valid notebook file types + filter[localize('notebookFiles', 'Notebooks')] = ['ipynb']; + let file = await vscode.window.showOpenDialog({ + filters: filter + }); + if (file) { + let doc = await vscode.workspace.openTextDocument(file[0]); + vscode.window.showTextDocument(doc); + } + } catch (err) { + vscode.window.showErrorMessage(err); + } +} + +// this method is called when your extension is deactivated +export function deactivate() { +} diff --git a/extensions/notebook/src/typings/refs.d.ts b/extensions/notebook/src/typings/refs.d.ts new file mode 100644 index 0000000000..ee283e0c24 --- /dev/null +++ b/extensions/notebook/src/typings/refs.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// diff --git a/extensions/notebook/tsconfig.json b/extensions/notebook/tsconfig.json new file mode 100644 index 0000000000..b341a65dab --- /dev/null +++ b/extensions/notebook/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", "es2015.promise" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/notebook/yarn.lock b/extensions/notebook/yarn.lock new file mode 100644 index 0000000000..6767cb8d8c --- /dev/null +++ b/extensions/notebook/yarn.lock @@ -0,0 +1,13 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@8.0.33": + version "8.0.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" + integrity sha512-vmCdO8Bm1ExT+FWfC9sd9r4jwqM7o97gGy2WBshkkXbf/2nLAJQUrZfIhw27yVOtLUev6kSZc4cav/46KbDd8A== + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 70b74c766b..b9f22db380 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -19,6 +19,7 @@ import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput'; import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput'; import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry'; import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; +import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils'; const fs = require('fs'); @@ -183,17 +184,6 @@ function getNotebookFileExtensions() { return notebookRegistry.getSupportedFileExtensions(); } -function getProviderForFileName(fileName: string) { - let fileExt = path.extname(fileName); - if (fileExt && fileExt.startsWith('.')) { - fileExt = fileExt.slice(1,fileExt.length); - let notebookRegistry = Registry.as(Extensions.NotebookProviderContribution); - return notebookRegistry.getProviderForFileType(fileExt); - } - return DEFAULT_NOTEBOOK_PROVIDER; -} - - /** * Checks whether the given EditorInput is set to either undefined or sql mode * @param input The EditorInput to check the mode of diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts index 837ab67944..0be6bbd7c9 100644 --- a/src/sql/parts/notebook/models/cell.ts +++ b/src/sql/parts/notebook/models/cell.ts @@ -35,7 +35,7 @@ export class CellModel implements ICellModel { private _active: boolean; private _cellUri: URI; - constructor(private factory: IModelFactory, cellData?: nb.ICell, private _options?: ICellModelOptions) { + constructor(private factory: IModelFactory, cellData?: nb.ICellContents, private _options?: ICellModelOptions) { this.id = `${modelId++}`; CellModel.CreateLanguageMappings(); // Do nothing for now @@ -263,8 +263,8 @@ export class CellModel implements ICellModel { return transient['display_id'] as string; } - public toJSON(): nb.ICell { - let cellJson: Partial = { + public toJSON(): nb.ICellContents { + let cellJson: Partial = { cell_type: this._cellType, source: this._source, metadata: { @@ -275,10 +275,10 @@ export class CellModel implements ICellModel { cellJson.outputs = this._outputs; cellJson.execution_count = 1; // TODO: keep track of actual execution count } - return cellJson as nb.ICell; + return cellJson as nb.ICellContents; } - public fromJSON(cell: nb.ICell): void { + public fromJSON(cell: nb.ICellContents): void { if (!cell) { return; } diff --git a/src/sql/parts/notebook/models/modelFactory.ts b/src/sql/parts/notebook/models/modelFactory.ts index 37dfca2639..d224b6fa7e 100644 --- a/src/sql/parts/notebook/models/modelFactory.ts +++ b/src/sql/parts/notebook/models/modelFactory.ts @@ -13,7 +13,7 @@ import { ClientSession } from './clientSession'; export class ModelFactory implements IModelFactory { - public createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel { + public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel { return new CellModel(this, cell, options); } diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index 1af66e4b48..b3e79c4641 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -348,7 +348,7 @@ export interface ICellModel { readonly onOutputsChanged: Event>; setFuture(future: FutureInternal): void; equals(cellModel: ICellModel): boolean; - toJSON(): nb.ICell; + toJSON(): nb.ICellContents; } export interface FutureInternal extends nb.IFuture { @@ -357,7 +357,7 @@ export interface FutureInternal extends nb.IFuture { export interface IModelFactory { - createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel; + createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel; createClientSession(options: IClientSessionOptions): IClientSession; } diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts index b4f1ef60d0..863b8789be 100644 --- a/src/sql/parts/notebook/models/notebookModel.ts +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -237,7 +237,7 @@ export class NotebookModel extends Disposable implements INotebookModel { } private createCell(cellType: CellType): ICellModel { - let singleCell: nb.ICell = { + let singleCell: nb.ICellContents = { cell_type: cellType, source: '', metadata: {}, @@ -389,7 +389,7 @@ export class NotebookModel extends Disposable implements INotebookModel { // Get default language if saved in notebook file // Otherwise, default to python - private getDefaultLanguageInfo(notebook: nb.INotebook): nb.ILanguageInfo { + private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo { return notebook!.metadata!.language_info || { name: 'python', version: '', @@ -398,7 +398,7 @@ export class NotebookModel extends Disposable implements INotebookModel { } // Get default kernel info if saved in notebook file - private getSavedKernelInfo(notebook: nb.INotebook): nb.IKernelInfo { + private getSavedKernelInfo(notebook: nb.INotebookContents): nb.IKernelInfo { return notebook!.metadata!.kernelspec; } @@ -490,8 +490,8 @@ export class NotebookModel extends Disposable implements INotebookModel { /** * Serialize the model to JSON. */ - toJSON(): nb.INotebook { - let cells: nb.ICell[] = this.cells.map(c => c.toJSON()); + toJSON(): nb.INotebookContents { + let cells: nb.ICellContents[] = this.cells.map(c => c.toJSON()); let metadata = Object.create(null) as nb.INotebookMetadata; // TODO update language and kernel when these change metadata.kernelspec = this._savedKernelInfo; diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index add5631e96..44890a16d1 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -7,7 +7,7 @@ import './notebookStyles'; import { nb } from 'sqlops'; -import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core'; +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; @@ -20,7 +20,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle'; import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; import { ICellModel, IModelFactory, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces'; import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement'; -import { INotebookService, INotebookParams, INotebookManager } from 'sql/services/notebook/notebookService'; +import { INotebookService, INotebookParams, INotebookManager, INotebookEditor } from 'sql/services/notebook/notebookService'; import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; import { NotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel'; import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; @@ -48,7 +48,7 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component'; selector: NOTEBOOK_SELECTOR, templateUrl: decodeURI(require.toUrl('./notebook.component.html')) }) -export class NotebookComponent extends AngularDisposable implements OnInit { +export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor { @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; private _model: NotebookModel; private _isInErrorState: boolean = false; @@ -73,7 +73,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit { @Inject(IEditorService) private editorService: IEditorService, @Inject(INotificationService) private notificationService: INotificationService, @Inject(INotebookService) private notebookService: INotebookService, - @Inject(IBootstrapParams) private notebookParams: INotebookParams, + @Inject(IBootstrapParams) private _notebookParams: INotebookParams, @Inject(IInstantiationService) private instantiationService: IInstantiationService, @Inject(IContextMenuService) private contextMenuService: IContextMenuService, @Inject(IContextViewService) private contextViewService: IContextViewService, @@ -108,10 +108,17 @@ export class NotebookComponent extends AngularDisposable implements OnInit { ngOnInit() { this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this.updateTheme(this.themeService.getColorTheme()); + this.notebookService.addNotebookEditor(this); this.initActionBar(); this.doLoad(); } + ngOnDestroy() { + if (this.notebookService) { + this.notebookService.removeNotebookEditor(this); + } + } + public get model(): NotebookModel { return this._model; } @@ -201,16 +208,16 @@ export class NotebookComponent extends AngularDisposable implements OnInit { } private async loadModel(): Promise { - this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri); + this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri); let model = new NotebookModel({ factory: this.modelFactory, - notebookUri: this.notebookParams.notebookUri, + notebookUri: this._notebookParams.notebookUri, connectionService: this.connectionManagementService, notificationService: this.notificationService, notebookManager: this.notebookManager }, false, this.profile); model.onError((errInfo: INotification) => this.handleModelError(errInfo)); - await model.requestModelLoad(this.notebookParams.isTrusted); + await model.requestModelLoad(this._notebookParams.isTrusted); model.contentChanged((change) => this.handleContentChanged(change)); this._model = model; this.updateToolbarComponents(this._model.trustedMode); @@ -231,10 +238,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit { } private get modelFactory(): IModelFactory { - if (!this.notebookParams.modelFactory) { - this.notebookParams.modelFactory = new ModelFactory(); + if (!this._notebookParams.modelFactory) { + this._notebookParams.modelFactory = new ModelFactory(); } - return this.notebookParams.modelFactory; + return this._notebookParams.modelFactory; } private handleModelError(notification: INotification): void { this.notificationService.notify(notification); @@ -337,4 +344,24 @@ export class NotebookComponent extends AngularDisposable implements OnInit { return undefined; } + public get notebookParams(): INotebookParams { + return this._notebookParams; + } + + public get id(): string { + return this._notebookParams.notebookUri.toString(); + } + + isActive(): boolean { + return this.editorService.activeEditor === this.notebookParams.input; + } + + isVisible(): boolean { + let notebookEditor = this.notebookParams.input; + return this.editorService.visibleEditors.some(e => e === notebookEditor); + } + + isDirty(): boolean { + return this.notebookParams.input.isDirty(); + } } diff --git a/src/sql/parts/notebook/notebook.contribution.ts b/src/sql/parts/notebook/notebook.contribution.ts index c7c1d64f02..66d6e19017 100644 --- a/src/sql/parts/notebook/notebook.contribution.ts +++ b/src/sql/parts/notebook/notebook.contribution.ts @@ -4,60 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { Action } from 'vs/base/common/actions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { Schemas } from 'vs/base/common/network'; -import URI from 'vs/base/common/uri'; -import { localize } from 'vs/nls'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookInput, NotebookInputModel, notebooksEnabledCondition } from 'sql/parts/notebook/notebookInput'; +import { NotebookInput } from 'sql/parts/notebook/notebookInput'; import { NotebookEditor } from 'sql/parts/notebook/notebookEditor'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry'; - -const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB'; -const Extensions = { - NotebookProviderContribution: 'notebook.providers' -}; - -let counter = 0; - -/** - * todo: Will remove this code. - * This is the entry point to open the new Notebook - */ -export class NewNotebookAction extends Action { - - public static ID = 'workbench.action.newnotebook'; - public static LABEL = localize('workbench.action.newnotebook.description', 'New Notebook'); - - constructor( - id: string, - label: string, - @IEditorService private _editorService: IEditorService, - @IInstantiationService private _instantiationService: IInstantiationService - ) { - super(id, label); - } - - public run(): TPromise { - let title = `Untitled-${counter++}`; - let untitledUri = URI.from({ scheme: Schemas.untitled, path: title }); - let model = new NotebookInputModel(untitledUri, undefined, false, undefined); - if(!model.providerId) - { - let notebookRegistry = Registry.as(Extensions.NotebookProviderContribution); - model.providerId = notebookRegistry.getProviderForFileType(DEFAULT_NOTEBOOK_FILETYPE); - } - let input = this._instantiationService.createInstance(NotebookInput, title, model); - return this._editorService.openEditor(input, { pinned: true }).then(() => undefined); - } -} // Model View editor registration const viewModelEditorDescriptor = new EditorDescriptor( @@ -68,31 +18,3 @@ const viewModelEditorDescriptor = new EditorDescriptor( Registry.as(EditorExtensions.Editors) .registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]); - -// Feature flag for built-in Notebooks. Will be removed in the future. -const configurationRegistry = Registry.as(ConfigExtensions.Configuration); -configurationRegistry.registerConfiguration({ - 'id': 'notebook', - 'title': 'Notebook', - 'type': 'object', - 'properties': { - 'notebook.enabled': { - 'type': 'boolean', - 'default': false, - 'description': localize('notebook.enabledDescription', 'Enable viewing notebook files using built-in notebook editor.') - } - } -}); - -// this is the entry point to open the new Notebook -CommandsRegistry.registerCommand(NewNotebookAction.ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService).createInstance(NewNotebookAction, NewNotebookAction.ID, NewNotebookAction.LABEL).run(); -}); - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: NewNotebookAction.ID, - title:NewNotebookAction.LABEL, - }, - when: notebooksEnabledCondition -}); \ No newline at end of file diff --git a/src/sql/parts/notebook/notebookEditor.ts b/src/sql/parts/notebook/notebookEditor.ts index 3df4253354..ba262f892c 100644 --- a/src/sql/parts/notebook/notebookEditor.ts +++ b/src/sql/parts/notebook/notebookEditor.ts @@ -86,6 +86,7 @@ export class NotebookEditor extends BaseEditor { input.hasBootstrapped = true; let params: INotebookParams = { notebookUri: input.notebookUri, + input: input, providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER, isTrusted: input.isTrusted }; diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index c65d0632e3..ec5b3f82c3 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -11,6 +11,7 @@ import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/edi import { Emitter, Event } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import * as resources from 'vs/base/common/resources'; import { INotebookService } from 'sql/services/notebook/notebookService'; @@ -88,15 +89,6 @@ export class NotebookInput extends EditorInput { ) { super(); this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire()); - this.onDispose(() => { - if (this.notebookService) { - this.notebookService.handleNotebookClosed(this.notebookUri); - } - }); - } - - public get title(): string { - return this._title; } public get notebookUri(): URI { @@ -116,6 +108,10 @@ export class NotebookInput extends EditorInput { } public getName(): string { + if (!this._title) { + this._title = resources.basenameOrAuthority(this._model.notebookUri); + } + return this._title; } diff --git a/src/sql/parts/notebook/notebookUtils.ts b/src/sql/parts/notebook/notebookUtils.ts index d250f5c4bb..9fd25beb05 100644 --- a/src/sql/parts/notebook/notebookUtils.ts +++ b/src/sql/parts/notebook/notebookUtils.ts @@ -5,11 +5,15 @@ 'use strict'; +import * as path from 'path'; import { nb } from 'sqlops'; import * as os from 'os'; import * as pfs from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { IOutputChannel } from 'vs/workbench/parts/output/common/output'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { INotebookProviderRegistry, Extensions } from 'sql/services/notebook/notebookRegistry'; +import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE } from 'sql/services/notebook/notebookService'; /** @@ -36,3 +40,23 @@ export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Pr await pfs.mkdirp(dirPath); } } + +export function getProviderForFileName(fileName: string): string { + let fileExt = path.extname(fileName); + let provider: string; + let notebookRegistry = Registry.as(Extensions.NotebookProviderContribution); + // First try to get provider for actual file type + if (fileExt && fileExt.startsWith('.')) { + fileExt = fileExt.slice(1,fileExt.length); + provider = notebookRegistry.getProviderForFileType(fileExt); + } + // Fallback to provider for default file type (assume this is a global handler) + if (!provider) { + provider = notebookRegistry.getProviderForFileType(DEFAULT_NOTEBOOK_FILETYPE); + } + // Finally if all else fails, use the built-in handler + if (!provider) { + provider = DEFAULT_NOTEBOOK_PROVIDER; + } + return provider; +} diff --git a/src/sql/services/notebook/localContentManager.ts b/src/sql/services/notebook/localContentManager.ts index 520652f8b0..41ac81c79e 100644 --- a/src/sql/services/notebook/localContentManager.ts +++ b/src/sql/services/notebook/localContentManager.ts @@ -12,10 +12,9 @@ import * as pfs from 'vs/base/node/pfs'; import URI from 'vs/base/common/uri'; import ContentManager = nb.ContentManager; -import INotebook = nb.INotebook; export class LocalContentManager implements ContentManager { - public async getNotebookContents(notebookUri: URI): Promise { + public async getNotebookContents(notebookUri: URI): Promise { if (!notebookUri) { return undefined; } @@ -23,10 +22,10 @@ export class LocalContentManager implements ContentManager { let path = notebookUri.fsPath; // Note: intentionally letting caller handle exceptions let notebookFileBuffer = await pfs.readFile(path); - return json.parse(notebookFileBuffer.toString()); + return json.parse(notebookFileBuffer.toString()); } - public async save(notebookUri: URI, notebook: INotebook): Promise { + public async save(notebookUri: URI, notebook: nb.INotebookContents): Promise { // Convert to JSON with pretty-print functionality let contents = JSON.stringify(notebook, undefined, ' '); let path = notebookUri.fsPath; diff --git a/src/sql/services/notebook/notebookService.ts b/src/sql/services/notebook/notebookService.ts index 3a7c9b04e7..784ac224b1 100644 --- a/src/sql/services/notebook/notebookService.ts +++ b/src/sql/services/notebook/notebookService.ts @@ -6,21 +6,28 @@ 'use strict'; import * as sqlops from 'sqlops'; + +import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import URI from 'vs/base/common/uri'; import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry'; import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookInput } from 'sql/parts/notebook/notebookInput'; export const SERVICE_ID = 'notebookService'; export const INotebookService = createDecorator(SERVICE_ID); export const DEFAULT_NOTEBOOK_PROVIDER = 'builtin'; +export const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB'; export interface INotebookService { _serviceBrand: any; + onNotebookEditorAdd: Event; + onNotebookEditorRemove: Event; + /** * Register a metadata provider */ @@ -40,7 +47,11 @@ export interface INotebookService { */ getOrCreateNotebookManager(providerId: string, uri: URI): Thenable; - handleNotebookClosed(uri: URI): void; + addNotebookEditor(editor: INotebookEditor): void; + + removeNotebookEditor(editor: INotebookEditor): void; + + listNotebookEditors(): INotebookEditor[]; shutdown(): void; @@ -62,8 +73,18 @@ export interface INotebookManager { export interface INotebookParams extends IBootstrapParams { notebookUri: URI; + input: NotebookInput; providerId: string; isTrusted: boolean; profile?: IConnectionProfile; modelFactory?: ModelFactory; +} + +export interface INotebookEditor { + readonly notebookParams: INotebookParams; + readonly id: string; + isDirty(): boolean; + isActive(): boolean; + isVisible(): boolean; + save(): Promise; } \ No newline at end of file diff --git a/src/sql/services/notebook/notebookServiceImpl.ts b/src/sql/services/notebook/notebookServiceImpl.ts index d7c4e7ffd6..836ca7c579 100644 --- a/src/sql/services/notebook/notebookServiceImpl.ts +++ b/src/sql/services/notebook/notebookServiceImpl.ts @@ -10,20 +10,26 @@ import { localize } from 'vs/nls'; import URI from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; -import { INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; +import { + INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER, + DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor +} from 'sql/services/notebook/notebookService'; import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry'; import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories'; import { LocalContentManager } from 'sql/services/notebook/localContentManager'; import { SessionManager } from 'sql/services/notebook/sessionManager'; import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry'; +import { Emitter, Event } from 'vs/base/common/event'; -const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB'; export class NotebookService implements INotebookService { _serviceBrand: any; private _mimeRegistry: RenderMimeRegistry; private _providers: Map = new Map(); private _managers: Map = new Map(); + private _onNotebookEditorAdd = new Emitter(); + private _onNotebookEditorRemove = new Emitter(); + private _editors = new Map(); constructor() { this.registerDefaultProvider(); @@ -71,8 +77,34 @@ export class NotebookService implements INotebookService { return manager; } - handleNotebookClosed(notebookUri: URI): void { + get onNotebookEditorAdd(): Event { + return this._onNotebookEditorAdd.event; + } + get onNotebookEditorRemove(): Event { + return this._onNotebookEditorRemove.event; + } + + addNotebookEditor(editor: INotebookEditor): void { + this._editors.set(editor.id, editor); + this._onNotebookEditorAdd.fire(editor); + } + + removeNotebookEditor(editor: INotebookEditor): void { + if (this._editors.delete(editor.id)) { + this._onNotebookEditorRemove.fire(editor); + } // Remove the manager from the tracked list, and let the notebook provider know that it should update its mappings + this.sendNotebookCloseToProvider(editor); + } + + listNotebookEditors(): INotebookEditor[] { + let editors = []; + this._editors.forEach(e => editors.push(e)); + return editors; + } + + private sendNotebookCloseToProvider(editor: INotebookEditor) { + let notebookUri = editor.notebookParams.notebookUri; let uriString = notebookUri.toString(); let manager = this._managers.get(uriString); if (manager) { @@ -82,7 +114,6 @@ export class NotebookService implements INotebookService { } } - // PRIVATE HELPERS ///////////////////////////////////////////////////// private doWithProvider(providerId: string, op: (provider: INotebookProvider) => Thenable): Thenable { // Make sure the provider exists before attempting to retrieve accounts @@ -109,8 +140,6 @@ export class NotebookService implements INotebookService { } return this._mimeRegistry; } - - } export class BuiltinProvider implements INotebookProvider { diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index a9eb3d71c2..db00f795a5 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1368,6 +1368,201 @@ declare module 'sqlops' { } export namespace nb { + /** + * All notebook documents currently known to the system. + * + * @readonly + */ + export let notebookDocuments: NotebookDocument[]; + + /** + * The currently active Notebook editor or `undefined`. The active editor is the one + * that currently has focus or, when none has focus, the one that has changed + * input most recently. + */ + export let activeNotebookEditor: NotebookEditor | undefined; + + /** + * The currently visible editors or an empty array. + */ + export let visibleNotebookEditors: NotebookEditor[]; + + /** + * An event that is emitted when a [notebook document](#NotebookDocument) is opened. + * + * To add an event listener when a visible text document is opened, use the [TextEditor](#TextEditor) events in the + * [window](#window) namespace. Note that: + * + * - The event is emitted before the [document](#NotebookDocument) is updated in the + * [active notebook editor](#nb.activeNotebookEditor) + * - When a [notebook document](#NotebookDocument) is already open (e.g.: open in another visible notebook editor) this event is not emitted + * + */ + export const onDidOpenNotebookDocument: vscode.Event; + + /** + * An event that is emitted when a [notebook's](#NotebookDocument) cell contents are changed. + */ + export const onDidChangeNotebookCell: vscode.Event; + + /** + * Show the given document in a notebook editor. A [column](#ViewColumn) can be provided + * to control where the editor is being shown. Might change the [active editor](#nb.activeNotebookEditor). + * + * The document is denoted by an [uri](#Uri). Depending on the [scheme](#Uri.scheme) the + * following rules apply: + * `file`-scheme: Open a file on disk, will be rejected if the file does not exist or cannot be loaded. + * `untitled`-scheme: A new file that should be saved on disk, e.g. `untitled:c:\frodo\new.js`. The language + * will be derived from the file name. + * For all other schemes the registered notebook providers are consulted. + * + * @param document A document to be shown. + * @param column A view column in which the [editor](#NotebookEditor) should be shown. The default is the [active](#ViewColumn.Active), other values + * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) + * to open the editor to the side of the currently active one. + * @param preserveFocus When `true` the editor will not take focus. + * @return A promise that resolves to a [notebook editor](#NotebookEditor). + */ + export function showNotebookDocument(uri: vscode.Uri, showOptions?: NotebookShowOptions): Thenable; + + export interface NotebookDocument { + /** + * The associated uri for this notebook document. + * + * *Note* that most documents use the `file`-scheme, which means they are files on disk. However, **not** all documents are + * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. + * + */ + readonly uri: vscode.Uri; + + /** + * The file system path of the associated resource. Shorthand + * notation for [TextDocument.uri.fsPath](#TextDocument.uri). Independent of the uri scheme. + */ + readonly fileName: string; + + /** + * Is this document representing an untitled file which has never been saved yet. *Note* that + * this does not mean the document will be saved to disk, use [`uri.scheme`](#Uri.scheme) + * to figure out where a document will be [saved](#FileSystemProvider), e.g. `file`, `ftp` etc. + */ + readonly isUntitled: boolean; + + /** + * The identifier of the Notebook provider associated with this document. + */ + readonly providerId: string; + + /** + * `true` if there are unpersisted changes. + */ + readonly isDirty: boolean; + /** + * `true` if the document have been closed. A closed document isn't synchronized anymore + * and won't be re-used when the same resource is opened again. + */ + readonly isClosed: boolean; + + /** + * All cells. + */ + readonly cells: NotebookCell[]; + + /** + * Save the underlying file. + * + * @return A promise that will resolve to true when the file + * has been saved. If the file was not dirty or the save failed, + * will return false. + */ + save(): Thenable; + } + + export interface NotebookEditor { + /** + * The document associated with this editor. The document will be the same for the entire lifetime of this editor. + */ + readonly document: NotebookDocument; + /** + * The column in which this editor shows. Will be `undefined` in case this + * isn't one of the main editors, e.g an embedded editor, or when the editor + * column is larger than three. + */ + viewColumn?: vscode.ViewColumn; + } + + export interface NotebookCell { + contents: ICellContents; + } + + export interface NotebookShowOptions { + /** + * An optional view column in which the [editor](#NotebookEditor) should be shown. + * The default is the [active](#ViewColumn.Active), other values are adjusted to + * be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is + * not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the + * editor to the side of the currently active one. + */ + viewColumn?: vscode.ViewColumn; + + /** + * An optional flag that when `true` will stop the [editor](#NotebookEditor) from taking focus. + */ + preserveFocus?: boolean; + + /** + * An optional flag that controls if an [editor](#NotebookEditor)-tab will be replaced + * with the next editor or if it will be kept. + */ + preview?: boolean; + + /** + * An optional string indicating which notebook provider to initially use + */ + providerId?: string; + + /** + * Optional ID indicating the initial connection to use for this editor + */ + connectionId?: string; + } + + /** + * Represents an event describing the change in a [notebook documents's cells](#NotebookDocument.cells). + */ + export interface NotebookCellChangeEvent { + /** + * The [notebook document](#NotebookDocument) for which the selections have changed. + */ + notebook: NotebookDocument; + /** + * The new value for the [notebook documents's cells](#NotebookDocument.cells). + */ + cell: NotebookCell[]; + /** + * The [change kind](#TextEditorSelectionChangeKind) which has triggered this + * event. Can be `undefined`. + */ + kind?: vscode.TextEditorSelectionChangeKind; + } + + /** + * Register a notebook provider. The supported file types handled by this + * provider are defined in the `package.json: + * ```json + * { + * "contributes": { + * "notebook.providers": [{ + * "provider": "providername", + * "fileExtensions": ["FILEEXT"] + * }] + * } + * } + * ``` + * @export + * @param {NotebookProvider} provider + * @returns {vscode.Disposable} + */ export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable; export interface NotebookProvider { @@ -1431,7 +1626,7 @@ declare module 'sqlops' { /* Reads contents from a Uri representing a local or remote notebook and returns a * JSON object containing the cells and metadata about the notebook */ - getNotebookContents(notebookUri: vscode.Uri): Thenable; + getNotebookContents(notebookUri: vscode.Uri): Thenable; /** * Save a file. @@ -1443,12 +1638,19 @@ declare module 'sqlops' { * @returns A thenable which resolves with the file content model when the * file is saved. */ - save(notebookUri: vscode.Uri, notebook: INotebook): Thenable; + save(notebookUri: vscode.Uri, notebook: INotebookContents): Thenable; } - export interface INotebook { - readonly cells: ICell[]; + /** + * Interface defining the file format contents of a notebook, usually in a serializable + * format. This interface does not have any methods for manipulating or interacting + * with a notebook object. + * + */ + export interface INotebookContents { + + readonly cells: ICellContents[]; readonly metadata: INotebookMetadata; readonly nbformat: number; readonly nbformat_minor: number; @@ -1477,7 +1679,13 @@ declare module 'sqlops' { version: string; } - export interface ICell { + /** + * Interface defining the file format contents of a notebook cell, usually in a serializable + * format. This interface does not have any methods for manipulating or interacting + * with a cell object. + * + */ + export interface ICellContents { cell_type: CellType; source: string | string[]; metadata: { diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts index 0ae03c6fe6..ad609c8289 100644 --- a/src/sql/workbench/api/node/extHostNotebook.ts +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -15,6 +15,7 @@ import URI, { UriComponents } from 'vs/base/common/uri'; import { ExtHostNotebookShape, MainThreadNotebookShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { Event, Emitter } from 'vs/base/common/event'; type Adapter = sqlops.nb.NotebookProvider | sqlops.nb.NotebookManager | sqlops.nb.ISession | sqlops.nb.IKernel | sqlops.nb.IFuture; @@ -23,6 +24,12 @@ export class ExtHostNotebook implements ExtHostNotebookShape { private readonly _proxy: MainThreadNotebookShape; private _adapters = new Map(); + private _onDidOpenNotebook = new Emitter(); + private _onDidChangeNotebookCell = new Emitter(); + + public readonly onDidOpenNotebookDocument: Event = this._onDidOpenNotebook.event; + public readonly onDidChangeNotebookCell: Event = this._onDidChangeNotebookCell.event; + // Notebook URI to manager lookup. constructor(_mainContext: IMainContext) { this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook); @@ -63,11 +70,11 @@ export class ExtHostNotebook implements ExtHostNotebookShape { return this._withServerManager(managerHandle, (serverManager) => serverManager.stopServer()); } - $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable { + $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable { return this._withContentManager(managerHandle, (contentManager) => contentManager.getNotebookContents(URI.revive(notebookUri))); } - $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable { + $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebookContents): Thenable { return this._withContentManager(managerHandle, (contentManager) => contentManager.save(URI.revive(notebookUri), notebook)); } diff --git a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts new file mode 100644 index 0000000000..2e524bcd86 --- /dev/null +++ b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts @@ -0,0 +1,281 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; + +import { Event, Emitter } from 'vs/base/common/event'; +import { readonly } from 'vs/base/common/errors'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Schemas } from 'vs/base/common/network'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import { ok } from 'vs/base/common/assert'; + +import { + MainThreadNotebookShape, SqlMainContext, INotebookDocumentsAndEditorsDelta, + ExtHostNotebookDocumentsAndEditorsShape, MainThreadNotebookDocumentsAndEditorsShape, INotebookShowOptions +} from 'sql/workbench/api/node/sqlExtHost.protocol'; + + +export class ExtHostNotebookDocumentData implements IDisposable { + private _document: sqlops.nb.NotebookDocument; + private _cells: sqlops.nb.NotebookCell[]; + private _isDisposed: boolean = false; + + constructor(private readonly _proxy: MainThreadNotebookDocumentsAndEditorsShape, + private readonly _uri: URI, + private readonly _providerId: string, + private _isDirty: boolean + ) { + // TODO add cell mapping support + this._cells = []; + } + + dispose(): void { + // we don't really dispose documents but let + // extensions still read from them. some + // operations, live saving, will now error tho + ok(!this._isDisposed); + this._isDisposed = true; + this._isDirty = false; + } + + + get document(): sqlops.nb.NotebookDocument { + if (!this._document) { + const data = this; + this._document = { + get uri() { return data._uri; }, + get fileName() { return data._uri.fsPath; }, + get isUntitled() { return data._uri.scheme === Schemas.untitled; }, + get providerId() { return data._providerId; }, + get isClosed() { return data._isDisposed; }, + get isDirty() { return data._isDirty; }, + get cells() { return data._cells; }, + save() { return data._save(); }, + }; + } + return Object.freeze(this._document); + } + + private _save(): Thenable { + if (this._isDisposed) { + return TPromise.wrapError(new Error('Document has been closed')); + } + return this._proxy.$trySaveDocument(this._uri); + + } +} + +export class ExtHostNotebookEditor implements sqlops.nb.NotebookEditor, IDisposable { + private _disposed: boolean = false; + + constructor( + private _proxy: MainThreadNotebookShape, + private _id: string, + private readonly _documentData: ExtHostNotebookDocumentData, + private _viewColumn: vscode.ViewColumn + ) { + + } + + dispose() { + ok(!this._disposed); + this._disposed = true; + } + + get document(): sqlops.nb.NotebookDocument { + return this._documentData.document; + } + + set document(value) { + throw readonly('document'); + } + + get viewColumn(): vscode.ViewColumn { + return this._viewColumn; + } + + set viewColumn(value) { + throw readonly('viewColumn'); + } + + + get id(): string { + return this._id; + } +} + +export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocumentsAndEditorsShape { + + private _disposables: Disposable[] = []; + + private _activeEditorId: string; + private _proxy: MainThreadNotebookDocumentsAndEditorsShape; + + private readonly _editors = new Map(); + private readonly _documents = new Map(); + + private readonly _onDidAddDocuments = new Emitter(); + private readonly _onDidRemoveDocuments = new Emitter(); + private readonly _onDidChangeVisibleNotebookEditors = new Emitter(); + private readonly _onDidChangeActiveNotebookEditor = new Emitter(); + + readonly onDidAddDocuments: Event = this._onDidAddDocuments.event; + readonly onDidRemoveDocuments: Event = this._onDidRemoveDocuments.event; + readonly onDidChangeVisibleNotebookEditors: Event = this._onDidChangeVisibleNotebookEditors.event; + readonly onDidChangeActiveNotebookEditor: Event = this._onDidChangeActiveNotebookEditor.event; + + constructor( + private readonly _mainContext: IMainContext, + ) { + if (this._mainContext) { + this._proxy = this._mainContext.getProxy(SqlMainContext.MainThreadNotebookDocumentsAndEditors); + } + } + + dispose() { + this._disposables = dispose(this._disposables); + } + + //#region Main Thread accessible methods + $acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void { + + const removedDocuments: ExtHostNotebookDocumentData[] = []; + const addedDocuments: ExtHostNotebookDocumentData[] = []; + const removedEditors: ExtHostNotebookEditor[] = []; + + if (delta.removedDocuments) { + for (const uriComponent of delta.removedDocuments) { + const uri = URI.revive(uriComponent); + const id = uri.toString(); + const data = this._documents.get(id); + this._documents.delete(id); + removedDocuments.push(data); + } + } + + if (delta.addedDocuments) { + for (const data of delta.addedDocuments) { + const resource = URI.revive(data.uri); + ok(!this._documents.has(resource.toString()), `document '${resource} already exists!'`); + + const documentData = new ExtHostNotebookDocumentData( + this._proxy, + resource, + data.providerId, + data.isDirty + ); + this._documents.set(resource.toString(), documentData); + addedDocuments.push(documentData); + } + } + + if (delta.removedEditors) { + for (const id of delta.removedEditors) { + const editor = this._editors.get(id); + this._editors.delete(id); + removedEditors.push(editor); + } + } + + if (delta.addedEditors) { + for (const data of delta.addedEditors) { + const resource = URI.revive(data.documentUri); + ok(this._documents.has(resource.toString()), `document '${resource}' does not exist`); + ok(!this._editors.has(data.id), `editor '${data.id}' already exists!`); + + const documentData = this._documents.get(resource.toString()); + const editor = new ExtHostNotebookEditor( + this._mainContext.getProxy(SqlMainContext.MainThreadNotebook), + data.id, + documentData, + typeConverters.ViewColumn.to(data.editorPosition) + ); + this._editors.set(data.id, editor); + } + } + + if (delta.newActiveEditor !== undefined) { + ok(delta.newActiveEditor === null || this._editors.has(delta.newActiveEditor), `active editor '${delta.newActiveEditor}' does not exist`); + this._activeEditorId = delta.newActiveEditor; + } + + dispose(removedDocuments); + dispose(removedEditors); + + // now that the internal state is complete, fire events + if (delta.removedDocuments) { + this._onDidRemoveDocuments.fire(removedDocuments); + } + if (delta.addedDocuments) { + this._onDidAddDocuments.fire(addedDocuments); + } + + if (delta.removedEditors || delta.addedEditors) { + this._onDidChangeVisibleNotebookEditors.fire(this.getAllEditors()); + } + if (delta.newActiveEditor !== undefined) { + this._onDidChangeActiveNotebookEditor.fire(this.getActiveEditor()); + } + } + //#endregion + + //#region Extension accessible methods + showNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions): Thenable { + return this.doShowNotebookDocument(uri, showOptions); + } + + private async doShowNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions): Promise { + let options: INotebookShowOptions = {}; + if (showOptions) { + options.preserveFocus = showOptions.preserveFocus; + options.position = showOptions.viewColumn; + options.providerId = showOptions.providerId; + options.connectionId = showOptions.connectionId; + } + let id = await this._proxy.$tryShowNotebookDocument(uri, options); + let editor = this.getEditor(id); + if (editor) { + return editor; + } else { + throw new Error(`Failed to show notebook document ${uri.toString()}, should show in editor #${id}`); + } + } + + getDocument(strUrl: string): ExtHostNotebookDocumentData { + return this._documents.get(strUrl); + } + + getAllDocuments(): ExtHostNotebookDocumentData[] { + const result: ExtHostNotebookDocumentData[] = []; + this._documents.forEach(data => result.push(data)); + return result; + } + + getEditor(id: string): ExtHostNotebookEditor { + return this._editors.get(id); + } + + getActiveEditor(): ExtHostNotebookEditor | undefined { + if (!this._activeEditorId) { + return undefined; + } else { + return this._editors.get(this._activeEditorId); + } + } + + getAllEditors(): ExtHostNotebookEditor[] { + const result: ExtHostNotebookEditor[] = []; + this._editors.forEach(data => result.push(data)); + return result; + } + //#endregion +} diff --git a/src/sql/workbench/api/node/mainThreadNotebook.ts b/src/sql/workbench/api/node/mainThreadNotebook.ts index cff7da7c89..e45b92c438 100644 --- a/src/sql/workbench/api/node/mainThreadNotebook.ts +++ b/src/sql/workbench/api/node/mainThreadNotebook.ts @@ -153,11 +153,11 @@ class ContentManagerWrapper implements sqlops.nb.ContentManager { constructor(private handle: number, private _proxy: Proxies) { } - getNotebookContents(notebookUri: URI): Thenable { + getNotebookContents(notebookUri: URI): Thenable { return this._proxy.ext.$getNotebookContents(this.handle, notebookUri); } - save(path: URI, notebook: sqlops.nb.INotebook): Thenable { + save(path: URI, notebook: sqlops.nb.INotebookContents): Thenable { return this._proxy.ext.$save(this.handle, path, notebook); } } diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts new file mode 100644 index 0000000000..17298df3df --- /dev/null +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -0,0 +1,378 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as sqlops from 'sqlops'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { viewColumnToEditorGroup } from 'vs/workbench/api/shared/editor'; + +import { + SqlMainContext, MainThreadNotebookDocumentsAndEditorsShape, SqlExtHostContext, ExtHostNotebookDocumentsAndEditorsShape, + INotebookDocumentsAndEditorsDelta, INotebookEditorAddData, INotebookShowOptions, INotebookModelAddedData +} from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { NotebookInputModel, NotebookInput } from 'sql/parts/notebook/notebookInput'; +import { INotebookService, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { INotebookProviderRegistry, Extensions } from 'sql/services/notebook/notebookRegistry'; +import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils'; + +class MainThreadNotebookEditor extends Disposable { + + constructor(public readonly editor: INotebookEditor) { + super(); + } + + public get uri(): URI { + return this.editor.notebookParams.notebookUri; + } + + public get id(): string { + return this.editor.id; + } + + public get isDirty(): boolean { + return this.editor.isDirty(); + } + + public get providerId(): string { + return this.editor.notebookParams.providerId; + } + + public save(): Thenable { + return this.editor.save(); + } + + public matches(input: NotebookInput): boolean { + if (!input) { + return false; + } + return input === this.editor.notebookParams.input; + } +} + +function wait(timeMs: number): Promise { + return new Promise(resolve => setTimeout(resolve, timeMs)); +} + + +namespace mapset { + + export function setValues(set: Set): T[] { + // return Array.from(set); + let ret: T[] = []; + set.forEach(v => ret.push(v)); + return ret; + } + + export function mapValues(map: Map): T[] { + // return Array.from(map.values()); + let ret: T[] = []; + map.forEach(v => ret.push(v)); + return ret; + } +} + +namespace delta { + + export function ofSets(before: Set, after: Set): { removed: T[], added: T[] } { + const removed: T[] = []; + const added: T[] = []; + before.forEach(element => { + if (!after.has(element)) { + removed.push(element); + } + }); + after.forEach(element => { + if (!before.has(element)) { + added.push(element); + } + }); + return { removed, added }; + } + + export function ofMaps(before: Map, after: Map): { removed: V[], added: V[] } { + const removed: V[] = []; + const added: V[] = []; + before.forEach((value, index) => { + if (!after.has(index)) { + removed.push(value); + } + }); + after.forEach((value, index) => { + if (!before.has(index)) { + added.push(value); + } + }); + return { removed, added }; + } +} + +class NotebookEditorStateDelta { + + readonly isEmpty: boolean; + + constructor( + readonly removedEditors: INotebookEditor[], + readonly addedEditors: INotebookEditor[], + readonly oldActiveEditor: string, + readonly newActiveEditor: string, + ) { + this.isEmpty = + this.removedEditors.length === 0 + && this.addedEditors.length === 0 + && oldActiveEditor === newActiveEditor; + } + + toString(): string { + let ret = 'NotebookEditorStateDelta\n'; + ret += `\tRemoved Editors: [${this.removedEditors.map(e => e.id).join(', ')}]\n`; + ret += `\tAdded Editors: [${this.addedEditors.map(e => e.id).join(', ')}]\n`; + ret += `\tNew Active Editor: ${this.newActiveEditor}\n`; + return ret; + } +} + +class NotebookEditorState { + + static compute(before: NotebookEditorState, after: NotebookEditorState): NotebookEditorStateDelta { + if (!before) { + return new NotebookEditorStateDelta( + [], mapset.mapValues(after.textEditors), + undefined, after.activeEditor + ); + } + const editorDelta = delta.ofMaps(before.textEditors, after.textEditors); + const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined; + const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined; + + return new NotebookEditorStateDelta( + editorDelta.removed, editorDelta.added, + oldActiveEditor, newActiveEditor + ); + } + + constructor( + readonly textEditors: Map, + readonly activeEditor: string) { } +} + +class MainThreadNotebookDocumentAndEditorStateComputer extends Disposable { + + private _currentState: NotebookEditorState; + + constructor( + private readonly _onDidChangeState: (delta: NotebookEditorStateDelta) => void, + @IEditorService private readonly _editorService: IEditorService, + @INotebookService private readonly _notebookService: INotebookService + ) { + super(); + this._register(this._editorService.onDidActiveEditorChange(this._updateState, this)); + this._register(this._editorService.onDidVisibleEditorsChange(this._updateState, this)); + this._register(this._notebookService.onNotebookEditorAdd(this._onDidAddEditor, this)); + this._register(this._notebookService.onNotebookEditorRemove(this._onDidRemoveEditor, this)); + + this._updateState(); + } + + private _onDidAddEditor(e: INotebookEditor): void { + // TODO hook to cell change and other events + this._updateState(); + } + + private _onDidRemoveEditor(e: INotebookEditor): void { + // TODO remove event listeners + this._updateState(); + } + + private _updateState(): void { + // editor + const editors = new Map(); + let activeEditor: string = undefined; + + for (const editor of this._notebookService.listNotebookEditors()) { + editors.set(editor.id, editor); + if (editor.isActive()) { + activeEditor = editor.id; + } + } + + // compute new state and compare against old + const newState = new NotebookEditorState(editors, activeEditor); + const delta = NotebookEditorState.compute(this._currentState, newState); + if (!delta.isEmpty) { + this._currentState = newState; + this._onDidChangeState(delta); + } + } +} + +@extHostNamedCustomer(SqlMainContext.MainThreadNotebookDocumentsAndEditors) +export class MainThreadNotebookDocumentsAndEditors extends Disposable implements MainThreadNotebookDocumentsAndEditorsShape { + private _proxy: ExtHostNotebookDocumentsAndEditorsShape; + private _notebookEditors = new Map(); + + constructor( + extHostContext: IExtHostContext, + @IInstantiationService private _instantiationService: IInstantiationService, + @IEditorService private _editorService: IEditorService, + @IEditorGroupsService private _editorGroupService: IEditorGroupsService + ) { + super(); + if (extHostContext) { + this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostNotebookDocumentsAndEditors); + } + + // Create a state computer that actually tracks all required changes. This is hooked to onDelta which notifies extension host + this._register(this._instantiationService.createInstance(MainThreadNotebookDocumentAndEditorStateComputer, delta => this._onDelta(delta))); + } + + //#region extension host callable APIs + $trySaveDocument(uri: UriComponents): Thenable { + let uriString = URI.revive(uri).toString(); + let editor = this._notebookEditors.get(uriString); + if (editor) { + return editor.save(); + } else { + return Promise.resolve(false); + } + } + + $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): TPromise { + return TPromise.wrap(this.doOpenEditor(resource, options)); + } + //#endregion + + private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise { + const uri = URI.revive(resource); + + const editorOptions: ITextEditorOptions = { + preserveFocus: options.preserveFocus, + pinned: options.pinned + }; + let model = new NotebookInputModel(uri, undefined, false, undefined); + let providerId = options.providerId; + if(!providerId) + { + // Ensure there is always a sensible provider ID for this file type + providerId = getProviderForFileName(uri.fsPath); + } + + model.providerId = providerId; + let input = this._instantiationService.createInstance(NotebookInput, undefined, model); + + let editor = await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position)); + if (!editor) { + return undefined; + } + return this.waitOnEditor(input); + } + + private async waitOnEditor(input: NotebookInput): Promise { + let id: string = undefined; + let attemptsLeft = 10; + let timeoutMs = 20; + while (!id && attemptsLeft > 0) { + id = this.findNotebookEditorIdFor(input); + if (!id) { + await wait(timeoutMs); + } + } + return id; + } + + findNotebookEditorIdFor(input: NotebookInput): string { + let foundId: string = undefined; + this._notebookEditors.forEach(e => { + if (e.matches(input)) { + foundId = e.id; + } + }); + return foundId; + } + + getEditor(id: string): MainThreadNotebookEditor { + return this._notebookEditors.get(id); + } + + private _onDelta(delta: NotebookEditorStateDelta): void { + let removedEditors: string[] = []; + let removedDocuments: URI[] = []; + let addedEditors: MainThreadNotebookEditor[] = []; + + // added editors + for (const editor of delta.addedEditors) { + const mainThreadEditor = new MainThreadNotebookEditor(editor); + + this._notebookEditors.set(editor.id, mainThreadEditor); + addedEditors.push(mainThreadEditor); + } + + // removed editors + for (const { id } of delta.removedEditors) { + const mainThreadEditor = this._notebookEditors.get(id); + if (mainThreadEditor) { + removedDocuments.push(mainThreadEditor.uri); + mainThreadEditor.dispose(); + this._notebookEditors.delete(id); + removedEditors.push(id); + } + } + + let extHostDelta: INotebookDocumentsAndEditorsDelta = Object.create(null); + let empty = true; + if (delta.newActiveEditor !== undefined) { + empty = false; + extHostDelta.newActiveEditor = delta.newActiveEditor; + } + if (removedDocuments.length > 0) { + empty = false; + extHostDelta.removedDocuments = removedDocuments; + } + if (removedEditors.length > 0) { + empty = false; + extHostDelta.removedEditors = removedEditors; + } + if (delta.addedEditors.length > 0) { + empty = false; + extHostDelta.addedDocuments = []; + extHostDelta.addedEditors = []; + for (let editor of addedEditors) { + extHostDelta.addedEditors.push(this._toNotebookEditorAddData(editor)); + // For now, add 1 document for each editor. In the future these may be trackable independently + extHostDelta.addedDocuments.push(this._toNotebookModelAddData(editor)); + } + } + + if (!empty) { + this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta); + } + } + + private _toNotebookEditorAddData(editor: MainThreadNotebookEditor): INotebookEditorAddData { + let addData: INotebookEditorAddData = { + documentUri: editor.uri, + editorPosition: undefined, + id: editor.editor.id + }; + return addData; + } + + private _toNotebookModelAddData(editor: MainThreadNotebookEditor): INotebookModelAddedData { + let addData: INotebookModelAddedData = { + uri: editor.uri, + isDirty: editor.isDirty, + providerId: editor.providerId + }; + return addData; + } +} diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 4199340173..051de9b857 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -38,6 +38,7 @@ import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelVi import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement'; import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook'; +import { ExtHostNotebookDocumentsAndEditors } from 'sql/workbench/api/node/extHostNotebookDocumentsAndEditors'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription): typeof vscode; @@ -75,6 +76,7 @@ export function createApiFactory( const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement)); const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol)); const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol)); + const extHostNotebookDocumentsAndEditors = rpcProtocol.set(SqlExtHostContext.ExtHostNotebookDocumentsAndEditors, new ExtHostNotebookDocumentsAndEditors(rpcProtocol)); return { @@ -420,6 +422,24 @@ export function createApiFactory( }; const nb = { + get notebookDocuments() { + return extHostNotebookDocumentsAndEditors.getAllDocuments().map(doc => doc.document); + }, + get activeNotebookEditor() { + return extHostNotebookDocumentsAndEditors.getActiveEditor(); + }, + get visibleNotebookEditors() { + return extHostNotebookDocumentsAndEditors.getAllEditors(); + }, + get onDidOpenNotebookDocument() { + return extHostNotebook.onDidOpenNotebookDocument; + }, + get onDidChangeNotebookCell() { + return extHostNotebook.onDidChangeNotebookCell; + }, + showNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions) { + return extHostNotebookDocumentsAndEditors.showNotebookDocument(uri, showOptions); + }, registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable { return extHostNotebook.registerNotebookProvider(provider); } diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts index f8baed3939..10bdb07b32 100644 --- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts +++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts @@ -24,6 +24,7 @@ import 'sql/workbench/api/node/mainThreadQueryEditor'; import 'sql/workbench/api/node/mainThreadModelView'; import 'sql/workbench/api/node/mainThreadModelViewDialog'; import 'sql/workbench/api/node/mainThreadNotebook'; +import 'sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors'; import 'sql/workbench/api/node/mainThreadAccountManagement'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index b8b99133b0..f94f58a7e2 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -23,6 +23,7 @@ import { IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, IModelViewWizardDetails, IModelViewWizardPageDetails, INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType, INotebookFutureDone } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; export abstract class ExtHostAccountManagementShape { $autoOAuthCancelled(handle: number): Thenable { throw ni(); } @@ -572,7 +573,9 @@ export const SqlMainContext = { MainThreadDashboard: createMainId('MainThreadDashboard'), MainThreadModelViewDialog: createMainId('MainThreadModelViewDialog'), MainThreadQueryEditor: createMainId('MainThreadQueryEditor'), - MainThreadNotebook: createMainId('MainThreadNotebook') + MainThreadNotebook: createMainId('MainThreadNotebook'), + MainThreadNotebookDocumentsAndEditors: createMainId('MainThreadNotebookDocumentsAndEditors') + }; export const SqlExtHostContext = { @@ -592,7 +595,8 @@ export const SqlExtHostContext = { ExtHostDashboard: createExtId('ExtHostDashboard'), ExtHostModelViewDialog: createExtId('ExtHostModelViewDialog'), ExtHostQueryEditor: createExtId('ExtHostQueryEditor'), - ExtHostNotebook: createExtId('ExtHostNotebook') + ExtHostNotebook: createExtId('ExtHostNotebook'), + ExtHostNotebookDocumentsAndEditors: createExtId('ExtHostNotebookDocumentsAndEditors') }; export interface MainThreadDashboardShape extends IDisposable { @@ -750,8 +754,8 @@ export interface ExtHostNotebookShape { $doStopServer(managerHandle: number): Thenable; // Content Manager APIs - $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable; - $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable; + $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable; + $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebookContents): Thenable; // Session Manager APIs $refreshSpecs(managerHandle: number): Thenable; @@ -780,3 +784,39 @@ export interface MainThreadNotebookShape extends IDisposable { $onFutureDone(futureId: number, done: INotebookFutureDone): void; } +export interface INotebookDocumentsAndEditorsDelta { + removedDocuments?: UriComponents[]; + addedDocuments?: INotebookModelAddedData[]; + removedEditors?: string[]; + addedEditors?: INotebookEditorAddData[]; + newActiveEditor?: string; +} + +export interface INotebookModelAddedData { + uri: UriComponents; + providerId: string; + isDirty: boolean; +} + +export interface INotebookEditorAddData { + id: string; + documentUri: UriComponents; + editorPosition: EditorViewColumn; +} + +export interface INotebookShowOptions { + position?: EditorViewColumn; + preserveFocus?: boolean; + pinned?: boolean; + providerId?: string; + connectionId?: string; +} + +export interface ExtHostNotebookDocumentsAndEditorsShape { + $acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; +} + +export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable { + $trySaveDocument(uri: UriComponents): Thenable; + $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): TPromise; +} \ No newline at end of file diff --git a/src/sqltest/workbench/api/mainThreadNotebook.test.ts b/src/sqltest/workbench/api/mainThreadNotebook.test.ts index 07ba48e4a1..4a15440614 100644 --- a/src/sqltest/workbench/api/mainThreadNotebook.test.ts +++ b/src/sqltest/workbench/api/mainThreadNotebook.test.ts @@ -131,10 +131,10 @@ class ExtHostNotebookStub implements ExtHostNotebookShape { $doStopServer(managerHandle: number): Thenable { throw new Error('Method not implemented.'); } - $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable { + $getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable { throw new Error('Method not implemented.'); } - $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable { + $save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebookContents): Thenable { throw new Error('Method not implemented.'); } $refreshSpecs(managerHandle: number): Thenable {