Skip to content

Commit

Permalink
custom editors - implement auto save and backup creation over working…
Browse files Browse the repository at this point in the history
… copy events (#84672)
  • Loading branch information
bpasero committed Jan 12, 2020
1 parent 71df4b2 commit e6651b9
Show file tree
Hide file tree
Showing 27 changed files with 720 additions and 537 deletions.
2 changes: 0 additions & 2 deletions src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,8 +787,6 @@ export const HotExitConfiguration = {
ON_EXIT_AND_WINDOW_CLOSE: 'onExitAndWindowClose'
};

export const CONTENT_CHANGE_EVENT_BUFFER_DELAY = 1000;

export const FILES_ASSOCIATIONS_CONFIG = 'files.associations';
export const FILES_EXCLUDE_CONFIG = 'files.exclude';

Expand Down
73 changes: 55 additions & 18 deletions src/vs/workbench/browser/parts/editor/editorAutoSave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
this._register(this.hostService.onDidChangeFocus(focused => this.onWindowFocusChange(focused)));
this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange()));
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.onAutoSaveConfigurationChange(config, true)));
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.onDidWorkingCopyChangeDirty(workingCopy)));

// Working Copy events
this._register(this.workingCopyService.onDidRegister(c => this.onDidRegister(c)));
this._register(this.workingCopyService.onDidUnregister(c => this.onDidUnregister(c)));
this._register(this.workingCopyService.onDidChangeDirty(c => this.onDidChangeDirty(c)));
this._register(this.workingCopyService.onDidChangeContent(c => this.onDidChangeContent(c)));
}

private onWindowFocusChange(focused: boolean): void {
Expand Down Expand Up @@ -128,14 +133,34 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
}

private saveAllDirty(options?: ISaveOptions): void {
Promise.all(this.workingCopyService.workingCopies.map(workingCopy => {
for (const workingCopy of this.workingCopyService.workingCopies) {
if (workingCopy.isDirty() && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) {
workingCopy.save(options);
}
}));
}
}

private onDidRegister(workingCopy: IWorkingCopy): void {
this.scheduleAutoSave(workingCopy);
}

private onDidUnregister(workingCopy: IWorkingCopy): void {
this.discardAutoSave(workingCopy);
}

private onDidChangeDirty(workingCopy: IWorkingCopy): void {
if (!workingCopy.isDirty()) {
this.discardAutoSave(workingCopy);
}
}

private onDidChangeContent(workingCopy: IWorkingCopy): void {
if (workingCopy.isDirty()) {
this.scheduleAutoSave(workingCopy);
}
}

