From c410c946921f54940559f427599ce7fc94d23441 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Sep 2016 12:07:56 -0700 Subject: [PATCH 01/45] Add files.hotExit setting --- src/vs/platform/files/common/files.ts | 1 + src/vs/workbench/parts/files/browser/files.contribution.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 9f344bb715be2..6fad3eb13722d 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -476,6 +476,7 @@ export interface IFilesConfiguration { autoSaveDelay: number; eol: string; iconTheme: string; + hotExit: boolean; }; } diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index b08353b3bf70a..22fde7218f4d6 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -233,6 +233,12 @@ configurationRegistry.registerConfiguration({ 'type': 'object', 'default': (platform.isLinux || platform.isMacintosh) ? { '**/.git/objects/**': true, '**/node_modules/**': true } : { '**/.git/objects/**': true }, 'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Changing this setting requires a restart. When you experience Code consuming lots of cpu time on startup, you can exclude large folders to reduce the initial load.") + }, + 'files.hotExit': { + 'type': 'boolean', + // TODO: Switch to true once sufficiently stable + 'default': false, + 'description': nls.localize('hotExit', "Controls whether unsaved files are restored after relaunching. If this is enabled there will be no prompt to save when exiting the editor.") } } }); From ce42316e28713a74c198373b6f8ec3ce887560c3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Sep 2016 22:57:20 -0700 Subject: [PATCH 02/45] Start building out backup file code --- src/vs/platform/files/common/files.ts | 3 ++ .../parts/files/common/textFileService.ts | 32 +++++++++++++++++++ .../files/electron-browser/fileService.ts | 4 +++ .../services/files/node/fileService.ts | 5 +++ 4 files changed, 44 insertions(+) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 6fad3eb13722d..ad576cd437dbf 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -98,6 +98,9 @@ export interface IFileService { */ del(resource: URI, useTrash?: boolean): TPromise; + // TODO: doc + backupFile(resource: URI): TPromise; + /** * Imports the file to the parent identified by the resource. */ diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index d237f05826899..ba4bc9df45ba9 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -42,6 +42,8 @@ export abstract class TextFileService implements ITextFileService { private _onFilesAssociationChange: Emitter; private currentFilesAssociationConfig: { [key: string]: string; }; + private isHotExitEnabled: boolean; + private _onAutoSaveConfigurationChange: Emitter; private configuredAutoSaveDelay: number; private configuredAutoSaveOnFocusChange: boolean; @@ -115,6 +117,13 @@ export abstract class TextFileService implements ITextFileService { // Dirty files need treatment on shutdown if (this.getDirty().length) { + // If hot exit is enabled then save the dirty files in the workspace and then exit + if (this.isHotExitEnabled) { + return this.backupDirtyFiles().then(() => { + // TODO: return false here to actually exit + return true; + }); + } // If auto save is enabled, save all files and then check again for dirty files if (this.getAutoSaveMode() !== AutoSaveMode.OFF) { @@ -134,6 +143,27 @@ export abstract class TextFileService implements ITextFileService { return false; // no veto } + private backupDirtyFiles(): TPromise { + const toSave = this.getDirty(); + + // TODO: Reuse code in saveAll + const filesToSave: URI[] = []; + const untitledToSave: URI[] = []; + toSave.forEach(s => { + if (s.scheme === 'file') { + filesToSave.push(s); + } else if (s.scheme === 'untitled') { + untitledToSave.push(s); + } + }); + + return this.backupFiles(filesToSave, untitledToSave); + } + + private backupFiles(fileResources: URI[], untitledResources: URI[]): TPromise { + return null; + } + private confirmBeforeShutdown(): boolean | TPromise { const confirm = this.confirmSave(); @@ -209,6 +239,8 @@ export abstract class TextFileService implements ITextFileService { this.saveAll().done(null, errors.onUnexpectedError); } + this.isHotExitEnabled = configuration && configuration.files && configuration.files.hotExit; + // Check for change in files associations const filesAssociation = configuration && configuration.files && configuration.files.associations; if (!objects.equals(this.currentFilesAssociationConfig, filesAssociation)) { diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 15cb35d152ed8..b1c22c846d913 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -228,6 +228,10 @@ export class FileService implements IFileService { return this.raw.del(resource); } + public backupFile(resource: uri): TPromise { + return this.raw.backupFile(resource); + } + private doMoveItemToTrash(resource: uri): TPromise { const workspace = this.contextService.getWorkspace(); if (!workspace) { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index f49b9b0b1093c..4795125aa2273 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -408,6 +408,11 @@ export class FileService implements IFileService { return nfcall(extfs.del, absolutePath, this.tmpPath); } + public backupFile(resource: uri): TPromise { + // TODO: Implement + return null; + } + // Helpers private toAbsolutePath(arg1: uri | IFileStat): string { From 33e6a47e99a864ae9f1da81e148131613ef74c51 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Sep 2016 14:15:19 -0700 Subject: [PATCH 03/45] Shuffle backup code around --- src/vs/platform/files/common/files.ts | 2 +- src/vs/workbench/parts/files/common/files.ts | 8 ++++ .../parts/files/common/textFileService.ts | 44 +++++++------------ .../electron-browser/dirtyFilesTracker.ts | 22 ++++++++++ .../files/electron-browser/fileService.ts | 5 ++- .../services/files/node/fileService.ts | 10 +++-- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index db802c3b1f585..3c5fccdce7759 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -105,7 +105,7 @@ export interface IFileService { del(resource: URI, useTrash?: boolean): TPromise; // TODO: doc - backupFile(resource: URI): TPromise; + backupFile(resource: URI, content: string): TPromise; /** * Imports the file to the parent identified by the resource. diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 0469b7cb5d3a2..34ab61faea478 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -389,6 +389,9 @@ export interface ITextFileService extends IDisposable { */ confirmSave(resources?: URI[]): ConfirmResult; + // TODO: Doc + backup(resource: URI): void; + /** * Convinient fast access to the current auto save mode. */ @@ -398,4 +401,9 @@ export interface ITextFileService extends IDisposable { * Convinient fast access to the raw configured auto save settings. */ getAutoSaveConfiguration(): IAutoSaveConfiguration; + + /** + * Whether hot exit is enabled. + */ + isHotExitEnabled(): boolean; } \ No newline at end of file diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index b673f1127b6a8..d41dae471085f 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -42,7 +42,7 @@ export abstract class TextFileService implements ITextFileService { private _onFilesAssociationChange: Emitter; private currentFilesAssociationConfig: { [key: string]: string; }; - private isHotExitEnabled: boolean; + private configuredHotExit: boolean; private _onAutoSaveConfigurationChange: Emitter; private configuredAutoSaveDelay: number; @@ -118,11 +118,8 @@ export abstract class TextFileService implements ITextFileService { // Dirty files need treatment on shutdown if (this.getDirty().length) { // If hot exit is enabled then save the dirty files in the workspace and then exit - if (this.isHotExitEnabled) { - return this.backupDirtyFiles().then(() => { - // TODO: return false here to actually exit - return true; - }); + if (this.configuredHotExit) { + // TODO: Do last minute backup if needed } // If auto save is enabled, save all files and then check again for dirty files @@ -143,27 +140,6 @@ export abstract class TextFileService implements ITextFileService { return false; // no veto } - private backupDirtyFiles(): TPromise { - const toSave = this.getDirty(); - - // TODO: Reuse code in saveAll - const filesToSave: URI[] = []; - const untitledToSave: URI[] = []; - toSave.forEach(s => { - if (s.scheme === 'file') { - filesToSave.push(s); - } else if (s.scheme === 'untitled') { - untitledToSave.push(s); - } - }); - - return this.backupFiles(filesToSave, untitledToSave); - } - - private backupFiles(fileResources: URI[], untitledResources: URI[]): TPromise { - return null; - } - private confirmBeforeShutdown(): boolean | TPromise { const confirm = this.confirmSave(); @@ -239,7 +215,7 @@ export abstract class TextFileService implements ITextFileService { this.saveAll().done(null, errors.onUnexpectedError); } - this.isHotExitEnabled = configuration && configuration.files && configuration.files.hotExit; + this.configuredHotExit = configuration && configuration.files && configuration.files.hotExit; // Check for change in files associations const filesAssociation = configuration && configuration.files && configuration.files.associations; @@ -552,6 +528,14 @@ export abstract class TextFileService implements ITextFileService { }); } + public backup(resource: URI): void { + let model = this.getDirtyFileModels(resource); + if (!model || model.length === 0) { + return; + } + this.fileService.backupFile(resource, model[0].getValue()); + } + public getAutoSaveMode(): AutoSaveMode { if (this.configuredAutoSaveOnFocusChange) { return AutoSaveMode.ON_FOCUS_CHANGE; @@ -576,6 +560,10 @@ export abstract class TextFileService implements ITextFileService { }; } + public isHotExitEnabled(): boolean { + return this.configuredHotExit; + } + public dispose(): void { this.toUnbind = dispose(this.toUnbind); diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index 7f3dcd72d01d2..62e40c398fa57 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -19,6 +19,7 @@ import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {IActivityService, NumberBadge} from 'vs/workbench/services/activity/common/activityService'; +import {IFileService} from 'vs/platform/files/common/files'; import {IUntitledEditorService} from 'vs/workbench/services/untitled/common/untitledEditorService'; import arrays = require('vs/base/common/arrays'); @@ -36,6 +37,7 @@ export class DirtyFilesTracker implements IWorkbenchContribution { constructor( @ITextFileService private textFileService: ITextFileService, + @IFileService private fileService: IFileService, @ILifecycleService private lifecycleService: ILifecycleService, @IEditorGroupService private editorGroupService: IEditorGroupService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @@ -65,6 +67,8 @@ export class DirtyFilesTracker implements IWorkbenchContribution { } private onUntitledDidChangeDirty(resource: URI): void { + console.log('onUntitledDidChangeDirty', resource); + const gotDirty = this.untitledEditorService.isDirty(resource); if ((!this.isDocumentedEdited && gotDirty) || (this.isDocumentedEdited && !gotDirty)) { @@ -74,9 +78,21 @@ export class DirtyFilesTracker implements IWorkbenchContribution { if (gotDirty || this.lastDirtyCount > 0) { this.updateActivityBadge(); } + + // TODO: This needs to be moved elsewhere since dirty events cannot be reused because when a backup is + // performed then dirty status does not change. + if (this.textFileService.isHotExitEnabled()) { + console.log('trigger hot exit'); + // TODO: Delay/throttle + //let untitledEditorInput = this.untitledEditorService.get(resource); + //untitledEditorInput + //this.fileService.backupFile(resource); + } } private onTextFileDirty(e: TextFileModelChangeEvent): void { + console.log('onTextFileDirty', e); + if ((this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) && !this.isDocumentedEdited) { this.updateDocumentEdited(); // no indication needed when auto save is enabled for short delay } @@ -93,6 +109,12 @@ export class DirtyFilesTracker implements IWorkbenchContribution { if (!this.pendingDirtyHandle) { this.pendingDirtyHandle = setTimeout(() => this.doOpenDirtyResources(), 250); } + + if (this.textFileService.isHotExitEnabled()) { + console.log('trigger hot exit'); + // TODO: Delay/throttle + this.textFileService.backup(e.resource); + } } private doOpenDirtyResources(): void { diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 91334cf30201b..b1183a70c2681 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -232,8 +232,9 @@ export class FileService implements IFileService { return this.raw.del(resource); } - public backupFile(resource: uri): TPromise { - return this.raw.backupFile(resource); + public backupFile(resource: uri, content: string): TPromise { + // TODO: Use this.environmentService.userDataPath as backup path + return this.raw.backupFile(resource, content); } private doMoveItemToTrash(resource: uri): TPromise { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 60a544579e28c..aaebb142ae1c0 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -433,9 +433,13 @@ export class FileService implements IFileService { return nfcall(extfs.del, absolutePath, this.tmpPath); } - public backupFile(resource: uri): TPromise { - // TODO: Implement - return null; + public backupFile(resource: uri, content: string): TPromise { + // TODO: Implement properly + var backupName = paths.basename(resource.fsPath); + var backupPath = paths.join(process.env['HOME'], 'backup-test', backupName); + + let backupResource = uri.file(backupPath); + return this.updateContent(backupResource, content); } // Helpers From 9c9c0b2cd389a82446895f63d04d88addc480364 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 02:33:11 -0700 Subject: [PATCH 04/45] Perform backups on content change not dirty change --- src/vs/workbench/common/editor.ts | 9 ++++++ .../common/editor/untitledEditorInput.ts | 1 + .../common/editor/untitledEditorModel.ts | 7 +++++ .../common/editors/textFileEditorModel.ts | 8 +++++ .../editors/textFileEditorModelManager.ts | 11 +++++++ src/vs/workbench/parts/files/common/files.ts | 3 ++ .../electron-browser/dirtyFilesTracker.ts | 30 ++++++++++++++----- .../untitled/common/untitledEditorService.ts | 16 ++++++++++ 8 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index e2f4738ba105d..ba87f8c75f84c 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -121,12 +121,14 @@ export interface IEditorInputFactory { */ export abstract class EditorInput implements IEditorInput { private _onDispose: Emitter; + protected _onDidChangeContent: Emitter; protected _onDidChangeDirty: Emitter; protected _onDidChangeLabel: Emitter; private disposed: boolean; constructor() { + this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); this._onDidChangeLabel = new Emitter(); this._onDispose = new Emitter(); @@ -134,6 +136,13 @@ export abstract class EditorInput implements IEditorInput { this.disposed = false; } + /** + * Fired when the content of this input changes. + */ + public get onDidChangeContent(): Event { + return this._onDidChangeContent.event; + } + /** * Fired when the dirty state of this input changes. */ diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 17bf0df175b29..c89cc69aedd4e 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -160,6 +160,7 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { const model = this.instantiationService.createInstance(UntitledEditorModel, content, mime || MIME_TEXT, this.resource, this.hasAssociatedFilePath); // re-emit some events from the model + this.toUnbind.push(model.onDidChangeContent(() => this._onDidChangeContent.fire())); this.toUnbind.push(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this.toUnbind.push(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index b0a573577bddb..c56db79b8274c 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -23,6 +23,7 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS private configurationChangeListener: IDisposable; private dirty: boolean; + private _onDidChangeContent: Emitter; private _onDidChangeDirty: Emitter; private _onDidChangeEncoding: Emitter; @@ -45,12 +46,17 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this.hasAssociatedFilePath = hasAssociatedFilePath; this.dirty = hasAssociatedFilePath; // untitled associated to file path are dirty right away + this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); this._onDidChangeEncoding = new Emitter(); this.registerListeners(); } + public get onDidChangeContent(): Event { + return this._onDidChangeContent.event; + } + public get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } @@ -139,6 +145,7 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS } private onModelContentChanged(): void { + this._onDidChangeContent.fire(); // turn dirty if we were not if (!this.dirty) { diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts index dd27a823a564a..efe0af392f8bb 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts @@ -55,6 +55,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private inErrorMode: boolean; private lastSaveAttemptTime: number; private createTextEditorModelPromise: TPromise; + private _onDidContentChange: Emitter; private _onDidStateChange: Emitter; constructor( @@ -74,6 +75,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.resource = resource; this.toDispose = []; + this._onDidContentChange = new Emitter(); this._onDidStateChange = new Emitter(); this.toDispose.push(this._onDidStateChange); this.preferredEncoding = preferredEncoding; @@ -111,6 +113,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this._onDidStateChange.event; } + public get onDidContentChange(): Event { + return this._onDidContentChange.event; + } + /** * Set a save error handler to install code that executes when save errors occur. */ @@ -300,6 +306,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } + this._onDidContentChange.fire(void 0); + // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. // Note: we currently only do this check when auto-save is turned off because there you see diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts index 6a91400c223cf..773ea814862c6 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts @@ -24,6 +24,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { private toUnbind: IDisposable[]; + private _onModelContentChanged: Emitter; private _onModelDirty: Emitter; private _onModelSaveError: Emitter; private _onModelSaved: Emitter; @@ -43,6 +44,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { ) { this.toUnbind = []; + this._onModelContentChanged = new Emitter(); this._onModelDirty = new Emitter(); this._onModelSaveError = new Emitter(); this._onModelSaved = new Emitter(); @@ -142,6 +144,10 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return true; } + public get onModelContentChanged(): Event { + return this._onModelContentChanged.event; + } + public get onModelDirty(): Event { return this._onModelDirty.event; } @@ -212,6 +218,11 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { break; } }); + + // TODO: Dispose of me properly + model.onDidContentChange(() => { + this._onModelContentChanged.fire(model.getResource()); + }); } // Store pending loads to avoid race conditions diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 34ab61faea478..e885eb59f1b53 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -259,6 +259,7 @@ export interface IRawTextContent extends IBaseStat { export interface ITextFileEditorModelManager { + onModelContentChanged: Event; onModelDirty: Event; onModelSaveError: Event; onModelSaved: Event; @@ -274,6 +275,8 @@ export interface ITextFileEditorModelManager { export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport { + onDidContentChange: Event; + onDidStateChange: Event; getResource(): URI; diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index 62e40c398fa57..7b10b096c0cda 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -56,7 +56,9 @@ export class DirtyFilesTracker implements IWorkbenchContribution { private registerListeners(): void { // Local text file changes + this.toUnbind.push(this.untitledEditorService.onDidChangeContent(e => this.onUntitledDidChangeContent(e))); this.toUnbind.push(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); + this.toUnbind.push(this.textFileService.models.onModelContentChanged((resource) => this.onTextFileDidChangeContent(resource))); this.toUnbind.push(this.textFileService.models.onModelDirty(e => this.onTextFileDirty(e))); this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onTextFileSaved(e))); this.toUnbind.push(this.textFileService.models.onModelSaveError(e => this.onTextFileSaveError(e))); @@ -66,8 +68,21 @@ export class DirtyFilesTracker implements IWorkbenchContribution { this.lifecycleService.onShutdown(this.dispose, this); } + private onUntitledDidChangeContent(resource: URI): void { + console.log('onUntitledDidChangeContent', resource); + + if (this.textFileService.isHotExitEnabled()) { + console.log('trigger hot exit'); + // TODO: Delay/throttle + let untitledEditorInput = this.untitledEditorService.get(resource); + untitledEditorInput.resolve().then((model) => { + // TODO: Deal with encoding? + this.fileService.backupFile(resource, model.getValue()); + }); + } + } + private onUntitledDidChangeDirty(resource: URI): void { - console.log('onUntitledDidChangeDirty', resource); const gotDirty = this.untitledEditorService.isDirty(resource); @@ -78,15 +93,14 @@ export class DirtyFilesTracker implements IWorkbenchContribution { if (gotDirty || this.lastDirtyCount > 0) { this.updateActivityBadge(); } + } + + private onTextFileDidChangeContent(resource: URI): void { + console.log('onTextFileDidChangeContent', resource); - // TODO: This needs to be moved elsewhere since dirty events cannot be reused because when a backup is - // performed then dirty status does not change. if (this.textFileService.isHotExitEnabled()) { - console.log('trigger hot exit'); - // TODO: Delay/throttle - //let untitledEditorInput = this.untitledEditorService.get(resource); - //untitledEditorInput - //this.fileService.backupFile(resource); + let model = this.textFileService.models.get(resource); + this.fileService.backupFile(resource, model.getValue()); } } diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index 60057649a7dc7..37e51c056582e 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -16,6 +16,11 @@ export interface IUntitledEditorService { _serviceBrand: any; + /** + * Events for when untitled editors content change. + */ + onDidChangeContent: Event; + /** * Events for when untitled editors change (e.g. getting dirty, saved or reverted). */ @@ -73,14 +78,20 @@ export class UntitledEditorService implements IUntitledEditorService { private static CACHE: { [resource: string]: UntitledEditorInput } = Object.create(null); private static KNOWN_ASSOCIATED_FILE_PATHS: { [resource: string]: boolean } = Object.create(null); + private _onDidChangeContent: Emitter; private _onDidChangeDirty: Emitter; private _onDidChangeEncoding: Emitter; constructor(@IInstantiationService private instantiationService: IInstantiationService) { + this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); this._onDidChangeEncoding = new Emitter(); } + public get onDidChangeContent(): Event { + return this._onDidChangeContent.event; + } + public get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } @@ -171,6 +182,10 @@ export class UntitledEditorService implements IUntitledEditorService { this._onDidChangeEncoding.fire(resource); }); + const updateListener = input.onDidChangeContent(() => { + this._onDidChangeContent.fire(resource); + }); + // Remove from cache on dispose const onceDispose = once(input.onDispose); onceDispose(() => { @@ -178,6 +193,7 @@ export class UntitledEditorService implements IUntitledEditorService { delete UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[input.getResource().toString()]; dirtyListener.dispose(); encodingListener.dispose(); + updateListener.dispose(); }); // Add to cache From 9c0d19a188cf6df049eb741fc4d453930c05d59e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 02:45:09 -0700 Subject: [PATCH 05/45] Back up files to the user data directory --- .../services/files/electron-browser/fileService.ts | 2 +- src/vs/workbench/services/files/node/fileService.ts | 5 +++-- .../workbench/services/files/test/node/fileService.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index b1183a70c2681..c37132a54d7ad 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -78,7 +78,7 @@ export class FileService implements IFileService { // create service const workspace = this.contextService.getWorkspace(); - this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService); + this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService); // Listeners this.registerListeners(); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index aaebb142ae1c0..6b415b3f32657 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -31,6 +31,7 @@ import flow = require('vs/base/node/flow'); import {FileWatcher as UnixWatcherService} from 'vs/workbench/services/files/node/watcher/unix/watcherService'; import {FileWatcher as WindowsWatcherService} from 'vs/workbench/services/files/node/watcher/win32/watcherService'; import {toFileChangesEvent, normalize, IRawFileChange} from 'vs/workbench/services/files/node/watcher/common'; +import {IEnvironmentService} from 'vs/platform/environment/common/environment'; import {IEventService} from 'vs/platform/event/common/event'; export interface IEncodingOverride { @@ -82,7 +83,7 @@ export class FileService implements IFileService { private fileChangesWatchDelayer: ThrottledDelayer; private undeliveredRawFileChangesEvents: IRawFileChange[]; - constructor(basePath: string, options: IFileServiceOptions, private eventEmitter: IEventService) { + constructor(basePath: string, options: IFileServiceOptions, private eventEmitter: IEventService, private environmentService: IEnvironmentService) { this.basePath = basePath ? paths.normalize(basePath) : void 0; if (this.basePath && this.basePath.indexOf('\\\\') === 0 && strings.endsWith(this.basePath, paths.sep)) { @@ -436,7 +437,7 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { // TODO: Implement properly var backupName = paths.basename(resource.fsPath); - var backupPath = paths.join(process.env['HOME'], 'backup-test', backupName); + var backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', backupName); let backupResource = uri.file(backupPath); return this.updateContent(backupResource, content); diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index 8117b359be570..e929d7103a726 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -33,7 +33,7 @@ suite('FileService', () => { extfs.copy(sourceDir, testDir, () => { events = new utils.TestEventService(); - service = new FileService(testDir, { disableWatcher: true }, events); + service = new FileService(testDir, { disableWatcher: true }, events, null); done(); }); }); @@ -494,7 +494,7 @@ suite('FileService', () => { encoding: 'windows1252', encodingOverride: encodingOverride, disableWatcher: true - }, null); + }, null, null); _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).done(c => { assert.equal(c.encoding, 'windows1252'); @@ -520,7 +520,7 @@ suite('FileService', () => { let _service = new FileService(_testDir, { disableWatcher: true - }, null); + }, null, null); extfs.copy(_sourceDir, _testDir, () => { fs.readFile(resource.fsPath, (error, data) => { From a8b4f276e79279814b3804beb0a72e40c1993399 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 02:54:31 -0700 Subject: [PATCH 06/45] Backup to a unique location for this workspace --- src/vs/workbench/services/files/node/fileService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 6b415b3f32657..8160b33dec8a4 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -70,6 +70,7 @@ export class FileService implements IFileService { public _serviceBrand: any; + private static SESSION_BACKUP_ID = crypto.randomBytes(20).toString('hex'); // defined the directory to store backups for the session private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private static MAX_DEGREE_OF_PARALLEL_FS_OPS = 10; // degree of parallel fs calls that we accept at the same time @@ -437,7 +438,7 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { // TODO: Implement properly var backupName = paths.basename(resource.fsPath); - var backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', backupName); + var backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); let backupResource = uri.file(backupPath); return this.updateContent(backupResource, content); From 9e484995b2dafd6fff9381d1294f97be3b529e3a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 03:09:22 -0700 Subject: [PATCH 07/45] Discard backups on exit --- src/vs/platform/files/common/files.ts | 10 +++++++++- src/vs/workbench/parts/files/common/textFileService.ts | 7 ++++++- .../services/files/electron-browser/fileService.ts | 5 ++++- src/vs/workbench/services/files/node/fileService.ts | 5 +++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 3c5fccdce7759..425c8759ea184 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -104,9 +104,17 @@ export interface IFileService { */ del(resource: URI, useTrash?: boolean): TPromise; - // TODO: doc + /** + * Backs up the provided file to a temporary directory to be used by the hot + * exit feature and crash recovery. + */ backupFile(resource: URI, content: string): TPromise; + /** + * Discards all backups associated with this session. + */ + discardBackups(): TPromise; + /** * Imports the file to the parent identified by the resource. */ diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index d41dae471085f..fc663c97eb3cf 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -119,7 +119,8 @@ export abstract class TextFileService implements ITextFileService { if (this.getDirty().length) { // If hot exit is enabled then save the dirty files in the workspace and then exit if (this.configuredHotExit) { - // TODO: Do last minute backup if needed + // TODO: Do a last minute backup if required + // TODO: If this is the only instance opened, perform hot exit } // If auto save is enabled, save all files and then check again for dirty files @@ -129,6 +130,7 @@ export abstract class TextFileService implements ITextFileService { return this.confirmBeforeShutdown(); // we still have dirty files around, so confirm normally } + this.fileService.discardBackups(); return false; // all good, no veto }); } @@ -137,6 +139,7 @@ export abstract class TextFileService implements ITextFileService { return this.confirmBeforeShutdown(); } + this.fileService.discardBackups(); return false; // no veto } @@ -150,12 +153,14 @@ export abstract class TextFileService implements ITextFileService { return true; // veto if some saves failed } + this.fileService.discardBackups(); return false; // no veto }); } // Don't Save else if (confirm === ConfirmResult.DONT_SAVE) { + this.fileService.discardBackups(); return false; // no veto } diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index c37132a54d7ad..50fb8bc05d995 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -233,10 +233,13 @@ export class FileService implements IFileService { } public backupFile(resource: uri, content: string): TPromise { - // TODO: Use this.environmentService.userDataPath as backup path return this.raw.backupFile(resource, content); } + public discardBackups(): TPromise { + return this.raw.discardBackups(); + } + private doMoveItemToTrash(resource: uri): TPromise { const workspace = this.contextService.getWorkspace(); if (!workspace) { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 8160b33dec8a4..f7d45003d131e 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -441,9 +441,14 @@ export class FileService implements IFileService { var backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); let backupResource = uri.file(backupPath); + console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); } + public discardBackups(): TPromise { + return this.del(uri.file(paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID))); + } + // Helpers private toAbsolutePath(arg1: uri | IFileStat): string { From 2ded2a75ca602a75ccd4a33297352dfc60a9a6d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 11:29:13 -0700 Subject: [PATCH 08/45] Discard backups when the file is no longer dirty Make content update trigger after dirty status is evaluated --- src/vs/platform/files/common/files.ts | 5 ++++ .../common/editor/untitledEditorModel.ts | 4 ++-- .../common/editors/textFileEditorModel.ts | 6 +++-- .../electron-browser/dirtyFilesTracker.ts | 23 +++++++++++-------- .../files/electron-browser/fileService.ts | 4 ++++ .../services/files/node/fileService.ts | 13 ++++++++--- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 425c8759ea184..3840200ff8b69 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -110,6 +110,11 @@ export interface IFileService { */ backupFile(resource: URI, content: string): TPromise; + /** + * Discard the backup for the resource specified. + */ + discardBackup(resource: URI): TPromise; + /** * Discards all backups associated with this session. */ diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index c56db79b8274c..3b5e66d42a1c1 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -145,8 +145,6 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS } private onModelContentChanged(): void { - this._onDidChangeContent.fire(); - // turn dirty if we were not if (!this.dirty) { this.dirty = true; @@ -159,6 +157,8 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this.dirty = false; this._onDidChangeDirty.fire(); } + + this._onDidChangeContent.fire(); } public dispose(): void { diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts index efe0af392f8bb..e95a4328a59bb 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts @@ -306,8 +306,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } - this._onDidContentChange.fire(void 0); - // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. // Note: we currently only do this check when auto-save is turned off because there you see @@ -324,6 +322,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this._onDidStateChange.fire(StateChange.REVERTED); } + this._onDidContentChange.fire(void 0); + return; } @@ -340,6 +340,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date()); } } + + this._onDidContentChange.fire(void 0); } private makeDirty(): void { diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index 7b10b096c0cda..ef73716f61f5c 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -76,8 +76,13 @@ export class DirtyFilesTracker implements IWorkbenchContribution { // TODO: Delay/throttle let untitledEditorInput = this.untitledEditorService.get(resource); untitledEditorInput.resolve().then((model) => { - // TODO: Deal with encoding? - this.fileService.backupFile(resource, model.getValue()); + if (model.isDirty()) { + // TODO: Deal with encoding? + this.fileService.backupFile(resource, model.getValue()); + } else { + console.log('discard'); + this.fileService.discardBackup(resource); + } }); } } @@ -100,12 +105,16 @@ export class DirtyFilesTracker implements IWorkbenchContribution { if (this.textFileService.isHotExitEnabled()) { let model = this.textFileService.models.get(resource); - this.fileService.backupFile(resource, model.getValue()); + if (model.isDirty()) { + this.fileService.backupFile(resource, model.getValue()); + } else { + console.log('discard'); + this.fileService.discardBackup(resource); + } } } private onTextFileDirty(e: TextFileModelChangeEvent): void { - console.log('onTextFileDirty', e); if ((this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) && !this.isDocumentedEdited) { this.updateDocumentEdited(); // no indication needed when auto save is enabled for short delay @@ -123,12 +132,6 @@ export class DirtyFilesTracker implements IWorkbenchContribution { if (!this.pendingDirtyHandle) { this.pendingDirtyHandle = setTimeout(() => this.doOpenDirtyResources(), 250); } - - if (this.textFileService.isHotExitEnabled()) { - console.log('trigger hot exit'); - // TODO: Delay/throttle - this.textFileService.backup(e.resource); - } } private doOpenDirtyResources(): void { diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 50fb8bc05d995..0b1c233f2772f 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -236,6 +236,10 @@ export class FileService implements IFileService { return this.raw.backupFile(resource, content); } + public discardBackup(resource: uri): TPromise { + return this.raw.discardBackup(resource); + } + public discardBackups(): TPromise { return this.raw.discardBackups(); } diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index f7d45003d131e..ec2b175c70f58 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -437,14 +437,21 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { // TODO: Implement properly - var backupName = paths.basename(resource.fsPath); - var backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); + const backupName = paths.basename(resource.fsPath); + const backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); + const backupResource = uri.file(backupPath); - let backupResource = uri.file(backupPath); console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); } + public discardBackup(resource: uri): TPromise { + const backupName = paths.basename(resource.fsPath); + const backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); + const backupResource = uri.file(backupPath); + return this.del(backupResource); + } + public discardBackups(): TPromise { return this.del(uri.file(paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID))); } From 24adcf7ad2c943a1782ea172d8443fdaf931d9c0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 13:34:36 -0700 Subject: [PATCH 09/45] Fix tests --- src/vs/test/utils/servicesTestUtils.ts | 4 ++++ .../parts/files/common/textFileService.ts | 20 +++++++++++-------- .../test/browser/textFileService.test.ts | 14 ++++++++----- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index d0317c15d80c6..8ab29e78ce1a2 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -569,6 +569,10 @@ export const TestFileService = { name: paths.basename(res.fsPath) }; }); + }, + + discardBackups: function () { + return TPromise.as(void 0); } }; diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index fc663c97eb3cf..953481379652d 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -130,8 +130,9 @@ export abstract class TextFileService implements ITextFileService { return this.confirmBeforeShutdown(); // we still have dirty files around, so confirm normally } - this.fileService.discardBackups(); - return false; // all good, no veto + return this.fileService.discardBackups().then(() => { + return false; // all good, no veto + }); }); } @@ -139,8 +140,9 @@ export abstract class TextFileService implements ITextFileService { return this.confirmBeforeShutdown(); } - this.fileService.discardBackups(); - return false; // no veto + return this.fileService.discardBackups().then(() => { + return false; // no veto + }); } private confirmBeforeShutdown(): boolean | TPromise { @@ -153,15 +155,17 @@ export abstract class TextFileService implements ITextFileService { return true; // veto if some saves failed } - this.fileService.discardBackups(); - return false; // no veto + return this.fileService.discardBackups().then(() => { + return false; // no veto + }); }); } // Don't Save else if (confirm === ConfirmResult.DONT_SAVE) { - this.fileService.discardBackups(); - return false; // no veto + return this.fileService.discardBackups().then(() => { + return false; // no veto + }); } // Cancel diff --git a/src/vs/workbench/parts/files/test/browser/textFileService.test.ts b/src/vs/workbench/parts/files/test/browser/textFileService.test.ts index a6f2ee37a9280..8f655df603ff0 100644 --- a/src/vs/workbench/parts/files/test/browser/textFileService.test.ts +++ b/src/vs/workbench/parts/files/test/browser/textFileService.test.ts @@ -60,11 +60,14 @@ suite('Files - TextFileService', () => { accessor.untitledEditorService.revertAll(); }); - test('confirm onWillShutdown - no veto', function () { + test('confirm onWillShutdown - no veto', function (done) { const event = new ShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); - assert.ok(!event.value); + return (>event.value).then(veto => { + assert.ok(!veto); + done(); + }); }); test('confirm onWillShutdown - veto if user cancels', function (done) { @@ -97,9 +100,10 @@ suite('Files - TextFileService', () => { const event = new ShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); - assert.ok(!event.value); - - done(); + return (>event.value).then(veto => { + assert.ok(!veto); + done(); + }); }); }); From 08fad0bb5790ca89e042041a63a93cae908f98cc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 15:33:01 -0700 Subject: [PATCH 10/45] Move backup logic into models --- src/vs/platform/files/common/files.ts | 5 ++ src/vs/workbench/common/editor.ts | 9 ---- .../common/editor/untitledEditorInput.ts | 1 - .../common/editor/untitledEditorModel.ts | 48 +++++++++++++++---- .../common/editors/textFileEditorModel.ts | 42 ++++++++++++---- .../editors/textFileEditorModelManager.ts | 11 ----- src/vs/workbench/parts/files/common/files.ts | 8 ---- .../parts/files/common/textFileService.ts | 4 -- .../electron-browser/dirtyFilesTracker.ts | 35 -------------- .../files/electron-browser/fileService.ts | 6 ++- .../services/files/node/fileService.ts | 25 +++++++++- .../files/test/node/fileService.test.ts | 6 +-- .../untitled/common/untitledEditorService.ts | 16 ------- 13 files changed, 111 insertions(+), 105 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 3840200ff8b69..13fcec582a669 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -120,6 +120,11 @@ export interface IFileService { */ discardBackups(): TPromise; + /** + * Whether hot exit is enabled. + */ + isHotExitEnabled(): boolean; + /** * Imports the file to the parent identified by the resource. */ diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index ba87f8c75f84c..e2f4738ba105d 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -121,14 +121,12 @@ export interface IEditorInputFactory { */ export abstract class EditorInput implements IEditorInput { private _onDispose: Emitter; - protected _onDidChangeContent: Emitter; protected _onDidChangeDirty: Emitter; protected _onDidChangeLabel: Emitter; private disposed: boolean; constructor() { - this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); this._onDidChangeLabel = new Emitter(); this._onDispose = new Emitter(); @@ -136,13 +134,6 @@ export abstract class EditorInput implements IEditorInput { this.disposed = false; } - /** - * Fired when the content of this input changes. - */ - public get onDidChangeContent(): Event { - return this._onDidChangeContent.event; - } - /** * Fired when the dirty state of this input changes. */ diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index c89cc69aedd4e..17bf0df175b29 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -160,7 +160,6 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { const model = this.instantiationService.createInstance(UntitledEditorModel, content, mime || MIME_TEXT, this.resource, this.hasAssociatedFilePath); // re-emit some events from the model - this.toUnbind.push(model.onDidChangeContent(() => this._onDidChangeContent.fire())); this.toUnbind.push(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this.toUnbind.push(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 3b5e66d42a1c1..47c99cf7fe481 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -10,7 +10,7 @@ import {EditorModel, IEncodingSupport} from 'vs/workbench/common/editor'; import {StringEditorModel} from 'vs/workbench/common/editor/stringEditorModel'; import URI from 'vs/base/common/uri'; import {EndOfLinePreference} from 'vs/editor/common/editorCommon'; -import {IFilesConfiguration} from 'vs/platform/files/common/files'; +import {IFileService, IFilesConfiguration} from 'vs/platform/files/common/files'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; import {IModeService} from 'vs/editor/common/services/modeService'; import {IModelService} from 'vs/editor/common/services/modelService'; @@ -23,7 +23,6 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS private configurationChangeListener: IDisposable; private dirty: boolean; - private _onDidChangeContent: Emitter; private _onDidChangeDirty: Emitter; private _onDidChangeEncoding: Emitter; @@ -32,6 +31,8 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS private hasAssociatedFilePath: boolean; + private backupPromises: TPromise[]; + constructor( value: string, mime: string, @@ -39,6 +40,7 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS hasAssociatedFilePath: boolean, @IModeService modeService: IModeService, @IModelService modelService: IModelService, + @IFileService private fileService: IFileService, @IConfigurationService private configurationService: IConfigurationService ) { super(value, mime, resource, modeService, modelService); @@ -46,15 +48,12 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this.hasAssociatedFilePath = hasAssociatedFilePath; this.dirty = hasAssociatedFilePath; // untitled associated to file path are dirty right away - this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); this._onDidChangeEncoding = new Emitter(); - this.registerListeners(); - } + this.backupPromises = []; - public get onDidChangeContent(): Event { - return this._onDidChangeContent.event; + this.registerListeners(); } public get onDidChangeDirty(): Event { @@ -156,9 +155,18 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS else if (!this.hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') { this.dirty = false; this._onDidChangeDirty.fire(); + } - this._onDidChangeContent.fire(); + if (this.fileService.isHotExitEnabled()) { + if (this.dirty) { + console.log('backup'); + this.doBackup(); + } else { + console.log('discard'); + this.fileService.discardBackup(this.resource); + } + } } public dispose(): void { @@ -176,5 +184,29 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this._onDidChangeDirty.dispose(); this._onDidChangeEncoding.dispose(); + + this.cancelBackupPromises(); + console.log('discard'); + this.fileService.discardBackup(this.resource); + } + + private doBackup(): TPromise { + // Cancel any currently running backups to make this the one that succeeds + this.cancelBackupPromises(); + + // Create new backup promise and keep it + const promise = TPromise.timeout(1000).then(() => { + this.fileService.backupFile(this.resource, this.getValue()); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change + }); + + this.backupPromises.push(promise); + + return promise; + } + + private cancelBackupPromises(): void { + while (this.backupPromises.length) { + this.backupPromises.pop().cancel(); + } } } \ No newline at end of file diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts index e95a4328a59bb..d6bc40768020a 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts @@ -49,13 +49,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private autoSaveAfterMillies: number; private autoSaveAfterMilliesEnabled: boolean; private autoSavePromises: TPromise[]; + private backupPromises: TPromise[]; private mapPendingSaveToVersionId: { [versionId: string]: TPromise }; private disposed: boolean; private inConflictResolutionMode: boolean; private inErrorMode: boolean; private lastSaveAttemptTime: number; private createTextEditorModelPromise: TPromise; - private _onDidContentChange: Emitter; private _onDidStateChange: Emitter; constructor( @@ -75,12 +75,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.resource = resource; this.toDispose = []; - this._onDidContentChange = new Emitter(); this._onDidStateChange = new Emitter(); this.toDispose.push(this._onDidStateChange); this.preferredEncoding = preferredEncoding; this.dirty = false; this.autoSavePromises = []; + this.backupPromises = []; this.versionId = 0; this.lastSaveAttemptTime = 0; this.mapPendingSaveToVersionId = {}; @@ -113,10 +113,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this._onDidStateChange.event; } - public get onDidContentChange(): Event { - return this._onDidContentChange.event; - } - /** * Set a save error handler to install code that executes when save errors occur. */ @@ -322,7 +318,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this._onDidStateChange.fire(StateChange.REVERTED); } - this._onDidContentChange.fire(void 0); + if (this.fileService.isHotExitEnabled()) { + console.log('discard'); + this.fileService.discardBackup(this.resource); + } return; } @@ -341,7 +340,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - this._onDidContentChange.fire(void 0); + if (this.fileService.isHotExitEnabled()) { + console.log('backup'); + this.doBackup(); + } } private makeDirty(): void { @@ -382,6 +384,26 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } + private doBackup(): TPromise { + // Cancel any currently running backups to make this the one that succeeds + this.cancelBackupPromises(); + + // Create new backup promise and keep it + const promise = TPromise.timeout(1000).then(() => { + this.fileService.backupFile(this.resource, this.getValue()); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change + }); + + this.backupPromises.push(promise); + + return promise; + } + + private cancelBackupPromises(): void { + while (this.backupPromises.length) { + this.backupPromises.pop().cancel(); + } + } + /** * Saves the current versionId of this editor model if it is dirty. */ @@ -713,6 +735,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.createTextEditorModelPromise = null; this.cancelAutoSavePromises(); + this.cancelBackupPromises(); + + console.log('discard'); + this.fileService.discardBackup(this.resource); super.dispose(); } diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts index 773ea814862c6..6a91400c223cf 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts @@ -24,7 +24,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { private toUnbind: IDisposable[]; - private _onModelContentChanged: Emitter; private _onModelDirty: Emitter; private _onModelSaveError: Emitter; private _onModelSaved: Emitter; @@ -44,7 +43,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { ) { this.toUnbind = []; - this._onModelContentChanged = new Emitter(); this._onModelDirty = new Emitter(); this._onModelSaveError = new Emitter(); this._onModelSaved = new Emitter(); @@ -144,10 +142,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return true; } - public get onModelContentChanged(): Event { - return this._onModelContentChanged.event; - } - public get onModelDirty(): Event { return this._onModelDirty.event; } @@ -218,11 +212,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { break; } }); - - // TODO: Dispose of me properly - model.onDidContentChange(() => { - this._onModelContentChanged.fire(model.getResource()); - }); } // Store pending loads to avoid race conditions diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index e885eb59f1b53..2467114e2b46e 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -259,7 +259,6 @@ export interface IRawTextContent extends IBaseStat { export interface ITextFileEditorModelManager { - onModelContentChanged: Event; onModelDirty: Event; onModelSaveError: Event; onModelSaved: Event; @@ -275,8 +274,6 @@ export interface ITextFileEditorModelManager { export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport { - onDidContentChange: Event; - onDidStateChange: Event; getResource(): URI; @@ -404,9 +401,4 @@ export interface ITextFileService extends IDisposable { * Convinient fast access to the raw configured auto save settings. */ getAutoSaveConfiguration(): IAutoSaveConfiguration; - - /** - * Whether hot exit is enabled. - */ - isHotExitEnabled(): boolean; } \ No newline at end of file diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index 953481379652d..dd9168cea6da4 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -569,10 +569,6 @@ export abstract class TextFileService implements ITextFileService { }; } - public isHotExitEnabled(): boolean { - return this.configuredHotExit; - } - public dispose(): void { this.toUnbind = dispose(this.toUnbind); diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index ef73716f61f5c..a0578d2c59931 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -56,9 +56,7 @@ export class DirtyFilesTracker implements IWorkbenchContribution { private registerListeners(): void { // Local text file changes - this.toUnbind.push(this.untitledEditorService.onDidChangeContent(e => this.onUntitledDidChangeContent(e))); this.toUnbind.push(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); - this.toUnbind.push(this.textFileService.models.onModelContentChanged((resource) => this.onTextFileDidChangeContent(resource))); this.toUnbind.push(this.textFileService.models.onModelDirty(e => this.onTextFileDirty(e))); this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onTextFileSaved(e))); this.toUnbind.push(this.textFileService.models.onModelSaveError(e => this.onTextFileSaveError(e))); @@ -68,25 +66,6 @@ export class DirtyFilesTracker implements IWorkbenchContribution { this.lifecycleService.onShutdown(this.dispose, this); } - private onUntitledDidChangeContent(resource: URI): void { - console.log('onUntitledDidChangeContent', resource); - - if (this.textFileService.isHotExitEnabled()) { - console.log('trigger hot exit'); - // TODO: Delay/throttle - let untitledEditorInput = this.untitledEditorService.get(resource); - untitledEditorInput.resolve().then((model) => { - if (model.isDirty()) { - // TODO: Deal with encoding? - this.fileService.backupFile(resource, model.getValue()); - } else { - console.log('discard'); - this.fileService.discardBackup(resource); - } - }); - } - } - private onUntitledDidChangeDirty(resource: URI): void { const gotDirty = this.untitledEditorService.isDirty(resource); @@ -100,20 +79,6 @@ export class DirtyFilesTracker implements IWorkbenchContribution { } } - private onTextFileDidChangeContent(resource: URI): void { - console.log('onTextFileDidChangeContent', resource); - - if (this.textFileService.isHotExitEnabled()) { - let model = this.textFileService.models.get(resource); - if (model.isDirty()) { - this.fileService.backupFile(resource, model.getValue()); - } else { - console.log('discard'); - this.fileService.discardBackup(resource); - } - } - } - private onTextFileDirty(e: TextFileModelChangeEvent): void { if ((this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) && !this.isDocumentedEdited) { diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 0b1c233f2772f..ff532292618a8 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -78,7 +78,7 @@ export class FileService implements IFileService { // create service const workspace = this.contextService.getWorkspace(); - this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService); + this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService, this.configurationService); // Listeners this.registerListeners(); @@ -244,6 +244,10 @@ export class FileService implements IFileService { return this.raw.discardBackups(); } + public isHotExitEnabled(): boolean { + return this.raw.isHotExitEnabled(); + } + private doMoveItemToTrash(resource: uri): TPromise { const workspace = this.contextService.getWorkspace(); if (!workspace) { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index ec2b175c70f58..ebdcc61668756 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -31,8 +31,11 @@ import flow = require('vs/base/node/flow'); import {FileWatcher as UnixWatcherService} from 'vs/workbench/services/files/node/watcher/unix/watcherService'; import {FileWatcher as WindowsWatcherService} from 'vs/workbench/services/files/node/watcher/win32/watcherService'; import {toFileChangesEvent, normalize, IRawFileChange} from 'vs/workbench/services/files/node/watcher/common'; +import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import {IEnvironmentService} from 'vs/platform/environment/common/environment'; import {IEventService} from 'vs/platform/event/common/event'; +import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; +import {IFilesConfiguration} from 'vs/platform/files/common/files'; export interface IEncodingOverride { resource: uri; @@ -74,6 +77,7 @@ export class FileService implements IFileService { private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private static MAX_DEGREE_OF_PARALLEL_FS_OPS = 10; // degree of parallel fs calls that we accept at the same time + private toUnbind: IDisposable[]; private basePath: string; private tmpPath: string; private options: IFileServiceOptions; @@ -84,7 +88,9 @@ export class FileService implements IFileService { private fileChangesWatchDelayer: ThrottledDelayer; private undeliveredRawFileChangesEvents: IRawFileChange[]; - constructor(basePath: string, options: IFileServiceOptions, private eventEmitter: IEventService, private environmentService: IEnvironmentService) { + private configuredHotExit: boolean; + + constructor(basePath: string, options: IFileServiceOptions, private eventEmitter: IEventService, private environmentService: IEnvironmentService, private configurationService: IConfigurationService) { this.basePath = basePath ? paths.normalize(basePath) : void 0; if (this.basePath && this.basePath.indexOf('\\\\') === 0 && strings.endsWith(this.basePath, paths.sep)) { @@ -117,6 +123,17 @@ export class FileService implements IFileService { this.activeFileChangesWatchers = Object.create(null); this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); this.undeliveredRawFileChangesEvents = []; + + // Configuration changes + this.toUnbind = []; + this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config))); + + const configuration = this.configurationService.getConfiguration(); + this.onConfigurationChange(configuration); + } + + private onConfigurationChange(configuration: IFilesConfiguration): void { + this.configuredHotExit = configuration && configuration.files && configuration.files.hotExit; } public updateOptions(options: IFileServiceOptions): void { @@ -456,6 +473,10 @@ export class FileService implements IFileService { return this.del(uri.file(paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID))); } + public isHotExitEnabled(): boolean { + return this.configuredHotExit; + } + // Helpers private toAbsolutePath(arg1: uri | IFileStat): string { @@ -693,6 +714,8 @@ export class FileService implements IFileService { watcher.close(); } this.activeFileChangesWatchers = Object.create(null); + + this.toUnbind = dispose(this.toUnbind); } } diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index e929d7103a726..9855dd227ed45 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -33,7 +33,7 @@ suite('FileService', () => { extfs.copy(sourceDir, testDir, () => { events = new utils.TestEventService(); - service = new FileService(testDir, { disableWatcher: true }, events, null); + service = new FileService(testDir, { disableWatcher: true }, events, null, null); done(); }); }); @@ -494,7 +494,7 @@ suite('FileService', () => { encoding: 'windows1252', encodingOverride: encodingOverride, disableWatcher: true - }, null, null); + }, null, null, null); _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).done(c => { assert.equal(c.encoding, 'windows1252'); @@ -520,7 +520,7 @@ suite('FileService', () => { let _service = new FileService(_testDir, { disableWatcher: true - }, null, null); + }, null, null, null); extfs.copy(_sourceDir, _testDir, () => { fs.readFile(resource.fsPath, (error, data) => { diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index 37e51c056582e..60057649a7dc7 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -16,11 +16,6 @@ export interface IUntitledEditorService { _serviceBrand: any; - /** - * Events for when untitled editors content change. - */ - onDidChangeContent: Event; - /** * Events for when untitled editors change (e.g. getting dirty, saved or reverted). */ @@ -78,20 +73,14 @@ export class UntitledEditorService implements IUntitledEditorService { private static CACHE: { [resource: string]: UntitledEditorInput } = Object.create(null); private static KNOWN_ASSOCIATED_FILE_PATHS: { [resource: string]: boolean } = Object.create(null); - private _onDidChangeContent: Emitter; private _onDidChangeDirty: Emitter; private _onDidChangeEncoding: Emitter; constructor(@IInstantiationService private instantiationService: IInstantiationService) { - this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); this._onDidChangeEncoding = new Emitter(); } - public get onDidChangeContent(): Event { - return this._onDidChangeContent.event; - } - public get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } @@ -182,10 +171,6 @@ export class UntitledEditorService implements IUntitledEditorService { this._onDidChangeEncoding.fire(resource); }); - const updateListener = input.onDidChangeContent(() => { - this._onDidChangeContent.fire(resource); - }); - // Remove from cache on dispose const onceDispose = once(input.onDispose); onceDispose(() => { @@ -193,7 +178,6 @@ export class UntitledEditorService implements IUntitledEditorService { delete UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[input.getResource().toString()]; dirtyListener.dispose(); encodingListener.dispose(); - updateListener.dispose(); }); // Add to cache From 54108a54eceaf5db6e8a8abeeb1ee21118a799b5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 21 Sep 2016 20:27:24 -0700 Subject: [PATCH 11/45] Use unique backup path for file and untitled schemes --- .../services/files/node/fileService.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index ebdcc61668756..5206b1ba38843 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -453,24 +453,20 @@ export class FileService implements IFileService { } public backupFile(resource: uri, content: string): TPromise { - // TODO: Implement properly - const backupName = paths.basename(resource.fsPath); - const backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); - const backupResource = uri.file(backupPath); - + if (resource.scheme === 'file') { + // TODO: Persist hash -> file map on disk (json file?) + } + const backupResource = this.getBackupPath(resource); console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); } public discardBackup(resource: uri): TPromise { - const backupName = paths.basename(resource.fsPath); - const backupPath = paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID, backupName); - const backupResource = uri.file(backupPath); - return this.del(backupResource); + return this.del(this.getBackupPath(resource)); } public discardBackups(): TPromise { - return this.del(uri.file(paths.join(this.environmentService.userDataPath, 'File Backups', FileService.SESSION_BACKUP_ID))); + return this.del(uri.file(this.getBackupRoot())); } public isHotExitEnabled(): boolean { @@ -479,6 +475,16 @@ export class FileService implements IFileService { // Helpers + private getBackupPath(resource: uri): uri { + const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); + const backupPath = paths.join(this.getBackupRoot(), resource.scheme, backupName); + return uri.file(backupPath); + } + + private getBackupRoot(): string { + return paths.join(this.environmentService.userDataPath, 'Backups', FileService.SESSION_BACKUP_ID); + } + private toAbsolutePath(arg1: uri | IFileStat): string { let resource: uri; if (arg1 instanceof uri) { From c860d62cba4b97778db15d6729a02a468c2afe6a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Sep 2016 03:23:37 -0700 Subject: [PATCH 12/45] Add new workspaces to backups as they are created on main process --- src/vs/code/electron-main/backup.ts | 66 ++++++++++++++++++++++++++++ src/vs/code/electron-main/main.ts | 8 ++-- src/vs/code/electron-main/windows.ts | 25 ++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/vs/code/electron-main/backup.ts diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts new file mode 100644 index 0000000000000..119029acb034f --- /dev/null +++ b/src/vs/code/electron-main/backup.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as fs from 'original-fs'; +import * as path from 'path'; +import * as arrays from 'vs/base/common/arrays'; +import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; +import {IEnvService} from 'vs/code/electron-main/env'; + +export const IBackupService = createDecorator('backupService'); + +export interface IBackupService { + getBackupWorkspaces(): string[]; + clearBackupWorkspaces(): void; + pushBackupWorkspaces(workspaces: string[]): void; +} + +export class BackupService implements IBackupService { + + private filePath: string; + private fileContent: string[]; + + constructor( + @IEnvService private envService: IEnvService + ) { + this.filePath = path.join(envService.appHome, 'Backups', 'workspaces.json'); + } + + public getBackupWorkspaces(): string[] { + if (!this.fileContent) { + this.load(); + } + return this.fileContent; + } + + public clearBackupWorkspaces(): void { + this.fileContent = []; + this.save(); + } + + public pushBackupWorkspaces(workspaces: string[]): void { + this.fileContent = arrays.distinct(this.fileContent.concat(workspaces).filter(workspace => { + return workspace !== null; + })); + this.save(); + } + + private load(): void { + try { + this.fileContent = JSON.parse(fs.readFileSync(this.filePath).toString()); // invalid JSON or permission issue can happen here + } catch (error) { + this.fileContent = []; + } + } + + private save(): void { + try { + fs.writeFileSync(this.filePath, JSON.stringify(this.fileContent)); + } catch (error) { + } + } +} \ No newline at end of file diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 7315a2005c495..0be51a99a9dbc 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -33,6 +33,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService, MainLogService } from 'vs/code/electron-main/log'; import { IStorageService, StorageService } from 'vs/code/electron-main/storage'; +import { IBackupService, BackupService } from 'vs/code/electron-main/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -219,11 +220,11 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: IProce // Open our first window if (envService.cliArgs['new-window'] && envService.cliArgs.paths.length === 0) { - windowsService.open({ cli: envService.cliArgs, forceNewWindow: true, forceEmpty: true }); // new window if "-n" was used without paths + windowsService.open({ cli: envService.cliArgs, forceNewWindow: true, forceEmpty: true, restoreBackups: true }); // new window if "-n" was used without paths } else if (global.macOpenFiles && global.macOpenFiles.length && (!envService.cliArgs.paths || !envService.cliArgs.paths.length)) { - windowsService.open({ cli: envService.cliArgs, pathsToOpen: global.macOpenFiles }); // mac: open-file event received on startup + windowsService.open({ cli: envService.cliArgs, pathsToOpen: global.macOpenFiles, restoreBackups: true }); // mac: open-file event received on startup } else { - windowsService.open({ cli: envService.cliArgs, forceNewWindow: envService.cliArgs['new-window'], diffMode: envService.cliArgs.diff }); // default: read paths from cli + windowsService.open({ cli: envService.cliArgs, forceNewWindow: envService.cliArgs['new-window'], diffMode: envService.cliArgs.diff, restoreBackups: true }); // default: read paths from cli } } @@ -422,6 +423,7 @@ function start(): void { services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); services.set(IRequestService, new SyncDescriptor(RequestService)); services.set(IUpdateService, new SyncDescriptor(UpdateManager)); + services.set(IBackupService, new SyncDescriptor(BackupService)); const instantiationService = new InstantiationService(services); diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index ea64a23dd336b..5ffe87d36eb7b 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -14,6 +14,7 @@ import * as types from 'vs/base/common/types'; import * as arrays from 'vs/base/common/arrays'; import { assign, mixin } from 'vs/base/common/objects'; import { EventEmitter } from 'events'; +import { IBackupService } from 'vs/code/electron-main/backup'; import { IStorageService } from 'vs/code/electron-main/storage'; import { IPath, VSCodeWindow, ReadyState, IWindowConfiguration, IWindowState as ISingleWindowState, defaultWindowState, IWindowSettings } from 'vs/code/electron-main/window'; import { ipcMain as ipc, app, screen, crashReporter, BrowserWindow, dialog } from 'electron'; @@ -46,6 +47,7 @@ export interface IOpenConfiguration { forceEmpty?: boolean; windowToUse?: VSCodeWindow; diffMode?: boolean; + restoreBackups?: boolean; } interface IWindowState { @@ -163,7 +165,8 @@ export class WindowsManager implements IWindowsService { @IEnvService private envService: IEnvService, @ILifecycleService private lifecycleService: ILifecycleService, @IUpdateService private updateService: IUpdateService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IBackupService private backupService: IBackupService ) { } onOpen(clb: (path: IPath) => void): () => void { @@ -599,6 +602,21 @@ export class WindowsManager implements IWindowsService { iPathsToOpen = this.cliToPaths(openConfig.cli, ignoreFileNotFound); } + // Restore backups if they exist and it's the first instance + if (openConfig.restoreBackups) { + const backupWorkspaces = this.backupService.getBackupWorkspaces(); + if (backupWorkspaces.length > 0) { + backupWorkspaces.forEach(workspace => { + iPathsToOpen.push(this.toIPath(workspace)); + }); + // Get rid of duplicates + iPathsToOpen = arrays.distinct(iPathsToOpen, path => { + return path.workspacePath; + }); + this.backupService.clearBackupWorkspaces(); + } + } + let filesToOpen: IPath[] = []; let filesToDiff: IPath[] = []; let foldersToOpen = iPathsToOpen.filter(iPath => iPath.workspacePath && !iPath.filePath); @@ -727,6 +745,11 @@ export class WindowsManager implements IWindowsService { // Emit events iPathsToOpen.forEach(iPath => this.eventEmitter.emit(EventTypes.OPEN, iPath)); + // Add to backups + this.backupService.pushBackupWorkspaces(iPathsToOpen.map((path) => { + return path.workspacePath; + })); + return arrays.distinct(usedWindows); } From a638f0277ac708c0d8d0bb6b1696066d5d8098e1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Sep 2016 16:10:24 -0700 Subject: [PATCH 13/45] Remove workspace from workspaces.json when not last window --- src/vs/platform/backup/common/backup.ts | 19 ++++++ src/vs/test/utils/servicesTestUtils.ts | 8 ++- src/vs/workbench/common/backup.ts | 66 +++++++++++++++++++ src/vs/workbench/electron-browser/shell.ts | 6 ++ .../parts/files/common/textFileService.ts | 23 +++++-- .../files/electron-browser/textFileService.ts | 4 +- 6 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 src/vs/platform/backup/common/backup.ts create mode 100644 src/vs/workbench/common/backup.ts diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts new file mode 100644 index 0000000000000..2129e7132692b --- /dev/null +++ b/src/vs/platform/backup/common/backup.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; + +export const IBackupService = createDecorator('backupService'); + +export interface IBackupService { + _serviceBrand: any; + + getBackupWorkspaces(): string[]; + clearBackupWorkspaces(): void; + pushBackupWorkspaces(workspaces: string[]): void; + removeWorkspace(workspace: string): void; +} diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index 21de5d06a7d32..d7717c8e96704 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -18,6 +18,7 @@ import Event, {Emitter} from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import {IConfigurationService, getConfigurationValue, IConfigurationValue} from 'vs/platform/configuration/common/configuration'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; +import {IBackupService} from 'vs/platform/backup/common/backup'; import {IQuickOpenService} from 'vs/workbench/services/quickopen/common/quickOpenService'; import {IPartService} from 'vs/workbench/services/part/common/partService'; import {IEditorInput, IEditorOptions, IEditorModel, Position, Direction, IEditor, IResourceInput, ITextEditorModel} from 'vs/platform/editor/common/editor'; @@ -106,9 +107,10 @@ export class TestTextFileService extends TextFileService { @IEditorGroupService editorGroupService: IEditorGroupService, @IFileService fileService: IFileService, @IUntitledEditorService untitledEditorService: IUntitledEditorService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IBackupService backupService: IBackupService ) { - super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, editorService, fileService, untitledEditorService, instantiationService); + super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, editorService, fileService, untitledEditorService, instantiationService, backupService); } public setPromptPath(path: string): void { @@ -612,7 +614,7 @@ export class TestLifecycleService implements ILifecycleService { public _serviceBrand: any; public willShutdown: boolean; - + private _onWillShutdown = new Emitter(); private _onShutdown = new Emitter(); diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts new file mode 100644 index 0000000000000..b972b9dd3152d --- /dev/null +++ b/src/vs/workbench/common/backup.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as fs from 'original-fs'; +import * as path from 'path'; +import * as arrays from 'vs/base/common/arrays'; +import {IBackupService} from 'vs/platform/backup/common/backup'; +import {IEnvironmentService} from 'vs/platform/environment/common/environment'; + +export class BackupService implements IBackupService { + + public _serviceBrand: any; + + private filePath: string; + private fileContent: string[]; + + constructor( + @IEnvironmentService private environmentService: IEnvironmentService + ) { + this.filePath = path.join(environmentService.userDataPath, 'Backups', 'workspaces.json'); + } + + public getBackupWorkspaces(): string[] { + this.load(); + return this.fileContent; + } + + public clearBackupWorkspaces(): void { + this.fileContent = []; + this.save(); + } + + public pushBackupWorkspaces(workspaces: string[]): void { + this.fileContent = arrays.distinct(this.fileContent.concat(workspaces).filter(workspace => { + return workspace !== null; + })); + this.save(); + } + + public removeWorkspace(workspace: string): void { + this.load(); + this.fileContent = this.fileContent.filter((ws) => { + return ws !== workspace; + }); + this.save(); + } + + private load(): void { + try { + this.fileContent = JSON.parse(fs.readFileSync(this.filePath).toString()); // invalid JSON or permission issue can happen here + } catch (error) { + this.fileContent = []; + } + } + + private save(): void { + try { + fs.writeFileSync(this.filePath, JSON.stringify(this.fileContent)); + } catch (error) { + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 5aa5f5dad682f..bd62972681f43 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -22,6 +22,7 @@ import {ContextViewService} from 'vs/platform/contextview/browser/contextViewSer import timer = require('vs/base/common/timer'); import {Workbench} from 'vs/workbench/electron-browser/workbench'; import {Storage, inMemoryLocalStorageInstance} from 'vs/workbench/common/storage'; +import {BackupService} from 'vs/workbench/common/backup'; import {ITelemetryService, NullTelemetryService, loadExperiments} from 'vs/platform/telemetry/common/telemetry'; import {ITelemetryAppenderChannel, TelemetryAppenderClient} from 'vs/platform/telemetry/common/telemetryIpc'; import {TelemetryService, ITelemetryServiceConfig} from 'vs/platform/telemetry/common/telemetryService'; @@ -51,6 +52,7 @@ import {IEditorWorkerService} from 'vs/editor/common/services/editorWorkerServic import {MainProcessExtensionService} from 'vs/workbench/api/node/mainThreadExtensionService'; import {IOptions} from 'vs/workbench/common/options'; import {IStorageService} from 'vs/platform/storage/common/storage'; +import {IBackupService} from 'vs/platform/backup/common/backup'; import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection'; import {InstantiationService} from 'vs/platform/instantiation/common/instantiationService'; import {IContextViewService} from 'vs/platform/contextview/browser/contextView'; @@ -240,6 +242,10 @@ export class WorkbenchShell { }); }, errors.onUnexpectedError); + // Backup + const backupService = instantiationService.createInstance(BackupService); + serviceCollection.set(IBackupService, backupService); + // Storage const disableWorkspaceStorage = this.environmentService.extensionTestsPath || (!this.workspace && !this.environmentService.extensionDevelopmentPath); // without workspace or in any extension test, we use inMemory storage unless we develop an extension where we want to preserve state this.storageService = instantiationService.createInstance(Storage, window.localStorage, disableWorkspaceStorage ? inMemoryLocalStorageInstance : window.localStorage); diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index dd9168cea6da4..47fecf76f2480 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -26,6 +26,7 @@ import {UntitledEditorModel} from 'vs/workbench/common/editor/untitledEditorMode import {BinaryEditorModel} from 'vs/workbench/common/editor/binaryEditorModel'; import {TextFileEditorModelManager} from 'vs/workbench/parts/files/common/editors/textFileEditorModelManager'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +import {IBackupService} from 'vs/platform/backup/common/backup'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -58,7 +59,8 @@ export abstract class TextFileService implements ITextFileService { @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IFileService protected fileService: IFileService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IBackupService private backupService: IBackupService ) { this.toUnbind = []; @@ -115,14 +117,21 @@ export abstract class TextFileService implements ITextFileService { private beforeShutdown(): boolean | TPromise { - // Dirty files need treatment on shutdown - if (this.getDirty().length) { - // If hot exit is enabled then save the dirty files in the workspace and then exit - if (this.configuredHotExit) { - // TODO: Do a last minute backup if required - // TODO: If this is the only instance opened, perform hot exit + // If hot exit is enabled then save the dirty files in the workspace and then exit + if (this.configuredHotExit) { + // TODO: Check if dirty + // TODO: Do a last minute backup if required + // TODO: There may be a better way of verifying that only a single window is open? + // Only remove the workspace from the backup service if it's not the last one or it's not dirty + if (this.backupService.getBackupWorkspaces().length > 1 || this.getDirty().length === 0) { + this.backupService.removeWorkspace(this.contextService.getWorkspace().resource.fsPath); + } else { + return false; // the backup will be restored, no veto } + } + // Dirty files need treatment on shutdown + if (this.getDirty().length) { // If auto save is enabled, save all files and then check again for dirty files if (this.getAutoSaveMode() !== AutoSaveMode.OFF) { return this.saveAll(false /* files only */).then(() => { diff --git a/src/vs/workbench/parts/files/electron-browser/textFileService.ts b/src/vs/workbench/parts/files/electron-browser/textFileService.ts index 76c1d01294ba5..83819a9160201 100644 --- a/src/vs/workbench/parts/files/electron-browser/textFileService.ts +++ b/src/vs/workbench/parts/files/electron-browser/textFileService.ts @@ -29,6 +29,7 @@ import {ModelBuilder} from 'vs/editor/node/model/modelBuilder'; import product from 'vs/platform/product'; import {IEnvironmentService} from 'vs/platform/environment/common/environment'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +import {IBackupService} from 'vs/platform/backup/common/backup'; export class TextFileService extends AbstractTextFileService { @@ -42,6 +43,7 @@ export class TextFileService extends AbstractTextFileService { @IInstantiationService instantiationService: IInstantiationService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, + @IBackupService backupService: IBackupService, @IModeService private modeService: IModeService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IEditorGroupService editorGroupService: IEditorGroupService, @@ -49,7 +51,7 @@ export class TextFileService extends AbstractTextFileService { @IModelService private modelService: IModelService, @IEnvironmentService private environmentService: IEnvironmentService ) { - super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, editorService, fileService, untitledEditorService, instantiationService); + super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, editorService, fileService, untitledEditorService, instantiationService, backupService); } public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise { From 61d2696bba905dfbe8f304c1bdb9b3583d193cc3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Oct 2016 01:45:25 -0700 Subject: [PATCH 14/45] Get hot exit working end-to-end (very rough) --- src/vs/code/electron-main/backup.ts | 37 ++++++--- src/vs/code/electron-main/windows.ts | 22 +++--- src/vs/platform/backup/common/backup.ts | 7 +- src/vs/workbench/common/backup.ts | 75 ++++++++++++++----- src/vs/workbench/common/editor.ts | 2 + src/vs/workbench/common/options.ts | 2 + src/vs/workbench/electron-browser/main.ts | 1 + .../workbench/electron-browser/workbench.ts | 17 ++++- .../files/common/editors/fileEditorInput.ts | 7 +- src/vs/workbench/parts/files/common/files.ts | 2 + .../node/configurationEditingService.test.ts | 2 +- .../services/editor/browser/editorService.ts | 20 +++-- .../files/electron-browser/fileService.ts | 6 +- .../services/files/node/fileService.ts | 19 ++++- .../files/test/node/fileService.test.ts | 6 +- .../textfile/common/textFileEditorModel.ts | 43 ++++++++--- .../common/textFileEditorModelManager.ts | 5 +- .../services/textfile/common/textfiles.ts | 4 +- .../common/editor/editorStacksModel.test.ts | 3 + 19 files changed, 208 insertions(+), 72 deletions(-) diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts index cdb354c424994..abb948dad273f 100644 --- a/src/vs/code/electron-main/backup.ts +++ b/src/vs/code/electron-main/backup.ts @@ -7,7 +7,6 @@ import * as fs from 'original-fs'; import * as path from 'path'; -import * as arrays from 'vs/base/common/arrays'; import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -17,43 +16,61 @@ export interface IBackupService { getBackupWorkspaces(): string[]; clearBackupWorkspaces(): void; pushBackupWorkspaces(workspaces: string[]): void; + getBackupFiles(workspace: string): string[]; +} + +interface IBackupFormat { + folderWorkspaces?: { + [workspacePath: string]: string[] + }; } export class BackupService implements IBackupService { private filePath: string; - private fileContent: string[]; + private fileContent: IBackupFormat; constructor( - @IEnvironmentService private envService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService ) { - this.filePath = path.join(envService.userDataPath, 'Backups', 'workspaces.json'); + this.filePath = path.join(environmentService.userDataPath, 'Backups', 'workspaces.json'); } public getBackupWorkspaces(): string[] { if (!this.fileContent) { this.load(); } - return this.fileContent; + return Object.keys(this.fileContent.folderWorkspaces || {}); } public clearBackupWorkspaces(): void { - this.fileContent = []; + this.fileContent = { + folderWorkspaces: {} + }; this.save(); } public pushBackupWorkspaces(workspaces: string[]): void { - this.fileContent = arrays.distinct(this.fileContent.concat(workspaces).filter(workspace => { - return workspace !== null; - })); + this.load(); + if (!this.fileContent.folderWorkspaces) { + this.fileContent.folderWorkspaces = {}; + } + workspaces.forEach(workspace => { + this.fileContent.folderWorkspaces[workspace] = this.fileContent.folderWorkspaces[workspace] || []; + }); this.save(); } + public getBackupFiles(workspace: string): string[] { + this.load(); + return this.fileContent.folderWorkspaces[workspace]; + } + private load(): void { try { this.fileContent = JSON.parse(fs.readFileSync(this.filePath).toString()); // invalid JSON or permission issue can happen here } catch (error) { - this.fileContent = []; + this.fileContent = {}; } } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index f2a75b2e7acdc..e695032bba368 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -605,19 +605,16 @@ export class WindowsManager implements IWindowsService { iPathsToOpen = this.cliToPaths(openConfig.cli, ignoreFileNotFound); } - // Restore backups if they exist and it's the first instance + // Add any existing backup workspaces if (openConfig.restoreBackups) { - const backupWorkspaces = this.backupService.getBackupWorkspaces(); - if (backupWorkspaces.length > 0) { - backupWorkspaces.forEach(workspace => { - iPathsToOpen.push(this.toIPath(workspace)); - }); - // Get rid of duplicates - iPathsToOpen = arrays.distinct(iPathsToOpen, path => { - return path.workspacePath; - }); - this.backupService.clearBackupWorkspaces(); - } + // TODO: Ensure the workspaces being added actually have backups + this.backupService.getBackupWorkspaces().forEach(ws => { + iPathsToOpen.push(this.toIPath(ws)); + }); + // Get rid of duplicates + iPathsToOpen = arrays.distinct(iPathsToOpen, path => { + return path.workspacePath; + }); } let filesToOpen: IPath[] = []; @@ -749,6 +746,7 @@ export class WindowsManager implements IWindowsService { iPathsToOpen.forEach(iPath => this.eventEmitter.emit(EventTypes.OPEN, iPath)); // Add to backups + console.log('iPathsToOpen', iPathsToOpen); this.backupService.pushBackupWorkspaces(iPathsToOpen.map((path) => { return path.workspacePath; })); diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index 2129e7132692b..975f50e2196b2 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -6,6 +6,7 @@ 'use strict'; import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; +import Uri from 'vs/base/common/uri'; export const IBackupService = createDecorator('backupService'); @@ -14,6 +15,10 @@ export interface IBackupService { getBackupWorkspaces(): string[]; clearBackupWorkspaces(): void; - pushBackupWorkspaces(workspaces: string[]): void; removeWorkspace(workspace: string): void; + + registerBackupFile(resource: Uri): void; + deregisterBackupFile(resource: Uri): void; + getBackupFiles(workspace: string): string[]; + getBackupResource(resource: Uri): Uri; } diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts index b972b9dd3152d..df70000154ace 100644 --- a/src/vs/workbench/common/backup.ts +++ b/src/vs/workbench/common/backup.ts @@ -5,61 +5,98 @@ 'use strict'; -import * as fs from 'original-fs'; import * as path from 'path'; +import * as crypto from 'crypto'; +import * as fs from 'original-fs'; import * as arrays from 'vs/base/common/arrays'; -import {IBackupService} from 'vs/platform/backup/common/backup'; +import Uri from 'vs/base/common/uri'; +import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {IEnvironmentService} from 'vs/platform/environment/common/environment'; +import {IBackupService} from 'vs/platform/backup/common/backup'; + +interface IBackupFormat { + folderWorkspaces?: { + [workspacePath: string]: string[] + }; +} export class BackupService implements IBackupService { public _serviceBrand: any; - private filePath: string; - private fileContent: string[]; + private workspaceResource: Uri; + private workspacesJsonFilePath: string; + private fileContent: IBackupFormat; constructor( - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IWorkspaceContextService contextService: IWorkspaceContextService ) { - this.filePath = path.join(environmentService.userDataPath, 'Backups', 'workspaces.json'); + this.workspacesJsonFilePath = path.join(environmentService.userDataPath, 'Backups', 'workspaces.json'); + this.workspaceResource = contextService.getWorkspace().resource; } public getBackupWorkspaces(): string[] { this.load(); - return this.fileContent; + return Object.keys(this.fileContent.folderWorkspaces || {}); } public clearBackupWorkspaces(): void { - this.fileContent = []; + this.fileContent = { + folderWorkspaces: {} + }; this.save(); } - public pushBackupWorkspaces(workspaces: string[]): void { - this.fileContent = arrays.distinct(this.fileContent.concat(workspaces).filter(workspace => { - return workspace !== null; - })); + public removeWorkspace(workspace: string): void { + this.load(); + if (!this.fileContent.folderWorkspaces) { + return; + } + delete this.fileContent.folderWorkspaces[workspace]; this.save(); } - public removeWorkspace(workspace: string): void { + public getBackupFiles(workspace: string): string[] { + this.load(); + return this.fileContent.folderWorkspaces[workspace]; + } + + public getBackupResource(resource: Uri): Uri { + + let workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); + const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); + const backupPath = path.join(this.environmentService.userDataPath, 'Backups', workspaceHash, resource.scheme, backupName); + console.log('getBackupResource ' + Uri.file(backupPath)); + return Uri.file(backupPath); + } + + public registerBackupFile(resource: Uri): void { + this.load(); + if (arrays.contains(this.fileContent.folderWorkspaces[this.workspaceResource.fsPath], resource.fsPath)) { + return; + } + this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].push(resource.fsPath); + this.save(); + } + + public deregisterBackupFile(resource: Uri): void { this.load(); - this.fileContent = this.fileContent.filter((ws) => { - return ws !== workspace; - }); + this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].filter(value => value !== resource.fsPath); this.save(); } private load(): void { try { - this.fileContent = JSON.parse(fs.readFileSync(this.filePath).toString()); // invalid JSON or permission issue can happen here + this.fileContent = JSON.parse(fs.readFileSync(this.workspacesJsonFilePath).toString()); // invalid JSON or permission issue can happen here } catch (error) { - this.fileContent = []; + this.fileContent = {}; } } private save(): void { try { - fs.writeFileSync(this.filePath, JSON.stringify(this.fileContent)); + fs.writeFileSync(this.workspacesJsonFilePath, JSON.stringify(this.fileContent)); } catch (error) { } } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index a8ee5aa293097..0b72da84a066c 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -343,6 +343,8 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport { */ setResource(resource: URI): void; + setRestoreResource(resource: URI): void; + /** * Sets the preferred encodingt to use for this input. */ diff --git a/src/vs/workbench/common/options.ts b/src/vs/workbench/common/options.ts index d74f2844fb754..08f6b5ad60659 100644 --- a/src/vs/workbench/common/options.ts +++ b/src/vs/workbench/common/options.ts @@ -22,4 +22,6 @@ export interface IOptions { * Instructs the workbench to open a diff of the provided files right after startup. */ filesToDiff?: IResourceInput[]; + + filesToRestore?: IResourceInput[]; } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index bd1902de4637e..0055601b24731 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -48,6 +48,7 @@ export function startup(configuration: IWindowConfiguration): TPromise { const filesToOpen = configuration.filesToOpen && configuration.filesToOpen.length ? toInputs(configuration.filesToOpen) : null; const filesToCreate = configuration.filesToCreate && configuration.filesToCreate.length ? toInputs(configuration.filesToCreate) : null; const filesToDiff = configuration.filesToDiff && configuration.filesToDiff.length ? toInputs(configuration.filesToDiff) : null; + const shellOptions: IOptions = { filesToOpen, filesToCreate, diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 312f556882909..520edbd620013 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -16,6 +16,7 @@ import {Delayer} from 'vs/base/common/async'; import assert = require('vs/base/common/assert'); import timer = require('vs/base/common/timer'); import errors = require('vs/base/common/errors'); +import Uri from 'vs/base/common/uri'; import {toErrorMessage} from 'vs/base/common/errorMessage'; import {Registry} from 'vs/platform/platform'; import {isWindows, isLinux} from 'vs/base/common/platform'; @@ -74,6 +75,7 @@ import {MenuService} from 'vs/platform/actions/common/menuService'; import {IContextMenuService} from 'vs/platform/contextview/browser/contextView'; import {IEnvironmentService} from 'vs/platform/environment/common/environment'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; +import {IBackupService} from 'vs/platform/backup/common/backup'; export const MessagesVisibleContext = new RawContextKey('globalMessageVisible', false); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); @@ -155,7 +157,8 @@ export class Workbench implements IPartService { @ILifecycleService private lifecycleService: ILifecycleService, @IMessageService private messageService: IMessageService, @ITelemetryService private telemetryService: ITelemetryService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IBackupService private backupService: IBackupService ) { this.container = container; @@ -165,7 +168,12 @@ export class Workbench implements IPartService { serviceCollection }; - this.hasFilesToCreateOpenOrDiff = (options.filesToCreate && options.filesToCreate.length > 0) || (options.filesToOpen && options.filesToOpen.length > 0) || (options.filesToDiff && options.filesToDiff.length > 0); + // Restore any backups if they exist + options.filesToRestore = this.backupService.getBackupFiles(workspace.resource.fsPath).map(filePath => { + return { resource: Uri.file(filePath) }; + }); + + this.hasFilesToCreateOpenOrDiff = (options.filesToCreate && options.filesToCreate.length > 0) || (options.filesToOpen && options.filesToOpen.length > 0) || (options.filesToDiff && options.filesToDiff.length > 0) || (options.filesToRestore.length > 0); this.toDispose = []; this.toShutdown = []; @@ -292,6 +300,7 @@ export class Workbench implements IPartService { const wbopt = this.workbenchParams.options; const filesToCreate = wbopt.filesToCreate || []; const filesToOpen = wbopt.filesToOpen || []; + const filesToRestore = wbopt.filesToRestore || []; const filesToDiff = wbopt.filesToDiff; // Files to diff is exclusive @@ -311,7 +320,9 @@ export class Workbench implements IPartService { options.push(...filesToCreate.map(r => null)); // fill empty options for files to create because we dont have options there // Files to open - return TPromise.join(filesToOpen.map(resourceInput => this.editorService.createInput(resourceInput))).then((inputsToOpen) => { + let filesToOpenInputPromise = filesToOpen.map(resourceInput => this.editorService.createInput(resourceInput)); + let filesToRestoreInputPromise = filesToRestore.map(resourceInput => this.editorService.createInput(resourceInput, true)); + return TPromise.join(filesToOpenInputPromise.concat(filesToRestoreInputPromise)).then((inputsToOpen) => { inputs.push(...inputsToOpen); options.push(...filesToOpen.map(resourceInput => TextEditorOptions.from(resourceInput))); diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 15a70a2309139..e24f50f3fd1a4 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -24,6 +24,7 @@ import {IHistoryService} from 'vs/workbench/services/history/common/history'; */ export class FileEditorInput extends CommonFileEditorInput { private resource: URI; + private restoreResource: URI; private preferredEncoding: string; private forceOpenAsBinary: boolean; @@ -102,6 +103,10 @@ export class FileEditorInput extends CommonFileEditorInput { this.verboseDescription = null; } + public setRestoreResource(resource: URI): void { + this.restoreResource = resource; + } + public getResource(): URI { return this.resource; } @@ -195,7 +200,7 @@ export class FileEditorInput extends CommonFileEditorInput { } public resolve(refresh?: boolean): TPromise { - return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh).then(null, error => { + return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh, this.restoreResource).then(null, error => { // In case of an error that indicates that the file is binary or too large, just return with the binary editor model if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 9b03fadd5a0ef..794fb8ec88d52 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -42,6 +42,8 @@ export abstract class FileEditorInput extends EditorInput implements IFileEditor public abstract setResource(resource: URI): void; + public abstract setRestoreResource(resource: URI): void; + public abstract getResource(): URI; public abstract setPreferredEncoding(encoding: string): void; diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts index 0820137dafa49..a38291d318360 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts @@ -87,7 +87,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { const configurationService = new WorkspaceConfigurationService(workspaceContextService, new TestEventService(), environmentService); const textFileService = workbenchInstantiationService().createInstance(TestDirtyTextFileService, dirty); const events = new utils.TestEventService(); - const fileService = new FileService(noWorkspace ? null : workspaceDir, { disableWatcher: true }, events, environmentService, configurationService); + const fileService = new FileService(noWorkspace ? null : workspaceDir, { disableWatcher: true }, events, environmentService, configurationService, null, null); return configurationService.initialize().then(() => { return { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 37021f28a3d9d..bac9d0650bc88 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -22,6 +22,7 @@ import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/edito import {IEditorInput, IEditorModel, IEditorOptions, ITextEditorOptions, Position, Direction, IEditor, IResourceInput, ITextEditorModel} from 'vs/platform/editor/common/editor'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {AsyncDescriptor0} from 'vs/platform/instantiation/common/descriptors'; +import {IBackupService} from 'vs/platform/backup/common/backup'; export interface IEditorPart { openEditor(input?: IEditorInput, options?: IEditorOptions|ITextEditorOptions, sideBySide?: boolean): TPromise; @@ -46,6 +47,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { constructor( editorPart: IEditorPart | IWorkbenchEditorService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, + @IBackupService private backupService: IBackupService, @IInstantiationService private instantiationService?: IInstantiationService ) { this.editorPart = editorPart; @@ -107,7 +109,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { const schema = resourceInput.resource.scheme; if (schema === network.Schemas.http || schema === network.Schemas.https) { window.open(resourceInput.resource.toString(true)); - + return TPromise.as(null); } } @@ -208,8 +210,8 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { } public createInput(input: EditorInput): TPromise; - public createInput(input: IResourceInput): TPromise; - public createInput(input: any): TPromise { + public createInput(input: IResourceInput, restoreFromBackup?: boolean): TPromise; + public createInput(input: any, restoreFromBackup?: boolean): TPromise { // Workbench Input Support if (input instanceof EditorInput) { @@ -263,7 +265,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { // Base Text Editor Support for file resources else if (this.fileInputDescriptor && resourceInput.resource instanceof URI && resourceInput.resource.scheme === network.Schemas.file) { - return this.createFileInput(resourceInput.resource, resourceInput.encoding); + return this.createFileInput(resourceInput.resource, resourceInput.encoding, restoreFromBackup); } // Treat an URI as ResourceEditorInput @@ -277,11 +279,15 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { return TPromise.as(null); } - private createFileInput(resource: URI, encoding?: string): TPromise { + private createFileInput(resource: URI, encoding?: string, restoreFromBackup?: boolean): TPromise { return this.instantiationService.createInstance(this.fileInputDescriptor).then((typedFileInput) => { typedFileInput.setResource(resource); typedFileInput.setPreferredEncoding(encoding); + if (restoreFromBackup) { + typedFileInput.setRestoreResource(this.backupService.getBackupResource(resource)); + } + return typedFileInput; }); } @@ -315,11 +321,13 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { handler: IDelegatingWorkbenchEditorServiceHandler, @IUntitledEditorService untitledEditorService: IUntitledEditorService, @IInstantiationService instantiationService: IInstantiationService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IBackupService backupService: IBackupService ) { super( editorService, untitledEditorService, + backupService, instantiationService ); diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index bcb582ed1fe04..b6d8ae2fca1b9 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -26,6 +26,7 @@ import {IEnvironmentService} from 'vs/platform/environment/common/environment'; import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService'; import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; +import {IBackupService} from 'vs/platform/backup/common/backup'; import {shell} from 'electron'; @@ -51,7 +52,8 @@ export class FileService implements IFileService { @IEditorGroupService private editorGroupService: IEditorGroupService, @ILifecycleService private lifecycleService: ILifecycleService, @IMessageService private messageService: IMessageService, - @IStorageService private storageService: IStorageService + @IStorageService private storageService: IStorageService, + @IBackupService private backupService: IBackupService ) { this.toUnbind = []; this.activeOutOfWorkspaceWatchers = Object.create(null); @@ -81,7 +83,7 @@ export class FileService implements IFileService { // create service const workspace = this.contextService.getWorkspace(); - this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService, this.configurationService); + this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService, this.configurationService, this.backupService, this.contextService); // Listeners this.registerListeners(); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 808f8671b3ece..ed4cea64dc9ee 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -36,6 +36,8 @@ import {IEnvironmentService} from 'vs/platform/environment/common/environment'; import {IEventService} from 'vs/platform/event/common/event'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; import {IFilesConfiguration} from 'vs/platform/files/common/files'; +import {IBackupService} from 'vs/platform/backup/common/backup'; +import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; export interface IEncodingOverride { resource: uri; @@ -73,7 +75,6 @@ export class FileService implements IFileService { public _serviceBrand: any; - private static SESSION_BACKUP_ID = crypto.randomBytes(20).toString('hex'); // defined the directory to store backups for the session private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private static MAX_DEGREE_OF_PARALLEL_FS_OPS = 10; // degree of parallel fs calls that we accept at the same time @@ -90,7 +91,15 @@ export class FileService implements IFileService { private configuredHotExit: boolean; - constructor(basePath: string, options: IFileServiceOptions, private eventEmitter: IEventService, private environmentService: IEnvironmentService, private configurationService: IConfigurationService) { + constructor( + basePath: string, + options: IFileServiceOptions, + private eventEmitter: IEventService, + private environmentService: IEnvironmentService, + private configurationService: IConfigurationService, + private backupService: IBackupService, + private contextService: IWorkspaceContextService + ) { this.basePath = basePath ? paths.normalize(basePath) : void 0; if (this.basePath && this.basePath.indexOf('\\\\') === 0 && strings.endsWith(this.basePath, paths.sep)) { @@ -449,13 +458,16 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { if (resource.scheme === 'file') { // TODO: Persist hash -> file map on disk (json file?) + } + this.backupService.registerBackupFile(resource); const backupResource = this.getBackupPath(resource); console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); } public discardBackup(resource: uri): TPromise { + this.backupService.deregisterBackupFile(resource); return this.del(this.getBackupPath(resource)); } @@ -476,7 +488,8 @@ export class FileService implements IFileService { } private getBackupRoot(): string { - return paths.join(this.environmentService.userDataPath, 'Backups', FileService.SESSION_BACKUP_ID); + let workspaceHash = crypto.createHash('md5').update(this.contextService.getWorkspace().resource.fsPath).digest('hex'); + return paths.join(this.environmentService.userDataPath, 'Backups', workspaceHash); } private toAbsolutePath(arg1: uri | IFileStat): string { diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index 9855dd227ed45..48f733b2429b3 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -33,7 +33,7 @@ suite('FileService', () => { extfs.copy(sourceDir, testDir, () => { events = new utils.TestEventService(); - service = new FileService(testDir, { disableWatcher: true }, events, null, null); + service = new FileService(testDir, { disableWatcher: true }, events, null, null, null, null); done(); }); }); @@ -494,7 +494,7 @@ suite('FileService', () => { encoding: 'windows1252', encodingOverride: encodingOverride, disableWatcher: true - }, null, null, null); + }, null, null, null, null, null); _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).done(c => { assert.equal(c.encoding, 'windows1252'); @@ -520,7 +520,7 @@ suite('FileService', () => { let _service = new FileService(_testDir, { disableWatcher: true - }, null, null, null); + }, null, null, null, null, null); extfs.copy(_sourceDir, _testDir, () => { fs.readFile(resource.fsPath, (error, data) => { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 3ad57ef477d08..2e3f327b4f691 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -40,6 +40,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private static saveParticipant: ISaveParticipant; private resource: URI; + private restoreResource: URI; private contentEncoding: string; // encoding as reported from disk private preferredEncoding: string; // encoding as chosen by the user private dirty: boolean; @@ -182,8 +183,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil }); } + public setRestoreResource(resource: URI): void { + this.restoreResource = resource; + } + public load(force?: boolean /* bypass any caches and really go to disk */): TPromise { diag('load() - enter', this.resource, new Date()); + console.log(`load model ${this.resource.fsPath}`); // It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk // if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk @@ -259,18 +265,37 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil else { diag('load() - created text editor model', this.resource, new Date()); - this.createTextEditorModelPromise = this.createTextEditorModel(content.value, content.resource).then(() => { - this.createTextEditorModelPromise = null; + if (this.restoreResource) { + // TODO: De-duplicate code + console.log(`Attempting to restore resource ${this.restoreResource.fsPath}`); + this.createTextEditorModelPromise = this.textFileService.resolveTextContent(this.restoreResource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then((restoreContent) => { + return this.createTextEditorModel(restoreContent.value, content.resource).then(() => { + this.createTextEditorModelPromise = null; - this.setDirty(false); // Ensure we are not tracking a stale state - this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); + this.setDirty(true); // Ensure we are not tracking a stale state + this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); - return this; - }, (error) => { - this.createTextEditorModelPromise = null; + return this; + }, (error) => { + this.createTextEditorModelPromise = null; + + return TPromise.wrapError(error); + }); + }); + } else { + this.createTextEditorModelPromise = this.createTextEditorModel(content.value, content.resource).then(() => { + this.createTextEditorModelPromise = null; - return TPromise.wrapError(error); - }); + this.setDirty(false); // Ensure we are not tracking a stale state + this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); + + return this; + }, (error) => { + this.createTextEditorModelPromise = null; + + return TPromise.wrapError(error); + }); + } return this.createTextEditorModelPromise; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 5f126eaf103cc..310cfa881ea89 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -166,7 +166,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return this.mapResourceToModel[resource.toString()]; } - public loadOrCreate(resource: URI, encoding: string, refresh?: boolean): TPromise { + public loadOrCreate(resource: URI, encoding: string, refresh?: boolean, restoreResource?: URI): TPromise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders[resource.toString()]; @@ -189,6 +189,9 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { // Model does not exist else { model = this.instantiationService.createInstance(TextFileEditorModel, resource, encoding); + if (restoreResource) { + model.setRestoreResource(restoreResource); + } modelPromise = model.load(); // Install state change listener diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 57f13ef7b1654..e1a5e8efd03c4 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -192,7 +192,7 @@ export interface ITextFileEditorModelManager { getAll(resource?: URI): ITextFileEditorModel[]; - loadOrCreate(resource: URI, preferredEncoding: string, refresh?: boolean): TPromise; + loadOrCreate(resource: URI, preferredEncoding: string, refresh?: boolean, restoreResource?: URI): TPromise; } export interface IModelSaveOptions { @@ -217,6 +217,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport save(options?: IModelSaveOptions): TPromise; + setRestoreResource(resource: URI): void; + revert(): TPromise; setConflictResolutionMode(); diff --git a/src/vs/workbench/test/common/editor/editorStacksModel.test.ts b/src/vs/workbench/test/common/editor/editorStacksModel.test.ts index 94c3822ec4608..19ef0739b3e76 100644 --- a/src/vs/workbench/test/common/editor/editorStacksModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorStacksModel.test.ts @@ -144,6 +144,9 @@ class TestFileEditorInput extends EditorInput implements IFileEditorInput { public setResource(r: URI): void { } + public setRestoreResource(r: URI): void { + } + public setEncoding(encoding: string) { } From f7ea530cfe82a5a06d188689ca3e4d5f01e9a1a0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Oct 2016 11:13:05 -0700 Subject: [PATCH 15/45] Make workspaces.json loading more resilient --- src/vs/code/electron-main/backup.ts | 15 ++++++++++----- src/vs/workbench/common/backup.ts | 11 +++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts index abb948dad273f..a4135f5c6d6b9 100644 --- a/src/vs/code/electron-main/backup.ts +++ b/src/vs/code/electron-main/backup.ts @@ -7,7 +7,7 @@ import * as fs from 'original-fs'; import * as path from 'path'; -import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const IBackupService = createDecorator('backupService'); @@ -52,11 +52,10 @@ export class BackupService implements IBackupService { public pushBackupWorkspaces(workspaces: string[]): void { this.load(); - if (!this.fileContent.folderWorkspaces) { - this.fileContent.folderWorkspaces = {}; - } workspaces.forEach(workspace => { - this.fileContent.folderWorkspaces[workspace] = this.fileContent.folderWorkspaces[workspace] || []; + if (!this.fileContent.folderWorkspaces[workspace]) { + this.fileContent.folderWorkspaces[workspace] = []; + } }); this.save(); } @@ -72,6 +71,12 @@ export class BackupService implements IBackupService { } catch (error) { this.fileContent = {}; } + if (Array.isArray(this.fileContent) || typeof this.fileContent !== 'object') { + this.fileContent = {}; + } + if (!this.fileContent.folderWorkspaces) { + this.fileContent.folderWorkspaces = {}; + } } private save(): void { diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts index df70000154ace..9a4f3cc391fa6 100644 --- a/src/vs/workbench/common/backup.ts +++ b/src/vs/workbench/common/backup.ts @@ -10,9 +10,9 @@ import * as crypto from 'crypto'; import * as fs from 'original-fs'; import * as arrays from 'vs/base/common/arrays'; import Uri from 'vs/base/common/uri'; -import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; -import {IEnvironmentService} from 'vs/platform/environment/common/environment'; -import {IBackupService} from 'vs/platform/backup/common/backup'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IBackupService } from 'vs/platform/backup/common/backup'; interface IBackupFormat { folderWorkspaces?: { @@ -59,7 +59,7 @@ export class BackupService implements IBackupService { public getBackupFiles(workspace: string): string[] { this.load(); - return this.fileContent.folderWorkspaces[workspace]; + return this.fileContent.folderWorkspaces[workspace] || []; } public getBackupResource(resource: Uri): Uri { @@ -92,6 +92,9 @@ export class BackupService implements IBackupService { } catch (error) { this.fileContent = {}; } + if (!this.fileContent.folderWorkspaces) { + this.fileContent.folderWorkspaces = {}; + } } private save(): void { From 660f6bdce277cf5c094d1195587e90133c6d48b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Oct 2016 11:35:50 -0700 Subject: [PATCH 16/45] Open restored files pinned --- src/vs/workbench/electron-browser/workbench.ts | 4 ++-- .../workbench/services/textfile/common/textFileEditorModel.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 99eb8fc20904f..745977b17ecb0 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -174,7 +174,7 @@ export class Workbench implements IPartService { // Restore any backups if they exist options.filesToRestore = this.backupService.getBackupFiles(workspace.resource.fsPath).map(filePath => { - return { resource: Uri.file(filePath) }; + return { resource: Uri.file(filePath), options: { pinned: true } }; }); this.hasFilesToCreateOpenOrDiff = (options.filesToCreate && options.filesToCreate.length > 0) || (options.filesToOpen && options.filesToOpen.length > 0) || (options.filesToDiff && options.filesToDiff.length > 0) || (options.filesToRestore.length > 0); @@ -328,7 +328,7 @@ export class Workbench implements IPartService { let filesToRestoreInputPromise = filesToRestore.map(resourceInput => this.editorService.createInput(resourceInput, true)); return TPromise.join(filesToOpenInputPromise.concat(filesToRestoreInputPromise)).then((inputsToOpen) => { inputs.push(...inputsToOpen); - options.push(...filesToOpen.map(resourceInput => TextEditorOptions.from(resourceInput))); + options.push(...filesToOpen.concat(filesToRestore).map(resourceInput => TextEditorOptions.from(resourceInput))); return inputs.map((input, index) => { return { input, options: options[index] }; }); }); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index c0ffcb26db5dc..db1abd1584856 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -272,7 +272,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.createTextEditorModel(restoreContent.value, content.resource).then(() => { this.createTextEditorModelPromise = null; - this.setDirty(true); // Ensure we are not tracking a stale state + // TODO: This does not set the dirty indicator immediately, making it look like the file is not actually dirty + this.setDirty(true); this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); return this; From 86e1c2023aedc2f1d8c8d2ea8284b232ae01949e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Oct 2016 11:48:02 -0700 Subject: [PATCH 17/45] Fix tests --- src/vs/test/utils/servicesTestUtils.ts | 8 ++++++++ src/vs/workbench/services/files/node/fileService.ts | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index c54da60f64f16..92d702431457a 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -579,8 +579,16 @@ export const TestFileService = { }); }, + discardBackup: function (resource: URI) { + return TPromise.as(void 0); + }, + discardBackups: function () { return TPromise.as(void 0); + }, + + isHotExitEnabled: function () { + return false; } }; diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index e7d8402cf05f6..2946d843707e9 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -135,10 +135,12 @@ export class FileService implements IFileService { // Configuration changes this.toUnbind = []; - this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config))); + if (this.configurationService) { + this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config))); - const configuration = this.configurationService.getConfiguration(); - this.onConfigurationChange(configuration); + const configuration = this.configurationService.getConfiguration(); + this.onConfigurationChange(configuration); + } } private onConfigurationChange(configuration: IFilesConfiguration): void { From 4928801c7f0a415133f92c3a8f9eb65999a78a9f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Oct 2016 00:07:16 -0700 Subject: [PATCH 18/45] Get untitled files restoring from backup --- src/vs/platform/backup/common/backup.ts | 3 ++- src/vs/workbench/common/backup.ts | 15 ++++++++++++++- .../common/editor/untitledEditorInput.ts | 16 ++++++++++++++-- .../common/editor/untitledEditorModel.ts | 2 +- src/vs/workbench/common/options.ts | 1 + src/vs/workbench/electron-browser/workbench.ts | 16 +++++++++++++++- .../services/files/node/fileService.ts | 4 +--- .../untitled/common/untitledEditorService.ts | 17 ++++++++++++----- 8 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index 975f50e2196b2..1a86e74b51c1f 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -5,7 +5,7 @@ 'use strict'; -import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import Uri from 'vs/base/common/uri'; export const IBackupService = createDecorator('backupService'); @@ -20,5 +20,6 @@ export interface IBackupService { registerBackupFile(resource: Uri): void; deregisterBackupFile(resource: Uri): void; getBackupFiles(workspace: string): string[]; + getBackupUntitledFiles(workspace: string): string[]; getBackupResource(resource: Uri): Uri; } diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts index 9a4f3cc391fa6..c619b810ce796 100644 --- a/src/vs/workbench/common/backup.ts +++ b/src/vs/workbench/common/backup.ts @@ -62,9 +62,22 @@ export class BackupService implements IBackupService { return this.fileContent.folderWorkspaces[workspace] || []; } + public getBackupUntitledFiles(workspace: string): string[] { + const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); + const untitledDir = path.join(this.environmentService.userDataPath, 'Backups', workspaceHash, 'untitled'); + try { + const untitledFiles = fs.readdirSync(untitledDir).map(file => path.join(untitledDir, file)); + console.log('untitledFiles', untitledFiles); + return untitledFiles; + } catch (ex) { + console.log('untitled backups do not exist'); + return []; + } + } + public getBackupResource(resource: Uri): Uri { - let workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); + const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); const backupPath = path.join(this.environmentService.userDataPath, 'Backups', workspaceHash, resource.scheme, backupName); console.log('getBackupResource ' + Uri.file(backupPath)); diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 7761d5135a509..f8a88eb1e91bf 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -19,6 +19,9 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +// TODO: This file cannot depend on native node modules +import fs = require('fs'); + /** * An editor input to be used for untitled text buffers. */ @@ -28,6 +31,7 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { public static SCHEMA: string = 'untitled'; private resource: URI; + private restoreResource: URI; private hasAssociatedFilePath: boolean; private modeId: string; private cachedModel: UntitledEditorModel; @@ -46,7 +50,7 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { @ITextFileService private textFileService: ITextFileService ) { super(); - + console.log('UntitledEditorInput constructor', resource, hasAssociatedFilePath, modeId); this.resource = resource; this.hasAssociatedFilePath = hasAssociatedFilePath; this.modeId = modeId; @@ -66,6 +70,10 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { return this.resource; } + public setRestoreResource(resource: URI): void { + this.restoreResource = resource; + } + public getName(): string { return this.hasAssociatedFilePath ? paths.basename(this.resource.fsPath) : this.resource.fsPath; } @@ -140,7 +148,11 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { } private createModel(): UntitledEditorModel { - const content = ''; + let content = ''; + if (this.restoreResource) { + // TODO: This loading should probably go through fileService, fs cannot be a dependency in common/ + content = fs.readFileSync(this.restoreResource.fsPath, 'utf8'); + } const model = this.instantiationService.createInstance(UntitledEditorModel, content, this.modeId, this.resource, this.hasAssociatedFilePath); // re-emit some events from the model diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 9d1d45181f94d..168e5a93ea2e5 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -46,7 +46,7 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS super(value, modeId, resource, modeService, modelService); this.hasAssociatedFilePath = hasAssociatedFilePath; - this.dirty = hasAssociatedFilePath; // untitled associated to file path are dirty right away + this.dirty = hasAssociatedFilePath || value !== ''; // untitled associated to file path are dirty right away this._onDidChangeDirty = new Emitter(); this._onDidChangeEncoding = new Emitter(); diff --git a/src/vs/workbench/common/options.ts b/src/vs/workbench/common/options.ts index a805f1a652384..ce341d575d361 100644 --- a/src/vs/workbench/common/options.ts +++ b/src/vs/workbench/common/options.ts @@ -24,4 +24,5 @@ export interface IOptions { filesToDiff?: IResourceInput[]; filesToRestore?: IResourceInput[]; + untitledFilesToRestore?: IResourceInput[]; } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 745977b17ecb0..6d92c00035608 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -176,8 +176,16 @@ export class Workbench implements IPartService { options.filesToRestore = this.backupService.getBackupFiles(workspace.resource.fsPath).map(filePath => { return { resource: Uri.file(filePath), options: { pinned: true } }; }); + options.untitledFilesToRestore = this.backupService.getBackupUntitledFiles(workspace.resource.fsPath).map(untitledFilePath => { + return { resource: Uri.from({ path: untitledFilePath, scheme: 'untitled' }), options: { pinned: true } }; + }); - this.hasFilesToCreateOpenOrDiff = (options.filesToCreate && options.filesToCreate.length > 0) || (options.filesToOpen && options.filesToOpen.length > 0) || (options.filesToDiff && options.filesToDiff.length > 0) || (options.filesToRestore.length > 0); + this.hasFilesToCreateOpenOrDiff = + (options.filesToCreate && options.filesToCreate.length > 0) || + (options.filesToOpen && options.filesToOpen.length > 0) || + (options.filesToDiff && options.filesToDiff.length > 0) || + (options.filesToRestore.length > 0) || + (options.untitledFilesToRestore.length > 0); this.toDispose = []; this.toShutdown = []; @@ -305,6 +313,7 @@ export class Workbench implements IPartService { const filesToCreate = wbopt.filesToCreate || []; const filesToOpen = wbopt.filesToOpen || []; const filesToRestore = wbopt.filesToRestore || []; + const untitledFilesToRestore = wbopt.untitledFilesToRestore || []; const filesToDiff = wbopt.filesToDiff; // Files to diff is exclusive @@ -323,9 +332,14 @@ export class Workbench implements IPartService { inputs.push(...filesToCreate.map(resourceInput => this.untitledEditorService.createOrGet(resourceInput.resource))); options.push(...filesToCreate.map(r => null)); // fill empty options for files to create because we dont have options there + // Files to restore + inputs.push(...untitledFilesToRestore.map(resourceInput => this.untitledEditorService.createOrGet(null, null, resourceInput.resource))); + options.push(...untitledFilesToRestore.map(r => null)); // fill empty options for files to create because we dont have options there + // Files to open let filesToOpenInputPromise = filesToOpen.map(resourceInput => this.editorService.createInput(resourceInput)); let filesToRestoreInputPromise = filesToRestore.map(resourceInput => this.editorService.createInput(resourceInput, true)); + return TPromise.join(filesToOpenInputPromise.concat(filesToRestoreInputPromise)).then((inputsToOpen) => { inputs.push(...inputsToOpen); options.push(...filesToOpen.concat(filesToRestore).map(resourceInput => TextEditorOptions.from(resourceInput))); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 2946d843707e9..3cb3e807b17ad 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -459,10 +459,8 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { if (resource.scheme === 'file') { - // TODO: Persist hash -> file map on disk (json file?) - + this.backupService.registerBackupFile(resource); } - this.backupService.registerBackupFile(resource); const backupResource = this.getBackupPath(resource); console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index e064aa9da5f56..c533d90ef9664 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -6,6 +6,7 @@ import URI from 'vs/base/common/uri'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IBackupService } from 'vs/platform/backup/common/backup'; import arrays = require('vs/base/common/arrays'); import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; import Event, { Emitter, once } from 'vs/base/common/event'; @@ -58,7 +59,7 @@ export interface IUntitledEditorService { * It is valid to pass in a file resource. In that case the path will be used as identifier. * The use case is to be able to create a new file with a specific path with VSCode. */ - createOrGet(resource?: URI, modeId?: string): UntitledEditorInput; + createOrGet(resource?: URI, modeId?: string, restoreResource?: URI): UntitledEditorInput; /** * A check to find out if a untitled resource has a file path associated or not. @@ -76,7 +77,10 @@ export class UntitledEditorService implements IUntitledEditorService { private _onDidChangeDirty: Emitter; private _onDidChangeEncoding: Emitter; - constructor( @IInstantiationService private instantiationService: IInstantiationService) { + constructor( + @IBackupService private backupService: IBackupService, + @IInstantiationService private instantiationService: IInstantiationService + ) { this._onDidChangeDirty = new Emitter(); this._onDidChangeEncoding = new Emitter(); } @@ -130,7 +134,7 @@ export class UntitledEditorService implements IUntitledEditorService { .map((i) => i.getResource()); } - public createOrGet(resource?: URI, modeId?: string): UntitledEditorInput { + public createOrGet(resource?: URI, modeId?: string, restoreResource?: URI): UntitledEditorInput { let hasAssociatedFilePath = false; if (resource) { hasAssociatedFilePath = (resource.scheme === 'file'); @@ -147,10 +151,10 @@ export class UntitledEditorService implements IUntitledEditorService { } // Create new otherwise - return this.doCreate(resource, hasAssociatedFilePath, modeId); + return this.doCreate(resource, hasAssociatedFilePath, modeId, restoreResource); } - private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, modeId?: string): UntitledEditorInput { + private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, modeId?: string, restoreResource?: URI): UntitledEditorInput { if (!resource) { // Create new taking a resource URI that is not already taken @@ -162,6 +166,9 @@ export class UntitledEditorService implements IUntitledEditorService { } const input = this.instantiationService.createInstance(UntitledEditorInput, resource, hasAssociatedFilePath, modeId); + if (restoreResource) { + input.setRestoreResource(restoreResource); + } const dirtyListener = input.onDidChangeDirty(() => { this._onDidChangeDirty.fire(resource); From e4f0dec794e4a647ee9aa004470cec0732dc4836 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Oct 2016 01:12:41 -0700 Subject: [PATCH 19/45] Remove usage of fs in common/ layer --- .../common/editor/untitledEditorInput.ts | 30 +++++++++---------- .../workbench/electron-browser/workbench.ts | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index f8a88eb1e91bf..32dd24729e655 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -19,9 +19,6 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -// TODO: This file cannot depend on native node modules -import fs = require('fs'); - /** * An editor input to be used for untitled text buffers. */ @@ -50,7 +47,6 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { @ITextFileService private textFileService: ITextFileService ) { super(); - console.log('UntitledEditorInput constructor', resource, hasAssociatedFilePath, modeId); this.resource = resource; this.hasAssociatedFilePath = hasAssociatedFilePath; this.modeId = modeId; @@ -138,21 +134,25 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { return TPromise.as(this.cachedModel); } - // Otherwise Create Model and load - const model = this.createModel(); - return model.load().then((resolvedModel: UntitledEditorModel) => { - this.cachedModel = resolvedModel; + // Otherwise Create Model and load, restoring from backup if necessary + let restorePromise: TPromise; + if (this.restoreResource) { + restorePromise = this.textFileService.resolveTextContent(this.restoreResource).then(rawTextContent => rawTextContent.value.lines.join('\n')); + } else { + restorePromise = TPromise.as(''); + } + + return restorePromise.then(content => { + const model = this.createModel(content); + return model.load().then((resolvedModel: UntitledEditorModel) => { + this.cachedModel = resolvedModel; - return this.cachedModel; + return this.cachedModel; + }); }); } - private createModel(): UntitledEditorModel { - let content = ''; - if (this.restoreResource) { - // TODO: This loading should probably go through fileService, fs cannot be a dependency in common/ - content = fs.readFileSync(this.restoreResource.fsPath, 'utf8'); - } + private createModel(content: string): UntitledEditorModel { const model = this.instantiationService.createInstance(UntitledEditorModel, content, this.modeId, this.resource, this.hasAssociatedFilePath); // re-emit some events from the model diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 6d92c00035608..506dab74574f0 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -177,7 +177,7 @@ export class Workbench implements IPartService { return { resource: Uri.file(filePath), options: { pinned: true } }; }); options.untitledFilesToRestore = this.backupService.getBackupUntitledFiles(workspace.resource.fsPath).map(untitledFilePath => { - return { resource: Uri.from({ path: untitledFilePath, scheme: 'untitled' }), options: { pinned: true } }; + return { resource: Uri.file(untitledFilePath), options: { pinned: true } }; }); this.hasFilesToCreateOpenOrDiff = From 52c8af5abfb18dad5417ae97fe4441ec456d056e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Oct 2016 02:30:33 -0700 Subject: [PATCH 20/45] Perform a last minute backup when closing the last window --- .../common/editor/untitledEditorModel.ts | 10 +++- .../services/files/node/fileService.ts | 1 + .../textfile/browser/textFileService.ts | 54 +++++++++++++++++-- .../textfile/common/textFileEditorModel.ts | 14 ++++- .../services/textfile/common/textfiles.ts | 2 + 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 168e5a93ea2e5..454167b3d76b7 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -193,10 +193,18 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this.fileService.discardBackup(this.resource); } - private doBackup(): TPromise { + public backup(): TPromise { + return this.doBackup(true); + } + + private doBackup(immediate?: boolean): TPromise { // Cancel any currently running backups to make this the one that succeeds this.cancelBackupPromises(); + if (immediate) { + return this.fileService.backupFile(this.resource, this.getValue()).then(f => void 0); + } + // Create new backup promise and keep it const promise = TPromise.timeout(1000).then(() => { this.fileService.backupFile(this.resource, this.getValue()); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 3cb3e807b17ad..525e673af95c1 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -458,6 +458,7 @@ export class FileService implements IFileService { } public backupFile(resource: uri, content: string): TPromise { + // TODO: This should not backup unless necessary. Currently this is called for each file on close to ensure the files are backed up. if (resource.scheme === 'file') { this.backupService.registerBackupFile(resource); } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 690018a2e4454..adae95159056c 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -119,14 +119,12 @@ export abstract class TextFileService implements ITextFileService { // If hot exit is enabled then save the dirty files in the workspace and then exit if (this.configuredHotExit) { - // TODO: Check if dirty - // TODO: Do a last minute backup if required - // TODO: There may be a better way of verifying that only a single window is open? // Only remove the workspace from the backup service if it's not the last one or it's not dirty if (this.backupService.getBackupWorkspaces().length > 1 || this.getDirty().length === 0) { this.backupService.removeWorkspace(this.contextService.getWorkspace().resource.fsPath); } else { - return false; // the backup will be restored, no veto + // TODO: Better error handling here? Perhaps present confirm if there was an error? + return this.backupAll().then(() => false); // the backup will be restored, no veto } } @@ -384,6 +382,54 @@ export abstract class TextFileService implements ITextFileService { }); } + /** + * Performs an immedate backup of all dirty file and untitled models. + */ + private backupAll(): TPromise { + const toBackup = this.getDirty(); + + // split up between files and untitled + const filesToBackup: URI[] = []; + const untitledToBackup: URI[] = []; + toBackup.forEach(s => { + if (s.scheme === 'file') { + filesToBackup.push(s); + } else if (s.scheme === 'untitled') { + untitledToBackup.push(s); + } + }); + + return this.doBackupAll(filesToBackup, untitledToBackup); + } + + private doBackupAll(fileResources: URI[], untitledResources: URI[]): TPromise { + // Handle file resources first + const dirtyFileModels = this.getDirtyFileModels(fileResources); + + const mapResourceToResult: { [resource: string]: IResult } = Object.create(null); + dirtyFileModels.forEach(m => { + mapResourceToResult[m.getResource().toString()] = { + source: m.getResource() + }; + }); + + return TPromise.join(dirtyFileModels.map(model => { + return model.backup().then(() => { + mapResourceToResult[model.getResource().toString()].success = true; + }); + })).then(result => { + // Handle untitled resources + const untitledModelPromises = untitledResources.map(untitledResource => this.untitledEditorService.get(untitledResource)) + .filter(untitled => !!untitled) + .map(untitled => untitled.resolve()); + + return TPromise.join(untitledModelPromises).then(untitledModels => { + const untitledBackupPromises = untitledModels.map(model => model.backup()); + return TPromise.join(untitledBackupPromises).then(() => void 0); + }); + }); + } + private getFileModels(resources?: URI[]): ITextFileEditorModel[]; private getFileModels(resource?: URI): ITextFileEditorModel[]; private getFileModels(arg1?: any): ITextFileEditorModel[] { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index db1abd1584856..e0a68abbce363 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -412,10 +412,22 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private doBackup(): TPromise { + public backup(): TPromise { + if (!this.dirty) { + return TPromise.as(null); + } + + return this.doBackup(true); + } + + private doBackup(immediate?: boolean): TPromise { // Cancel any currently running backups to make this the one that succeeds this.cancelBackupPromises(); + if (immediate) { + return this.fileService.backupFile(this.resource, this.getValue()).then(f => void 0); + } + // Create new backup promise and keep it const promise = TPromise.timeout(1000).then(() => { this.fileService.backupFile(this.resource, this.getValue()); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index dcc091f51c4f1..e564139e8b4eb 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -217,6 +217,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport save(options?: IModelSaveOptions): TPromise; + backup(): TPromise; + setRestoreResource(resource: URI): void; revert(): TPromise; From 5f230ed17cadf8122ec268bc2d953247dc988305 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Oct 2016 14:01:32 -0700 Subject: [PATCH 21/45] Move workspaces.json path to IEnvironmentService --- src/vs/code/electron-main/backup.ts | 7 ++----- src/vs/platform/environment/common/environment.ts | 3 +++ src/vs/platform/environment/node/environmentService.ts | 6 ++++++ src/vs/workbench/common/backup.ts | 10 ++++------ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts index a4135f5c6d6b9..06dcf52344d8b 100644 --- a/src/vs/code/electron-main/backup.ts +++ b/src/vs/code/electron-main/backup.ts @@ -6,7 +6,6 @@ 'use strict'; import * as fs from 'original-fs'; -import * as path from 'path'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -27,13 +26,11 @@ interface IBackupFormat { export class BackupService implements IBackupService { - private filePath: string; private fileContent: IBackupFormat; constructor( @IEnvironmentService private environmentService: IEnvironmentService ) { - this.filePath = path.join(environmentService.userDataPath, 'Backups', 'workspaces.json'); } public getBackupWorkspaces(): string[] { @@ -67,7 +64,7 @@ export class BackupService implements IBackupService { private load(): void { try { - this.fileContent = JSON.parse(fs.readFileSync(this.filePath).toString()); // invalid JSON or permission issue can happen here + this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath).toString()); // invalid JSON or permission issue can happen here } catch (error) { this.fileContent = {}; } @@ -81,7 +78,7 @@ export class BackupService implements IBackupService { private save(): void { try { - fs.writeFileSync(this.filePath, JSON.stringify(this.fileContent)); + fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent)); } catch (error) { } } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 23998095f4f87..7cc3288d9e068 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -23,6 +23,9 @@ export interface IEnvironmentService { appSettingsPath: string; appKeybindingsPath: string; + backupHome: string; + backupWorkspacesPath: string; + disableExtensions: boolean; extensionsPath: string; extensionDevelopmentPath: string; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 51ccedd29dac6..cd24af86de344 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -76,6 +76,12 @@ export class EnvironmentService implements IEnvironmentService { @memoize get appKeybindingsPath(): string { return path.join(this.appSettingsHome, 'keybindings.json'); } + @memoize + get backupHome(): string { return path.join(this.userDataPath, 'Backups'); } + + @memoize + get backupWorkspacesPath(): string { return path.join(this.backupHome, 'workspaces.json'); } + @memoize get extensionsPath(): string { return path.normalize(this._args.extensionHomePath || path.join(this.userHome, 'extensions')); } diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts index c619b810ce796..847a7e5015dfa 100644 --- a/src/vs/workbench/common/backup.ts +++ b/src/vs/workbench/common/backup.ts @@ -25,14 +25,12 @@ export class BackupService implements IBackupService { public _serviceBrand: any; private workspaceResource: Uri; - private workspacesJsonFilePath: string; private fileContent: IBackupFormat; constructor( @IEnvironmentService private environmentService: IEnvironmentService, @IWorkspaceContextService contextService: IWorkspaceContextService ) { - this.workspacesJsonFilePath = path.join(environmentService.userDataPath, 'Backups', 'workspaces.json'); this.workspaceResource = contextService.getWorkspace().resource; } @@ -64,7 +62,7 @@ export class BackupService implements IBackupService { public getBackupUntitledFiles(workspace: string): string[] { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); - const untitledDir = path.join(this.environmentService.userDataPath, 'Backups', workspaceHash, 'untitled'); + const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); try { const untitledFiles = fs.readdirSync(untitledDir).map(file => path.join(untitledDir, file)); console.log('untitledFiles', untitledFiles); @@ -79,7 +77,7 @@ export class BackupService implements IBackupService { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); - const backupPath = path.join(this.environmentService.userDataPath, 'Backups', workspaceHash, resource.scheme, backupName); + const backupPath = path.join(this.environmentService.backupHome, workspaceHash, resource.scheme, backupName); console.log('getBackupResource ' + Uri.file(backupPath)); return Uri.file(backupPath); } @@ -101,7 +99,7 @@ export class BackupService implements IBackupService { private load(): void { try { - this.fileContent = JSON.parse(fs.readFileSync(this.workspacesJsonFilePath).toString()); // invalid JSON or permission issue can happen here + this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath).toString()); // invalid JSON or permission issue can happen here } catch (error) { this.fileContent = {}; } @@ -112,7 +110,7 @@ export class BackupService implements IBackupService { private save(): void { try { - fs.writeFileSync(this.workspacesJsonFilePath, JSON.stringify(this.fileContent)); + fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent)); } catch (error) { } } From 4fcfdc6accf84f63a5a1f2e691403d85f4b20d29 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Oct 2016 14:04:54 -0700 Subject: [PATCH 22/45] Replace all {} with Object.create(null) --- src/vs/code/electron-main/backup.ts | 10 +++++----- src/vs/workbench/common/backup.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts index 06dcf52344d8b..bc70a822358e7 100644 --- a/src/vs/code/electron-main/backup.ts +++ b/src/vs/code/electron-main/backup.ts @@ -37,12 +37,12 @@ export class BackupService implements IBackupService { if (!this.fileContent) { this.load(); } - return Object.keys(this.fileContent.folderWorkspaces || {}); + return Object.keys(this.fileContent.folderWorkspaces || Object.create(null)); } public clearBackupWorkspaces(): void { this.fileContent = { - folderWorkspaces: {} + folderWorkspaces: Object.create(null) }; this.save(); } @@ -66,13 +66,13 @@ export class BackupService implements IBackupService { try { this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath).toString()); // invalid JSON or permission issue can happen here } catch (error) { - this.fileContent = {}; + this.fileContent = Object.create(null); } if (Array.isArray(this.fileContent) || typeof this.fileContent !== 'object') { - this.fileContent = {}; + this.fileContent = Object.create(null); } if (!this.fileContent.folderWorkspaces) { - this.fileContent.folderWorkspaces = {}; + this.fileContent.folderWorkspaces = Object.create(null); } } diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts index 847a7e5015dfa..328e50c3b1f19 100644 --- a/src/vs/workbench/common/backup.ts +++ b/src/vs/workbench/common/backup.ts @@ -36,12 +36,12 @@ export class BackupService implements IBackupService { public getBackupWorkspaces(): string[] { this.load(); - return Object.keys(this.fileContent.folderWorkspaces || {}); + return Object.keys(this.fileContent.folderWorkspaces || Object.create(null)); } public clearBackupWorkspaces(): void { this.fileContent = { - folderWorkspaces: {} + folderWorkspaces: Object.create(null) }; this.save(); } @@ -101,10 +101,10 @@ export class BackupService implements IBackupService { try { this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath).toString()); // invalid JSON or permission issue can happen here } catch (error) { - this.fileContent = {}; + this.fileContent = Object.create(null); } if (!this.fileContent.folderWorkspaces) { - this.fileContent.folderWorkspaces = {}; + this.fileContent.folderWorkspaces = Object.create(null); } } From a822c887781cb6f3069c49c72bf97bf32d4ccf19 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Oct 2016 14:31:05 -0700 Subject: [PATCH 23/45] Remove unused service --- .../workbench/parts/files/electron-browser/dirtyFilesTracker.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index 1a4a6d84c131d..525d39ab02d66 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -18,7 +18,6 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; -import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activityService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -38,7 +37,6 @@ export class DirtyFilesTracker implements IWorkbenchContribution { constructor( @ITextFileService private textFileService: ITextFileService, - @IFileService private fileService: IFileService, @ILifecycleService private lifecycleService: ILifecycleService, @IEditorGroupService editorGroupService: IEditorGroupService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, From 1a29d14961c32633dd2bd3043c9614344279a469 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 01:20:05 -0700 Subject: [PATCH 24/45] Improve IBackupService names and add jsdoc jsdoc was only added for the renderer side interface as they will soon be merged together. --- src/vs/code/electron-main/backup.ts | 16 ++--- src/vs/code/electron-main/windows.ts | 4 +- src/vs/platform/backup/common/backup.ts | 64 ++++++++++++++++--- src/vs/workbench/common/backup.ts | 14 ++-- .../workbench/electron-browser/workbench.ts | 4 +- .../services/files/node/fileService.ts | 4 +- .../textfile/browser/textFileService.ts | 4 +- 7 files changed, 79 insertions(+), 31 deletions(-) diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts index bc70a822358e7..7706b35b8448e 100644 --- a/src/vs/code/electron-main/backup.ts +++ b/src/vs/code/electron-main/backup.ts @@ -12,10 +12,10 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' export const IBackupService = createDecorator('backupService'); export interface IBackupService { - getBackupWorkspaces(): string[]; - clearBackupWorkspaces(): void; - pushBackupWorkspaces(workspaces: string[]): void; - getBackupFiles(workspace: string): string[]; + getWorkspaceBackupPaths(): string[]; + clearWorkspaceBackupPaths(): void; + pushWorkspaceBackupPath(workspaces: string[]): void; + getWorkspaceBackupFiles(workspace: string): string[]; } interface IBackupFormat { @@ -33,21 +33,21 @@ export class BackupService implements IBackupService { ) { } - public getBackupWorkspaces(): string[] { + public getWorkspaceBackupPaths(): string[] { if (!this.fileContent) { this.load(); } return Object.keys(this.fileContent.folderWorkspaces || Object.create(null)); } - public clearBackupWorkspaces(): void { + public clearWorkspaceBackupPaths(): void { this.fileContent = { folderWorkspaces: Object.create(null) }; this.save(); } - public pushBackupWorkspaces(workspaces: string[]): void { + public pushWorkspaceBackupPath(workspaces: string[]): void { this.load(); workspaces.forEach(workspace => { if (!this.fileContent.folderWorkspaces[workspace]) { @@ -57,7 +57,7 @@ export class BackupService implements IBackupService { this.save(); } - public getBackupFiles(workspace: string): string[] { + public getWorkspaceBackupFiles(workspace: string): string[] { this.load(); return this.fileContent.folderWorkspaces[workspace]; } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index b8626dc853932..8a43305043fd3 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -608,7 +608,7 @@ export class WindowsManager implements IWindowsService { // Add any existing backup workspaces if (openConfig.restoreBackups) { // TODO: Ensure the workspaces being added actually have backups - this.backupService.getBackupWorkspaces().forEach(ws => { + this.backupService.getWorkspaceBackupPaths().forEach(ws => { iPathsToOpen.push(this.toIPath(ws)); }); // Get rid of duplicates @@ -747,7 +747,7 @@ export class WindowsManager implements IWindowsService { // Add to backups console.log('iPathsToOpen', iPathsToOpen); - this.backupService.pushBackupWorkspaces(iPathsToOpen.map((path) => { + this.backupService.pushWorkspaceBackupPath(iPathsToOpen.map((path) => { return path.workspacePath; })); diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index 1a86e74b51c1f..cf5363949a9c6 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -13,13 +13,61 @@ export const IBackupService = createDecorator('backupService'); export interface IBackupService { _serviceBrand: any; - getBackupWorkspaces(): string[]; - clearBackupWorkspaces(): void; - removeWorkspace(workspace: string): void; - - registerBackupFile(resource: Uri): void; - deregisterBackupFile(resource: Uri): void; - getBackupFiles(workspace: string): string[]; - getBackupUntitledFiles(workspace: string): string[]; + /** + * Gets the set of active workspace backup paths being tracked for restoration. + * + * @return The set of active workspace backup paths being tracked for restoration. + */ + getWorkspaceBackupPaths(): string[]; + + /** + * Clears all active workspace backup paths being tracked for restoration. + */ + clearWorkspaceBackupPaths(): void; + + /** + * Removes a workspace backup path being tracked for restoration, deregistering all associated + * resources for backup. + * + * @param workspace The absolute workspace path being removed. + */ + removeWorkspaceBackupPath(workspace: string): void; + + /** + * Gets the set of text files that are backed up for a particular workspace. + * + * @param workspace The workspace to get the backed up files for. + * @return The absolute paths for text files _that have backups_. + */ + getWorkspaceTextFilesWithBackups(workspace: string): string[]; + + /** + * Gets the set of untitled file backups for a particular workspace. + * + * @param workspace The workspace to get the backups for for. + * @return The absolute paths for all the untitled file _backups_. + */ + getWorkspaceUntitledFileBackups(workspace: string): string[]; + + /** + * Registers a resource for backup, flagging it for restoration. + * + * @param resource The resource that is being backed up. + */ + registerResourceForBackup(resource: Uri): void; + + /** + * Deregisters a resource for backup, unflagging it for restoration. + * + * @param resource The resource that is no longer being backed up. + */ + deregisterResourceForBackup(resource: Uri): void; + + /** + * Gets the backup resource for a particular resource within the current workspace. + * + * @param resource The resource that is backed up. + * @return The backup resource. + */ getBackupResource(resource: Uri): Uri; } diff --git a/src/vs/workbench/common/backup.ts b/src/vs/workbench/common/backup.ts index 328e50c3b1f19..f29f1b4e64df5 100644 --- a/src/vs/workbench/common/backup.ts +++ b/src/vs/workbench/common/backup.ts @@ -34,19 +34,19 @@ export class BackupService implements IBackupService { this.workspaceResource = contextService.getWorkspace().resource; } - public getBackupWorkspaces(): string[] { + public getWorkspaceBackupPaths(): string[] { this.load(); return Object.keys(this.fileContent.folderWorkspaces || Object.create(null)); } - public clearBackupWorkspaces(): void { + public clearWorkspaceBackupPaths(): void { this.fileContent = { folderWorkspaces: Object.create(null) }; this.save(); } - public removeWorkspace(workspace: string): void { + public removeWorkspaceBackupPath(workspace: string): void { this.load(); if (!this.fileContent.folderWorkspaces) { return; @@ -55,12 +55,12 @@ export class BackupService implements IBackupService { this.save(); } - public getBackupFiles(workspace: string): string[] { + public getWorkspaceTextFilesWithBackups(workspace: string): string[] { this.load(); return this.fileContent.folderWorkspaces[workspace] || []; } - public getBackupUntitledFiles(workspace: string): string[] { + public getWorkspaceUntitledFileBackups(workspace: string): string[] { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); try { @@ -82,7 +82,7 @@ export class BackupService implements IBackupService { return Uri.file(backupPath); } - public registerBackupFile(resource: Uri): void { + public registerResourceForBackup(resource: Uri): void { this.load(); if (arrays.contains(this.fileContent.folderWorkspaces[this.workspaceResource.fsPath], resource.fsPath)) { return; @@ -91,7 +91,7 @@ export class BackupService implements IBackupService { this.save(); } - public deregisterBackupFile(resource: Uri): void { + public deregisterResourceForBackup(resource: Uri): void { this.load(); this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].filter(value => value !== resource.fsPath); this.save(); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 13069ca839ebd..d43b7bffe3ecf 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -173,10 +173,10 @@ export class Workbench implements IPartService { }; // Restore any backups if they exist - options.filesToRestore = this.backupService.getBackupFiles(workspace.resource.fsPath).map(filePath => { + options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackups(workspace.resource.fsPath).map(filePath => { return { resource: Uri.file(filePath), options: { pinned: true } }; }); - options.untitledFilesToRestore = this.backupService.getBackupUntitledFiles(workspace.resource.fsPath).map(untitledFilePath => { + options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackups(workspace.resource.fsPath).map(untitledFilePath => { return { resource: Uri.file(untitledFilePath), options: { pinned: true } }; }); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 525e673af95c1..9c4d289cd1b23 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -460,7 +460,7 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { // TODO: This should not backup unless necessary. Currently this is called for each file on close to ensure the files are backed up. if (resource.scheme === 'file') { - this.backupService.registerBackupFile(resource); + this.backupService.registerResourceForBackup(resource); } const backupResource = this.getBackupPath(resource); console.log(`Backing up to ${backupResource.fsPath}`); @@ -468,7 +468,7 @@ export class FileService implements IFileService { } public discardBackup(resource: uri): TPromise { - this.backupService.deregisterBackupFile(resource); + this.backupService.deregisterResourceForBackup(resource); return this.del(this.getBackupPath(resource)); } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index adae95159056c..f0d236260786d 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -120,8 +120,8 @@ export abstract class TextFileService implements ITextFileService { // If hot exit is enabled then save the dirty files in the workspace and then exit if (this.configuredHotExit) { // Only remove the workspace from the backup service if it's not the last one or it's not dirty - if (this.backupService.getBackupWorkspaces().length > 1 || this.getDirty().length === 0) { - this.backupService.removeWorkspace(this.contextService.getWorkspace().resource.fsPath); + if (this.backupService.getWorkspaceBackupPaths().length > 1 || this.getDirty().length === 0) { + this.backupService.removeWorkspaceBackupPath(this.contextService.getWorkspace().resource.fsPath); } else { // TODO: Better error handling here? Perhaps present confirm if there was an error? return this.backupAll().then(() => false); // the backup will be restored, no veto From 3387b6c1b1cf0378724497c807de4ba2bdc319df Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 01:26:30 -0700 Subject: [PATCH 25/45] Move BackupService to vs/platform/backup/node/ --- .../common/backup.ts => platform/backup/node/backupService.ts} | 0 src/vs/workbench/electron-browser/shell.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/vs/{workbench/common/backup.ts => platform/backup/node/backupService.ts} (100%) diff --git a/src/vs/workbench/common/backup.ts b/src/vs/platform/backup/node/backupService.ts similarity index 100% rename from src/vs/workbench/common/backup.ts rename to src/vs/platform/backup/node/backupService.ts diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index ab4f9d295a4ac..496a34697130b 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -20,7 +20,7 @@ import product from 'vs/platform/product'; import pkg from 'vs/platform/package'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import timer = require('vs/base/common/timer'); -import { BackupService } from 'vs/workbench/common/backup'; +import { BackupService } from 'vs/platform/backup/node/backupService'; import { IBackupService } from 'vs/platform/backup/common/backup'; import { Workbench } from 'vs/workbench/electron-browser/workbench'; import { Storage, inMemoryLocalStorageInstance } from 'vs/workbench/node/storage'; From c280e0cac0c81de10802af3df4a8cc50b1e1b9ca Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 01:38:05 -0700 Subject: [PATCH 26/45] Merge main and renderer BackupServices --- src/vs/code/electron-main/backup.ts | 85 -------------------- src/vs/code/electron-main/main.ts | 3 +- src/vs/code/electron-main/windows.ts | 4 +- src/vs/platform/backup/common/backup.ts | 7 ++ src/vs/platform/backup/node/backupService.ts | 17 +++- 5 files changed, 26 insertions(+), 90 deletions(-) delete mode 100644 src/vs/code/electron-main/backup.ts diff --git a/src/vs/code/electron-main/backup.ts b/src/vs/code/electron-main/backup.ts deleted file mode 100644 index 7706b35b8448e..0000000000000 --- a/src/vs/code/electron-main/backup.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as fs from 'original-fs'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; - -export const IBackupService = createDecorator('backupService'); - -export interface IBackupService { - getWorkspaceBackupPaths(): string[]; - clearWorkspaceBackupPaths(): void; - pushWorkspaceBackupPath(workspaces: string[]): void; - getWorkspaceBackupFiles(workspace: string): string[]; -} - -interface IBackupFormat { - folderWorkspaces?: { - [workspacePath: string]: string[] - }; -} - -export class BackupService implements IBackupService { - - private fileContent: IBackupFormat; - - constructor( - @IEnvironmentService private environmentService: IEnvironmentService - ) { - } - - public getWorkspaceBackupPaths(): string[] { - if (!this.fileContent) { - this.load(); - } - return Object.keys(this.fileContent.folderWorkspaces || Object.create(null)); - } - - public clearWorkspaceBackupPaths(): void { - this.fileContent = { - folderWorkspaces: Object.create(null) - }; - this.save(); - } - - public pushWorkspaceBackupPath(workspaces: string[]): void { - this.load(); - workspaces.forEach(workspace => { - if (!this.fileContent.folderWorkspaces[workspace]) { - this.fileContent.folderWorkspaces[workspace] = []; - } - }); - this.save(); - } - - public getWorkspaceBackupFiles(workspace: string): string[] { - this.load(); - return this.fileContent.folderWorkspaces[workspace]; - } - - private load(): void { - try { - this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath).toString()); // invalid JSON or permission issue can happen here - } catch (error) { - this.fileContent = Object.create(null); - } - if (Array.isArray(this.fileContent) || typeof this.fileContent !== 'object') { - this.fileContent = Object.create(null); - } - if (!this.fileContent.folderWorkspaces) { - this.fileContent.folderWorkspaces = Object.create(null); - } - } - - private save(): void { - try { - fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent)); - } catch (error) { - } - } -} \ No newline at end of file diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index f67f42f11b658..61abd4bb8f766 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -32,7 +32,8 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService, MainLogService } from 'vs/code/electron-main/log'; import { IStorageService, StorageService } from 'vs/code/electron-main/storage'; -import { IBackupService, BackupService } from 'vs/code/electron-main/backup'; +import { IBackupService } from 'vs/platform/backup/common/backup'; +import { BackupService } from 'vs/platform/backup/node/backupService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 8a43305043fd3..9f033a30534c9 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -14,7 +14,7 @@ import * as types from 'vs/base/common/types'; import * as arrays from 'vs/base/common/arrays'; import { assign, mixin } from 'vs/base/common/objects'; import { EventEmitter } from 'events'; -import { IBackupService } from 'vs/code/electron-main/backup'; +import { IBackupService } from 'vs/platform/backup/common/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IStorageService } from 'vs/code/electron-main/storage'; import { IPath, VSCodeWindow, ReadyState, IWindowConfiguration, IWindowState as ISingleWindowState, defaultWindowState, IWindowSettings } from 'vs/code/electron-main/window'; @@ -747,7 +747,7 @@ export class WindowsManager implements IWindowsService { // Add to backups console.log('iPathsToOpen', iPathsToOpen); - this.backupService.pushWorkspaceBackupPath(iPathsToOpen.map((path) => { + this.backupService.pushWorkspaceBackupPaths(iPathsToOpen.map((path) => { return path.workspacePath; })); diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index cf5363949a9c6..fc07351717293 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -25,6 +25,13 @@ export interface IBackupService { */ clearWorkspaceBackupPaths(): void; + /** + * Pushes workspace backup paths to be tracked for restoration. + * + * @param workspaces The workspaces to add. + */ + pushWorkspaceBackupPaths(workspaces: string[]): void; + /** * Removes a workspace backup path being tracked for restoration, deregistering all associated * resources for backup. diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index f29f1b4e64df5..9abbb0153da89 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -29,9 +29,12 @@ export class BackupService implements IBackupService { constructor( @IEnvironmentService private environmentService: IEnvironmentService, - @IWorkspaceContextService contextService: IWorkspaceContextService + @IWorkspaceContextService contextService?: IWorkspaceContextService ) { - this.workspaceResource = contextService.getWorkspace().resource; + // IWorkspaceContextService will not exist on the main process + if (contextService) { + this.workspaceResource = contextService.getWorkspace().resource; + } } public getWorkspaceBackupPaths(): string[] { @@ -46,6 +49,16 @@ export class BackupService implements IBackupService { this.save(); } + public pushWorkspaceBackupPaths(workspaces: string[]): void { + this.load(); + workspaces.forEach(workspace => { + if (!this.fileContent.folderWorkspaces[workspace]) { + this.fileContent.folderWorkspaces[workspace] = []; + } + }); + this.save(); + } + public removeWorkspaceBackupPath(workspace: string): void { this.load(); if (!this.fileContent.folderWorkspaces) { From 10aa7095f6fa6104155bf97adfd8e4ad6dc5eb28 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 02:10:11 -0700 Subject: [PATCH 27/45] Disable hot exit on empty workspaces, fix uncaught exceptions --- src/vs/platform/backup/node/backupService.ts | 35 +++++++++++++++- .../workbench/electron-browser/workbench.ts | 20 +++++---- .../services/files/node/fileService.ts | 41 ++++++++++++++++--- .../textfile/browser/textFileService.ts | 3 +- 4 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index 9abbb0153da89..6e355b2cdc8cc 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -32,9 +32,17 @@ export class BackupService implements IBackupService { @IWorkspaceContextService contextService?: IWorkspaceContextService ) { // IWorkspaceContextService will not exist on the main process - if (contextService) { - this.workspaceResource = contextService.getWorkspace().resource; + if (!contextService) { + return; } + + // Hot exit is disabled for empty workspaces + const workspace = contextService.getWorkspace(); + if (!workspace) { + return; + } + + this.workspaceResource = workspace.resource; } public getWorkspaceBackupPaths(): string[] { @@ -52,6 +60,10 @@ export class BackupService implements IBackupService { public pushWorkspaceBackupPaths(workspaces: string[]): void { this.load(); workspaces.forEach(workspace => { + // Hot exit is disabled for empty workspaces + if (!workspace) { + return; + } if (!this.fileContent.folderWorkspaces[workspace]) { this.fileContent.folderWorkspaces[workspace] = []; } @@ -74,6 +86,11 @@ export class BackupService implements IBackupService { } public getWorkspaceUntitledFileBackups(workspace: string): string[] { + // Hot exit is disabled for empty workspaces + if (!this.workspaceResource) { + return; + } + const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); try { @@ -87,6 +104,10 @@ export class BackupService implements IBackupService { } public getBackupResource(resource: Uri): Uri { + // Hot exit is disabled for empty workspaces + if (!this.workspaceResource) { + return; + } const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); @@ -96,6 +117,11 @@ export class BackupService implements IBackupService { } public registerResourceForBackup(resource: Uri): void { + // Hot exit is disabled for empty workspaces + if (!this.workspaceResource) { + return; + } + this.load(); if (arrays.contains(this.fileContent.folderWorkspaces[this.workspaceResource.fsPath], resource.fsPath)) { return; @@ -105,6 +131,11 @@ export class BackupService implements IBackupService { } public deregisterResourceForBackup(resource: Uri): void { + // Hot exit is disabled for empty workspaces + if (!this.workspaceResource) { + return; + } + this.load(); this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].filter(value => value !== resource.fsPath); this.save(); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index d43b7bffe3ecf..0a91453ab58e5 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -172,20 +172,22 @@ export class Workbench implements IPartService { serviceCollection }; - // Restore any backups if they exist - options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackups(workspace.resource.fsPath).map(filePath => { - return { resource: Uri.file(filePath), options: { pinned: true } }; - }); - options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackups(workspace.resource.fsPath).map(untitledFilePath => { - return { resource: Uri.file(untitledFilePath), options: { pinned: true } }; - }); + // Restore any backups if they exist for this workspace (empty workspaces are not supported yet) + if (workspace) { + options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackups(workspace.resource.fsPath).map(filePath => { + return { resource: Uri.file(filePath), options: { pinned: true } }; + }); + options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackups(workspace.resource.fsPath).map(untitledFilePath => { + return { resource: Uri.file(untitledFilePath), options: { pinned: true } }; + }); + } this.hasFilesToCreateOpenOrDiff = (options.filesToCreate && options.filesToCreate.length > 0) || (options.filesToOpen && options.filesToOpen.length > 0) || (options.filesToDiff && options.filesToDiff.length > 0) || - (options.filesToRestore.length > 0) || - (options.untitledFilesToRestore.length > 0); + (options.filesToRestore && options.filesToRestore.length > 0) || + (options.untitledFilesToRestore && options.untitledFilesToRestore.length > 0); this.toDispose = []; this.toShutdown = []; diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 9c4d289cd1b23..f3e1da90faaf1 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -463,17 +463,36 @@ export class FileService implements IFileService { this.backupService.registerResourceForBackup(resource); } const backupResource = this.getBackupPath(resource); + + // Hot exit is disabled for empty workspaces + if (!backupResource) { + return TPromise.as(null); + } + console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); } public discardBackup(resource: uri): TPromise { this.backupService.deregisterResourceForBackup(resource); - return this.del(this.getBackupPath(resource)); + const backupResource = this.getBackupPath(resource); + + // Hot exit is disabled for empty workspaces + if (!backupResource) { + return TPromise.as(null); + } + + return this.del(backupResource); } public discardBackups(): TPromise { - return this.del(uri.file(this.getBackupRoot())); + // Hot exit is disabled for empty workspaces + const backupRootPath = this.getBackupRootPath(); + if (!backupRootPath) { + return TPromise.as(void 0); + } + + return this.del(uri.file(backupRootPath)); } public isHotExitEnabled(): boolean { @@ -483,13 +502,25 @@ export class FileService implements IFileService { // Helpers private getBackupPath(resource: uri): uri { + // Hot exit is disabled for empty workspaces + const backupRootPath = this.getBackupRootPath(); + if (!backupRootPath) { + return null; + } + const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); - const backupPath = paths.join(this.getBackupRoot(), resource.scheme, backupName); + const backupPath = paths.join(backupRootPath, resource.scheme, backupName); return uri.file(backupPath); } - private getBackupRoot(): string { - let workspaceHash = crypto.createHash('md5').update(this.contextService.getWorkspace().resource.fsPath).digest('hex'); + private getBackupRootPath(): string { + // Hot exit is disabled for empty workspaces + const workspace = this.contextService.getWorkspace(); + if (!workspace) { + return null; + } + + const workspaceHash = crypto.createHash('md5').update(workspace.resource.fsPath).digest('hex'); return paths.join(this.environmentService.userDataPath, 'Backups', workspaceHash); } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index f0d236260786d..d10171202ac56 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -231,7 +231,8 @@ export abstract class TextFileService implements ITextFileService { this.saveAll().done(null, errors.onUnexpectedError); } - this.configuredHotExit = configuration && configuration.files && configuration.files.hotExit; + // Hot exit is disabled for empty workspaces + this.configuredHotExit = this.contextService.getWorkspace() && configuration && configuration.files && configuration.files.hotExit; // Check for change in files associations const filesAssociation = configuration && configuration.files && configuration.files.associations; From 69a6a18d692dc049d41723ef2d67ed7c101b7c64 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 02:14:08 -0700 Subject: [PATCH 28/45] Remove empty line after function --- .../workbench/parts/files/electron-browser/dirtyFilesTracker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index 525d39ab02d66..23a6d5b33b711 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -66,7 +66,6 @@ export class DirtyFilesTracker implements IWorkbenchContribution { } private onUntitledDidChangeDirty(resource: URI): void { - const gotDirty = this.untitledEditorService.isDirty(resource); if ((!this.isDocumentedEdited && gotDirty) || (this.isDocumentedEdited && !gotDirty)) { From 9d4ec9999d199fe6c4b1e66c0436c080464058d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 12:04:28 -0700 Subject: [PATCH 29/45] Remove IWorkspaceContextService as dep on BackupService The dep can't be fulfilled on main process --- src/vs/platform/backup/node/backupService.ts | 18 ++++-------------- src/vs/workbench/electron-browser/shell.ts | 1 + 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index 6e355b2cdc8cc..077015009fc6d 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -10,7 +10,6 @@ import * as crypto from 'crypto'; import * as fs from 'original-fs'; import * as arrays from 'vs/base/common/arrays'; import Uri from 'vs/base/common/uri'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IBackupService } from 'vs/platform/backup/common/backup'; @@ -28,21 +27,12 @@ export class BackupService implements IBackupService { private fileContent: IBackupFormat; constructor( - @IEnvironmentService private environmentService: IEnvironmentService, - @IWorkspaceContextService contextService?: IWorkspaceContextService + @IEnvironmentService private environmentService: IEnvironmentService ) { - // IWorkspaceContextService will not exist on the main process - if (!contextService) { - return; - } - - // Hot exit is disabled for empty workspaces - const workspace = contextService.getWorkspace(); - if (!workspace) { - return; - } + } - this.workspaceResource = workspace.resource; + public setCurrentWorkspace(resource: Uri): void { + this.workspaceResource = resource; } public getWorkspaceBackupPaths(): string[] { diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 496a34697130b..08c77f5808896 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -247,6 +247,7 @@ export class WorkbenchShell { // Backup const backupService = instantiationService.createInstance(BackupService); + backupService.setCurrentWorkspace(this.contextService.getWorkspace().resource); serviceCollection.set(IBackupService, backupService); // Storage From a002932910cb21cc0d454f2ea40bca1c3074339b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 14 Oct 2016 12:50:21 -0700 Subject: [PATCH 30/45] Clean up pass --- src/vs/code/electron-main/windows.ts | 2 -- src/vs/platform/backup/node/backupService.ts | 8 ++------ src/vs/workbench/common/editor/untitledEditorModel.ts | 3 --- src/vs/workbench/electron-browser/main.ts | 1 - .../parts/files/electron-browser/dirtyFilesTracker.ts | 1 - src/vs/workbench/services/files/node/fileService.ts | 1 - .../services/textfile/common/textFileEditorModel.ts | 5 ----- 7 files changed, 2 insertions(+), 19 deletions(-) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 9f033a30534c9..15056fd45fb4d 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -607,7 +607,6 @@ export class WindowsManager implements IWindowsService { // Add any existing backup workspaces if (openConfig.restoreBackups) { - // TODO: Ensure the workspaces being added actually have backups this.backupService.getWorkspaceBackupPaths().forEach(ws => { iPathsToOpen.push(this.toIPath(ws)); }); @@ -746,7 +745,6 @@ export class WindowsManager implements IWindowsService { iPathsToOpen.forEach(iPath => this.eventEmitter.emit(EventTypes.OPEN, iPath)); // Add to backups - console.log('iPathsToOpen', iPathsToOpen); this.backupService.pushWorkspaceBackupPaths(iPathsToOpen.map((path) => { return path.workspacePath; })); diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index 077015009fc6d..a7760211d4b90 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -37,7 +37,7 @@ export class BackupService implements IBackupService { public getWorkspaceBackupPaths(): string[] { this.load(); - return Object.keys(this.fileContent.folderWorkspaces || Object.create(null)); + return Object.keys(this.fileContent.folderWorkspaces); } public clearWorkspaceBackupPaths(): void { @@ -84,11 +84,8 @@ export class BackupService implements IBackupService { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); try { - const untitledFiles = fs.readdirSync(untitledDir).map(file => path.join(untitledDir, file)); - console.log('untitledFiles', untitledFiles); - return untitledFiles; + return fs.readdirSync(untitledDir).map(file => path.join(untitledDir, file)); } catch (ex) { - console.log('untitled backups do not exist'); return []; } } @@ -102,7 +99,6 @@ export class BackupService implements IBackupService { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); const backupPath = path.join(this.environmentService.backupHome, workspaceHash, resource.scheme, backupName); - console.log('getBackupResource ' + Uri.file(backupPath)); return Uri.file(backupPath); } diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 454167b3d76b7..df1f5fc259052 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -163,10 +163,8 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS if (this.fileService.isHotExitEnabled()) { if (this.dirty) { - console.log('backup'); this.doBackup(); } else { - console.log('discard'); this.fileService.discardBackup(this.resource); } } @@ -189,7 +187,6 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this._onDidChangeEncoding.dispose(); this.cancelBackupPromises(); - console.log('discard'); this.fileService.discardBackup(this.resource); } diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 1abe08d931b79..f94821cb5a857 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -48,7 +48,6 @@ export function startup(configuration: IWindowConfiguration): TPromise { const filesToOpen = configuration.filesToOpen && configuration.filesToOpen.length ? toInputs(configuration.filesToOpen) : null; const filesToCreate = configuration.filesToCreate && configuration.filesToCreate.length ? toInputs(configuration.filesToCreate) : null; const filesToDiff = configuration.filesToDiff && configuration.filesToDiff.length ? toInputs(configuration.filesToDiff) : null; - const shellOptions: IOptions = { filesToOpen, filesToCreate, diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index 23a6d5b33b711..47a3caccbc8e0 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -78,7 +78,6 @@ export class DirtyFilesTracker implements IWorkbenchContribution { } private onTextFileDirty(e: TextFileModelChangeEvent): void { - if ((this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) && !this.isDocumentedEdited) { this.updateDocumentEdited(); // no indication needed when auto save is enabled for short delay } diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index f3e1da90faaf1..0cf572d7513af 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -469,7 +469,6 @@ export class FileService implements IFileService { return TPromise.as(null); } - console.log(`Backing up to ${backupResource.fsPath}`); return this.updateContent(backupResource, content); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index e0a68abbce363..ac651f021a10b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -189,7 +189,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil public load(force?: boolean /* bypass any caches and really go to disk */): TPromise { diag('load() - enter', this.resource, new Date()); - console.log(`load model ${this.resource.fsPath}`); // It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk // if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk @@ -267,7 +266,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (this.restoreResource) { // TODO: De-duplicate code - console.log(`Attempting to restore resource ${this.restoreResource.fsPath}`); this.createTextEditorModelPromise = this.textFileService.resolveTextContent(this.restoreResource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then((restoreContent) => { return this.createTextEditorModel(restoreContent.value, content.resource).then(() => { this.createTextEditorModelPromise = null; @@ -347,7 +345,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (this.fileService.isHotExitEnabled()) { - console.log('discard'); this.fileService.discardBackup(this.resource); } @@ -369,7 +366,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (this.fileService.isHotExitEnabled()) { - console.log('backup'); this.doBackup(); } } @@ -794,7 +790,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.cancelAutoSavePromises(); this.cancelBackupPromises(); - console.log('discard'); this.fileService.discardBackup(this.resource); super.dispose(); From 2b0a65afb0a7b9856881f59bb419ac05e4cab2a5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 17 Oct 2016 10:00:23 -0700 Subject: [PATCH 31/45] Remove async calls in all but critical window/workbench init Also clean up shutdown logic to fix issue where backup workspaces were not being evicted from workspaces.json. --- src/vs/code/electron-main/windows.ts | 2 +- src/vs/platform/backup/common/backup.ts | 17 +-- src/vs/platform/backup/node/backupService.ts | 103 +++++++++++------- .../services/files/node/fileService.ts | 34 +++--- .../textfile/browser/textFileService.ts | 34 ++++-- 5 files changed, 119 insertions(+), 71 deletions(-) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 15056fd45fb4d..0d1be5a10f003 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -607,7 +607,7 @@ export class WindowsManager implements IWindowsService { // Add any existing backup workspaces if (openConfig.restoreBackups) { - this.backupService.getWorkspaceBackupPaths().forEach(ws => { + this.backupService.getWorkspaceBackupPathsSync().forEach(ws => { iPathsToOpen.push(this.toIPath(ws)); }); // Get rid of duplicates diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index fc07351717293..dc1d650d0970b 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -5,8 +5,9 @@ 'use strict'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import Uri from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TPromise } from 'vs/base/common/winjs.base'; export const IBackupService = createDecorator('backupService'); @@ -18,12 +19,14 @@ export interface IBackupService { * * @return The set of active workspace backup paths being tracked for restoration. */ - getWorkspaceBackupPaths(): string[]; + getWorkspaceBackupPaths(): TPromise; /** - * Clears all active workspace backup paths being tracked for restoration. + * Gets the set of active workspace backup paths being tracked for restoration. + * + * @return The set of active workspace backup paths being tracked for restoration. */ - clearWorkspaceBackupPaths(): void; + getWorkspaceBackupPathsSync(): string[]; /** * Pushes workspace backup paths to be tracked for restoration. @@ -38,7 +41,7 @@ export interface IBackupService { * * @param workspace The absolute workspace path being removed. */ - removeWorkspaceBackupPath(workspace: string): void; + removeWorkspaceBackupPath(workspace: string): TPromise; /** * Gets the set of text files that are backed up for a particular workspace. @@ -61,14 +64,14 @@ export interface IBackupService { * * @param resource The resource that is being backed up. */ - registerResourceForBackup(resource: Uri): void; + registerResourceForBackup(resource: Uri): TPromise; /** * Deregisters a resource for backup, unflagging it for restoration. * * @param resource The resource that is no longer being backed up. */ - deregisterResourceForBackup(resource: Uri): void; + deregisterResourceForBackup(resource: Uri): TPromise; /** * Gets the backup resource for a particular resource within the current workspace. diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index a7760211d4b90..0f0dd3ac037a8 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -7,11 +7,13 @@ import * as path from 'path'; import * as crypto from 'crypto'; -import * as fs from 'original-fs'; import * as arrays from 'vs/base/common/arrays'; +import fs = require('fs'); import Uri from 'vs/base/common/uri'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IBackupService } from 'vs/platform/backup/common/backup'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { nfcall } from 'vs/base/common/async'; interface IBackupFormat { folderWorkspaces?: { @@ -35,20 +37,25 @@ export class BackupService implements IBackupService { this.workspaceResource = resource; } - public getWorkspaceBackupPaths(): string[] { - this.load(); - return Object.keys(this.fileContent.folderWorkspaces); + public getWorkspaceBackupPaths(): TPromise { + return this.load().then(() => { + return Object.keys(this.fileContent.folderWorkspaces); + }); } - public clearWorkspaceBackupPaths(): void { - this.fileContent = { - folderWorkspaces: Object.create(null) - }; - this.save(); + public getWorkspaceBackupPathsSync(): string[] { + this.loadSync(); + return Object.keys(this.fileContent.folderWorkspaces); } public pushWorkspaceBackupPaths(workspaces: string[]): void { - this.load(); + // Only allow this on the main thread in the window initialization's critical path due to + // the usage of synchronous IO. + if (this.workspaceResource) { + throw new Error('pushWorkspaceBackupPaths should only be called on the main process'); + } + + this.loadSync(); workspaces.forEach(workspace => { // Hot exit is disabled for empty workspaces if (!workspace) { @@ -58,20 +65,22 @@ export class BackupService implements IBackupService { this.fileContent.folderWorkspaces[workspace] = []; } }); - this.save(); + this.saveSync(); } - public removeWorkspaceBackupPath(workspace: string): void { - this.load(); - if (!this.fileContent.folderWorkspaces) { - return; - } - delete this.fileContent.folderWorkspaces[workspace]; - this.save(); + public removeWorkspaceBackupPath(workspace: string): TPromise { + return this.load().then(() => { + if (!this.fileContent.folderWorkspaces) { + return; + } + delete this.fileContent.folderWorkspaces[workspace]; + return this.save(); + }); } public getWorkspaceTextFilesWithBackups(workspace: string): string[] { - this.load(); + // Allow sync here as it's only used in workbench initialization's critical path + this.loadSync(); return this.fileContent.folderWorkspaces[workspace] || []; } @@ -83,6 +92,8 @@ export class BackupService implements IBackupService { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); + + // Allow sync here as it's only used in workbench initialization's critical path try { return fs.readdirSync(untitledDir).map(file => path.join(untitledDir, file)); } catch (ex) { @@ -102,34 +113,48 @@ export class BackupService implements IBackupService { return Uri.file(backupPath); } - public registerResourceForBackup(resource: Uri): void { + public registerResourceForBackup(resource: Uri): TPromise { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { - return; + return TPromise.as(void 0); } - this.load(); - if (arrays.contains(this.fileContent.folderWorkspaces[this.workspaceResource.fsPath], resource.fsPath)) { - return; - } - this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].push(resource.fsPath); - this.save(); + return this.load().then(() => { + if (arrays.contains(this.fileContent.folderWorkspaces[this.workspaceResource.fsPath], resource.fsPath)) { + return TPromise.as(void 0); + } + this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].push(resource.fsPath); + return this.save(); + }); } - public deregisterResourceForBackup(resource: Uri): void { + public deregisterResourceForBackup(resource: Uri): TPromise { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { return; } - this.load(); - this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].filter(value => value !== resource.fsPath); - this.save(); + return this.load().then(() => { + this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].filter(value => value !== resource.fsPath); + return this.save(); + }); + } + + private load(): TPromise { + return nfcall(fs.readFile, this.environmentService.backupWorkspacesPath, 'utf8').then(content => { + return JSON.parse(content.toString()); + }).then(null, () => Object.create(null)).then(content => { + this.fileContent = content; + if (!this.fileContent.folderWorkspaces) { + this.fileContent.folderWorkspaces = Object.create(null); + } + return void 0; + }); } - private load(): void { + private loadSync(): void { try { - this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath).toString()); // invalid JSON or permission issue can happen here + this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath, 'utf8').toString()); // invalid JSON or permission issue can happen here } catch (error) { this.fileContent = Object.create(null); } @@ -138,10 +163,14 @@ export class BackupService implements IBackupService { } } - private save(): void { + private save(): TPromise { + return nfcall(fs.writeFile, this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent), { encoding: 'utf8' }); + } + + private saveSync(): void { try { - fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent)); - } catch (error) { + fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent), { encoding: 'utf8' }); + } catch (ex) { } } } \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 0cf572d7513af..eb14800ad728a 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -459,29 +459,35 @@ export class FileService implements IFileService { public backupFile(resource: uri, content: string): TPromise { // TODO: This should not backup unless necessary. Currently this is called for each file on close to ensure the files are backed up. + let registerResourcePromise: TPromise; if (resource.scheme === 'file') { - this.backupService.registerResourceForBackup(resource); + registerResourcePromise = this.backupService.registerResourceForBackup(resource); + } else { + registerResourcePromise = TPromise.as(void 0); } - const backupResource = this.getBackupPath(resource); + return registerResourcePromise.then(() => { + const backupResource = this.getBackupPath(resource); - // Hot exit is disabled for empty workspaces - if (!backupResource) { - return TPromise.as(null); - } + // Hot exit is disabled for empty workspaces + if (!backupResource) { + return TPromise.as(null); + } - return this.updateContent(backupResource, content); + return this.updateContent(backupResource, content); + }); } public discardBackup(resource: uri): TPromise { - this.backupService.deregisterResourceForBackup(resource); - const backupResource = this.getBackupPath(resource); + return this.backupService.deregisterResourceForBackup(resource).then(() => { + const backupResource = this.getBackupPath(resource); - // Hot exit is disabled for empty workspaces - if (!backupResource) { - return TPromise.as(null); - } + // Hot exit is disabled for empty workspaces + if (!backupResource) { + return TPromise.as(null); + } - return this.del(backupResource); + return this.del(backupResource); + }); } public discardBackups(): TPromise { diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index d10171202ac56..69f110e93f4bd 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -116,16 +116,22 @@ export abstract class TextFileService implements ITextFileService { } private beforeShutdown(): boolean | TPromise { - // If hot exit is enabled then save the dirty files in the workspace and then exit if (this.configuredHotExit) { - // Only remove the workspace from the backup service if it's not the last one or it's not dirty - if (this.backupService.getWorkspaceBackupPaths().length > 1 || this.getDirty().length === 0) { - this.backupService.removeWorkspaceBackupPath(this.contextService.getWorkspace().resource.fsPath); - } else { + // If there are no dirty files, clean up and exit + if (this.getDirty().length === 0) { + return this.cleanupBackupsBeforeShutdown(); + } + + return this.backupService.getWorkspaceBackupPaths().then(workspaceBackupPaths => { + // Only remove the workspace from the backup service if it's not the last one or it's not dirty + if (workspaceBackupPaths.length > 1) { + return this.confirmBeforeShutdown(); + } + // TODO: Better error handling here? Perhaps present confirm if there was an error? return this.backupAll().then(() => false); // the backup will be restored, no veto - } + }); } // Dirty files need treatment on shutdown @@ -162,17 +168,13 @@ export abstract class TextFileService implements ITextFileService { return true; // veto if some saves failed } - return this.fileService.discardBackups().then(() => { - return false; // no veto - }); + return this.cleanupBackupsBeforeShutdown(); }); } // Don't Save else if (confirm === ConfirmResult.DONT_SAVE) { - return this.fileService.discardBackups().then(() => { - return false; // no veto - }); + return this.cleanupBackupsBeforeShutdown(); } // Cancel @@ -181,6 +183,14 @@ export abstract class TextFileService implements ITextFileService { } } + private cleanupBackupsBeforeShutdown(): boolean | TPromise { + return this.backupService.removeWorkspaceBackupPath(this.contextService.getWorkspace().resource.fsPath).then(() => { + return this.fileService.discardBackups().then(() => { + return false; // no veto + }); + }); + } + private onWindowFocusLost(): void { if (this.configuredAutoSaveOnWindowChange && this.isDirty()) { this.saveAll(void 0, SaveReason.WINDOW_CHANGE).done(null, errors.onUnexpectedError); From 9aa7b9c142b1a949d9041c64e499546509acc251 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 17 Oct 2016 10:26:28 -0700 Subject: [PATCH 32/45] Improve exit logic when errors occur --- .../common/editor/untitledEditorModel.ts | 4 +++ .../textfile/browser/textFileService.ts | 32 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index df1f5fc259052..d524c67f4a320 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -116,6 +116,10 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS return this.dirty; } + public getResource(): URI { + return this.resource; + } + public revert(): void { this.dirty = false; diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 69f110e93f4bd..aeccd8720f0e9 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -129,8 +129,14 @@ export abstract class TextFileService implements ITextFileService { return this.confirmBeforeShutdown(); } - // TODO: Better error handling here? Perhaps present confirm if there was an error? - return this.backupAll().then(() => false); // the backup will be restored, no veto + // Backup and hot exit + return this.backupAll().then(result => { + if (result.results.some(r => !r.success)) { + return true; // veto if some backups failed + } + + return false; // the backup went smoothly, no veto + }); }); } @@ -396,7 +402,7 @@ export abstract class TextFileService implements ITextFileService { /** * Performs an immedate backup of all dirty file and untitled models. */ - private backupAll(): TPromise { + private backupAll(): TPromise { const toBackup = this.getDirty(); // split up between files and untitled @@ -413,7 +419,7 @@ export abstract class TextFileService implements ITextFileService { return this.doBackupAll(filesToBackup, untitledToBackup); } - private doBackupAll(fileResources: URI[], untitledResources: URI[]): TPromise { + private doBackupAll(fileResources: URI[], untitledResources: URI[]): TPromise { // Handle file resources first const dirtyFileModels = this.getDirtyFileModels(fileResources); @@ -428,15 +434,27 @@ export abstract class TextFileService implements ITextFileService { return model.backup().then(() => { mapResourceToResult[model.getResource().toString()].success = true; }); - })).then(result => { + })).then(results => { // Handle untitled resources const untitledModelPromises = untitledResources.map(untitledResource => this.untitledEditorService.get(untitledResource)) .filter(untitled => !!untitled) .map(untitled => untitled.resolve()); return TPromise.join(untitledModelPromises).then(untitledModels => { - const untitledBackupPromises = untitledModels.map(model => model.backup()); - return TPromise.join(untitledBackupPromises).then(() => void 0); + const untitledBackupPromises = untitledModels.map(model => { + mapResourceToResult[model.getResource().toString()] = { + source: model.getResource(), + target: model.getResource() + }; + return model.backup().then(() => { + mapResourceToResult[model.getResource().toString()].success = true; + }); + }); + return TPromise.join(untitledBackupPromises).then(() => { + return { + results: Object.keys(mapResourceToResult).map(k => mapResourceToResult[k]) + }; + }); }); }); } From 1a46093364e52ec46483cc45abc94c0d12f36e40 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 17 Oct 2016 11:15:41 -0700 Subject: [PATCH 33/45] Fix unit tests --- src/vs/test/utils/servicesTestUtils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index 6c901096dc0be..e0fb18e8e6fa6 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -175,6 +175,7 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(IHistoryService, 'getHistory', []); instantiationService.stub(IModelService, createMockModelService(instantiationService)); instantiationService.stub(IFileService, TestFileService); + instantiationService.stub(IBackupService, TestBackupService); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IMessageService, new TestMessageService()); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); @@ -586,6 +587,12 @@ export const TestFileService = { } }; +export const TestBackupService = { + removeWorkspaceBackupPath: function () { + return TPromise.as(void 0); + } +}; + export class TestConfigurationService extends EventEmitter implements IConfigurationService { public _serviceBrand: any; From 084764d13bdf330c8688869971da28b4a122cabb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 17 Oct 2016 11:26:39 -0700 Subject: [PATCH 34/45] Add null checks to fix integration tests --- src/vs/platform/backup/node/backupService.ts | 8 ++++++-- src/vs/workbench/electron-browser/shell.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index 0f0dd3ac037a8..542e3ee79d834 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -135,8 +135,12 @@ export class BackupService implements IBackupService { } return this.load().then(() => { - this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].filter(value => value !== resource.fsPath); - return this.save(); + const workspace = this.fileContent.folderWorkspaces[this.workspaceResource.fsPath]; + if (workspace) { + this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = workspace.filter(value => value !== resource.fsPath); + return this.save(); + } + return TPromise.as(void 0); }); } diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index b1b1850f15502..12bc0e6b256c9 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -247,7 +247,7 @@ export class WorkbenchShell { // Backup const backupService = instantiationService.createInstance(BackupService); - backupService.setCurrentWorkspace(this.contextService.getWorkspace().resource); + backupService.setCurrentWorkspace(this.contextService.getWorkspace() ? this.contextService.getWorkspace().resource : null); serviceCollection.set(IBackupService, backupService); // Storage From da56a72f44c488a697f31d0553d1bb0545e4b5a2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 17 Oct 2016 11:47:06 -0700 Subject: [PATCH 35/45] Push backup resource aquisition to FileEditorInput --- src/vs/workbench/common/editor.ts | 5 ++++- .../parts/files/common/editors/fileEditorInput.ts | 13 ++++++++----- src/vs/workbench/parts/files/common/files.ts | 2 +- .../services/editor/browser/editorService.ts | 11 ++--------- .../untitled/common/untitledEditorService.ts | 2 -- .../test/common/editor/editorStacksModel.test.ts | 2 +- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 7ac5270250784..e976da7145a85 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -319,7 +319,10 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport { */ setResource(resource: URI): void; - setRestoreResource(resource: URI): void; + /** + * Sets whether to restore the resource from backup. + */ + setRestoreFromBackup(restore: boolean): void; /** * Sets the preferred encodingt to use for this input. diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 3616b30adebab..36dc6c1ef6153 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -16,6 +16,7 @@ import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent, L import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEventService } from 'vs/platform/event/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IBackupService } from 'vs/platform/backup/common/backup'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -24,9 +25,9 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; */ export class FileEditorInput extends CommonFileEditorInput { private resource: URI; - private restoreResource: URI; private preferredEncoding: string; private forceOpenAsBinary: boolean; + private restoreFromBackup: boolean; private name: string; private description: string; @@ -44,7 +45,8 @@ export class FileEditorInput extends CommonFileEditorInput { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IHistoryService private historyService: IHistoryService, @IEventService private eventService: IEventService, - @ITextFileService private textFileService: ITextFileService + @ITextFileService private textFileService: ITextFileService, + @IBackupService private backupService: IBackupService ) { super(); @@ -103,8 +105,8 @@ export class FileEditorInput extends CommonFileEditorInput { this.verboseDescription = null; } - public setRestoreResource(resource: URI): void { - this.restoreResource = resource; + public setRestoreFromBackup(restore: boolean): void { + this.restoreFromBackup = restore; } public getResource(): URI { @@ -200,7 +202,8 @@ export class FileEditorInput extends CommonFileEditorInput { } public resolve(refresh?: boolean): TPromise { - return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh, this.restoreResource).then(null, error => { + const backupResource = this.restoreFromBackup ? this.backupService.getBackupResource(this.resource) : null; + return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh, backupResource).then(null, error => { // In case of an error that indicates that the file is binary or too large, just return with the binary editor model if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 45507b56b3e30..6daf31d6b2aad 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -42,7 +42,7 @@ export abstract class FileEditorInput extends EditorInput implements IFileEditor public abstract setResource(resource: URI): void; - public abstract setRestoreResource(resource: URI): void; + public abstract setRestoreFromBackup(restore: boolean): void; public abstract getResource(): URI; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 25cee23fd2f00..1b7831c99084b 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -10,7 +10,6 @@ import network = require('vs/base/common/network'); import { Registry } from 'vs/platform/platform'; import { basename, dirname } from 'vs/base/common/paths'; import types = require('vs/base/common/types'); -import { IBackupService } from 'vs/platform/backup/common/backup'; import { IDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommonCodeEditor, IModel, EditorType, IEditor as ICommonEditor } from 'vs/editor/common/editorCommon'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -47,7 +46,6 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { constructor( editorPart: IEditorPart | IWorkbenchEditorService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, - @IBackupService private backupService: IBackupService, @IInstantiationService private instantiationService?: IInstantiationService ) { this.editorPart = editorPart; @@ -283,10 +281,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { return this.instantiationService.createInstance(this.fileInputDescriptor).then((typedFileInput) => { typedFileInput.setResource(resource); typedFileInput.setPreferredEncoding(encoding); - - if (restoreFromBackup) { - typedFileInput.setRestoreResource(this.backupService.getBackupResource(resource)); - } + typedFileInput.setRestoreFromBackup(restoreFromBackup); return typedFileInput; }); @@ -321,13 +316,11 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { handler: IDelegatingWorkbenchEditorServiceHandler, @IUntitledEditorService untitledEditorService: IUntitledEditorService, @IInstantiationService instantiationService: IInstantiationService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IBackupService backupService: IBackupService + @IWorkbenchEditorService editorService: IWorkbenchEditorService ) { super( editorService, untitledEditorService, - backupService, instantiationService ); diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index c533d90ef9664..f240fcde1ecc6 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -6,7 +6,6 @@ import URI from 'vs/base/common/uri'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IBackupService } from 'vs/platform/backup/common/backup'; import arrays = require('vs/base/common/arrays'); import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; import Event, { Emitter, once } from 'vs/base/common/event'; @@ -78,7 +77,6 @@ export class UntitledEditorService implements IUntitledEditorService { private _onDidChangeEncoding: Emitter; constructor( - @IBackupService private backupService: IBackupService, @IInstantiationService private instantiationService: IInstantiationService ) { this._onDidChangeDirty = new Emitter(); diff --git a/src/vs/workbench/test/common/editor/editorStacksModel.test.ts b/src/vs/workbench/test/common/editor/editorStacksModel.test.ts index 0f0cafe42b587..ff3addbc3eed0 100644 --- a/src/vs/workbench/test/common/editor/editorStacksModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorStacksModel.test.ts @@ -144,7 +144,7 @@ class TestFileEditorInput extends EditorInput implements IFileEditorInput { public setResource(r: URI): void { } - public setRestoreResource(r: URI): void { + public setRestoreFromBackup(restore: boolean): void { } public setEncoding(encoding: string) { From e53f008803e2362fe31f899a1ae7e0d112d1a08f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 18 Oct 2016 15:24:25 -0700 Subject: [PATCH 36/45] Fix sync-related backupservice issues, add tests --- src/vs/code/electron-main/windows.ts | 2 +- src/vs/platform/backup/common/backup.ts | 6 +- src/vs/platform/backup/node/backupService.ts | 24 ++- .../backup/test/backupService.test.ts | 164 ++++++++++++++++++ .../workbench/electron-browser/workbench.ts | 4 +- 5 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 src/vs/platform/backup/test/backupService.test.ts diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 0d1be5a10f003..ea2ceaa4599c1 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -745,7 +745,7 @@ export class WindowsManager implements IWindowsService { iPathsToOpen.forEach(iPath => this.eventEmitter.emit(EventTypes.OPEN, iPath)); // Add to backups - this.backupService.pushWorkspaceBackupPaths(iPathsToOpen.map((path) => { + this.backupService.pushWorkspaceBackupPathsSync(iPathsToOpen.map((path) => { return path.workspacePath; })); diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index dc1d650d0970b..ffb63dfb8760e 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -33,7 +33,7 @@ export interface IBackupService { * * @param workspaces The workspaces to add. */ - pushWorkspaceBackupPaths(workspaces: string[]): void; + pushWorkspaceBackupPathsSync(workspaces: string[]): void; /** * Removes a workspace backup path being tracked for restoration, deregistering all associated @@ -49,7 +49,7 @@ export interface IBackupService { * @param workspace The workspace to get the backed up files for. * @return The absolute paths for text files _that have backups_. */ - getWorkspaceTextFilesWithBackups(workspace: string): string[]; + getWorkspaceTextFilesWithBackupsSync(workspace: string): string[]; /** * Gets the set of untitled file backups for a particular workspace. @@ -57,7 +57,7 @@ export interface IBackupService { * @param workspace The workspace to get the backups for for. * @return The absolute paths for all the untitled file _backups_. */ - getWorkspaceUntitledFileBackups(workspace: string): string[]; + getWorkspaceUntitledFileBackupsSync(workspace: string): string[]; /** * Registers a resource for backup, flagging it for restoration. diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index 542e3ee79d834..db851b1f064d9 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -9,11 +9,11 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as arrays from 'vs/base/common/arrays'; import fs = require('fs'); +import pfs = require('vs/base/node/pfs'); import Uri from 'vs/base/common/uri'; import { IBackupService } from 'vs/platform/backup/common/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; -import { nfcall } from 'vs/base/common/async'; interface IBackupFormat { folderWorkspaces?: { @@ -48,7 +48,7 @@ export class BackupService implements IBackupService { return Object.keys(this.fileContent.folderWorkspaces); } - public pushWorkspaceBackupPaths(workspaces: string[]): void { + public pushWorkspaceBackupPathsSync(workspaces: string[]): void { // Only allow this on the main thread in the window initialization's critical path due to // the usage of synchronous IO. if (this.workspaceResource) { @@ -78,13 +78,13 @@ export class BackupService implements IBackupService { }); } - public getWorkspaceTextFilesWithBackups(workspace: string): string[] { + public getWorkspaceTextFilesWithBackupsSync(workspace: string): string[] { // Allow sync here as it's only used in workbench initialization's critical path this.loadSync(); return this.fileContent.folderWorkspaces[workspace] || []; } - public getWorkspaceUntitledFileBackups(workspace: string): string[] { + public getWorkspaceUntitledFileBackupsSync(workspace: string): string[] { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { return; @@ -120,6 +120,9 @@ export class BackupService implements IBackupService { } return this.load().then(() => { + if (!(this.workspaceResource.fsPath in this.fileContent.folderWorkspaces)) { + this.fileContent.folderWorkspaces[this.workspaceResource.fsPath] = []; + } if (arrays.contains(this.fileContent.folderWorkspaces[this.workspaceResource.fsPath], resource.fsPath)) { return TPromise.as(void 0); } @@ -145,7 +148,7 @@ export class BackupService implements IBackupService { } private load(): TPromise { - return nfcall(fs.readFile, this.environmentService.backupWorkspacesPath, 'utf8').then(content => { + return pfs.readFile(this.environmentService.backupWorkspacesPath, 'utf8').then(content => { return JSON.parse(content.toString()); }).then(null, () => Object.create(null)).then(content => { this.fileContent = content; @@ -168,12 +171,19 @@ export class BackupService implements IBackupService { } private save(): TPromise { - return nfcall(fs.writeFile, this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent), { encoding: 'utf8' }); + const data = JSON.stringify(this.fileContent); + return pfs.mkdirp(this.environmentService.backupHome).then(() => { + return pfs.writeFile(this.environmentService.backupWorkspacesPath, data); + }); } private saveSync(): void { try { - fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent), { encoding: 'utf8' }); + // The user data directory must exist so only the Backup directory needs to be checked. + if (!fs.existsSync(this.environmentService.backupHome)) { + fs.mkdirSync(this.environmentService.backupHome); + } + fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent)); } catch (ex) { } } diff --git a/src/vs/platform/backup/test/backupService.test.ts b/src/vs/platform/backup/test/backupService.test.ts new file mode 100644 index 0000000000000..c0580cb180aa5 --- /dev/null +++ b/src/vs/platform/backup/test/backupService.test.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import crypto = require('crypto'); +import os = require('os'); +import path = require('path'); +import extfs = require('vs/base/node/extfs'); +import pfs = require('vs/base/node/pfs'); +import Uri from 'vs/base/common/uri'; +import { nfcall } from 'vs/base/common/async'; +import { TestEnvironmentService } from 'vs/test/utils/servicesTestUtils'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { BackupService } from 'vs/platform/backup/node/backupService'; + +suite('BackupService', () => { + let environmentService: IEnvironmentService; + let backupService: BackupService; + + setup(done => { + environmentService = TestEnvironmentService; + backupService = new BackupService(environmentService); + + // Delete any existing backups completely, this in itself is a test to ensure that the + // the backupHome directory is re-created + nfcall(extfs.del, environmentService.backupHome, os.tmpdir()).then(() => { + done(); + }); + }); + + teardown(done => { + nfcall(extfs.del, environmentService.backupHome, os.tmpdir()).then(() => { + done(); + }); + }); + + test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => { + backupService.pushWorkspaceBackupPathsSync(['/foo', '/bar']); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo', '/bar']); + }); + + test('pushWorkspaceBackupPathsSync should throw if a workspace is set', () => { + backupService.setCurrentWorkspace(Uri.file('/foo')); + assert.throws(() => backupService.pushWorkspaceBackupPathsSync(['/foo', '/bar'])); + }); + + test('removeWorkspaceBackupPath should remove workspaces from workspaces.json', done => { + backupService.pushWorkspaceBackupPathsSync(['/foo', '/bar']); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo', '/bar']); + backupService.removeWorkspaceBackupPath('/foo').then(() => { + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/bar']); + backupService.removeWorkspaceBackupPath('/bar').then(() => { + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), []); + done(); + }); + }); + }); + + test('removeWorkspaceBackupPath should fail gracefully when removing a path that doesn\'t exist', done => { + backupService.pushWorkspaceBackupPathsSync(['/foo']); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo']); + backupService.removeWorkspaceBackupPath('/bar').then(() => { + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo']); + done(); + }); + }); + + test('registerResourceForBackup should register backups to workspaces.json', done => { + backupService.setCurrentWorkspace(Uri.file('/foo')); + backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync('/foo'), ['/bar']); + done(); + }); + }); + + test('deregisterResourceForBackup should deregister backups from workspaces.json', done => { + backupService.setCurrentWorkspace(Uri.file('/foo')); + backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync('/foo'), ['/bar']); + backupService.deregisterResourceForBackup(Uri.file('/bar')).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync('/foo'), []); + done(); + }); + }); + }); + + test('getBackupResource should get the correct backup path for text files', () => { + // Format should be: /// + const workspaceResource = Uri.file('/foo'); + backupService.setCurrentWorkspace(workspaceResource); + const backupResource = Uri.file('/bar'); + const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); + const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); + const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash); + assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); + }); + + test('getBackupResource should get the correct backup path for untitled files', () => { + // Format should be: /// + const workspaceResource = Uri.file('/bar'); + backupService.setCurrentWorkspace(workspaceResource); + const backupResource = Uri.from({ scheme: 'untitled' }); + const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); + const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); + const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash); + assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); + }); + + test('getBackupResource should get the correct backup path for text files', () => { + // Format should be: /// + const workspaceResource = Uri.file('/foo'); + backupService.setCurrentWorkspace(workspaceResource); + const backupResource = Uri.file('/bar'); + const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); + const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); + const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash); + assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); + }); + + test('getBackupResource should get the correct backup path for untitled files', () => { + // Format should be: /// + const workspaceResource = Uri.file('/foo'); + backupService.setCurrentWorkspace(workspaceResource); + const backupResource = Uri.from({ scheme: 'untitled' }); + const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); + const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); + const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash); + assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); + }); + + test('getWorkspaceTextFilesWithBackupsSync should return text file resources that have backups', done => { + const workspaceResource = Uri.file('/foo'); + backupService.setCurrentWorkspace(workspaceResource); + backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource.fsPath), ['/bar']); + backupService.registerResourceForBackup(Uri.file('/baz')).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource.fsPath), ['/bar', '/baz']); + done(); + }); + }); + }); + + test('getWorkspaceUntitledFileBackupsSync should return untitled file backup resources', done => { + const workspaceResource = Uri.file('/foo'); + backupService.setCurrentWorkspace(workspaceResource); + const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); + const untitledBackupDir = path.join(environmentService.backupHome, workspaceHash, 'untitled'); + const untitledBackup1 = path.join(untitledBackupDir, 'bar'); + const untitledBackup2 = path.join(untitledBackupDir, 'foo'); + pfs.mkdirp(untitledBackupDir).then(() => { + pfs.writeFile(untitledBackup1, 'test').then(() => { + assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource.fsPath), [untitledBackup1]); + pfs.writeFile(untitledBackup2, 'test').then(() => { + assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource.fsPath), [untitledBackup1, untitledBackup2]); + done(); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 5748818354a17..7a5aab9f40581 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -174,10 +174,10 @@ export class Workbench implements IPartService { // Restore any backups if they exist for this workspace (empty workspaces are not supported yet) if (workspace) { - options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackups(workspace.resource.fsPath).map(filePath => { + options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackupsSync(workspace.resource.fsPath).map(filePath => { return { resource: Uri.file(filePath), options: { pinned: true } }; }); - options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackups(workspace.resource.fsPath).map(untitledFilePath => { + options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackupsSync(workspace.resource.fsPath).map(untitledFilePath => { return { resource: Uri.file(untitledFilePath), options: { pinned: true } }; }); } From f9cc78f67b70b2acdacd3b5fbace7c5c24c97886 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 08:35:46 -0700 Subject: [PATCH 37/45] Fix duplicate instances being restored on non-Linux --- src/vs/code/electron-main/windows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 258559d31379f..9f3230fefbd4c 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -631,7 +631,7 @@ export class WindowsManager implements IWindowsService { }); // Get rid of duplicates iPathsToOpen = arrays.distinct(iPathsToOpen, path => { - return path.workspacePath; + return platform.isLinux ? path.workspacePath : path.workspacePath.toLowerCase(); }); } From ecc83b880555b6704797e72b3ae1b81da86d63f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 09:13:05 -0700 Subject: [PATCH 38/45] Use Uri exclusively to deal with paths in BackupService --- src/vs/code/electron-main/windows.ts | 3 +- src/vs/platform/backup/common/backup.ts | 8 ++--- src/vs/platform/backup/node/backupService.ts | 19 ++++++----- .../backup/test/backupService.test.ts | 34 +++++++++---------- .../workbench/electron-browser/workbench.ts | 4 +-- .../textfile/browser/textFileService.ts | 2 +- 6 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 9f3230fefbd4c..2ff7aac6517da 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -29,6 +29,7 @@ import { IWindowEventService } from 'vs/code/common/windows'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import CommonEvent, { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product'; +import Uri from 'vs/base/common/uri'; import { ParsedArgs } from 'vs/platform/environment/node/argv'; const EventTypes = { @@ -765,7 +766,7 @@ export class WindowsManager implements IWindowsService { // Add to backups this.backupService.pushWorkspaceBackupPathsSync(iPathsToOpen.map((path) => { - return path.workspacePath; + return Uri.file(path.workspacePath); })); return arrays.distinct(usedWindows); diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index ffb63dfb8760e..fe55e94179367 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -33,7 +33,7 @@ export interface IBackupService { * * @param workspaces The workspaces to add. */ - pushWorkspaceBackupPathsSync(workspaces: string[]): void; + pushWorkspaceBackupPathsSync(workspaces: Uri[]): void; /** * Removes a workspace backup path being tracked for restoration, deregistering all associated @@ -41,7 +41,7 @@ export interface IBackupService { * * @param workspace The absolute workspace path being removed. */ - removeWorkspaceBackupPath(workspace: string): TPromise; + removeWorkspaceBackupPath(workspace: Uri): TPromise; /** * Gets the set of text files that are backed up for a particular workspace. @@ -49,7 +49,7 @@ export interface IBackupService { * @param workspace The workspace to get the backed up files for. * @return The absolute paths for text files _that have backups_. */ - getWorkspaceTextFilesWithBackupsSync(workspace: string): string[]; + getWorkspaceTextFilesWithBackupsSync(workspace: Uri): string[]; /** * Gets the set of untitled file backups for a particular workspace. @@ -57,7 +57,7 @@ export interface IBackupService { * @param workspace The workspace to get the backups for for. * @return The absolute paths for all the untitled file _backups_. */ - getWorkspaceUntitledFileBackupsSync(workspace: string): string[]; + getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[]; /** * Registers a resource for backup, flagging it for restoration. diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index db851b1f064d9..f18471ec6bdfe 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -48,7 +48,7 @@ export class BackupService implements IBackupService { return Object.keys(this.fileContent.folderWorkspaces); } - public pushWorkspaceBackupPathsSync(workspaces: string[]): void { + public pushWorkspaceBackupPathsSync(workspaces: Uri[]): void { // Only allow this on the main thread in the window initialization's critical path due to // the usage of synchronous IO. if (this.workspaceResource) { @@ -61,36 +61,37 @@ export class BackupService implements IBackupService { if (!workspace) { return; } - if (!this.fileContent.folderWorkspaces[workspace]) { - this.fileContent.folderWorkspaces[workspace] = []; + + if (!this.fileContent.folderWorkspaces[workspace.fsPath]) { + this.fileContent.folderWorkspaces[workspace.fsPath] = []; } }); this.saveSync(); } - public removeWorkspaceBackupPath(workspace: string): TPromise { + public removeWorkspaceBackupPath(workspace: Uri): TPromise { return this.load().then(() => { if (!this.fileContent.folderWorkspaces) { return; } - delete this.fileContent.folderWorkspaces[workspace]; + delete this.fileContent.folderWorkspaces[workspace.fsPath]; return this.save(); }); } - public getWorkspaceTextFilesWithBackupsSync(workspace: string): string[] { + public getWorkspaceTextFilesWithBackupsSync(workspace: Uri): string[] { // Allow sync here as it's only used in workbench initialization's critical path this.loadSync(); - return this.fileContent.folderWorkspaces[workspace] || []; + return this.fileContent.folderWorkspaces[workspace.fsPath] || []; } - public getWorkspaceUntitledFileBackupsSync(workspace: string): string[] { + public getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[] { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { return; } - const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); + const workspaceHash = crypto.createHash('md5').update(workspace.fsPath).digest('hex'); const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); // Allow sync here as it's only used in workbench initialization's critical path diff --git a/src/vs/platform/backup/test/backupService.test.ts b/src/vs/platform/backup/test/backupService.test.ts index c0580cb180aa5..f94212339f320 100644 --- a/src/vs/platform/backup/test/backupService.test.ts +++ b/src/vs/platform/backup/test/backupService.test.ts @@ -39,21 +39,21 @@ suite('BackupService', () => { }); test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => { - backupService.pushWorkspaceBackupPathsSync(['/foo', '/bar']); - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo', '/bar']); + backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo'), Uri.file('/bar')]); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [Uri.file('/foo'), Uri.file('/bar')]); }); test('pushWorkspaceBackupPathsSync should throw if a workspace is set', () => { backupService.setCurrentWorkspace(Uri.file('/foo')); - assert.throws(() => backupService.pushWorkspaceBackupPathsSync(['/foo', '/bar'])); + assert.throws(() => backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo'), Uri.file('/bar')])); }); test('removeWorkspaceBackupPath should remove workspaces from workspaces.json', done => { - backupService.pushWorkspaceBackupPathsSync(['/foo', '/bar']); + backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo'), Uri.file('/bar')]); assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo', '/bar']); - backupService.removeWorkspaceBackupPath('/foo').then(() => { + backupService.removeWorkspaceBackupPath(Uri.file('/foo')).then(() => { assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/bar']); - backupService.removeWorkspaceBackupPath('/bar').then(() => { + backupService.removeWorkspaceBackupPath(Uri.file('/bar')).then(() => { assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), []); done(); }); @@ -61,10 +61,10 @@ suite('BackupService', () => { }); test('removeWorkspaceBackupPath should fail gracefully when removing a path that doesn\'t exist', done => { - backupService.pushWorkspaceBackupPathsSync(['/foo']); - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo']); - backupService.removeWorkspaceBackupPath('/bar').then(() => { - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo']); + backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo')]); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [Uri.file('/foo')]); + backupService.removeWorkspaceBackupPath(Uri.file('/bar')).then(() => { + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [Uri.file('/foo')]); done(); }); }); @@ -72,7 +72,7 @@ suite('BackupService', () => { test('registerResourceForBackup should register backups to workspaces.json', done => { backupService.setCurrentWorkspace(Uri.file('/foo')); backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync('/foo'), ['/bar']); + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(Uri.file('/foo')), ['/bar']); done(); }); }); @@ -80,9 +80,9 @@ suite('BackupService', () => { test('deregisterResourceForBackup should deregister backups from workspaces.json', done => { backupService.setCurrentWorkspace(Uri.file('/foo')); backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync('/foo'), ['/bar']); + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(Uri.file('/foo')), ['/bar']); backupService.deregisterResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync('/foo'), []); + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(Uri.file('/foo')), []); done(); }); }); @@ -136,9 +136,9 @@ suite('BackupService', () => { const workspaceResource = Uri.file('/foo'); backupService.setCurrentWorkspace(workspaceResource); backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource.fsPath), ['/bar']); + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource), ['/bar']); backupService.registerResourceForBackup(Uri.file('/baz')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource.fsPath), ['/bar', '/baz']); + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource), ['/bar', '/baz']); done(); }); }); @@ -153,9 +153,9 @@ suite('BackupService', () => { const untitledBackup2 = path.join(untitledBackupDir, 'foo'); pfs.mkdirp(untitledBackupDir).then(() => { pfs.writeFile(untitledBackup1, 'test').then(() => { - assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource.fsPath), [untitledBackup1]); + assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource), [untitledBackup1]); pfs.writeFile(untitledBackup2, 'test').then(() => { - assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource.fsPath), [untitledBackup1, untitledBackup2]); + assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource), [untitledBackup1, untitledBackup2]); done(); }); }); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index d8192525fc64a..8438c95d91302 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -175,10 +175,10 @@ export class Workbench implements IPartService { // Restore any backups if they exist for this workspace (empty workspaces are not supported yet) if (workspace) { - options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackupsSync(workspace.resource.fsPath).map(filePath => { + options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackupsSync(workspace.resource).map(filePath => { return { resource: Uri.file(filePath), options: { pinned: true } }; }); - options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackupsSync(workspace.resource.fsPath).map(untitledFilePath => { + options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackupsSync(workspace.resource).map(untitledFilePath => { return { resource: Uri.file(untitledFilePath), options: { pinned: true } }; }); } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index aeccd8720f0e9..e37d32ac41d77 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -190,7 +190,7 @@ export abstract class TextFileService implements ITextFileService { } private cleanupBackupsBeforeShutdown(): boolean | TPromise { - return this.backupService.removeWorkspaceBackupPath(this.contextService.getWorkspace().resource.fsPath).then(() => { + return this.backupService.removeWorkspaceBackupPath(this.contextService.getWorkspace().resource).then(() => { return this.fileService.discardBackups().then(() => { return false; // no veto }); From 30bb0b77180c0e3ff38a0d3a82f5c1202886c6f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 09:58:43 -0700 Subject: [PATCH 39/45] Fix tests to work on Windows --- .../backup/test/backupService.test.ts | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/src/vs/platform/backup/test/backupService.test.ts b/src/vs/platform/backup/test/backupService.test.ts index f94212339f320..a0312e6a2cfe0 100644 --- a/src/vs/platform/backup/test/backupService.test.ts +++ b/src/vs/platform/backup/test/backupService.test.ts @@ -6,6 +6,7 @@ 'use strict'; import * as assert from 'assert'; +import * as platform from 'vs/base/common/platform'; import crypto = require('crypto'); import os = require('os'); import path = require('path'); @@ -18,6 +19,10 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { BackupService } from 'vs/platform/backup/node/backupService'; suite('BackupService', () => { + const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo'); + const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar'); + const bazFile = Uri.file(platform.isWindows ? 'C:\\baz' : '/baz'); + let environmentService: IEnvironmentService; let backupService: BackupService; @@ -39,21 +44,21 @@ suite('BackupService', () => { }); test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => { - backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo'), Uri.file('/bar')]); - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [Uri.file('/foo'), Uri.file('/bar')]); + backupService.pushWorkspaceBackupPathsSync([fooFile, barFile]); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [fooFile.fsPath, barFile.fsPath]); }); test('pushWorkspaceBackupPathsSync should throw if a workspace is set', () => { - backupService.setCurrentWorkspace(Uri.file('/foo')); - assert.throws(() => backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo'), Uri.file('/bar')])); + backupService.setCurrentWorkspace(fooFile); + assert.throws(() => backupService.pushWorkspaceBackupPathsSync([fooFile])); }); test('removeWorkspaceBackupPath should remove workspaces from workspaces.json', done => { - backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo'), Uri.file('/bar')]); - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/foo', '/bar']); - backupService.removeWorkspaceBackupPath(Uri.file('/foo')).then(() => { - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), ['/bar']); - backupService.removeWorkspaceBackupPath(Uri.file('/bar')).then(() => { + backupService.pushWorkspaceBackupPathsSync([fooFile, barFile]); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [fooFile.fsPath, barFile.fsPath]); + backupService.removeWorkspaceBackupPath(fooFile).then(() => { + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [barFile.fsPath]); + backupService.removeWorkspaceBackupPath(barFile).then(() => { assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), []); done(); }); @@ -61,28 +66,28 @@ suite('BackupService', () => { }); test('removeWorkspaceBackupPath should fail gracefully when removing a path that doesn\'t exist', done => { - backupService.pushWorkspaceBackupPathsSync([Uri.file('/foo')]); - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [Uri.file('/foo')]); - backupService.removeWorkspaceBackupPath(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [Uri.file('/foo')]); + backupService.pushWorkspaceBackupPathsSync([fooFile]); + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [fooFile.fsPath]); + backupService.removeWorkspaceBackupPath(barFile).then(() => { + assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [fooFile.fsPath]); done(); }); }); test('registerResourceForBackup should register backups to workspaces.json', done => { - backupService.setCurrentWorkspace(Uri.file('/foo')); - backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(Uri.file('/foo')), ['/bar']); + backupService.setCurrentWorkspace(fooFile); + backupService.registerResourceForBackup(barFile).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(fooFile), [barFile.fsPath]); done(); }); }); test('deregisterResourceForBackup should deregister backups from workspaces.json', done => { - backupService.setCurrentWorkspace(Uri.file('/foo')); - backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(Uri.file('/foo')), ['/bar']); - backupService.deregisterResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(Uri.file('/foo')), []); + backupService.setCurrentWorkspace(fooFile); + backupService.registerResourceForBackup(barFile).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(fooFile), [barFile.fsPath]); + backupService.deregisterResourceForBackup(barFile).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(fooFile), []); done(); }); }); @@ -90,62 +95,62 @@ suite('BackupService', () => { test('getBackupResource should get the correct backup path for text files', () => { // Format should be: /// - const workspaceResource = Uri.file('/foo'); + const workspaceResource = fooFile; backupService.setCurrentWorkspace(workspaceResource); - const backupResource = Uri.file('/bar'); + const backupResource = barFile; const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash); + const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); test('getBackupResource should get the correct backup path for untitled files', () => { // Format should be: /// - const workspaceResource = Uri.file('/bar'); + const workspaceResource = barFile; backupService.setCurrentWorkspace(workspaceResource); const backupResource = Uri.from({ scheme: 'untitled' }); const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash); + const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); test('getBackupResource should get the correct backup path for text files', () => { // Format should be: /// - const workspaceResource = Uri.file('/foo'); + const workspaceResource = fooFile; backupService.setCurrentWorkspace(workspaceResource); - const backupResource = Uri.file('/bar'); + const backupResource = barFile; const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash); + const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); test('getBackupResource should get the correct backup path for untitled files', () => { // Format should be: /// - const workspaceResource = Uri.file('/foo'); + const workspaceResource = fooFile; backupService.setCurrentWorkspace(workspaceResource); const backupResource = Uri.from({ scheme: 'untitled' }); const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash); + const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); test('getWorkspaceTextFilesWithBackupsSync should return text file resources that have backups', done => { - const workspaceResource = Uri.file('/foo'); + const workspaceResource = fooFile; backupService.setCurrentWorkspace(workspaceResource); - backupService.registerResourceForBackup(Uri.file('/bar')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource), ['/bar']); - backupService.registerResourceForBackup(Uri.file('/baz')).then(() => { - assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource), ['/bar', '/baz']); + backupService.registerResourceForBackup(barFile).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource), [barFile.fsPath]); + backupService.registerResourceForBackup(bazFile).then(() => { + assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(workspaceResource), [barFile.fsPath, bazFile.fsPath]); done(); }); }); }); test('getWorkspaceUntitledFileBackupsSync should return untitled file backup resources', done => { - const workspaceResource = Uri.file('/foo'); + const workspaceResource = fooFile; backupService.setCurrentWorkspace(workspaceResource); const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const untitledBackupDir = path.join(environmentService.backupHome, workspaceHash, 'untitled'); From eb2a8e126932262bbb20acbabe182f078f6bf609 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 10:21:19 -0700 Subject: [PATCH 40/45] Fix some empty workspace edge cases on Mac --- src/vs/code/electron-main/windows.ts | 7 +++++-- .../services/textfile/browser/textFileService.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 2ff7aac6517da..2171303a5ebeb 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -632,6 +632,9 @@ export class WindowsManager implements IWindowsService { }); // Get rid of duplicates iPathsToOpen = arrays.distinct(iPathsToOpen, path => { + if (!('workspacePath' in path)) { + return path.workspacePath; + } return platform.isLinux ? path.workspacePath : path.workspacePath.toLowerCase(); }); } @@ -764,8 +767,8 @@ export class WindowsManager implements IWindowsService { // Emit events iPathsToOpen.forEach(iPath => this.eventEmitter.emit(EventTypes.OPEN, iPath)); - // Add to backups - this.backupService.pushWorkspaceBackupPathsSync(iPathsToOpen.map((path) => { + // Start tracking workspace backups + this.backupService.pushWorkspaceBackupPathsSync(iPathsToOpen.filter(path => 'workspacePath' in path).map(path => { return Uri.file(path.workspacePath); })); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index e37d32ac41d77..1b6b854fda5ce 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -117,7 +117,7 @@ export abstract class TextFileService implements ITextFileService { private beforeShutdown(): boolean | TPromise { // If hot exit is enabled then save the dirty files in the workspace and then exit - if (this.configuredHotExit) { + if (this.configuredHotExit && this.contextService.getWorkspace()) { // If there are no dirty files, clean up and exit if (this.getDirty().length === 0) { return this.cleanupBackupsBeforeShutdown(); @@ -190,7 +190,11 @@ export abstract class TextFileService implements ITextFileService { } private cleanupBackupsBeforeShutdown(): boolean | TPromise { - return this.backupService.removeWorkspaceBackupPath(this.contextService.getWorkspace().resource).then(() => { + const workspace = this.contextService.getWorkspace(); + if (!workspace) { + return TPromise.as(false); // no backups to cleanup, no eto + } + return this.backupService.removeWorkspaceBackupPath(workspace.resource).then(() => { return this.fileService.discardBackups().then(() => { return false; // no veto }); From a0402376dd3fe056c7e9a3a2e5a4af9769af5817 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 10:48:35 -0700 Subject: [PATCH 41/45] Disable hot exit on Mac See #12400 & #13305 --- src/vs/workbench/services/textfile/browser/textFileService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 1b6b854fda5ce..bf65b91ad3009 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as platform from 'vs/base/common/platform'; import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); @@ -117,7 +118,8 @@ export abstract class TextFileService implements ITextFileService { private beforeShutdown(): boolean | TPromise { // If hot exit is enabled then save the dirty files in the workspace and then exit - if (this.configuredHotExit && this.contextService.getWorkspace()) { + // Hot exit is currently disabled for both empty workspaces (#13733) and on Mac (#13305) + if (this.configuredHotExit && this.contextService.getWorkspace() && !platform.isMacintosh) { // If there are no dirty files, clean up and exit if (this.getDirty().length === 0) { return this.cleanupBackupsBeforeShutdown(); From 36f1d703e7175da4b11ab52bb3ea6b6f131b038b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 11:05:10 -0700 Subject: [PATCH 42/45] jsdoc and comment cleanup --- src/vs/workbench/services/files/node/fileService.ts | 1 - .../services/textfile/common/textFileEditorModel.ts | 2 -- src/vs/workbench/services/textfile/common/textfiles.ts | 7 ++++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index eb14800ad728a..a860358536ad4 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -458,7 +458,6 @@ export class FileService implements IFileService { } public backupFile(resource: uri, content: string): TPromise { - // TODO: This should not backup unless necessary. Currently this is called for each file on close to ensure the files are backed up. let registerResourcePromise: TPromise; if (resource.scheme === 'file') { registerResourcePromise = this.backupService.registerResourceForBackup(resource); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index ac651f021a10b..e215ddea72d6f 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -265,12 +265,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil diag('load() - created text editor model', this.resource, new Date()); if (this.restoreResource) { - // TODO: De-duplicate code this.createTextEditorModelPromise = this.textFileService.resolveTextContent(this.restoreResource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then((restoreContent) => { return this.createTextEditorModel(restoreContent.value, content.resource).then(() => { this.createTextEditorModelPromise = null; - // TODO: This does not set the dirty indicator immediately, making it look like the file is not actually dirty this.setDirty(true); this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index e564139e8b4eb..a033f7813e743 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -322,7 +322,12 @@ export interface ITextFileService extends IDisposable { */ confirmSave(resources?: URI[]): ConfirmResult; - // TODO: Doc + /** + * Backs up the provided file to a temporary directory to be used by the hot + * exit feature and crash recovery. + * + * @param resource The resource to backup. + */ backup(resource: URI): void; /** From e9be33034e797be6235849cb39a07f551895d215 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 15:06:03 -0700 Subject: [PATCH 43/45] Add backup tests for FileService --- src/vs/platform/backup/node/backupService.ts | 35 ++++++--- .../backup/test/backupService.test.ts | 8 +- src/vs/test/utils/servicesTestUtils.ts | 50 +++++++++++- .../node/configurationEditingService.test.ts | 2 +- .../files/electron-browser/fileService.ts | 2 +- .../services/files/node/fileService.ts | 9 +-- .../files/test/node/fileService.test.ts | 76 ++++++++++++++++++- .../textfile/browser/textFileService.ts | 2 +- 8 files changed, 152 insertions(+), 32 deletions(-) diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index f18471ec6bdfe..4912e66928c6b 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -72,7 +72,7 @@ export class BackupService implements IBackupService { public removeWorkspaceBackupPath(workspace: Uri): TPromise { return this.load().then(() => { if (!this.fileContent.folderWorkspaces) { - return; + return TPromise.as(void 0); } delete this.fileContent.folderWorkspaces[workspace.fsPath]; return this.save(); @@ -88,7 +88,7 @@ export class BackupService implements IBackupService { public getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[] { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { - return; + return []; } const workspaceHash = crypto.createHash('md5').update(workspace.fsPath).digest('hex'); @@ -105,7 +105,7 @@ export class BackupService implements IBackupService { public getBackupResource(resource: Uri): Uri { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { - return; + return null; } const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); @@ -135,7 +135,7 @@ export class BackupService implements IBackupService { public deregisterResourceForBackup(resource: Uri): TPromise { // Hot exit is disabled for empty workspaces if (!this.workspaceResource) { - return; + return TPromise.as(void 0); } return this.load().then(() => { @@ -149,14 +149,27 @@ export class BackupService implements IBackupService { } private load(): TPromise { - return pfs.readFile(this.environmentService.backupWorkspacesPath, 'utf8').then(content => { - return JSON.parse(content.toString()); - }).then(null, () => Object.create(null)).then(content => { - this.fileContent = content; - if (!this.fileContent.folderWorkspaces) { - this.fileContent.folderWorkspaces = Object.create(null); + return pfs.fileExists(this.environmentService.backupWorkspacesPath).then(exists => { + if (!exists) { + this.fileContent = { + folderWorkspaces: Object.create(null) + }; + return TPromise.as(void 0); } - return void 0; + + return pfs.readFile(this.environmentService.backupWorkspacesPath, 'utf8').then(content => { + try { + return JSON.parse(content.toString()); + } catch (ex) { + return Object.create(null); + } + }).then(content => { + this.fileContent = content; + if (!this.fileContent.folderWorkspaces) { + this.fileContent.folderWorkspaces = Object.create(null); + } + return TPromise.as(void 0); + }); }); } diff --git a/src/vs/platform/backup/test/backupService.test.ts b/src/vs/platform/backup/test/backupService.test.ts index a0312e6a2cfe0..ddf6905013b6e 100644 --- a/src/vs/platform/backup/test/backupService.test.ts +++ b/src/vs/platform/backup/test/backupService.test.ts @@ -32,15 +32,11 @@ suite('BackupService', () => { // Delete any existing backups completely, this in itself is a test to ensure that the // the backupHome directory is re-created - nfcall(extfs.del, environmentService.backupHome, os.tmpdir()).then(() => { - done(); - }); + extfs.del(environmentService.backupHome, os.tmpdir(), done); }); teardown(done => { - nfcall(extfs.del, environmentService.backupHome, os.tmpdir()).then(() => { - done(); - }); + extfs.del(environmentService.backupHome, os.tmpdir(), done); }); test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => { diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index e0fb18e8e6fa6..71e3fcdbf4954 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -175,7 +175,7 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(IHistoryService, 'getHistory', []); instantiationService.stub(IModelService, createMockModelService(instantiationService)); instantiationService.stub(IFileService, TestFileService); - instantiationService.stub(IBackupService, TestBackupService); + instantiationService.stub(IBackupService, new TestBackupService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IMessageService, new TestMessageService()); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); @@ -574,6 +574,10 @@ export const TestFileService = { }); }, + backupFile: function (resource: URI, content: string) { + return TPromise.as(void 0); + }, + discardBackup: function (resource: URI) { return TPromise.as(void 0); }, @@ -587,10 +591,50 @@ export const TestFileService = { } }; -export const TestBackupService = { - removeWorkspaceBackupPath: function () { +export class TestBackupService implements IBackupService { + public _serviceBrand: any; + + // Lists used for verification in tests + public registeredResources: URI[] = []; + public deregisteredResources: URI[] = []; + + public getWorkspaceBackupPaths(): TPromise { + return TPromise.as([]); + } + + public getWorkspaceBackupPathsSync(): string[] { + return []; + } + + public pushWorkspaceBackupPathsSync(workspaces: URI[]): void { + return null; + } + + public removeWorkspaceBackupPath(workspace: URI): TPromise { return TPromise.as(void 0); } + + public getWorkspaceTextFilesWithBackupsSync(workspace: URI): string[] { + return []; + } + + public getWorkspaceUntitledFileBackupsSync(workspace: URI): string[] { + return []; + } + + public registerResourceForBackup(resource: URI): TPromise { + this.registeredResources.push(resource); + return TPromise.as(void 0); + } + + public deregisterResourceForBackup(resource: URI): TPromise { + this.deregisteredResources.push(resource); + return TPromise.as(void 0); + } + + public getBackupResource(resource: URI): URI { + return null; + } }; export class TestConfigurationService extends EventEmitter implements IConfigurationService { diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts index 89b0f76ebba2e..5ef7628a4cbc5 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts @@ -87,7 +87,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { const configurationService = new WorkspaceConfigurationService(workspaceContextService, new TestEventService(), environmentService); const textFileService = workbenchInstantiationService().createInstance(TestDirtyTextFileService, dirty); const events = new utils.TestEventService(); - const fileService = new FileService(noWorkspace ? null : workspaceDir, { disableWatcher: true }, events, environmentService, configurationService, null, null); + const fileService = new FileService(noWorkspace ? null : workspaceDir, { disableWatcher: true }, events, environmentService, configurationService, null); return configurationService.initialize().then(() => { return { diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index f2abf61e6afdc..e9d2e319a03cc 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -83,7 +83,7 @@ export class FileService implements IFileService { // create service const workspace = this.contextService.getWorkspace(); - this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService, this.configurationService, this.backupService, this.contextService); + this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService, this.configurationService, this.backupService); // Listeners this.registerListeners(); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index a860358536ad4..4c0e8a68fa6fe 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -37,7 +37,6 @@ import { IBackupService } from 'vs/platform/backup/common/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export interface IEncodingOverride { resource: uri; @@ -97,8 +96,7 @@ export class FileService implements IFileService { private eventEmitter: IEventService, private environmentService: IEnvironmentService, private configurationService: IConfigurationService, - private backupService: IBackupService, - private contextService: IWorkspaceContextService + private backupService: IBackupService ) { this.basePath = basePath ? paths.normalize(basePath) : void 0; @@ -519,12 +517,11 @@ export class FileService implements IFileService { private getBackupRootPath(): string { // Hot exit is disabled for empty workspaces - const workspace = this.contextService.getWorkspace(); - if (!workspace) { + if (!this.basePath) { return null; } - const workspaceHash = crypto.createHash('md5').update(workspace.resource.fsPath).digest('hex'); + const workspaceHash = crypto.createHash('md5').update(this.basePath).digest('hex'); return paths.join(this.environmentService.userDataPath, 'Backups', workspaceHash); } diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index 792799945f9a2..e4bfe95f411fa 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -9,11 +9,14 @@ import fs = require('fs'); import path = require('path'); import os = require('os'); import assert = require('assert'); +import crypto = require('crypto'); import { TPromise } from 'vs/base/common/winjs.base'; import { FileService, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; import { EventType, FileChangesEvent, FileOperationResult, IFileOperationResult } from 'vs/platform/files/common/files'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { nfcall } from 'vs/base/common/async'; +import { TestBackupService, TestEnvironmentService } from 'vs/test/utils/servicesTestUtils'; import uri from 'vs/base/common/uri'; import uuid = require('vs/base/common/uuid'); import extfs = require('vs/base/node/extfs'); @@ -33,7 +36,7 @@ suite('FileService', () => { extfs.copy(sourceDir, testDir, () => { events = new utils.TestEventService(); - service = new FileService(testDir, { disableWatcher: true }, events, null, null, null, null); + service = new FileService(testDir, { disableWatcher: true }, events, null, null, null); done(); }); }); @@ -275,6 +278,73 @@ suite('FileService', () => { }); }); + suite('backups', () => { + const environment = TestEnvironmentService; + const fooResource = uri.file('/foo'); + const barResource = uri.file('/bar'); + + let _service: FileService; + let backup: TestBackupService; + let workspaceHash; + let workspaceBackupRoot; + let fooFileHash; + let barFileHash; + let fooBackupPath; + let barBackupPath; + + setup((done) => { + extfs.del(TestEnvironmentService.backupHome, os.tmpdir(), done); + backup = new TestBackupService(); + _service = new FileService(testDir, { disableWatcher: true }, events, environment, null, backup); + workspaceHash = crypto.createHash('md5').update(testDir).digest('hex'); + workspaceBackupRoot = path.join(environment.backupHome, workspaceHash, 'file'); + fooFileHash = crypto.createHash('md5').update(fooResource.fsPath).digest('hex'); + barFileHash = crypto.createHash('md5').update(barResource.fsPath).digest('hex'); + fooBackupPath = path.join(workspaceBackupRoot, fooFileHash); + barBackupPath = path.join(workspaceBackupRoot, barFileHash); + }); + + teardown((done) => { + extfs.del(TestEnvironmentService.backupHome, os.tmpdir(), done); + }); + + test('backupFile', function (done: () => void) { + _service.backupFile(fooResource, 'test').then(() => { + assert.equal(fs.readdirSync(workspaceBackupRoot).length, 1); + assert.equal(fs.existsSync(fooBackupPath), true); + assert.deepEqual(backup.registeredResources, [fooResource]); + assert.equal(fs.readFileSync(fooBackupPath), 'test'); + done(); + }); + }); + + test('discardBackup', function (done: () => void) { + _service.backupFile(fooResource, 'test').then(() => { + assert.equal(fs.readdirSync(workspaceBackupRoot).length, 1); + _service.discardBackup(fooResource).then(() => { + assert.equal(fs.existsSync(fooBackupPath), false); + assert.equal(fs.readdirSync(workspaceBackupRoot).length, 0); + done(); + }); + }); + }); + + test('discardBackups', function (done: () => void) { + _service.backupFile(fooResource, 'test').then(() => { + assert.equal(fs.readdirSync(workspaceBackupRoot).length, 1); + _service.backupFile(barResource, 'test').then(() => { + assert.equal(fs.readdirSync(workspaceBackupRoot).length, 2); + _service.discardBackups().then(() => { + assert.equal(fs.existsSync(fooBackupPath), false); + assert.equal(fs.existsSync(barBackupPath), false); + assert.equal(fs.existsSync(workspaceBackupRoot), false); + done(); + }); + }); + }); + }); + }); + test('resolveFile', function (done: () => void) { service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).done(r => { assert.equal(r.children.length, 6); @@ -494,7 +564,7 @@ suite('FileService', () => { encoding: 'windows1252', encodingOverride: encodingOverride, disableWatcher: true - }, null, null, null, null, null); + }, null, null, null, null); _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).done(c => { assert.equal(c.encoding, 'windows1252'); @@ -520,7 +590,7 @@ suite('FileService', () => { let _service = new FileService(_testDir, { disableWatcher: true - }, null, null, null, null, null); + }, null, null, null, null); extfs.copy(_sourceDir, _testDir, () => { fs.readFile(resource.fsPath, (error, data) => { diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index bf65b91ad3009..5681d8b955563 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -194,7 +194,7 @@ export abstract class TextFileService implements ITextFileService { private cleanupBackupsBeforeShutdown(): boolean | TPromise { const workspace = this.contextService.getWorkspace(); if (!workspace) { - return TPromise.as(false); // no backups to cleanup, no eto + return false; // no backups to cleanup, no eto } return this.backupService.removeWorkspaceBackupPath(workspace.resource).then(() => { return this.fileService.discardBackups().then(() => { From 0c48efcfdfe80161a8fb4c839de737dfa1e0f68e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 15:17:23 -0700 Subject: [PATCH 44/45] Add untitled backup tests to fileService --- .../files/test/node/fileService.test.ts | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index e4bfe95f411fa..262577c417c65 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -282,35 +282,37 @@ suite('FileService', () => { const environment = TestEnvironmentService; const fooResource = uri.file('/foo'); const barResource = uri.file('/bar'); + const untitledResource = uri.from({ scheme: 'untitled' }); let _service: FileService; let backup: TestBackupService; let workspaceHash; let workspaceBackupRoot; - let fooFileHash; - let barFileHash; let fooBackupPath; let barBackupPath; + let untitledBackupPath; setup((done) => { extfs.del(TestEnvironmentService.backupHome, os.tmpdir(), done); backup = new TestBackupService(); _service = new FileService(testDir, { disableWatcher: true }, events, environment, null, backup); workspaceHash = crypto.createHash('md5').update(testDir).digest('hex'); - workspaceBackupRoot = path.join(environment.backupHome, workspaceHash, 'file'); - fooFileHash = crypto.createHash('md5').update(fooResource.fsPath).digest('hex'); - barFileHash = crypto.createHash('md5').update(barResource.fsPath).digest('hex'); - fooBackupPath = path.join(workspaceBackupRoot, fooFileHash); - barBackupPath = path.join(workspaceBackupRoot, barFileHash); + workspaceBackupRoot = path.join(environment.backupHome, workspaceHash); + const fooFileHash = crypto.createHash('md5').update(fooResource.fsPath).digest('hex'); + const barFileHash = crypto.createHash('md5').update(barResource.fsPath).digest('hex'); + const untitledFileHash = crypto.createHash('md5').update(untitledResource.fsPath).digest('hex'); + fooBackupPath = path.join(workspaceBackupRoot, 'file', fooFileHash); + barBackupPath = path.join(workspaceBackupRoot, 'file', barFileHash); + untitledBackupPath = path.join(workspaceBackupRoot, 'untitled', untitledFileHash); }); teardown((done) => { extfs.del(TestEnvironmentService.backupHome, os.tmpdir(), done); }); - test('backupFile', function (done: () => void) { + test('backupFile - text file', function (done: () => void) { _service.backupFile(fooResource, 'test').then(() => { - assert.equal(fs.readdirSync(workspaceBackupRoot).length, 1); + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'file')).length, 1); assert.equal(fs.existsSync(fooBackupPath), true); assert.deepEqual(backup.registeredResources, [fooResource]); assert.equal(fs.readFileSync(fooBackupPath), 'test'); @@ -318,31 +320,63 @@ suite('FileService', () => { }); }); - test('discardBackup', function (done: () => void) { + test('backupFile - untitled file', function (done: () => void) { + _service.backupFile(untitledResource, 'test').then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'untitled')).length, 1); + assert.equal(fs.existsSync(untitledBackupPath), true); + // Untitled files are not registered to workspaces.json as they do not have paths + assert.equal(fs.readFileSync(untitledBackupPath), 'test'); + done(); + }); + }); + + test('discardBackup - text file', function (done: () => void) { _service.backupFile(fooResource, 'test').then(() => { - assert.equal(fs.readdirSync(workspaceBackupRoot).length, 1); + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'file')).length, 1); _service.discardBackup(fooResource).then(() => { assert.equal(fs.existsSync(fooBackupPath), false); - assert.equal(fs.readdirSync(workspaceBackupRoot).length, 0); + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'file')).length, 0); + done(); + }); + }); + }); + + test('discardBackup - untitled file', function (done: () => void) { + _service.backupFile(untitledResource, 'test').then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'untitled')).length, 1); + _service.discardBackup(untitledResource).then(() => { + assert.equal(fs.existsSync(untitledBackupPath), false); + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'untitled')).length, 0); done(); }); }); }); - test('discardBackups', function (done: () => void) { + test('discardBackups - text file', function (done: () => void) { _service.backupFile(fooResource, 'test').then(() => { - assert.equal(fs.readdirSync(workspaceBackupRoot).length, 1); + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'file')).length, 1); _service.backupFile(barResource, 'test').then(() => { - assert.equal(fs.readdirSync(workspaceBackupRoot).length, 2); + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'file')).length, 2); _service.discardBackups().then(() => { assert.equal(fs.existsSync(fooBackupPath), false); assert.equal(fs.existsSync(barBackupPath), false); - assert.equal(fs.existsSync(workspaceBackupRoot), false); + assert.equal(fs.existsSync(path.join(workspaceBackupRoot, 'file')), false); done(); }); }); }); }); + + test('discardBackups - untitled file', function (done: () => void) { + _service.backupFile(untitledResource, 'test').then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'untitled')).length, 1); + _service.discardBackups().then(() => { + assert.equal(fs.existsSync(untitledBackupPath), false); + assert.equal(fs.existsSync(path.join(workspaceBackupRoot, 'untitled')), false); + done(); + }); + }); + }); }); test('resolveFile', function (done: () => void) { From 4e34bfc81e6fcfef72340b602d37e22fdef4a1b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 19 Oct 2016 20:29:53 -0700 Subject: [PATCH 45/45] Use a temp folder for backupservice tests --- src/vs/platform/backup/node/backupService.ts | 44 +++++++++++++------ .../backup/test/backupService.test.ts | 32 +++++++++----- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/vs/platform/backup/node/backupService.ts b/src/vs/platform/backup/node/backupService.ts index 4912e66928c6b..599672754eec1 100644 --- a/src/vs/platform/backup/node/backupService.ts +++ b/src/vs/platform/backup/node/backupService.ts @@ -27,16 +27,29 @@ export class BackupService implements IBackupService { private workspaceResource: Uri; private fileContent: IBackupFormat; + private backupHome: string; + private backupWorkspacesPath: string; constructor( - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService ) { + this.backupHome = environmentService.backupHome; + this.backupWorkspacesPath = environmentService.backupWorkspacesPath; } public setCurrentWorkspace(resource: Uri): void { this.workspaceResource = resource; } + /** + * Due to the Environment service not being initialized when it's needed on the main thread + * side, this is here so that tests can override the paths pulled from it. + */ + public setBackupPathsForTest(backupHome: string, backupWorkspacesPath: string) { + this.backupHome = backupHome; + this.backupWorkspacesPath = backupWorkspacesPath; + } + public getWorkspaceBackupPaths(): TPromise { return this.load().then(() => { return Object.keys(this.fileContent.folderWorkspaces); @@ -92,7 +105,7 @@ export class BackupService implements IBackupService { } const workspaceHash = crypto.createHash('md5').update(workspace.fsPath).digest('hex'); - const untitledDir = path.join(this.environmentService.backupHome, workspaceHash, 'untitled'); + const untitledDir = path.join(this.backupHome, workspaceHash, 'untitled'); // Allow sync here as it's only used in workbench initialization's critical path try { @@ -110,7 +123,7 @@ export class BackupService implements IBackupService { const workspaceHash = crypto.createHash('md5').update(this.workspaceResource.fsPath).digest('hex'); const backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex'); - const backupPath = path.join(this.environmentService.backupHome, workspaceHash, resource.scheme, backupName); + const backupPath = path.join(this.backupHome, workspaceHash, resource.scheme, backupName); return Uri.file(backupPath); } @@ -149,7 +162,7 @@ export class BackupService implements IBackupService { } private load(): TPromise { - return pfs.fileExists(this.environmentService.backupWorkspacesPath).then(exists => { + return pfs.fileExists(this.backupWorkspacesPath).then(exists => { if (!exists) { this.fileContent = { folderWorkspaces: Object.create(null) @@ -157,7 +170,7 @@ export class BackupService implements IBackupService { return TPromise.as(void 0); } - return pfs.readFile(this.environmentService.backupWorkspacesPath, 'utf8').then(content => { + return pfs.readFile(this.backupWorkspacesPath, 'utf8').then(content => { try { return JSON.parse(content.toString()); } catch (ex) { @@ -174,11 +187,16 @@ export class BackupService implements IBackupService { } private loadSync(): void { - try { - this.fileContent = JSON.parse(fs.readFileSync(this.environmentService.backupWorkspacesPath, 'utf8').toString()); // invalid JSON or permission issue can happen here - } catch (error) { + if (fs.existsSync(this.backupWorkspacesPath)) { + try { + this.fileContent = JSON.parse(fs.readFileSync(this.backupWorkspacesPath, 'utf8').toString()); // invalid JSON or permission issue can happen here + } catch (error) { + this.fileContent = Object.create(null); + } + } else { this.fileContent = Object.create(null); } + if (!this.fileContent.folderWorkspaces) { this.fileContent.folderWorkspaces = Object.create(null); } @@ -186,18 +204,18 @@ export class BackupService implements IBackupService { private save(): TPromise { const data = JSON.stringify(this.fileContent); - return pfs.mkdirp(this.environmentService.backupHome).then(() => { - return pfs.writeFile(this.environmentService.backupWorkspacesPath, data); + return pfs.mkdirp(this.backupHome).then(() => { + return pfs.writeFile(this.backupWorkspacesPath, data); }); } private saveSync(): void { try { // The user data directory must exist so only the Backup directory needs to be checked. - if (!fs.existsSync(this.environmentService.backupHome)) { - fs.mkdirSync(this.environmentService.backupHome); + if (!fs.existsSync(this.backupHome)) { + fs.mkdirSync(this.backupHome); } - fs.writeFileSync(this.environmentService.backupWorkspacesPath, JSON.stringify(this.fileContent)); + fs.writeFileSync(this.backupWorkspacesPath, JSON.stringify(this.fileContent)); } catch (ex) { } } diff --git a/src/vs/platform/backup/test/backupService.test.ts b/src/vs/platform/backup/test/backupService.test.ts index ddf6905013b6e..66321ddda6de4 100644 --- a/src/vs/platform/backup/test/backupService.test.ts +++ b/src/vs/platform/backup/test/backupService.test.ts @@ -19,24 +19,34 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { BackupService } from 'vs/platform/backup/node/backupService'; suite('BackupService', () => { + const parentDir = path.join(os.tmpdir(), 'vsctests', 'service') + const backupHome = path.join(parentDir, 'Backups'); + const backupWorkspacesHome = path.join(backupHome, 'workspaces.json'); + const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo'); const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar'); const bazFile = Uri.file(platform.isWindows ? 'C:\\baz' : '/baz'); - let environmentService: IEnvironmentService; let backupService: BackupService; setup(done => { - environmentService = TestEnvironmentService; + const environmentService = TestEnvironmentService; + backupService = new BackupService(environmentService); + backupService.setBackupPathsForTest(backupHome, backupWorkspacesHome); - // Delete any existing backups completely, this in itself is a test to ensure that the - // the backupHome directory is re-created - extfs.del(environmentService.backupHome, os.tmpdir(), done); + // Delete any existing backups completely and then re-create it. + extfs.del(backupHome, os.tmpdir(), () => { + pfs.mkdirp(backupHome).then(() => { + pfs.writeFileAndFlush(backupWorkspacesHome, '').then(() => { + done(); + }); + }); + }); }); teardown(done => { - extfs.del(environmentService.backupHome, os.tmpdir(), done); + extfs.del(backupHome, os.tmpdir(), done); }); test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => { @@ -96,7 +106,7 @@ suite('BackupService', () => { const backupResource = barFile; const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash)).fsPath; + const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); @@ -107,7 +117,7 @@ suite('BackupService', () => { const backupResource = Uri.from({ scheme: 'untitled' }); const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash)).fsPath; + const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); @@ -118,7 +128,7 @@ suite('BackupService', () => { const backupResource = barFile; const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'file', filePathHash)).fsPath; + const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); @@ -129,7 +139,7 @@ suite('BackupService', () => { const backupResource = Uri.from({ scheme: 'untitled' }); const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex'); - const expectedPath = Uri.file(path.join(environmentService.backupHome, workspaceHash, 'untitled', filePathHash)).fsPath; + const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath; assert.equal(backupService.getBackupResource(backupResource).fsPath, expectedPath); }); @@ -149,7 +159,7 @@ suite('BackupService', () => { const workspaceResource = fooFile; backupService.setCurrentWorkspace(workspaceResource); const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'); - const untitledBackupDir = path.join(environmentService.backupHome, workspaceHash, 'untitled'); + const untitledBackupDir = path.join(backupHome, workspaceHash, 'untitled'); const untitledBackup1 = path.join(untitledBackupDir, 'bar'); const untitledBackup2 = path.join(untitledBackupDir, 'foo'); pfs.mkdirp(untitledBackupDir).then(() => {