From d62e809c18eecdbd26471945422a554c1d62a162 Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Wed, 12 Sep 2018 14:35:19 -0700 Subject: [PATCH] Support isDirty flag for model view editors and begin plumb through of save support (#2547) * Add dirty and save support to model view * Add issue # for a TODO --- .../modelEditor/modelViewInput.ts | 73 +++++++++++++++++-- src/sql/sqlops.proposed.d.ts | 16 ++++ .../api/node/extHostModelViewDialog.ts | 32 +++++++- .../api/node/mainThreadModelViewDialog.ts | 32 +++++++- .../workbench/api/node/sqlExtHost.protocol.ts | 4 +- 5 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/sql/parts/modelComponents/modelEditor/modelViewInput.ts b/src/sql/parts/modelComponents/modelEditor/modelViewInput.ts index 27da41925f..fc56251d4b 100644 --- a/src/sql/parts/modelComponents/modelEditor/modelViewInput.ts +++ b/src/sql/parts/modelComponents/modelEditor/modelViewInput.ts @@ -3,16 +3,50 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as sqlops from 'sqlops'; + import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { DialogPane } from 'sql/platform/dialog/dialogPane'; +import { Emitter, Event } from 'vs/base/common/event'; -import * as sqlops from 'sqlops'; +export type ModeViewSaveHandler = (handle: number) => Thenable; + +export class ModelViewInputModel extends EditorModel { + private dirty: boolean; + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } + + constructor(public readonly modelViewId, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { + super(); + this.dirty = false; + } + + get isDirty(): boolean { + return this.dirty; + } + + public setDirty(dirty: boolean): void { + if (this.dirty === dirty) { + return; + } + + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + + save(): TPromise { + if (this.saveHandler) { + return TPromise.wrap(this.saveHandler(this.handle)); + } + return TPromise.wrap(true); + } +} export class ModelViewInput extends EditorInput { public static ID: string = 'workbench.editorinputs.ModelViewEditorInput'; @@ -20,14 +54,15 @@ export class ModelViewInput extends EditorInput { private _dialogPaneContainer: HTMLElement; private _dialogPane: DialogPane; - constructor(private _title: string, private _modelViewId: string, + constructor(private _title: string, private _model: ModelViewInputModel, private _options: sqlops.ModelViewEditorOptions, @IInstantiationService private _instantiationService: IInstantiationService, @IPartService private readonly _partService: IPartService ) { super(); + this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire()); this._container = document.createElement('div'); - this._container.id = `modelView-${_modelViewId}`; + this._container.id = `modelView-${_model.modelViewId}`; this._partService.getContainer(Parts.EDITOR_PART).appendChild(this._container); } @@ -37,7 +72,7 @@ export class ModelViewInput extends EditorInput { } public get modelViewId(): string { - return this._modelViewId; + return this._model.modelViewId; } public getTypeId(): string { @@ -85,6 +120,31 @@ export class ModelViewInput extends EditorInput { return this._options; } + /** + * An editor that is dirty will be asked to be saved once it closes. + */ + isDirty(): boolean { + return this._model.isDirty; + } + + /** + * Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result. + */ + confirmSave(): TPromise { + // TODO #2530 support save on close / confirm save. This is significantly more work + // as we need to either integrate with textFileService (seems like this isn't viable) + // or register our own complimentary service that handles the lifecycle operations such + // as close all, auto save etc. + return TPromise.wrap(ConfirmResult.DONT_SAVE); + } + + /** + * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. + */ + save(): TPromise { + return this._model.save(); + } + public dispose(): void { if (this._dialogPane) { this._dialogPane.dispose(); @@ -93,6 +153,9 @@ export class ModelViewInput extends EditorInput { this._container.remove(); this._container = undefined; } + if (this._model) { + this._model.dispose(); + } super.dispose(); } } diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 295f63d750..c13005f2de 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1115,11 +1115,22 @@ declare module 'sqlops' { export function createModelViewEditor(title: string, options?: ModelViewEditorOptions): ModelViewEditor; export interface ModelViewEditor extends window.modelviewdialog.ModelViewPanel { + /** + * `true` if there are unpersisted changes. + * This is editable to support extensions updating the dirty status. + */ + isDirty: boolean; /** * Opens the editor */ openEditor(position?: vscode.ViewColumn): Thenable; + + /** + * Registers a save handler for this editor. This will be called if [supportsSave](#ModelViewEditorOptions.supportsSave) + * is set to true and the editor is marked as dirty + */ + registerSaveHandler(handler: () => Thenable); } } @@ -1128,6 +1139,11 @@ declare module 'sqlops' { * Should the model view editor's context be kept around even when the editor is no longer visible? It is false by default */ readonly retainContextWhenHidden?: boolean; + + /** + * Does this model view editor support save? + */ + readonly supportsSave?: boolean; } export enum DataProviderType { diff --git a/src/sql/workbench/api/node/extHostModelViewDialog.ts b/src/sql/workbench/api/node/extHostModelViewDialog.ts index 0a9d1941da..29fbb39b07 100644 --- a/src/sql/workbench/api/node/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/node/extHostModelViewDialog.ts @@ -75,6 +75,9 @@ class ModelViewPanelImpl implements sqlops.window.modelviewdialog.ModelViewPanel } class ModelViewEditorImpl extends ModelViewPanelImpl implements sqlops.workspace.ModelViewEditor { + private _isDirty: boolean; + private _saveHandler: () => Thenable; + constructor( extHostModelViewDialog: ExtHostModelViewDialog, extHostModelView: ExtHostModelViewShape, @@ -84,10 +87,32 @@ class ModelViewEditorImpl extends ModelViewPanelImpl implements sqlops.workspace private _options: sqlops.ModelViewEditorOptions ) { super('modelViewEditor', extHostModelViewDialog, extHostModelView, extensionLocation); + this._isDirty = false; } public openEditor(position?: vscode.ViewColumn): Thenable { - return this._proxy.$openEditor(this._modelViewId, this._title, this._options, position); + return this._proxy.$openEditor(this.handle, this._modelViewId, this._title, this._options, position); + } + + public get isDirty(): boolean { + return this._isDirty; + } + + public set isDirty(value: boolean) { + this._isDirty = value; + this._proxy.$setDirty(this.handle, value); + } + + registerSaveHandler(handler: () => Thenable) { + this._saveHandler = handler; + } + + public handleSave(): Thenable { + if (this._saveHandler) { + return Promise.resolve(this._saveHandler()); + } else { + return Promise.resolve(true); + } } } @@ -470,6 +495,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { return dialog.validateClose(); } + public $handleSave(handle: number): Thenable { + let editor = this._objectsByHandle.get(handle) as ModelViewEditorImpl; + return editor.handleSave(); + } + public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { let handle = this.getHandle(dialog); this.updateDialogContent(dialog); diff --git a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts index b0b69042f4..68ff5d16f0 100644 --- a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts @@ -6,6 +6,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditor } from 'vs/workbench/common/editor'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -14,7 +15,7 @@ import { MainThreadModelViewDialogShape, SqlMainContext, ExtHostModelViewDialogS import { Dialog, DialogTab, DialogButton, WizardPage, Wizard } from 'sql/platform/dialog/dialogTypes'; import { CustomDialogService } from 'sql/platform/dialog/customDialogService'; import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, IModelViewWizardPageDetails, IModelViewWizardDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; -import { ModelViewInput } from 'sql/parts/modelComponents/modelEditor/modelViewInput'; +import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/parts/modelComponents/modelEditor/modelViewInput'; import * as vscode from 'vscode'; import * as sqlops from 'sqlops'; @@ -28,6 +29,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape private readonly _wizardPages = new Map(); private readonly _wizardPageHandles = new Map(); private readonly _wizards = new Map(); + private readonly _editorInputModels = new Map(); private _dialogService: CustomDialogService; constructor( @@ -43,15 +45,18 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape throw new Error('Method not implemented.'); } - public $openEditor(modelViewId: string, title: string, options?: sqlops.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable { + public $openEditor(handle: number, modelViewId: string, title: string, options?: sqlops.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable { return new Promise((resolve, reject) => { - let input = this._instatiationService.createInstance(ModelViewInput, title, modelViewId, options); + let saveHandler: ModeViewSaveHandler = options && options.supportsSave ? (h) => this.handleSave(h) : undefined; + let model = new ModelViewInputModel(modelViewId, handle, saveHandler); + let input = this._instatiationService.createInstance(ModelViewInput, title, model, options); let editorOptions = { preserveFocus: true, pinned: true }; - this._editorService.openEditor(input, editorOptions, position as any).then(() => { + this._editorService.openEditor(input, editorOptions, position as any).then((editor) => { + this._editorInputModels.set(handle, model); resolve(); }, error => { reject(error); @@ -59,6 +64,10 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape }); } + private handleSave(handle: number): Thenable { + return this._proxy.$handleSave(handle); + } + public $openDialog(handle: number): Thenable { let dialog = this.getDialog(handle); this._dialogService.showDialog(dialog); @@ -213,6 +222,21 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape return Promise.resolve(); } + $setDirty(handle: number, isDirty: boolean): void { + let model = this.getEditor(handle); + if (model) { + model.setDirty(isDirty); + } + } + + private getEditor(handle: number): ModelViewInputModel { + let model = this._editorInputModels.get(handle); + if (!model) { + throw new Error('No editor matching the given handle'); + } + return model; + } + private getDialog(handle: number): Dialog { let dialog = this._dialogs.get(handle); if (!dialog) { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 1ac756f739..d80cc1a787 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -672,10 +672,11 @@ export interface ExtHostModelViewDialogShape { $updateWizardPageInfo(handle: number, pageHandles: number[], currentPageIndex: number): void; $validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable; $validateDialogClose(handle: number): Thenable; + $handleSave(handle: number): Thenable; } export interface MainThreadModelViewDialogShape extends IDisposable { - $openEditor(modelViewId: string, title: string, options?: sqlops.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable; + $openEditor(handle: number, modelViewId: string, title: string, options?: sqlops.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable; $openDialog(handle: number): Thenable; $closeDialog(handle: number): Thenable; $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable; @@ -688,6 +689,7 @@ export interface MainThreadModelViewDialogShape extends IDisposable { $addWizardPage(wizardHandle: number, pageHandle: number, pageIndex: number): Thenable; $removeWizardPage(wizardHandle: number, pageIndex: number): Thenable; $setWizardPage(wizardHandle: number, pageIndex: number): Thenable; + $setDirty(handle: number, isDirty: boolean): void; } export interface ExtHostQueryEditorShape { }