private onDidWorkingCopyChangeDirty(workingCopy: IWorkingCopy): void {
private scheduleAutoSave(workingCopy: IWorkingCopy): void {
if (typeof this.autoSaveAfterDelay !== 'number') {
return; // auto save after delay must be enabled
}
Expand All @@ -145,22 +170,34 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
}

// Clear any running auto save operation
dispose(this.pendingAutoSavesAfterDelay.get(workingCopy));
this.pendingAutoSavesAfterDelay.delete(workingCopy);
this.discardAutoSave(workingCopy);

// Working copy got dirty - start auto save
if (workingCopy.isDirty()) {
this.logService.trace(`[editor auto save] starting auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString());
this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString());

const handle = setTimeout(() => {
if (workingCopy.isDirty()) {
workingCopy.save({ reason: SaveReason.AUTO });
}
}, this.autoSaveAfterDelay);
// Schedule new auto save
const handle = setTimeout(() => {

this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => clearTimeout(handle)));
} else {
this.logService.trace(`[editor auto save] clearing auto save`, workingCopy.resource.toString());
}
// Clear disposable
this.pendingAutoSavesAfterDelay.delete(workingCopy);

// Save if dirty
if (workingCopy.isDirty()) {
this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString());

workingCopy.save({ reason: SaveReason.AUTO });
}
}, this.autoSaveAfterDelay);

// Keep in map for disposal as needed
this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => {
this.logService.trace(`[editor auto save] clearing pending auto save`, workingCopy.resource.toString());

clearTimeout(handle);
}));
}

private discardAutoSave(workingCopy: IWorkingCopy): void {
dispose(this.pendingAutoSavesAfterDelay.get(workingCopy));
this.pendingAutoSavesAfterDelay.delete(workingCopy);
}
}
6 changes: 1 addition & 5 deletions src/vs/workbench/common/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export interface ISaveOptions {
context?: SaveContext;

/**
* Forces to load the contents of the working copy
* Forces to save the contents of the working copy
* again even if the working copy is not dirty.
*/
force?: boolean;
Expand Down Expand Up @@ -571,10 +571,6 @@ export abstract class TextEditorInput extends EditorInput {
}

async save(groupId: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> {
if (this.isReadonly()) {
return false; // return early if editor is readonly
}

return this.textFileService.save(this.resource, options);
}

Expand Down
14 changes: 9 additions & 5 deletions src/vs/workbench/common/editor/untitledTextEditorInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin

private static readonly MEMOIZER = createMemoizer();

private readonly _onDidModelChangeContent = this._register(new Emitter<void>());
readonly onDidModelChangeContent = this._onDidModelChangeContent.event;

private readonly _onDidModelChangeEncoding = this._register(new Emitter<void>());
readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event;

Expand Down Expand Up @@ -147,6 +144,8 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin
}

isDirty(): boolean {

// Always trust the model first if existing
if (this.cachedModel) {
return this.cachedModel.isDirty();
}
Expand All @@ -156,7 +155,13 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin
return false;
}

// untitled files with an associated path or associated resource
// A input with initial value is always dirty
if (this.initialValue && this.initialValue.length > 0) {
return true;
}

// A input with associated path is always dirty because it is the intent
// of the user to create a new file at that location through saving
return this.hasAssociatedFilePath;
}

Expand Down Expand Up @@ -274,7 +279,6 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin
const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, this.preferredMode, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding));

// re-emit some events from the model
this._register(model.onDidChangeContent(() => this._onDidModelChangeContent.fire()));
this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire()));

Expand Down
30 changes: 10 additions & 20 deletions src/vs/workbench/common/editor/untitledTextEditorModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import { IEncodingSupport, ISaveOptions } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { URI } from 'vs/base/common/uri';
import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { Event, Emitter } from 'vs/base/common/event';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { ITextBufferFactory } from 'vs/editor/common/model';
Expand All @@ -21,22 +19,19 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile

export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport, IWorkingCopy {

static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
private readonly _onDidChangeContent = this._register(new Emitter<void>());
readonly onDidChangeContent = this._onDidChangeContent.event;

private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
readonly onDidChangeDirty = this._onDidChangeDirty.event;

private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;

private readonly _onDidChangeEncoding: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeEncoding: Event<void> = this._onDidChangeEncoding.event;
private readonly _onDidChangeEncoding = this._register(new Emitter<void>());
readonly onDidChangeEncoding = this._onDidChangeEncoding.event;

readonly capabilities = WorkingCopyCapabilities.Untitled;

private dirty = false;
private versionId = 0;
private readonly contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
private configuredEncoding: string | undefined;

constructor(
Expand Down Expand Up @@ -124,18 +119,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
async revert(): Promise<boolean> {
this.setDirty(false);

// Handle content change event buffered
this.contentChangeEventScheduler.schedule();

return true;
}

backup(): Promise<void> {
async backup(): Promise<void> {
if (this.isResolved()) {
return this.backupFileService.backupResource(this.resource, this.createSnapshot(), this.versionId);
}

return Promise.resolve();
}

hasBackup(): boolean {
Expand Down Expand Up @@ -204,8 +194,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
this.setDirty(true);
}

// Handle content change event buffered
this.contentChangeEventScheduler.schedule();
// Emit as event
this._onDidChangeContent.fire();
}

isReadonly(): boolean {
Expand Down
8 changes: 4 additions & 4 deletions src/vs/workbench/contrib/backup/common/backup.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { BackupModelTracker } from 'vs/workbench/contrib/backup/common/backupModelTracker';
import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker';
import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';

// Register Backup Model Tracker
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupModelTracker, LifecyclePhase.Starting);
// Register Backup Tracker
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupTracker, LifecyclePhase.Starting);

// Register Backup Restorer
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting);
85 changes: 0 additions & 85 deletions src/vs/workbench/contrib/backup/common/backupModelTracker.ts

This file was deleted.

Loading

0 comments on commit e6651b9

Please sign in to comment.