diff --git a/extensions/notebook-test/.vscode/launch.json b/extensions/notebook-test/.vscode/launch.json new file mode 100644 index 00000000000..7c2f6f64689 --- /dev/null +++ b/extensions/notebook-test/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/extensions/notebook-test/README.md b/extensions/notebook-test/README.md new file mode 100644 index 00000000000..b44bd72341a --- /dev/null +++ b/extensions/notebook-test/README.md @@ -0,0 +1 @@ +# Notebook test \ No newline at end of file diff --git a/extensions/notebook-test/extension.webpack.config.js b/extensions/notebook-test/extension.webpack.config.js new file mode 100644 index 00000000000..de88398eca0 --- /dev/null +++ b/extensions/notebook-test/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + } +}); diff --git a/extensions/notebook-test/icon.png b/extensions/notebook-test/icon.png new file mode 100644 index 00000000000..64dcf7d463c Binary files /dev/null and b/extensions/notebook-test/icon.png differ diff --git a/extensions/notebook-test/icon.svg b/extensions/notebook-test/icon.svg new file mode 100644 index 00000000000..0eb0f4c103b --- /dev/null +++ b/extensions/notebook-test/icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/extensions/notebook-test/package.json b/extensions/notebook-test/package.json new file mode 100644 index 00000000000..2fe2d7a979c --- /dev/null +++ b/extensions/notebook-test/package.json @@ -0,0 +1,46 @@ +{ + "name": "notebook-test", + "displayName": "Notebook Test", + "description": "Notebook test for execution and outputs", + "extensionKind": [ + "ui", + "workspace" + ], + "version": "1.0.0", + "publisher": "vscode", + "icon": "icon.png", + "enableProposedApi": true, + "license": "MIT", + "engines": { + "vscode": "^1.40.0" + }, + "main": "./out/extension", + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "contributes": { + "notebookProvider": [ + { + "viewType": "jupyter", + "displayName": "Jupyter", + "selector": [ + { + "filenamePattern": "*.test.ipynb" + } + ] + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "dependencies": { + "vscode-extension-telemetry": "0.1.1", + "vscode-nls": "^4.0.0", + "typescript": "^3.6.4" + } +} diff --git a/extensions/notebook-test/src/extension.ts b/extensions/notebook-test/src/extension.ts new file mode 100644 index 00000000000..f235e315ebe --- /dev/null +++ b/extensions/notebook-test/src/extension.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { NotebookProvider } from './notebookProvider'; + +export function activate(context: vscode.ExtensionContext) { + console.log(context.extensionPath); + + context.subscriptions.push(vscode.window.registerNotebookProvider('jupyter', new NotebookProvider())); +} + diff --git a/extensions/notebook-test/src/notebookProvider.ts b/extensions/notebook-test/src/notebookProvider.ts new file mode 100644 index 00000000000..227374bae93 --- /dev/null +++ b/extensions/notebook-test/src/notebookProvider.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export class Cell implements vscode.ICell { + public outputs: any[] = []; + + constructor( + public source: string[], + public cell_type: 'markdown' | 'code', + private _outputs: any[] + ) { + + } + + fillInOutputs() { + this.outputs = this._outputs; + } +} + +export class JupyterNotebook implements vscode.INotebook { + constructor( + public metadata: vscode.IMetadata, + public cells: Cell[] + ) { + + } +} + +export class NotebookProvider implements vscode.NotebookProvider { + private _onDidChangeNotebook = new vscode.EventEmitter<{ resource: vscode.Uri; notebook: vscode.INotebook; }>(); + onDidChangeNotebook: vscode.Event<{ resource: vscode.Uri; notebook: vscode.INotebook; }> = this._onDidChangeNotebook.event; + private _notebook: JupyterNotebook; + private _notebooks: Map = new Map(); + + constructor() { + this._notebook = new JupyterNotebook( + { + language_info: { + file_extension: 'ipynb' + } + }, + [ + new Cell([ + '# header\n', + 'body\n' + ], + 'markdown', + [] + ), + new Cell([ + 'print(a)', + ], + 'code', + [ + { + 'output_type': 'stream', + 'name': 'stdout', + 'text': 'hi, stdout\n' + } + ] + ), + new Cell( + [ + 'import time, sys\n', + 'for i in range(8):\n', + ' print(i)\n', + ' time.sleep(0.5)' + ], + 'code', + [ + { + 'name': 'stdout', + 'text': '0\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '1\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '2\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '3\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '4\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '5\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '6\n', + 'output_type': 'stream' + }, + { + 'name': 'stdout', + 'text': '7\n', + 'output_type': 'stream' + } + ] + ), + new Cell( + [ + 'print(a + 4)' + ], + 'code', + [ + { + 'output_type': 'error', + 'ename': 'NameError', + 'evalue': 'name \'a\' is not defined', + 'traceback': [ + '\u001b[0;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)', + '\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m', + '\u001b[0;31mNameError\u001b[0m: name \'a\' is not defined' + ] + } + ] + ) + ] + ); + } + + async resolveNotebook(resource: vscode.Uri): Promise { + if (this._notebooks.has(resource)) { + return this._notebooks.get(resource); + } + + this._notebooks.set(resource, this._notebook); + + return Promise.resolve(this._notebook); + } + + async executeNotebook(resource: vscode.Uri): Promise { + this._notebook.cells.forEach(cell => cell.fillInOutputs()); + + this._onDidChangeNotebook.fire({ resource, notebook: this._notebook }); + return; + } +} \ No newline at end of file diff --git a/extensions/notebook-test/src/typings/ref.d.ts b/extensions/notebook-test/src/typings/ref.d.ts new file mode 100644 index 00000000000..954bab971e3 --- /dev/null +++ b/extensions/notebook-test/src/typings/ref.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// diff --git a/extensions/notebook-test/tsconfig.json b/extensions/notebook-test/tsconfig.json new file mode 100644 index 00000000000..d0797affbad --- /dev/null +++ b/extensions/notebook-test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../shared.tsconfig.json", + "compilerOptions": { + "outDir": "./out", + "experimentalDecorators": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/notebook-test/yarn.lock b/extensions/notebook-test/yarn.lock new file mode 100644 index 00000000000..e79cafaa0b2 --- /dev/null +++ b/extensions/notebook-test/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +applicationinsights@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" + integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg== + dependencies: + diagnostic-channel "0.2.0" + diagnostic-channel-publishers "0.2.1" + zone.js "0.7.6" + +diagnostic-channel-publishers@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" + integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM= + +diagnostic-channel@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" + integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc= + dependencies: + semver "^5.3.0" + +semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== + +vscode-extension-telemetry@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b" + integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA== + dependencies: + applicationinsights "1.0.8" + +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== + +zone.js@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" + integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk= diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 3a1399ee682..92cb6015de9 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1447,6 +1447,85 @@ export interface IWebviewPanelOptions { readonly retainContextWhenHidden?: boolean; } +/** + * @internal + */ + +export interface INotebookSelectors { + readonly filenamePattern?: string; +} + +/** + * @internal + */ +export interface IStreamOutput { + output_type: 'stream'; + text: string; +} + +/** + * @internal + */ +export interface IErrorOutput { + output_type: 'error'; + evalue: string; + traceback: string[]; +} + +/** + * @internal + */ +export interface IDisplayOutput { + output_type: 'display_data'; + data: { string: string }; +} + +/** + * @internal + */ +export interface IGenericOutput { + output_type: string; +} + +/** + * @internal + */ +export type IOutput = IStreamOutput | any; + +/** + * @internal + */ +export interface ICell { + handle: number; + source: string[]; + cell_type: 'markdown' | 'code'; + outputs: IOutput[]; + onDidChangeOutputs?: Event; +} + +/** + * @internal + */ +export interface LanguageInfo { + file_extension: string; +} + +/** + * @internal + */ +export interface IMetadata { + language_info: LanguageInfo; +} + +/** + * @internal + */ +export interface INotebook { + handle: number; + metadata: IMetadata; + cells: ICell[]; + onDidChangeCells?: Event; +} export interface CodeLens { range: IRange; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 19a434638c0..4b21ff75041 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1286,4 +1286,62 @@ declare module 'vscode' { } //#endregion + + //#region Peng: Notebook + + export interface IStreamOutput { + output_type: 'stream'; + text: string; + } + + export interface IErrorOutput { + output_type: 'error'; + evalue: string; + traceback: string[]; + } + + export interface IDisplayOutput { + output_type: 'display_data'; + data: { string: string }; + } + + export interface IGenericOutput { + output_type: string; + } + + export type IOutput = IStreamOutput | any; + + export interface ICell { + source: string[]; + cell_type: 'markdown' | 'code'; + outputs: IOutput[]; + } + + export interface LanguageInfo { + file_extension: string; + } + + export interface IMetadata { + language_info: LanguageInfo; + } + + export interface INotebook { + metadata: IMetadata; + cells: ICell[]; + } + + export interface NotebookProvider { + onDidChangeNotebook?: Event<{ resource: Uri, notebook: INotebook }>; + resolveNotebook(resource: Uri): Promise; + executeNotebook(resource: Uri): Promise; + } + + namespace window { + export function registerNotebookProvider( + notebookType: string, + provider: NotebookProvider + ): Disposable; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 2905c524113..b536a76ea5d 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -54,6 +54,7 @@ import './mainThreadWindow'; import './mainThreadWebview'; import './mainThreadWorkspace'; import './mainThreadComments'; +import './mainThreadNotebook'; import './mainThreadTask'; import './mainThreadLabelService'; import 'vs/workbench/api/common/apiCommands'; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts new file mode 100644 index 00000000000..c3172e15762 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { INotebook, IMetadata, ICell, IOutput } from 'vs/editor/common/modes'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class MainThreadCell implements ICell { + private _onDidChangeOutputs = new Emitter(); + onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + + private _outputs: IOutput[]; + + public get outputs(): IOutput[] { + return this._outputs; + } + + public set outputs(newOutputs: IOutput[]) { + this._outputs = newOutputs; + this._onDidChangeOutputs.fire(); + } + + constructor( + public handle: number, + public source: string[], + public cell_type: 'markdown' | 'code', + outputs: IOutput[] + ) { + this._outputs = outputs; + } +} + +export class MainThreadNotebook implements INotebook { + + private readonly _onDidChangeCells = new Emitter(); + get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + private _mapping: Map = new Map(); + public cells: MainThreadCell[]; + + constructor( + public handle: number, + public metadata: IMetadata, + cells: ICell[] + ) { + this.cells = []; + cells.forEach(cell => { + let mainCell = new MainThreadCell(cell.handle, cell.source, cell.cell_type, cell.outputs); + this._mapping.set(cell.handle, mainCell); + this.cells.push(mainCell); + }); + } + + updateCells(newCells: ICell[]) { + // todo, handle cell insertion and deletion + newCells.forEach(newCell => { + let cell = this._mapping.get(newCell.handle); + if (cell) { + cell.outputs = newCell.outputs; + } + }); + + this._onDidChangeCells.fire(); + } +} + +@extHostNamedCustomer(MainContext.MainThreadNotebook) +export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { + private readonly _notebookProviders = new Map(); + private readonly _proxy: ExtHostNotebookShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookService private _notebookService: INotebookService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); + } + + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): void { + let controller = new MainThreadNotebookController(this); + this._notebookProviders.set(viewType, controller); + this._notebookService.registerNotebookController(viewType, controller); + } + + $unregisterNotebookProvider(viewType: string): void { + this._notebookProviders.delete(viewType); + this._notebookService.unregisterNotebookProvider(viewType); + } + + $updateNotebook(viewType: string, uri: URI, notebook: INotebook): void { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateNotebook(uri, notebook); + } + } + + resolveNotebook(viewType: string, uri: URI): Promise { + return this._proxy.$resolveNotebook(viewType, uri); + } + + executeNotebook(viewType: string, uri: URI): Promise { + return this._proxy.$executeNotebook(viewType, uri); + } +} + +export class MainThreadNotebookController implements IMainNotebookController { + private _mapping: Map = new Map(); + + constructor( + private _mainThreadNotebook: MainThreadNotebooks + ) { + } + + async resolveNotebook(viewType: string, uri: URI): Promise { + let notebook = await this._mainThreadNotebook.resolveNotebook(viewType, uri); + if (notebook) { + let mainthreadNotebook = new MainThreadNotebook(notebook.handle, notebook.metadata, notebook.cells); + this._mapping.set(uri.toString(), mainthreadNotebook); + return mainthreadNotebook; + } + return undefined; + } + + async executeNotebook(viewType: string, uri: URI): Promise { + this._mainThreadNotebook.executeNotebook(viewType, uri); + } + + updateNotebook(uri: URI, notebook: INotebook): void { + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + + if (mainthreadNotebook) { + mainthreadNotebook.updateCells(notebook.cells); + } + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ea5ad7991fa..27b473b210f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -67,6 +67,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -123,6 +124,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -550,6 +552,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createInputBox(): vscode.InputBox { return extHostQuickOpen.createInputBox(extension.identifier); + }, + registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => { + return extHostNotebook.registerNotebookProvider(extension, viewType, provider); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3dab81c9c55..957149605f3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -556,6 +556,11 @@ export interface WebviewExtensionDescription { readonly location: UriComponents; } +export interface NotebookExtensionDescription { + readonly id: ExtensionIdentifier; + readonly location: UriComponents; +} + export enum WebviewEditorCapabilities { Editable, } @@ -606,6 +611,12 @@ export interface ExtHostWebviewsShape { $onSaveAs(handle: WebviewPanelHandle, resource: UriComponents, targetResource: UriComponents): Promise; } +export interface MainThreadNotebookShape extends IDisposable { + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): void; + $unregisterNotebookProvider(viewType: string): void; + $updateNotebook(viewType: string, resource: UriComponents, notebook: modes.INotebook): void; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1387,6 +1398,11 @@ export interface ExtHostCommentsShape { $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; } +export interface ExtHostNotebookShape { + $resolveNotebook(viewType: string, uri: URI): Promise; + $executeNotebook(viewType: string, uri: URI): Promise; +} + export interface ExtHostStorageShape { $acceptValue(shared: boolean, key: string, value: object | undefined): void; } @@ -1431,7 +1447,8 @@ export const MainContext = { MainThreadSearch: createMainId('MainThreadSearch'), MainThreadTask: createMainId('MainThreadTask'), MainThreadWindow: createMainId('MainThreadWindow'), - MainThreadLabelService: createMainId('MainThreadLabelService') + MainThreadLabelService: createMainId('MainThreadLabelService'), + MainThreadNotebook: createMainId('MainThreadNotebook') }; export const ExtHostContext = { @@ -1465,5 +1482,6 @@ export const ExtHostContext = { ExtHostStorage: createMainId('ExtHostStorage'), ExtHostUrls: createExtId('ExtHostUrls'), ExtHostOutputService: createMainId('ExtHostOutputService'), - ExtHosLabelService: createMainId('ExtHostLabelService') + ExtHosLabelService: createMainId('ExtHostLabelService'), + ExtHostNotebook: createMainId('ExtHostNotebook') }; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts new file mode 100644 index 00000000000..2a8cf310204 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ExtHostNotebookShape, IMainContext, MainThreadNotebookShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Disposable as VSCodeDisposable } from './extHostTypes'; +import { INotebook } from 'vs/editor/common/modes'; +import { URI } from 'vs/base/common/uri'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export class ExtHostCell implements vscode.ICell { + private static _handlePool: number = 0; + readonly handle = ExtHostCell._handlePool++; + + source: string[]; + cell_type: 'markdown' | 'code'; + outputs: any[]; + + constructor( + raw_cell: vscode.ICell + ) { + this.source = raw_cell.source; + this.cell_type = raw_cell.cell_type; + this.outputs = raw_cell.outputs; + } +} + +export class ExtHostNotebook implements vscode.INotebook { + private static _handlePool: number = 0; + readonly handle = ExtHostNotebook._handlePool++; + public cells: ExtHostCell[]; + public cellMapping: Map = new Map(); + + constructor( + public metadata: vscode.IMetadata, + private raw_cells: vscode.ICell[] + ) { + this.cells = raw_cells.map(cell => { + let extHostCell = new ExtHostCell(cell); + this.cellMapping.set(cell, extHostCell); + return extHostCell; + }); + } + + updateCells(newCells: vscode.ICell[]) { + newCells.forEach(cell => { + let extHostCell = this.cellMapping.get(cell); + + if (extHostCell) { + extHostCell.outputs = cell.outputs; + } + }); + + // trigger update + } +} + +export class ExtHostNotebookController implements ExtHostNotebookShape { + private readonly _proxy: MainThreadNotebookShape; + private readonly _notebookProviders = new Map(); + private readonly _notebookCache = new Map(); + private readonly _localStore = new DisposableStore(); + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + } + + public registerNotebookProvider( + extension: IExtensionDescription, + viewType: string, + provider: vscode.NotebookProvider, + ): vscode.Disposable { + + if (this._notebookProviders.has(viewType)) { + throw new Error(`Notebook provider for '${viewType}' already registered`); + } + + this._notebookProviders.set(viewType, { extension, provider }); + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType); + + if (provider.onDidChangeNotebook) { + this._localStore.add(provider.onDidChangeNotebook!((e) => { + let resource = e.resource; + let notebook = e.notebook; + + let cachedNotebook = this._notebookCache.get(resource.toString()); + + if (cachedNotebook) { + cachedNotebook.updateCells(notebook.cells); + + this._proxy.$updateNotebook(viewType, resource, cachedNotebook); + } + })); + } + + return new VSCodeDisposable(() => { + this._notebookProviders.delete(viewType); + this._proxy.$unregisterNotebookProvider(viewType); + }); + } + + async $resolveNotebook(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + let notebook = await provider.provider.resolveNotebook(uri); + if (notebook) { + let extHostNotebook = new ExtHostNotebook(notebook.metadata, notebook.cells); + this._notebookCache.set(uri.toString(), extHostNotebook); + return extHostNotebook; + } + } + + return Promise.resolve(undefined); + } + + async $executeNotebook(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.provider.executeNotebook(uri); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts index a16cbdbdfc4..dd396ca3b6d 100644 --- a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts @@ -6,7 +6,6 @@ import 'vs/css!./notebook'; import * as DOM from 'vs/base/browser/dom'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ICell } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -34,6 +33,7 @@ import { Emitter } from 'vs/base/common/event'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import * as UUID from 'vs/base/common/uuid'; +import { ICell } from 'vs/editor/common/modes'; export class ViewCell { private _textModel: ITextModel | null = null; @@ -47,6 +47,9 @@ export class ViewCell { protected readonly _onDidChangeEditingState = new Emitter(); readonly onDidChangeEditingState = this._onDidChangeEditingState.event; + protected readonly _onDidChangeOutputs = new Emitter(); + readonly onDidChangeOutputs = this._onDidChangeOutputs.event; + get cellType() { return this.cell.cell_type; } @@ -77,6 +80,11 @@ export class ViewCell { private readonly modeService: IModeService ) { this.id = UUID.generateUuid(); + if (this.cell.onDidChangeOutputs) { + this.cell.onDidChangeOutputs(() => { + this._onDidChangeOutputs.fire(); + }); + } } hasDynamicHeight() { @@ -563,7 +571,54 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.showContextMenu(element, e.posx, top + height); }); - this.disposables.set(templateData.cellContainer, listener); + let rerenderOutput = element.onDidChangeOutputs(() => { + if (element.outputs.length > 0) { + let hasDynamicHeight = true; + for (let i = 0; i < element.outputs.length; i++) { + let result = MimeTypeRenderer.render(element.outputs[i], this.themeService, this.webviewService); + if (result) { + hasDynamicHeight = hasDynamicHeight || result?.hasDynamicHeight; + templateData.outputContainer?.appendChild(result.element); + if (result.shadowContent) { + this.handler.createContentWidget(element, result.shadowContent, totalHeight + 8); + } + } + } + + if (height !== undefined && hasDynamicHeight) { + let dimensions = DOM.getClientArea(templateData.outputContainer!); + const elementSizeObserver = new ElementSizeObserver(templateData.outputContainer!, dimensions, () => { + if (templateData.outputContainer && document.body.contains(templateData.outputContainer!)) { + let height = elementSizeObserver.getHeight(); + if (dimensions.height !== height) { + element.setDynamicHeight(totalHeight + 32 + height); + this.handler.layoutElement(element, totalHeight + 32 + height); + } + + elementSizeObserver.dispose(); + } + }); + elementSizeObserver.startObserving(); + if (!hasDynamicHeight && dimensions.height !== 0) { + element.setDynamicHeight(totalHeight + 32 + dimensions.height); + this.handler.layoutElement(element, totalHeight + 32 + dimensions.height); + } + + this.disposables.set(templateData.cellContainer, { + dispose: () => { + elementSizeObserver.dispose(); + } + }); + } + } + }); + + this.disposables.set(templateData.cellContainer, { + dispose: () => { + listener.dispose(); + rerenderOutput.dispose(); + } + }); if (templateData.outputContainer) { templateData.outputContainer!.innerHTML = ''; diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts new file mode 100644 index 00000000000..57237791ecc --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import * as nls from 'vs/nls'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; +import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookProvider'; + +namespace NotebookEditorContribution { + export const viewType = 'viewType'; + export const displayName = 'displayName'; + export const selector = 'selector'; +} + + +interface INotebookEditorContribution { + readonly [NotebookEditorContribution.viewType]: string; + readonly [NotebookEditorContribution.displayName]: string; + readonly [NotebookEditorContribution.selector]?: readonly NotebookSelector[]; +} + +const notebookContribution: IJSONSchema = { + description: nls.localize('contributes.notebook', 'Contributes notebook.'), + type: 'array', + defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], + items: { + type: 'object', + required: [ + NotebookEditorContribution.viewType, + NotebookEditorContribution.displayName, + NotebookEditorContribution.selector, + ], + properties: { + [NotebookEditorContribution.viewType]: { + type: 'string', + description: nls.localize('contributes.notebook.viewType', 'Unique identifier of the notebook.'), + }, + [NotebookEditorContribution.displayName]: { + type: 'string', + description: nls.localize('contributes.notebook.displayName', 'Human readable name of the notebook.'), + }, + [NotebookEditorContribution.selector]: { + type: 'array', + description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'), + items: { + type: 'object', + properties: { + filenamePattern: { + type: 'string', + description: nls.localize('contributes.notebook.selector.filenamePattern', 'Glob that the notebook is enabled for.'), + }, + } + } + } + } + } +}; + +export const notebookExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'notebookProvider', + deps: [languagesExtPoint], + jsonSchema: notebookContribution +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 720613fe8ba..40ff2cfa622 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -16,6 +16,12 @@ import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/ed import { endsWith } from 'vs/base/common/strings'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { INotebookService, NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; +import { IActiveCodeEditor, isDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Action } from 'vs/base/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -28,39 +34,112 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); + +export class ExecuteNotebookAction extends Action { + + static readonly ID = 'workbench.action.executeNotebook'; + static readonly LABEL = 'Execute Notebook'; + + constructor( + id: string, + label: string, + @IEditorService private readonly editorService: IEditorService, + @INotebookService private readonly notebookService: INotebookService + + ) { + super(id, label); + } + + async run(): Promise { + let resource = this.editorService.activeEditor?.getResource(); + + if (resource) { + let notebookProviders = this.notebookService.getContributedNotebook(resource!); + + if (notebookProviders.length > 0) { + let viewType = notebookProviders[0].id; + this.notebookService.executeNotebook(viewType, resource); + } + } + } +} + export class NotebookContribution implements IWorkbenchContribution { private _resourceMapping: Map = new Map(); constructor( @IEditorService private readonly editorService: IEditorService, + @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group)); + + this.registerCommands(); } private onEditorOpening(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { const resource = editor.getResource(); + let viewType: string | undefined = undefined; - if ( - !resource || - !endsWith(resource.path, '.ipynb') - ) { - return undefined; + if (resource) { + let notebookProviders = this.notebookService.getContributedNotebook(resource!); + + if (notebookProviders.length > 0) { + viewType = notebookProviders[0].id; + } } - if (this._resourceMapping.has(resource.path)) { - const input = this._resourceMapping.get(resource.path); + if (viewType === undefined) { + if ( + !resource || + !endsWith(resource.path, '.ipynb') + ) { + return undefined; + } + } + + if (this._resourceMapping.has(resource!.path)) { + const input = this._resourceMapping.get(resource!.path); return { override: this.editorService.openEditor(input!, options, group) }; } - const input = this.instantiationService.createInstance(NotebookEditorInput, editor); - this._resourceMapping.set(resource.path, input); + + const input = this.instantiationService.createInstance(NotebookEditorInput, editor, viewType); + this._resourceMapping.set(resource!.path, input); return { override: this.editorService.openEditor(input, { ...options, ignoreOverrides: true }, group) }; } + + private registerCommands() { + const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); + + workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ExecuteNotebookAction, ExecuteNotebookAction.ID, ExecuteNotebookAction.LABEL), 'Execute Notebook', 'Notebook'); + } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); + + +registerSingleton(INotebookService, NotebookService); + + +export function getActiveEditor(editorService: IEditorService): IActiveCodeEditor | null { + let activeTextEditorWidget = editorService.activeTextEditorWidget; + + if (isDiffEditor(activeTextEditorWidget)) { + if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) { + activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor(); + } else { + activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor(); + } + } + + if (!isCodeEditor(activeTextEditorWidget) || !activeTextEditorWidget.hasModel()) { + return null; + } + + return activeTextEditorWidget; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 25227033cda..d91ba5ff578 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -191,6 +191,10 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { } this.model = model; + this.localStore.add(this.model.onDidChangeCells(() => { + this.updateViewCells(); + })); + this.viewCells = model.getNotebook().cells.map(cell => { return new ViewCell(cell, false, this.modelService, this.modeService); }); @@ -243,8 +247,13 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { }, 0); } + updateViewCells() { + this.list?.rerender(); + } + insertEmptyNotebookCell(cell: ViewCell, type: 'code' | 'markdown', direction: 'above' | 'below') { let newCell = new ViewCell({ + handle: -1, cell_type: type, source: [], outputs: [] diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index fcc278a19e2..3747ea0cc41 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -9,59 +9,30 @@ import { ITextModel } from 'vs/editor/common/model'; import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { Emitter } from 'vs/base/common/event'; - -export interface IStreamOutput { - output_type: 'stream'; - text: string; -} - -export interface IErrorOutput { - output_type: 'error'; - evalue: string; - traceback: string[]; -} - -export interface IDisplayOutput { - output_type: 'display_data'; - data: { string: string }; -} - -export interface IGenericOutput { - output_type: string; -} - -export type IOutput = IStreamOutput | any; - -export interface ICell { - source: string[]; - cell_type: 'markdown' | 'code'; - outputs: IOutput[]; -} - -export interface LanguageInfo { - file_extension: string; -} -export interface IMetadata { - language_info: LanguageInfo; -} -export interface INotebook { - metadata: IMetadata; - cells: ICell[]; -} - +import { Emitter, Event } from 'vs/base/common/event'; +import { INotebook, ICell } from 'vs/editor/common/modes'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; export class NotebookEditorModel extends EditorModel { - private _notebook: INotebook | undefined; private _dirty = false; protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; + private readonly _onDidChangeCells = new Emitter(); + get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + constructor( - public readonly textModel: ITextModel + public readonly textModel: ITextModel, + private _notebook: INotebook | undefined ) { super(); + + if (_notebook && _notebook.onDidChangeCells) { + this._register(_notebook.onDidChangeCells(() => { + this._onDidChangeCells.fire(); + })); + } } isDirty() { @@ -115,6 +86,8 @@ export class NotebookEditorInput extends EditorInput { constructor( public readonly editorInput: IEditorInput, + public readonly viewType: string | undefined, + @INotebookService private readonly notebookService: INotebookService, @ITextFileService private readonly textFileService: ITextFileService, @ITextModelService private readonly textModelResolverService: ITextModelService ) { @@ -142,13 +115,22 @@ export class NotebookEditorInput extends EditorInput { return Promise.resolve(true); } + getResource() { + return this.editorInput.getResource(); + } + resolve(): Promise { if (!this.promise) { this.promise = this.textModelResolverService.createModelReference(this.editorInput.getResource()!) - .then(ref => { + .then(async ref => { const textModel = ref.object.textEditorModel; - this.textModel = new NotebookEditorModel(textModel); + let notebook: INotebook | undefined = undefined; + if (this.viewType !== undefined) { + notebook = await this.notebookService.resolveNotebook(this.viewType, this.editorInput.getResource()!); + } + + this.textModel = new NotebookEditorModel(textModel, notebook); this.textModel.onDidChangeDirty(() => this._onDidChangeDirty.fire()); return this.textModel; }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts new file mode 100644 index 00000000000..fd53a249c99 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebook } from 'vs/editor/common/modes'; +import { URI } from 'vs/base/common/uri'; +import { notebookExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; + +export const INotebookService = createDecorator('notebookService'); + +export interface IMainNotebookController { + resolveNotebook(viewType: string, uri: URI): Promise; + executeNotebook(viewType: string, uri: URI): Promise; + updateNotebook(uri: URI, notebook: INotebook): void; +} + +export interface INotebookService { + _serviceBrand: undefined; + registerNotebookController(viewType: string, controller: IMainNotebookController): void; + unregisterNotebookProvider(viewType: string): void; + resolveNotebook(viewType: string, uri: URI): Promise; + executeNotebook(viewType: string, uri: URI): Promise; + getContributedNotebook(resource: URI): readonly NotebookProviderInfo[]; +} + +export class NotebookInfoStore { + private readonly contributedEditors = new Map(); + + public clear() { + this.contributedEditors.clear(); + } + + public get(viewType: string): NotebookProviderInfo | undefined { + return this.contributedEditors.get(viewType); + } + + public add(info: NotebookProviderInfo): void { + if (this.contributedEditors.has(info.id)) { + console.log(`Custom editor with id '${info.id}' already registered`); + return; + } + this.contributedEditors.set(info.id, info); + } + + public getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] { + return Array.from(this.contributedEditors.values()).filter(customEditor => + customEditor.matches(resource)); + } +} + + +export class NotebookService extends Disposable implements INotebookService { + _serviceBrand: undefined; + private readonly _notebookProviders = new Map(); + public notebookProviderInfoStore: NotebookInfoStore = new NotebookInfoStore(); + + constructor() { + super(); + + notebookExtensionPoint.setHandler((extensions) => { + this.notebookProviderInfoStore.clear(); + + for (const extension of extensions) { + for (const notebookContribution of extension.value) { + this.notebookProviderInfoStore.add(new NotebookProviderInfo({ + id: notebookContribution.viewType, + displayName: notebookContribution.displayName, + selector: notebookContribution.selector || [], + })); + } + } + // console.log(this._notebookProviderInfoStore); + }); + + } + + registerNotebookController(viewType: string, controller: IMainNotebookController) { + this._notebookProviders.set(viewType, controller); + } + + unregisterNotebookProvider(viewType: string): void { + this._notebookProviders.delete(viewType); + } + + resolveNotebook(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.resolveNotebook(viewType, uri); + } + + return Promise.resolve(undefined); + } + + async executeNotebook(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.executeNotebook(viewType, uri); + } + + return; + } + + public getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] { + return this.notebookProviderInfoStore.getContributedNotebook(resource); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts index f2cbd9ad0a7..165a9fb25b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts @@ -7,10 +7,10 @@ import * as DOM from 'vs/base/browser/dom'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { RGBA, Color } from 'vs/base/common/color'; import { ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; -import { IOutput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { isArray } from 'vs/base/common/types'; import { NotebookHandler } from 'vs/workbench/contrib/notebook/browser/cellRenderer'; +import { IOutput } from 'vs/editor/common/modes'; export function registerMineTypeRenderer(types: string[], renderer: IMimeRenderer) { types.forEach(type => { diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts new file mode 100644 index 00000000000..3a2f2bf15ee --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as glob from 'vs/base/common/glob'; +import { URI } from 'vs/base/common/uri'; +import { basename } from 'vs/base/common/resources'; + +export interface NotebookSelector { + readonly filenamePattern?: string; +} + +export class NotebookProviderInfo { + + public readonly id: string; + public readonly displayName: string; + public readonly selector: readonly NotebookSelector[]; + + constructor(descriptor: { + readonly id: string; + readonly displayName: string; + readonly selector: readonly NotebookSelector[]; + }) { + this.id = descriptor.id; + this.displayName = descriptor.displayName; + this.selector = descriptor.selector; + } + + matches(resource: URI): boolean { + return this.selector.some(selector => NotebookProviderInfo.selectorMatches(selector, resource)); + } + + static selectorMatches(selector: NotebookSelector, resource: URI): boolean { + if (selector.filenamePattern) { + if (glob.match(selector.filenamePattern.toLowerCase(), basename(resource).toLowerCase())) { + return true; + } + } + return false; + } +}