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,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<boolean> {
return this.editor.save();
}
public matches(input: NotebookInput): boolean {
if (!input) {
return false;
}
return input === this.editor.notebookParams.input;
}
}
function wait(timeMs: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, timeMs));
}
namespace mapset {
export function setValues<T>(set: Set<T>): T[] {
// return Array.from(set);
let ret: T[] = [];
set.forEach(v => ret.push(v));
return ret;
}
export function mapValues<T>(map: Map<any, T>): T[] {
// return Array.from(map.values());
let ret: T[] = [];
map.forEach(v => ret.push(v));
return ret;
}
}
namespace delta {
export function ofSets<T>(before: Set<T>, after: Set<T>): { 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<K, V>(before: Map<K, V>, after: Map<K, V>): { 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<string, INotebookEditor>,
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<string, INotebookEditor>();
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<string, MainThreadNotebookEditor>();
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<boolean> {
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<string> {
return TPromise.wrap(this.doOpenEditor(resource, options));
}
//#endregion
private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise<string> {
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<string> {
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;
}
}