提交 112a795b 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #12400 from Microsoft/tyriar/101_hot_exit

Preserve files on exit (aka hot exit)
......@@ -32,6 +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 } 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';
......@@ -255,11 +257,11 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
// Open our first window
if (environmentService.args['new-window'] && environmentService.args._.length === 0) {
windowsService.open({ cli: environmentService.args, forceNewWindow: true, forceEmpty: true }); // new window if "-n" was used without paths
windowsService.open({ cli: environmentService.args, forceNewWindow: true, forceEmpty: true, restoreBackups: true }); // new window if "-n" was used without paths
} else if (global.macOpenFiles && global.macOpenFiles.length && (!environmentService.args._ || !environmentService.args._.length)) {
windowsService.open({ cli: environmentService.args, pathsToOpen: global.macOpenFiles }); // mac: open-file event received on startup
windowsService.open({ cli: environmentService.args, pathsToOpen: global.macOpenFiles, restoreBackups: true }); // mac: open-file event received on startup
} else {
windowsService.open({ cli: environmentService.args, forceNewWindow: environmentService.args['new-window'], diffMode: environmentService.args.diff }); // default: read paths from cli
windowsService.open({ cli: environmentService.args, forceNewWindow: environmentService.args['new-window'], diffMode: environmentService.args.diff, restoreBackups: true }); // default: read paths from cli
}
}
......@@ -474,6 +476,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));
services.set(IURLService, new SyncDescriptor(URLService, args['open-url']));
const instantiationService = new InstantiationService(services);
......
......@@ -15,6 +15,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/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';
......@@ -28,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 = {
......@@ -50,6 +52,7 @@ export interface IOpenConfiguration {
forceEmpty?: boolean;
windowToUse?: VSCodeWindow;
diffMode?: boolean;
restoreBackups?: boolean;
}
interface IWindowState {
......@@ -167,7 +170,8 @@ export class WindowsManager implements IWindowsService {
@IEnvironmentService private environmentService: IEnvironmentService,
@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 {
......@@ -621,6 +625,20 @@ export class WindowsManager implements IWindowsService {
iPathsToOpen = this.cliToPaths(openConfig.cli, ignoreFileNotFound);
}
// Add any existing backup workspaces
if (openConfig.restoreBackups) {
this.backupService.getWorkspaceBackupPathsSync().forEach(ws => {
iPathsToOpen.push(this.toIPath(ws));
});
// Get rid of duplicates
iPathsToOpen = arrays.distinct(iPathsToOpen, path => {
if (!('workspacePath' in path)) {
return path.workspacePath;
}
return platform.isLinux ? path.workspacePath : path.workspacePath.toLowerCase();
});
}
let filesToOpen: IPath[] = [];
let filesToDiff: IPath[] = [];
let foldersToOpen = iPathsToOpen.filter(iPath => iPath.workspacePath && !iPath.filePath);
......@@ -749,6 +767,11 @@ export class WindowsManager implements IWindowsService {
// Emit events
iPathsToOpen.forEach(iPath => this.eventEmitter.emit(EventTypes.OPEN, iPath));
// Start tracking workspace backups
this.backupService.pushWorkspaceBackupPathsSync(iPathsToOpen.filter(path => 'workspacePath' in path).map(path => {
return Uri.file(path.workspacePath);
}));
return arrays.distinct(usedWindows);
}
......
/*---------------------------------------------------------------------------------------------
* 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 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<IBackupService>('backupService');
export interface IBackupService {
_serviceBrand: any;
/**
* 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(): TPromise<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.
*/
getWorkspaceBackupPathsSync(): string[];
/**
* Pushes workspace backup paths to be tracked for restoration.
*
* @param workspaces The workspaces to add.
*/
pushWorkspaceBackupPathsSync(workspaces: Uri[]): 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: Uri): TPromise<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_.
*/
getWorkspaceTextFilesWithBackupsSync(workspace: Uri): 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_.
*/
getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[];
/**
* Registers a resource for backup, flagging it for restoration.
*
* @param resource The resource that is being backed up.
*/
registerResourceForBackup(resource: Uri): TPromise<void>;
/**
* Deregisters a resource for backup, unflagging it for restoration.
*
* @param resource The resource that is no longer being backed up.
*/
deregisterResourceForBackup(resource: Uri): TPromise<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;
}
/*---------------------------------------------------------------------------------------------
* 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 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';
interface IBackupFormat {
folderWorkspaces?: {
[workspacePath: string]: string[]
};
}
export class BackupService implements IBackupService {
public _serviceBrand: any;
private workspaceResource: Uri;
private fileContent: IBackupFormat;
private backupHome: string;
private backupWorkspacesPath: string;
constructor(
@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<string[]> {
return this.load().then(() => {
return Object.keys(this.fileContent.folderWorkspaces);
});
}
public getWorkspaceBackupPathsSync(): string[] {
this.loadSync();
return Object.keys(this.fileContent.folderWorkspaces);
}
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) {
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) {
return;
}
if (!this.fileContent.folderWorkspaces[workspace.fsPath]) {
this.fileContent.folderWorkspaces[workspace.fsPath] = [];
}
});
this.saveSync();
}
public removeWorkspaceBackupPath(workspace: Uri): TPromise<void> {
return this.load().then(() => {
if (!this.fileContent.folderWorkspaces) {
return TPromise.as(void 0);
}
delete this.fileContent.folderWorkspaces[workspace.fsPath];
return this.save();
});
}
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.fsPath] || [];
}
public getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[] {
// Hot exit is disabled for empty workspaces
if (!this.workspaceResource) {
return [];
}
const workspaceHash = crypto.createHash('md5').update(workspace.fsPath).digest('hex');
const untitledDir = path.join(this.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) {
return [];
}
}
public getBackupResource(resource: Uri): Uri {
// Hot exit is disabled for empty workspaces
if (!this.workspaceResource) {
return null;
}
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.backupHome, workspaceHash, resource.scheme, backupName);
return Uri.file(backupPath);
}
public registerResourceForBackup(resource: Uri): TPromise<void> {
// Hot exit is disabled for empty workspaces
if (!this.workspaceResource) {
return TPromise.as(void 0);
}
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);
}
this.fileContent.folderWorkspaces[this.workspaceResource.fsPath].push(resource.fsPath);
return this.save();
});
}
public deregisterResourceForBackup(resource: Uri): TPromise<void> {
// Hot exit is disabled for empty workspaces
if (!this.workspaceResource) {
return TPromise.as(void 0);
}
return this.load().then(() => {
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);
});
}
private load(): TPromise<void> {
return pfs.fileExists(this.backupWorkspacesPath).then(exists => {
if (!exists) {
this.fileContent = {
folderWorkspaces: Object.create(null)
};
return TPromise.as(void 0);
}
return pfs.readFile(this.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);
});
});
}
private loadSync(): void {
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);
}
}
private save(): TPromise<void> {
const data = JSON.stringify(this.fileContent);
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.backupHome)) {
fs.mkdirSync(this.backupHome);
}
fs.writeFileSync(this.backupWorkspacesPath, JSON.stringify(this.fileContent));
} catch (ex) {
}
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 * as platform from 'vs/base/common/platform';
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', () => {
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 backupService: BackupService;
setup(done => {
const environmentService = TestEnvironmentService;
backupService = new BackupService(environmentService);
backupService.setBackupPathsForTest(backupHome, backupWorkspacesHome);
// 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(backupHome, os.tmpdir(), done);
});
test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => {
backupService.pushWorkspaceBackupPathsSync([fooFile, barFile]);
assert.deepEqual(backupService.getWorkspaceBackupPathsSync(), [fooFile.fsPath, barFile.fsPath]);
});
test('pushWorkspaceBackupPathsSync should throw if a workspace is set', () => {
backupService.setCurrentWorkspace(fooFile);
assert.throws(() => backupService.pushWorkspaceBackupPathsSync([fooFile]));
});
test('removeWorkspaceBackupPath should remove workspaces from workspaces.json', done => {
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();
});
});
});
test('removeWorkspaceBackupPath should fail gracefully when removing a path that doesn\'t exist', done => {
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(fooFile);
backupService.registerResourceForBackup(barFile).then(() => {
assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(fooFile), [barFile.fsPath]);
done();
});
});
test('deregisterResourceForBackup should deregister backups from workspaces.json', done => {
backupService.setCurrentWorkspace(fooFile);
backupService.registerResourceForBackup(barFile).then(() => {
assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(fooFile), [barFile.fsPath]);
backupService.deregisterResourceForBackup(barFile).then(() => {
assert.deepEqual(backupService.getWorkspaceTextFilesWithBackupsSync(fooFile), []);
done();
});
});
});
test('getBackupResource should get the correct backup path for text files', () => {
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
const workspaceResource = fooFile;
backupService.setCurrentWorkspace(workspaceResource);
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(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: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
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 = Uri.file(path.join(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: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
const workspaceResource = fooFile;
backupService.setCurrentWorkspace(workspaceResource);
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(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: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
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 = Uri.file(path.join(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 = fooFile;
backupService.setCurrentWorkspace(workspaceResource);
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 = fooFile;
backupService.setCurrentWorkspace(workspaceResource);
const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex');
const untitledBackupDir = path.join(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), [untitledBackup1]);
pfs.writeFile(untitledBackup2, 'test').then(() => {
assert.deepEqual(backupService.getWorkspaceUntitledFileBackupsSync(workspaceResource), [untitledBackup1, untitledBackup2]);
done();
});
});
});
});
});
\ No newline at end of file
......@@ -23,6 +23,9 @@ export interface IEnvironmentService {
appSettingsPath: string;
appKeybindingsPath: string;
backupHome: string;
backupWorkspacesPath: string;
disableExtensions: boolean;
extensionsPath: string;
extensionDevelopmentPath: string;
......
......@@ -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')); }
......
......@@ -104,6 +104,27 @@ export interface IFileService {
*/
del(resource: URI, useTrash?: boolean): TPromise<void>;
/**
* 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<IFileStat>;
/**
* Discard the backup for the resource specified.
*/
discardBackup(resource: URI): TPromise<void>;
/**
* Discards all backups associated with this session.
*/
discardBackups(): TPromise<void>;
/**
* Whether hot exit is enabled.
*/
isHotExitEnabled(): boolean;
/**
* Imports the file to the parent identified by the resource.
*/
......@@ -475,6 +496,7 @@ export interface IFilesConfiguration {
autoSave: string;
autoSaveDelay: number;
eol: string;
hotExit: boolean;
};
}
......
......@@ -17,6 +17,7 @@ import { Storage, InMemoryLocalStorage } from 'vs/workbench/common/storage';
import { IEditorGroup, ConfirmResult } from 'vs/workbench/common/editor';
import Event, { Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
import {IBackupService} from 'vs/platform/backup/common/backup';
import { IConfigurationService, getConfigurationValue, IConfigurationValue } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService';
......@@ -110,9 +111,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 {
......@@ -173,6 +175,7 @@ export function workbenchInstantiationService(): IInstantiationService {
instantiationService.stub(IHistoryService, 'getHistory', []);
instantiationService.stub(IModelService, createMockModelService(instantiationService));
instantiationService.stub(IFileService, TestFileService);
instantiationService.stub(IBackupService, new TestBackupService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IMessageService, new TestMessageService());
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
......@@ -569,6 +572,68 @@ export const TestFileService = {
name: paths.basename(res.fsPath)
};
});
},
backupFile: function (resource: URI, content: string) {
return TPromise.as(void 0);
},
discardBackup: function (resource: URI) {
return TPromise.as(void 0);
},
discardBackups: function () {
return TPromise.as(void 0);
},
isHotExitEnabled: function () {
return false;
}
};
export class TestBackupService implements IBackupService {
public _serviceBrand: any;
// Lists used for verification in tests
public registeredResources: URI[] = [];
public deregisteredResources: URI[] = [];
public getWorkspaceBackupPaths(): TPromise<string[]> {
return TPromise.as([]);
}
public getWorkspaceBackupPathsSync(): string[] {
return [];
}
public pushWorkspaceBackupPathsSync(workspaces: URI[]): void {
return null;
}
public removeWorkspaceBackupPath(workspace: URI): TPromise<void> {
return TPromise.as(void 0);
}
public getWorkspaceTextFilesWithBackupsSync(workspace: URI): string[] {
return [];
}
public getWorkspaceUntitledFileBackupsSync(workspace: URI): string[] {
return [];
}
public registerResourceForBackup(resource: URI): TPromise<void> {
this.registeredResources.push(resource);
return TPromise.as(void 0);
}
public deregisterResourceForBackup(resource: URI): TPromise<void> {
this.deregisteredResources.push(resource);
return TPromise.as(void 0);
}
public getBackupResource(resource: URI): URI {
return null;
}
};
......
......@@ -319,6 +319,11 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport {
*/
setResource(resource: URI): void;
/**
* Sets whether to restore the resource from backup.
*/
setRestoreFromBackup(restore: boolean): void;
/**
* Sets the preferred encodingt to use for this input.
*/
......
......@@ -28,6 +28,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 +47,6 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput {
@ITextFileService private textFileService: ITextFileService
) {
super();
this.resource = resource;
this.hasAssociatedFilePath = hasAssociatedFilePath;
this.modeId = modeId;
......@@ -66,6 +66,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;
}
......@@ -130,17 +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<string>;
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 {
const content = '';
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
......
......@@ -11,7 +11,7 @@ import { StringEditorModel } from 'vs/workbench/common/editor/stringEditorModel'
import URI from 'vs/base/common/uri';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
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';
......@@ -31,6 +31,8 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
private hasAssociatedFilePath: boolean;
private backupPromises: TPromise<void>[];
constructor(
value: string,
modeId: string,
......@@ -38,16 +40,19 @@ 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, 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<void>();
this._onDidChangeEncoding = new Emitter<void>();
this.backupPromises = [];
this.registerListeners();
}
......@@ -111,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;
......@@ -153,6 +162,15 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
else if (!this.dirty) {
this.dirty = true;
this._onDidChangeDirty.fire();
}
if (this.fileService.isHotExitEnabled()) {
if (this.dirty) {
this.doBackup();
} else {
this.fileService.discardBackup(this.resource);
}
}
}
......@@ -171,5 +189,36 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
this._onDidChangeDirty.dispose();
this._onDidChangeEncoding.dispose();
this.cancelBackupPromises();
this.fileService.discardBackup(this.resource);
}
public backup(): TPromise<void> {
return this.doBackup(true);
}
private doBackup(immediate?: boolean): TPromise<void> {
// 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
});
this.backupPromises.push(promise);
return promise;
}
private cancelBackupPromises(): void {
while (this.backupPromises.length) {
this.backupPromises.pop().cancel();
}
}
}
\ No newline at end of file
......@@ -22,4 +22,7 @@ export interface IOptions {
* Instructs the workbench to open a diff of the provided files right after startup.
*/
filesToDiff?: IResourceInput[];
filesToRestore?: IResourceInput[];
untitledFilesToRestore?: IResourceInput[];
}
\ No newline at end of file
......@@ -20,6 +20,8 @@ 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/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/common/storage';
import { ITelemetryService, NullTelemetryService, loadExperiments } from 'vs/platform/telemetry/common/telemetry';
......@@ -243,6 +245,11 @@ export class WorkbenchShell {
});
}, errors.onUnexpectedError);
// Backup
const backupService = instantiationService.createInstance(BackupService);
backupService.setCurrentWorkspace(this.contextService.getWorkspace() ? this.contextService.getWorkspace().resource : null);
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);
......
......@@ -16,6 +16,8 @@ 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 { IBackupService } from 'vs/platform/backup/common/backup';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Registry } from 'vs/platform/platform';
import { isWindows, isLinux } from 'vs/base/common/platform';
......@@ -160,7 +162,8 @@ export class Workbench implements IPartService {
@IMessageService private messageService: IMessageService,
@IConfigurationService private configurationService: IConfigurationService,
@ITelemetryService private telemetryService: ITelemetryService,
@IEnvironmentService private environmentService: IEnvironmentService
@IEnvironmentService private environmentService: IEnvironmentService,
@IBackupService private backupService: IBackupService
) {
this.container = container;
......@@ -170,7 +173,22 @@ 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 for this workspace (empty workspaces are not supported yet)
if (workspace) {
options.filesToRestore = this.backupService.getWorkspaceTextFilesWithBackupsSync(workspace.resource).map(filePath => {
return { resource: Uri.file(filePath), options: { pinned: true } };
});
options.untitledFilesToRestore = this.backupService.getWorkspaceUntitledFileBackupsSync(workspace.resource).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 && options.filesToRestore.length > 0) ||
(options.untitledFilesToRestore && options.untitledFilesToRestore.length > 0);
this.toDispose = [];
this.toShutdown = [];
......@@ -297,6 +315,8 @@ export class Workbench implements IPartService {
const wbopt = this.workbenchParams.options;
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
......@@ -315,10 +335,17 @@ 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
return TPromise.join<EditorInput>(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<EditorInput>(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] }; });
});
......
......@@ -213,6 +213,12 @@ configurationRegistry.registerConfiguration({
'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.")
},
'editor.formatOnSave': {
'type': 'boolean',
'default': false,
......
......@@ -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';
......@@ -26,6 +27,7 @@ export class FileEditorInput extends CommonFileEditorInput {
private resource: URI;
private preferredEncoding: string;
private forceOpenAsBinary: boolean;
private restoreFromBackup: boolean;
private name: string;
private description: string;
......@@ -43,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();
......@@ -102,6 +105,10 @@ export class FileEditorInput extends CommonFileEditorInput {
this.verboseDescription = null;
}
public setRestoreFromBackup(restore: boolean): void {
this.restoreFromBackup = restore;
}
public getResource(): URI {
return this.resource;
}
......@@ -195,7 +202,8 @@ export class FileEditorInput extends CommonFileEditorInput {
}
public resolve(refresh?: boolean): TPromise<EditorModel> {
return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh).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 ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
......
......@@ -42,6 +42,8 @@ export abstract class FileEditorInput extends EditorInput implements IFileEditor
public abstract setResource(resource: URI): void;
public abstract setRestoreFromBackup(restore: boolean): void;
public abstract getResource(): URI;
public abstract setPreferredEncoding(encoding: string): void;
......
......@@ -33,6 +33,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IBackupService } from 'vs/platform/backup/common/backup';
class SettingsTestEnvironmentService extends EnvironmentService {
......@@ -55,9 +56,10 @@ class TestDirtyTextFileService extends TestTextFileService {
@IEditorGroupService editorGroupService: IEditorGroupService,
@IFileService fileService: IFileService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IInstantiationService instantiationService: IInstantiationService
@IInstantiationService instantiationService: IInstantiationService,
@IBackupService backupService: IBackupService
) {
super(lifecycleService, contextService, configurationService, telemetryService, editorService, editorGroupService, fileService, untitledEditorService, instantiationService);
super(lifecycleService, contextService, configurationService, telemetryService, editorService, editorGroupService, fileService, untitledEditorService, instantiationService, backupService);
}
public isDirty(resource?: URI): boolean {
......@@ -85,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);
const fileService = new FileService(noWorkspace ? null : workspaceDir, { disableWatcher: true }, events, environmentService, configurationService, null);
return configurationService.initialize().then(() => {
return {
......
......@@ -208,8 +208,8 @@ export class WorkbenchEditorService implements IWorkbenchEditorService {
}
public createInput(input: EditorInput): TPromise<EditorInput>;
public createInput(input: IResourceInput): TPromise<EditorInput>;
public createInput(input: any): TPromise<IEditorInput> {
public createInput(input: IResourceInput, restoreFromBackup?: boolean): TPromise<EditorInput>;
public createInput(input: any, restoreFromBackup?: boolean): TPromise<IEditorInput> {
// Workbench Input Support
if (input instanceof EditorInput) {
......@@ -263,7 +263,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,10 +277,11 @@ export class WorkbenchEditorService implements IWorkbenchEditorService {
return TPromise.as<EditorInput>(null);
}
private createFileInput(resource: URI, encoding?: string): TPromise<IFileEditorInput> {
private createFileInput(resource: URI, encoding?: string, restoreFromBackup?: boolean): TPromise<IFileEditorInput> {
return this.instantiationService.createInstance(this.fileInputDescriptor).then((typedFileInput) => {
typedFileInput.setResource(resource);
typedFileInput.setPreferredEncoding(encoding);
typedFileInput.setRestoreFromBackup(restoreFromBackup);
return typedFileInput;
});
......
......@@ -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';
......@@ -47,11 +48,12 @@ export class FileService implements IFileService {
@IEventService private eventService: IEventService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEnvironmentService environmentService: IEnvironmentService,
@IEnvironmentService private environmentService: IEnvironmentService,
@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.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, this.eventService, this.environmentService, this.configurationService, this.backupService);
// Listeners
this.registerListeners();
......@@ -241,6 +243,22 @@ export class FileService implements IFileService {
return this.raw.del(resource);
}
public backupFile(resource: uri, content: string): TPromise<IFileStat> {
return this.raw.backupFile(resource, content);
}
public discardBackup(resource: uri): TPromise<void> {
return this.raw.discardBackup(resource);
}
public discardBackups(): TPromise<void> {
return this.raw.discardBackups();
}
public isHotExitEnabled(): boolean {
return this.raw.isHotExitEnabled();
}
private doMoveItemToTrash(resource: uri): TPromise<void> {
const workspace = this.contextService.getWorkspace();
if (!workspace) {
......
......@@ -28,10 +28,15 @@ import pfs = require('vs/base/node/pfs');
import encoding = require('vs/base/node/encoding');
import mime = require('vs/base/node/mime');
import flow = require('vs/base/node/flow');
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
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 { IEventService } from 'vs/platform/event/common/event';
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';
export interface IEncodingOverride {
resource: uri;
......@@ -72,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;
......@@ -82,7 +88,16 @@ export class FileService implements IFileService {
private fileChangesWatchDelayer: ThrottledDelayer<void>;
private undeliveredRawFileChangesEvents: IRawFileChange[];
constructor(basePath: string, options: IFileServiceOptions, private eventEmitter: IEventService) {
private configuredHotExit: boolean;
constructor(
basePath: string,
options: IFileServiceOptions,
private eventEmitter: IEventService,
private environmentService: IEnvironmentService,
private configurationService: IConfigurationService,
private backupService: IBackupService
) {
this.basePath = basePath ? paths.normalize(basePath) : void 0;
if (this.basePath && this.basePath.indexOf('\\\\') === 0 && strings.endsWith(this.basePath, paths.sep)) {
......@@ -115,6 +130,19 @@ export class FileService implements IFileService {
this.activeFileChangesWatchers = Object.create(null);
this.fileChangesWatchDelayer = new ThrottledDelayer<void>(FileService.FS_EVENT_DELAY);
this.undeliveredRawFileChangesEvents = [];
// Configuration changes
this.toUnbind = [];
if (this.configurationService) {
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config)));
const configuration = this.configurationService.getConfiguration<IFilesConfiguration>();
this.onConfigurationChange(configuration);
}
}
private onConfigurationChange(configuration: IFilesConfiguration): void {
this.configuredHotExit = configuration && configuration.files && configuration.files.hotExit;
}
public updateOptions(options: IFileServiceOptions): void {
......@@ -427,8 +455,76 @@ export class FileService implements IFileService {
return nfcall(extfs.del, absolutePath, this.tmpPath);
}
public backupFile(resource: uri, content: string): TPromise<IFileStat> {
let registerResourcePromise: TPromise<void>;
if (resource.scheme === 'file') {
registerResourcePromise = this.backupService.registerResourceForBackup(resource);
} else {
registerResourcePromise = TPromise.as(void 0);
}
return registerResourcePromise.then(() => {
const backupResource = this.getBackupPath(resource);
// Hot exit is disabled for empty workspaces
if (!backupResource) {
return TPromise.as(null);
}
return this.updateContent(backupResource, content);
});
}
public discardBackup(resource: uri): TPromise<void> {
return this.backupService.deregisterResourceForBackup(resource).then(() => {
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<void> {
// 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 {
return this.configuredHotExit;
}
// 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(backupRootPath, resource.scheme, backupName);
return uri.file(backupPath);
}
private getBackupRootPath(): string {
// Hot exit is disabled for empty workspaces
if (!this.basePath) {
return null;
}
const workspaceHash = crypto.createHash('md5').update(this.basePath).digest('hex');
return paths.join(this.environmentService.userDataPath, 'Backups', workspaceHash);
}
private toAbsolutePath(arg1: uri | IFileStat): string {
let resource: uri;
if (arg1 instanceof uri) {
......@@ -664,6 +760,8 @@ export class FileService implements IFileService {
watcher.close();
}
this.activeFileChangesWatchers = Object.create(null);
this.toUnbind = dispose(this.toUnbind);
}
}
......
......@@ -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);
service = new FileService(testDir, { disableWatcher: true }, events, null, null, null);
done();
});
});
......@@ -275,6 +278,107 @@ suite('FileService', () => {
});
});
suite('backups', () => {
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 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);
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 - text file', function (done: () => void) {
_service.backupFile(fooResource, 'test').then(() => {
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');
done();
});
});
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(path.join(workspaceBackupRoot, 'file')).length, 1);
_service.discardBackup(fooResource).then(() => {
assert.equal(fs.existsSync(fooBackupPath), false);
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 - text file', function (done: () => void) {
_service.backupFile(fooResource, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupRoot, 'file')).length, 1);
_service.backupFile(barResource, 'test').then(() => {
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(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) {
service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).done(r => {
assert.equal(r.children.length, 6);
......@@ -494,7 +598,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 +624,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) => {
......
......@@ -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');
......@@ -26,6 +27,7 @@ import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorMo
import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/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.
......@@ -42,6 +44,8 @@ export abstract class TextFileService implements ITextFileService {
private _onFilesAssociationChange: Emitter<void>;
private currentFilesAssociationConfig: { [key: string]: string; };
private configuredHotExit: boolean;
private _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration>;
private configuredAutoSaveDelay: number;
private configuredAutoSaveOnFocusChange: boolean;
......@@ -56,7 +60,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 = [];
......@@ -112,10 +117,33 @@ export abstract class TextFileService implements ITextFileService {
}
private beforeShutdown(): boolean | TPromise<boolean> {
// If hot exit is enabled then save the dirty files in the workspace and then exit
// 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();
}
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();
}
// 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
});
});
}
// 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(() => {
......@@ -123,7 +151,9 @@ export abstract class TextFileService implements ITextFileService {
return this.confirmBeforeShutdown(); // we still have dirty files around, so confirm normally
}
return false; // all good, no veto
return this.fileService.discardBackups().then(() => {
return false; // all good, no veto
});
});
}
......@@ -131,7 +161,9 @@ export abstract class TextFileService implements ITextFileService {
return this.confirmBeforeShutdown();
}
return false; // no veto
return this.fileService.discardBackups().then(() => {
return false; // no veto
});
}
private confirmBeforeShutdown(): boolean | TPromise<boolean> {
......@@ -144,13 +176,13 @@ export abstract class TextFileService implements ITextFileService {
return true; // veto if some saves failed
}
return false; // no veto
return this.cleanupBackupsBeforeShutdown();
});
}
// Don't Save
else if (confirm === ConfirmResult.DONT_SAVE) {
return false; // no veto
return this.cleanupBackupsBeforeShutdown();
}
// Cancel
......@@ -159,6 +191,18 @@ export abstract class TextFileService implements ITextFileService {
}
}
private cleanupBackupsBeforeShutdown(): boolean | TPromise<boolean> {
const workspace = this.contextService.getWorkspace();
if (!workspace) {
return false; // no backups to cleanup, no eto
}
return this.backupService.removeWorkspaceBackupPath(workspace.resource).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);
......@@ -209,6 +253,9 @@ export abstract class TextFileService implements ITextFileService {
this.saveAll().done(null, errors.onUnexpectedError);
}
// 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;
if (!objects.equals(this.currentFilesAssociationConfig, filesAssociation)) {
......@@ -358,6 +405,66 @@ export abstract class TextFileService implements ITextFileService {
});
}
/**
* Performs an immedate backup of all dirty file and untitled models.
*/
private backupAll(): TPromise<ITextFileOperationResult> {
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<ITextFileOperationResult> {
// 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(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 => {
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])
};
});
});
});
}
private getFileModels(resources?: URI[]): ITextFileEditorModel[];
private getFileModels(resource?: URI): ITextFileEditorModel[];
private getFileModels(arg1?: any): ITextFileEditorModel[] {
......@@ -520,6 +627,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;
......
......@@ -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;
......@@ -51,6 +52,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private autoSaveAfterMillies: number;
private autoSaveAfterMilliesEnabled: boolean;
private autoSavePromises: TPromise<void>[];
private backupPromises: TPromise<void>[];
private mapPendingSaveToVersionId: { [versionId: string]: TPromise<void> };
private disposed: boolean;
private inConflictResolutionMode: boolean;
......@@ -82,6 +84,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.preferredEncoding = preferredEncoding;
this.dirty = false;
this.autoSavePromises = [];
this.backupPromises = [];
this.versionId = 0;
this.lastSaveAttemptTime = 0;
this.mapPendingSaveToVersionId = {};
......@@ -180,6 +183,10 @@ 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<EditorModel> {
diag('load() - enter', this.resource, new Date());
......@@ -257,18 +264,35 @@ 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) {
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(true);
this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e)));
return this;
}, (error) => {
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)));
return TPromise.wrapError(error);
});
});
} else {
this.createTextEditorModelPromise = this.createTextEditorModel(content.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)));
return this;
}, (error) => {
this.createTextEditorModelPromise = null;
return this;
}, (error) => {
this.createTextEditorModelPromise = null;
return TPromise.wrapError(error);
});
return TPromise.wrapError(error);
});
}
return this.createTextEditorModelPromise;
}
......@@ -318,6 +342,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this._onDidStateChange.fire(StateChange.REVERTED);
}
if (this.fileService.isHotExitEnabled()) {
this.fileService.discardBackup(this.resource);
}
return;
}
......@@ -334,6 +362,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
}
}
if (this.fileService.isHotExitEnabled()) {
this.doBackup();
}
}
private makeDirty(): void {
......@@ -374,6 +406,38 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
public backup(): TPromise<void> {
if (!this.dirty) {
return TPromise.as<void>(null);
}
return this.doBackup(true);
}
private doBackup(immediate?: boolean): TPromise<void> {
// 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
});
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.
*/
......@@ -722,6 +786,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.createTextEditorModelPromise = null;
this.cancelAutoSavePromises();
this.cancelBackupPromises();
this.fileService.discardBackup(this.resource);
super.dispose();
}
......
......@@ -171,7 +171,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return this.mapResourceToModel[resource.toString()];
}
public loadOrCreate(resource: URI, encoding: string, refresh?: boolean): TPromise<ITextFileEditorModel> {
public loadOrCreate(resource: URI, encoding: string, refresh?: boolean, restoreResource?: URI): TPromise<ITextFileEditorModel> {
// Return early if model is currently being loaded
const pendingLoad = this.mapResourceToPendingModelLoaders[resource.toString()];
......@@ -194,6 +194,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
......
......@@ -192,7 +192,7 @@ export interface ITextFileEditorModelManager {
getAll(resource?: URI): ITextFileEditorModel[];
loadOrCreate(resource: URI, preferredEncoding: string, refresh?: boolean): TPromise<ITextEditorModel>;
loadOrCreate(resource: URI, preferredEncoding: string, refresh?: boolean, restoreResource?: URI): TPromise<ITextEditorModel>;
}
export interface IModelSaveOptions {
......@@ -217,6 +217,10 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
save(options?: IModelSaveOptions): TPromise<void>;
backup(): TPromise<void>;
setRestoreResource(resource: URI): void;
revert(): TPromise<void>;
setConflictResolutionMode();
......@@ -318,6 +322,14 @@ export interface ITextFileService extends IDisposable {
*/
confirmSave(resources?: URI[]): ConfirmResult;
/**
* 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;
/**
* Convinient fast access to the current auto save mode.
*/
......
......@@ -27,6 +27,7 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelBuilder } from 'vs/editor/node/model/modelBuilder';
import product from 'vs/platform/product';
import { IBackupService } from 'vs/platform/backup/common/backup';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
......@@ -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<IRawTextContent> {
......
......@@ -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 (<TPromise<boolean>>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 (<TPromise<boolean>>event.value).then(veto => {
assert.ok(!veto);
done();
});
});
});
......
......@@ -58,7 +58,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 +76,9 @@ export class UntitledEditorService implements IUntitledEditorService {
private _onDidChangeDirty: Emitter<URI>;
private _onDidChangeEncoding: Emitter<URI>;
constructor( @IInstantiationService private instantiationService: IInstantiationService) {
constructor(
@IInstantiationService private instantiationService: IInstantiationService
) {
this._onDidChangeDirty = new Emitter<URI>();
this._onDidChangeEncoding = new Emitter<URI>();
}
......@@ -130,7 +132,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 +149,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 +164,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);
......
......@@ -144,6 +144,9 @@ class TestFileEditorInput extends EditorInput implements IFileEditorInput {
public setResource(r: URI): void {
}
public setRestoreFromBackup(restore: boolean): void {
}
public setEncoding(encoding: string) {
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册