/*--------------------------------------------------------------------------------------------- * 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 URI, { UriComponents } from 'vs/base/common/uri'; import { IExtHostContext, IUndoStopOptions } 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 { Schemas } from 'vs/base/common/network'; 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 } from 'sql/services/notebook/notebookService'; import { TPromise } from 'vs/base/common/winjs.base'; import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; import { disposed } from 'vs/base/common/errors'; 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; } public applyEdits(versionIdCheck: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): boolean { // TODO Handle version tracking // if (this._model.getVersionId() !== versionIdCheck) { // // throw new Error('Model has changed in the meantime!'); // // model changed in the meantime // return false; // } if (!this.editor) { // console.warn('applyEdits on invisible editor'); return false; } // TODO handle undo tracking // if (opts.undoStopBefore) { // this._codeEditor.pushUndoStop(); // } this.editor.executeEdits(edits); // if (opts.undoStopAfter) { // this._codeEditor.pushUndoStop(); // } return true; } } 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)); } $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): TPromise { let editor = this.getEditor(id); if (!editor) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } return TPromise.as(editor.applyEdits(modelVersionId, edits, opts)); } //#endregion private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise { const uri = URI.revive(resource); const editorOptions: ITextEditorOptions = { preserveFocus: options.preserveFocus, pinned: !options.preview }; let trusted = uri.scheme === Schemas.untitled; let model = new NotebookInputModel(uri, undefined, trusted, 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; } }