From 0b571737b77cc4c1e432afee5ab36450d24ed9df Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Mon, 12 Nov 2018 17:32:53 -0800 Subject: [PATCH] Support notebook file types contribution (#3196) * Support notebook file types contribution - Extensions can define a provider and what file types it should be used for - Verified that this works for Jupyter Content & Server Managers. - Starts Jupyter server as expected Not in this PR: - Support for session manager end to end - Tests --- src/sql/parts/common/customInputConverter.ts | 37 +++++- .../parts/notebook/notebook.contribution.ts | 7 +- src/sql/parts/notebook/notebookInput.ts | 11 +- src/sql/services/notebook/notebookRegistry.ts | 116 ++++++++++++++++++ src/sql/services/notebook/notebookService.ts | 2 + .../services/notebook/notebookServiceImpl.ts | 44 +++++-- 6 files changed, 198 insertions(+), 19 deletions(-) create mode 100644 src/sql/services/notebook/notebookRegistry.ts diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index fb3f161d53..0cc2cd7d31 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -3,16 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; + +import { Registry } from 'vs/platform/registry/common/platform'; import { EditorInput, IEditorInput } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; +import URI from 'vs/base/common/uri'; + import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput'; import { QueryInput } from 'sql/parts/query/common/queryInput'; -import URI from 'vs/base/common/uri'; import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService'; import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput'; import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput'; +import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry'; +import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; const fs = require('fs'); @@ -54,9 +60,15 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti uri = getNotebookEditorUri(input); if(uri){ //TODO: We need to pass in notebook data either through notebook input or notebook service - let fileName: string = input? input.getName() : 'untitled'; + let fileName: string = 'untitled'; + let providerId: string = DEFAULT_NOTEBOOK_PROVIDER; + if (input) { + fileName = input.getName(); + providerId = getProviderForFileName(fileName); + } let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined); - //TO DO: Second paramter has to be the content. + notebookInputModel.providerId = providerId; + //TO DO: Second parameter has to be the content. let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); return notebookInput; } @@ -91,7 +103,6 @@ export function getSupportedInputResource(input: IEditorInput): URI { // file extensions for the inputs we support (should be all upper case for comparison) const sqlFileTypes = ['SQL']; const sqlPlanFileTypes = ['SQLPLAN']; -const notebookFileType = ['IPYNB']; /** * If input is a supported query editor file, return it's URI. Otherwise return undefined. @@ -155,7 +166,7 @@ function getNotebookEditorUri(input: EditorInput): URI { if (!(input instanceof NotebookInput)) { let uri: URI = getSupportedInputResource(input); if (uri) { - if (hasFileExtension(notebookFileType, input, false)) { + if (hasFileExtension(getNotebookFileExtensions(), input, false)) { return uri; } } @@ -164,6 +175,22 @@ function getNotebookEditorUri(input: EditorInput): URI { return undefined; } +function getNotebookFileExtensions() { + let notebookRegistry = Registry.as(Extensions.NotebookProviderContribution); + 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/notebook.contribution.ts b/src/sql/parts/notebook/notebook.contribution.ts index e194b0598a..a4d097345a 100644 --- a/src/sql/parts/notebook/notebook.contribution.ts +++ b/src/sql/parts/notebook/notebook.contribution.ts @@ -13,10 +13,12 @@ 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 } from 'sql/parts/notebook/notebookInput'; import { NotebookEditor } from 'sql/parts/notebook/notebookEditor'; + let counter = 0; /** @@ -31,7 +33,8 @@ export class OpenNotebookAction extends Action { constructor( id: string, label: string, - @IEditorService private _editorService: IEditorService + @IEditorService private _editorService: IEditorService, + @IInstantiationService private _instantiationService: IInstantiationService ) { super(id, label); } @@ -40,7 +43,7 @@ export class OpenNotebookAction extends Action { return new TPromise((resolve, reject) => { let untitledUri = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter++}`}); let model = new NotebookInputModel(untitledUri, undefined, false, undefined); - let input = new NotebookInput('modelViewId', model,); + let input = this._instantiationService.createInstance(NotebookInput, 'modelViewId', model); this._editorService.openEditor(input, { pinned: true }); }); } diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index 117c54d244..eef4817d7a 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -10,6 +10,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; +import { INotebookService } from 'sql/services/notebook/notebookService'; export type ModeViewSaveHandler = (handle: number) => Thenable; @@ -66,11 +67,17 @@ export class NotebookInput extends EditorInput { // Holds the HTML content for the editor when the editor discards this input and loads another private _parentContainer: HTMLElement; - constructor(private _title: string, private _model: NotebookInputModel, + constructor(private _title: string, + private _model: NotebookInputModel, + @INotebookService private notebookService: INotebookService ) { super(); this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire()); - + this.onDispose(() => { + if (this.notebookService) { + this.notebookService.handleNotebookClosed(this.notebookUri); + } + }); } public get title(): string { diff --git a/src/sql/services/notebook/notebookRegistry.ts b/src/sql/services/notebook/notebookRegistry.ts new file mode 100644 index 0000000000..c8b818a66d --- /dev/null +++ b/src/sql/services/notebook/notebookRegistry.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { localize } from 'vs/nls'; +import * as platform from 'vs/platform/registry/common/platform'; + +export const Extensions = { + NotebookProviderContribution: 'notebook.providers' +}; + +export interface NotebookProviderDescription { + provider: string; + fileExtensions: string | string[]; +} + +let notebookProviderType: IJSONSchema = { + type: 'object', + default: { provider: '', fileExtensions: [] }, + properties: { + provider: { + description: localize('carbon.extension.contributes.notebook.provider', 'Identifier of the notebook provider.'), + type: 'string' + }, + fileExtensions: { + description: localize('carbon.extension.contributes.notebook.fileExtensions', 'What file extensions should be registered to this notebook provider'), + oneOf: [ + { type: 'string' }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + } + } +}; + +let notebookContrib: IJSONSchema = { + description: localize('vscode.extension.contributes.notebook.providers', "Contributes notebook providers."), + oneOf: [ + notebookProviderType, + { + type: 'array', + items: notebookProviderType + } + ] +}; + +export interface INotebookProviderRegistry { + registerNotebookProvider(provider: NotebookProviderDescription): void; + getSupportedFileExtensions(): string[]; + getProviderForFileType(fileType: string): string; +} + +class NotebookProviderRegistry implements INotebookProviderRegistry { + private providerIdToProviders = new Map(); + private fileToProviders = new Map(); + + registerNotebookProvider(provider: NotebookProviderDescription): void { + // Note: this method intentionally overrides default provider for a file type. + // This means that any built-in provider will be overridden by registered extensions + this.providerIdToProviders.set(provider.provider, provider); + if (provider.fileExtensions) { + if (Array.isArray(provider.fileExtensions)) { + for (let fileType of provider.fileExtensions) { + this.addFileProvider(fileType, provider); + } + } else { + this.addFileProvider(provider.fileExtensions, provider); + } + } + } + + private addFileProvider(fileType: string, provider: NotebookProviderDescription) { + this.fileToProviders.set(fileType.toUpperCase(), provider); + } + + getSupportedFileExtensions(): string[] { + return Array.from(this.fileToProviders.keys()); + } + + getProviderForFileType(fileType: string): string { + fileType = fileType.toUpperCase(); + let provider = this.fileToProviders.get(fileType); + return provider ? provider.provider : undefined; + } +} + +const notebookProviderRegistry = new NotebookProviderRegistry(); +platform.Registry.add(Extensions.NotebookProviderContribution, notebookProviderRegistry); + + +ExtensionsRegistry.registerExtensionPoint(Extensions.NotebookProviderContribution, [], notebookContrib).setHandler(extensions => { + + function handleExtension(contrib: NotebookProviderDescription, extension: IExtensionPointUser) { + notebookProviderRegistry.registerNotebookProvider(contrib); + } + + for (let extension of extensions) { + const { value } = extension; + if (Array.isArray(value)) { + for (let command of value) { + handleExtension(command, extension); + } + } else { + handleExtension(value, extension); + } + } +}); diff --git a/src/sql/services/notebook/notebookService.ts b/src/sql/services/notebook/notebookService.ts index 9df301b86d..3a7c9b04e7 100644 --- a/src/sql/services/notebook/notebookService.ts +++ b/src/sql/services/notebook/notebookService.ts @@ -40,6 +40,8 @@ export interface INotebookService { */ getOrCreateNotebookManager(providerId: string, uri: URI): Thenable; + handleNotebookClosed(uri: URI): void; + shutdown(): void; getMimeRegistry(): RenderMimeRegistry; diff --git a/src/sql/services/notebook/notebookServiceImpl.ts b/src/sql/services/notebook/notebookServiceImpl.ts index 56bf5beb15..61a26d8a66 100644 --- a/src/sql/services/notebook/notebookServiceImpl.ts +++ b/src/sql/services/notebook/notebookServiceImpl.ts @@ -6,26 +6,37 @@ 'use strict'; import { nb } from 'sqlops'; -import * as nls from 'vs/nls'; -import { INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; +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 { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry'; import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories'; import { LocalContentManager } from 'sql/services/notebook/localContentManager'; -import { session } from 'electron'; import { SessionManager } from 'sql/services/notebook/sessionManager'; +import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry'; + +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 _managers: Map = new Map(); constructor() { - mimeRegistry: RenderMimeRegistry; + this.registerDefaultProvider(); + } + + private registerDefaultProvider() { let defaultProvider = new BuiltinProvider(); this.registerProvider(defaultProvider.providerId, defaultProvider); + let registry = Registry.as(Extensions.NotebookProviderContribution); + registry.registerNotebookProvider({ + provider: defaultProvider.providerId, + fileExtensions: DEFAULT_NOTEBOOK_FILETYPE + }); } registerProvider(providerId: string, provider: INotebookProvider): void { @@ -47,24 +58,37 @@ export class NotebookService implements INotebookService { async getOrCreateNotebookManager(providerId: string, uri: URI): Promise { if (!uri) { - throw new Error(nls.localize('notebookUriNotDefined', 'No URI was passed when creating a notebook manager')); + throw new Error(localize('notebookUriNotDefined', 'No URI was passed when creating a notebook manager')); } - let manager = this._managers.get(uri); + let uriString = uri.toString(); + let manager = this._managers.get(uriString); if (!manager) { manager = await this.doWithProvider(providerId, (provider) => provider.getNotebookManager(uri)); if (manager) { - this._managers.set(uri, manager); + this._managers.set(uriString, manager); } } return manager; } + handleNotebookClosed(notebookUri: URI): void { + // Remove the manager from the tracked list, and let the notebook provider know that it should update its mappings + let uriString = notebookUri.toString(); + let manager = this._managers.get(uriString); + if (manager) { + this._managers.delete(uriString); + let provider = this._providers.get(manager.providerId); + provider.handleNotebookClosed(notebookUri); + } + } + + // PRIVATE HELPERS ///////////////////////////////////////////////////// private doWithProvider(providerId: string, op: (provider: INotebookProvider) => Thenable): Thenable { // Make sure the provider exists before attempting to retrieve accounts let provider = this._providers.get(providerId); if (!provider) { - return Promise.reject(new Error(nls.localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then(); + return Promise.reject(new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then(); } return op(provider);