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 {