Notebook extensibility: Move New Notebook and configuration to an extension (#3382)

initial support for Notebook extensibility. Fixes #3148 , Fixes #3382.

## Design notes
The extensibility patterns are modeled after the VSCode Document and Editor APIs but need to be different since core editor concepts are different - for example Notebooks have cells, and cells have contents rather than editors which have text lines.

Most importantly, a lot of the code is based on the MainThreadDocumentsAndEditors class, with some related classes (the MainThreadDocuments, and MainThreadEditors) brought in too. Given our current limitations I felt moving to add 3 full sets of extension host API classes was overkill so am currently using one. Will see if we need to change this in the future based on what we add in the additional APIs

## Limitations
The current implementation is limited to visible editors, rather than all documents in the workspace. We are not following the `openDocument` -> `showDocument` pattern, but instead just supporting `showDocument` directly.

## Changes in this PR
- Renamed existing APIs to make clear that they were about notebook contents, not about notebook behavior
- Added new APIs for querying notebook documents and editors 
- Added new API for opening a notebook
- Moved `New Notebook` command to an extension, and added an `Open Notebook` command too
- Moved notebook feature flag to the extension

## Not covered in this PR
- Need to actually implement support for defining the provider and connection IDs for a notebook. this will be important to support New Notebook from a big data connection in Object Explorer
- Need to add APIs for adding cells, to support 
- Need to implement the metadata for getting full notebook contents. I've only implemented to key APIs needed to make this all work.
This commit is contained in:
Kevin Cunnane
2018-12-03 18:50:44 -08:00
committed by GitHub
parent cb162b16f2
commit cac8cc99e1
33 changed files with 1286 additions and 148 deletions

View File

@@ -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<boolean> {
if (this._isDisposed) {
return TPromise.wrapError<boolean>(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<string, ExtHostNotebookEditor>();
private readonly _documents = new Map<string, ExtHostNotebookDocumentData>();
private readonly _onDidAddDocuments = new Emitter<ExtHostNotebookDocumentData[]>();
private readonly _onDidRemoveDocuments = new Emitter<ExtHostNotebookDocumentData[]>();
private readonly _onDidChangeVisibleNotebookEditors = new Emitter<ExtHostNotebookEditor[]>();
private readonly _onDidChangeActiveNotebookEditor = new Emitter<ExtHostNotebookEditor>();
readonly onDidAddDocuments: Event<ExtHostNotebookDocumentData[]> = this._onDidAddDocuments.event;
readonly onDidRemoveDocuments: Event<ExtHostNotebookDocumentData[]> = this._onDidRemoveDocuments.event;
readonly onDidChangeVisibleNotebookEditors: Event<ExtHostNotebookEditor[]> = this._onDidChangeVisibleNotebookEditors.event;
readonly onDidChangeActiveNotebookEditor: Event<ExtHostNotebookEditor> = 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<sqlops.nb.NotebookEditor> {
return this.doShowNotebookDocument(uri, showOptions);
}
private async doShowNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions): Promise<sqlops.nb.NotebookEditor> {
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
}