提交 961bf4f0 编写于 作者: D Daniel Imms

Merge remote-tracking branch 'origin/master' into tyriar/uplevel_ptyjs

......@@ -5,12 +5,14 @@
'use strict';
import Uri from 'vs/base/common/uri';
import { EventEmitter } from 'events';
import { ipcMain as ipc, app } from 'electron';
import { TPromise, TValueCallback } from 'vs/base/common/winjs.base';
import { ReadyState, VSCodeWindow } from 'vs/code/electron-main/window';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IBackupMainService } from 'vs/platform/backup/common/backup';
import { ILogService } from 'vs/code/electron-main/log';
import { IStorageService } from 'vs/code/electron-main/storage';
......@@ -52,7 +54,8 @@ export class LifecycleService implements ILifecycleService {
constructor(
@IEnvironmentService private environmentService: IEnvironmentService,
@ILogService private logService: ILogService,
@IStorageService private storageService: IStorageService
@IStorageService private storageService: IStorageService,
@IBackupMainService private backupService: IBackupMainService
) {
this.windowToCloseRequest = Object.create(null);
this.quitRequested = false;
......@@ -160,6 +163,14 @@ export class LifecycleService implements ILifecycleService {
const oneTimeCancelEvent = 'vscode:cancel' + oneTimeEventToken;
ipc.once(oneTimeOkEvent, () => {
// Clear out any workspace backups from workspaces.json that don't have any backups
if (vscodeWindow.openedWorkspacePath) {
const workspaceResource = Uri.file(vscodeWindow.openedWorkspacePath);
if (!this.backupService.hasWorkspaceBackup(workspaceResource)) {
this.backupService.removeWorkspaceBackupPathSync(workspaceResource);
}
}
c(false); // no veto
});
......@@ -175,7 +186,7 @@ export class LifecycleService implements ILifecycleService {
c(true); // veto
});
vscodeWindow.send('vscode:beforeUnload', { okChannel: oneTimeOkEvent, cancelChannel: oneTimeCancelEvent });
vscodeWindow.send('vscode:beforeUnload', { okChannel: oneTimeOkEvent, cancelChannel: oneTimeCancelEvent, quitRequested: this.quitRequested });
});
}
......
......@@ -34,6 +34,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 { IBackupMainService } from 'vs/platform/backup/common/backup';
import { BackupMainService } from 'vs/platform/backup/node/backupMainService';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
......@@ -240,11 +242,11 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
// Open our first window
if (environmentService.args['new-window'] && environmentService.args._.length === 0) {
windowsMainService.open({ cli: environmentService.args, forceNewWindow: true, forceEmpty: true }); // new window if "-n" was used without paths
windowsMainService.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)) {
windowsMainService.open({ cli: environmentService.args, pathsToOpen: global.macOpenFiles }); // mac: open-file event received on startup
windowsMainService.open({ cli: environmentService.args, pathsToOpen: global.macOpenFiles, restoreBackups: true }); // mac: open-file event received on startup
} else {
windowsMainService.open({ cli: environmentService.args, forceNewWindow: environmentService.args['new-window'], diffMode: environmentService.args.diff }); // default: read paths from cli
windowsMainService.open({ cli: environmentService.args, forceNewWindow: environmentService.args['new-window'], diffMode: environmentService.args.diff, restoreBackups: true }); // default: read paths from cli
}
});
}
......@@ -426,6 +428,7 @@ function createServices(args): IInstantiationService {
services.set(IConfigurationService, new SyncDescriptor(ConfigurationService));
services.set(IRequestService, new SyncDescriptor(RequestService));
services.set(IURLService, new SyncDescriptor(URLService, args['open-url']));
services.set(IBackupMainService, new SyncDescriptor(BackupMainService));
return new InstantiationService(services, true);
}
......
......@@ -17,6 +17,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { parseArgs } from 'vs/platform/environment/node/argv';
import product from 'vs/platform/product';
import { getCommonHTTPHeaders } from 'vs/platform/environment/node/http';
import { IBackupMainService } from 'vs/platform/backup/common/backup';
import Uri from 'vs/base/common/uri';
export interface IWindowState {
width?: number;
......@@ -103,6 +105,7 @@ export interface IWindowConfiguration extends ParsedArgs {
filesToOpen?: IPath[];
filesToCreate?: IPath[];
filesToDiff?: IPath[];
untitledToRestore?: IPath[];
}
export interface IWindowSettings {
......@@ -142,7 +145,8 @@ export class VSCodeWindow {
@ILogService private logService: ILogService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IConfigurationService private configurationService: IConfigurationService,
@IStorageService private storageService: IStorageService
@IStorageService private storageService: IStorageService,
@IBackupMainService private backupService: IBackupMainService
) {
this.options = config;
this._lastFocusTime = -1;
......@@ -426,6 +430,11 @@ export class VSCodeWindow {
delete configuration.filesToCreate;
delete configuration.filesToDiff;
// Update untitled files to restore so they come through in the reloaded window
configuration.untitledToRestore = this.backupService.getWorkspaceUntitledFileBackupsSync(Uri.file(configuration.workspacePath)).map(filePath => {
return { filePath };
});
// Some configuration things get inherited if the window is being reloaded and we are
// in plugin development mode. These options are all development related.
if (this.isPluginDevelopmentHost && cli) {
......
......@@ -14,6 +14,7 @@ import * as paths from 'vs/base/common/paths';
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 { IBackupMainService } from 'vs/platform/backup/common/backup';
import { trim } from 'vs/base/common/strings';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { IStorageService } from 'vs/code/electron-main/storage';
......@@ -28,6 +29,7 @@ import { createDecorator, IInstantiationService } from 'vs/platform/instantiatio
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import CommonEvent, { Emitter, once } from 'vs/base/common/event';
import product from 'vs/platform/product';
import Uri from 'vs/base/common/uri';
enum WindowError {
UNRESPONSIVE,
......@@ -43,6 +45,7 @@ export interface IOpenConfiguration {
forceEmpty?: boolean;
windowToUse?: VSCodeWindow;
diffMode?: boolean;
restoreBackups?: boolean;
}
interface IWindowState {
......@@ -150,6 +153,7 @@ export class WindowsManager implements IWindowsMainService {
@IStorageService private storageService: IStorageService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IBackupMainService private backupService: IBackupMainService,
@IConfigurationService private configurationService: IConfigurationService,
@ITelemetryService private telemetryService: ITelemetryService
) { }
......@@ -376,6 +380,28 @@ export class WindowsManager implements IWindowsMainService {
}
let configuration: IWindowConfiguration;
let openInNewWindow = openConfig.preferNewWindow || openConfig.forceNewWindow;
// Restore any existing backup workspaces
if (openConfig.restoreBackups) {
const workspacesWithBackups = this.backupService.getWorkspaceBackupPaths();
workspacesWithBackups.forEach(workspacePath => {
if (!fs.existsSync(workspacePath)) {
this.backupService.removeWorkspaceBackupPathSync(Uri.file(workspacePath));
return;
}
const untitledToRestore = this.backupService.getWorkspaceUntitledFileBackupsSync(Uri.file(workspacePath)).map(filePath => {
return { filePath: filePath };
});
configuration = this.toConfiguration(this.getWindowUserEnv(openConfig), openConfig.cli, workspacePath, [], [], [], untitledToRestore);
const browserWindow = this.openInBrowserWindow(configuration, openInNewWindow, openInNewWindow ? void 0 : openConfig.windowToUse);
usedWindows.push(browserWindow);
openInNewWindow = true; // any other folders to open must open in new window then
});
}
// Handle files to open/diff or to create when we dont open a folder
if (!foldersToOpen.length && (filesToOpen.length > 0 || filesToCreate.length > 0 || filesToDiff.length > 0)) {
......@@ -416,7 +442,6 @@ export class WindowsManager implements IWindowsMainService {
}
// Handle folders to open
let openInNewWindow = openConfig.preferNewWindow || openConfig.forceNewWindow;
if (foldersToOpen.length > 0) {
// Check for existing instances
......@@ -485,6 +510,9 @@ export class WindowsManager implements IWindowsMainService {
}
}
// Register new paths for backup
this.backupService.pushWorkspaceBackupPathsSync(iPathsToOpen.filter(p => p.workspacePath).map(p => Uri.file(p.workspacePath)));
// Emit events
iPathsToOpen.forEach(iPath => this._onPathOpen.fire(iPath));
......@@ -626,7 +654,7 @@ export class WindowsManager implements IWindowsMainService {
this.open({ cli: openConfig.cli, forceNewWindow: true, forceEmpty: openConfig.cli._.length === 0 });
}
private toConfiguration(userEnv: platform.IProcessEnvironment, cli: ParsedArgs, workspacePath?: string, filesToOpen?: IPath[], filesToCreate?: IPath[], filesToDiff?: IPath[]): IWindowConfiguration {
private toConfiguration(userEnv: platform.IProcessEnvironment, cli: ParsedArgs, workspacePath?: string, filesToOpen?: IPath[], filesToCreate?: IPath[], filesToDiff?: IPath[], untitledToRestore?: IPath[]): IWindowConfiguration {
const configuration: IWindowConfiguration = mixin({}, cli); // inherit all properties from CLI
configuration.appRoot = this.environmentService.appRoot;
configuration.execPath = process.execPath;
......@@ -635,6 +663,7 @@ export class WindowsManager implements IWindowsMainService {
configuration.filesToOpen = filesToOpen;
configuration.filesToCreate = filesToCreate;
configuration.filesToDiff = filesToDiff;
configuration.untitledToRestore = untitledToRestore;
return configuration;
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import Uri from 'vs/base/common/uri';
export interface IBackupWorkspacesFormat {
folderWorkspaces: string[];
}
export const IBackupMainService = createDecorator<IBackupMainService>('backupService');
export interface IBackupMainService {
_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(): 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.
*
* @param workspace The workspace to remove.
*/
removeWorkspaceBackupPathSync(workspace: Uri): void;
/**
* Gets the set of untitled file backups for a particular workspace.
*
* @param workspace The workspace to get the backups for.
* @return The absolute paths for all the untitled file _backups_.
*/
getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[];
/**
* Gets whether the workspace has backup(s) associated with it (ie. if the workspace backup
* directory exists).
*
* @param workspace The workspace to evaluate.
* @return Whether the workspace has backups.
*/
hasWorkspaceBackup(workspace: Uri): boolean;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import Uri from 'vs/base/common/uri';
import { readdirSync } from 'vs/base/node/extfs';
import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export class BackupMainService implements IBackupMainService {
public _serviceBrand: any;
protected backupHome: string;
protected workspacesJsonPath: string;
private workspacesJsonContent: IBackupWorkspacesFormat;
constructor(
@IEnvironmentService environmentService: IEnvironmentService
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
this.loadSync();
}
public getWorkspaceBackupPaths(): string[] {
return this.workspacesJsonContent.folderWorkspaces;
}
public pushWorkspaceBackupPathsSync(workspaces: Uri[]): void {
workspaces.forEach(workspace => {
// Hot exit is disabled for empty workspaces
if (!workspace) {
return;
}
if (this.workspacesJsonContent.folderWorkspaces.indexOf(workspace.fsPath) === -1) {
this.workspacesJsonContent.folderWorkspaces.push(workspace.fsPath);
}
});
this.saveSync();
}
public removeWorkspaceBackupPathSync(workspace: Uri): void {
if (!this.workspacesJsonContent.folderWorkspaces) {
return;
}
const index = this.workspacesJsonContent.folderWorkspaces.indexOf(workspace.fsPath);
if (index === -1) {
return;
}
this.workspacesJsonContent.folderWorkspaces.splice(index, 1);
this.saveSync();
}
public getWorkspaceUntitledFileBackupsSync(workspace: Uri): string[] {
const untitledDir = path.join(this.getWorkspaceBackupDirectory(workspace), 'untitled');
// Allow sync here as it's only used in workbench initialization's critical path
try {
return readdirSync(untitledDir).map(file => path.join(untitledDir, file));
} catch (ex) {
return [];
}
}
public hasWorkspaceBackup(workspace: Uri): boolean {
return fs.existsSync(this.getWorkspaceBackupDirectory(workspace));
}
private getWorkspaceBackupDirectory(workspace: Uri): string {
const workspaceHash = crypto.createHash('md5').update(workspace.fsPath).digest('hex');
return path.join(this.backupHome, workspaceHash);
}
protected loadSync(): void {
try {
this.workspacesJsonContent = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
} catch (error) {
this.workspacesJsonContent = Object.create(null);
}
// Ensure folderWorkspaces is a string[]
if (this.workspacesJsonContent.folderWorkspaces) {
const fws = this.workspacesJsonContent.folderWorkspaces;
if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
this.workspacesJsonContent = Object.create(null);
}
}
if (!this.workspacesJsonContent.folderWorkspaces) {
this.workspacesJsonContent.folderWorkspaces = [];
}
}
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.workspacesJsonPath, JSON.stringify(this.workspacesJsonContent));
} catch (ex) {
console.error('Could not save workspaces.json', ex);
}
}
}
/*---------------------------------------------------------------------------------------------
* 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 fs = require('fs');
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 { TestEnvironmentService } from 'vs/test/utils/servicesTestUtils';
import { BackupMainService } from 'vs/platform/backup/node/backupMainService';
import { IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup';
class TestBackupMainService extends BackupMainService {
constructor(backupHome: string, backupWorkspacesPath: string) {
super(TestEnvironmentService);
this.backupHome = backupHome;
this.workspacesJsonPath = backupWorkspacesPath;
// Force a reload with the new paths
this.loadSync();
}
}
suite('BackupMainService', () => {
const parentDir = path.join(os.tmpdir(), 'vsctests', 'service');
const backupHome = path.join(parentDir, 'Backups');
const backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo');
const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar');
const fooWorkspaceBackupDir = path.join(backupHome, crypto.createHash('md5').update(fooFile.fsPath).digest('hex'));
let service: BackupMainService;
setup(done => {
service = new TestBackupMainService(backupHome, backupWorkspacesPath);
// Delete any existing backups completely and then re-create it.
extfs.del(backupHome, os.tmpdir(), () => {
pfs.mkdirp(backupHome).then(() => {
done();
});
});
});
teardown(done => {
extfs.del(backupHome, os.tmpdir(), done);
});
test('getWorkspaceBackupPathsSync should return [] when workspaces.json doesn\'t exist', () => {
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
});
test('getWorkspaceBackupPathsSync should return [] when workspaces.json is not properly formed JSON', () => {
fs.writeFileSync(backupWorkspacesPath, '');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
});
test('getWorkspaceBackupPathsSync should return [] when folderWorkspaces in workspaces.json is absent', () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
});
test('getWorkspaceBackupPathsSync should return [] when folderWorkspaces in workspaces.json is not a string array', () => {
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}');
assert.deepEqual(service.getWorkspaceBackupPaths(), []);
});
test('pushWorkspaceBackupPathsSync should persist paths to workspaces.json', () => {
service.pushWorkspaceBackupPathsSync([fooFile, barFile]);
assert.deepEqual(service.getWorkspaceBackupPaths(), [fooFile.fsPath, barFile.fsPath]);
});
test('getWorkspaceUntitledFileBackupsSync should return untitled file backup resources', done => {
const untitledBackupDir = path.join(fooWorkspaceBackupDir, 'untitled');
const untitledBackup1 = path.join(untitledBackupDir, 'bar');
const untitledBackup2 = path.join(untitledBackupDir, 'foo');
pfs.mkdirp(untitledBackupDir).then(() => {
pfs.writeFile(untitledBackup1, 'test').then(() => {
assert.deepEqual(service.getWorkspaceUntitledFileBackupsSync(fooFile), [untitledBackup1]);
pfs.writeFile(untitledBackup2, 'test').then(() => {
assert.deepEqual(service.getWorkspaceUntitledFileBackupsSync(fooFile), [untitledBackup1, untitledBackup2]);
done();
});
});
});
});
test('removeWorkspaceBackupPath should remove workspaces from workspaces.json', done => {
service.pushWorkspaceBackupPathsSync([fooFile, barFile]);
service.removeWorkspaceBackupPathSync(fooFile);
pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderWorkspaces, [barFile.fsPath]);
service.removeWorkspaceBackupPathSync(barFile);
pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.folderWorkspaces, []);
done();
});
});
});
test('removeWorkspaceBackupPath should fail gracefully when removing a path that doesn\'t exist', done => {
const workspacesJson: IBackupWorkspacesFormat = { folderWorkspaces: [fooFile.fsPath] };
pfs.writeFileAndFlush(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => {
service.removeWorkspaceBackupPathSync(barFile);
pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath]);
done();
});
});
});
test('doesWorkspaceHaveBackups should return whether the workspace\'s backup exists', () => {
assert.equal(service.hasWorkspaceBackup(fooFile), false);
fs.mkdirSync(fooWorkspaceBackupDir);
assert.equal(service.hasWorkspaceBackup(fooFile), true);
});
});
\ No newline at end of file
......@@ -51,6 +51,9 @@ export interface IEnvironmentService {
appSettingsPath: string;
appKeybindingsPath: string;
backupHome: string;
backupWorkspacesPath: string;
disableExtensions: boolean;
extensionsPath: string;
extensionDevelopmentPath: string;
......
......@@ -79,6 +79,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['extensions-dir'] || path.join(this.userProductHome, 'extensions')); }
......
......@@ -469,6 +469,7 @@ export interface IFilesConfiguration {
autoSave: string;
autoSaveDelay: number;
eol: string;
hotExit: boolean;
};
}
......
......@@ -22,6 +22,7 @@ export const ILifecycleService = createDecorator<ILifecycleService>('lifecycleSe
export interface ShutdownEvent {
veto(value: boolean | TPromise<boolean>): void;
quitRequested: boolean;
}
/**
......@@ -38,6 +39,12 @@ export interface ILifecycleService {
*/
willShutdown: boolean;
/**
* A flag indications if the application is in the process of quitting all windows. This will be
* set before the onWillShutdown event is fired and reverted to false afterwards.
*/
quitRequested: boolean;
/**
* Fired before shutdown happens. Allows listeners to veto against the
* shutdown.
......@@ -54,6 +61,7 @@ export interface ILifecycleService {
export const NullLifecycleService: ILifecycleService = {
_serviceBrand: null,
willShutdown: false,
quitRequested: false,
onWillShutdown: () => ({ dispose() { } }),
onShutdown: () => ({ dispose() { } })
};
......@@ -18,6 +18,7 @@ import { StorageService, InMemoryLocalStorage } from 'vs/workbench/services/stor
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, IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService';
......@@ -114,9 +115,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, fileService, untitledEditorService, instantiationService);
super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, fileService, untitledEditorService, instantiationService, backupService);
}
public setPromptPath(path: string): void {
......@@ -177,6 +179,8 @@ export function workbenchInstantiationService(): IInstantiationService {
instantiationService.stub(IHistoryService, 'getHistory', []);
instantiationService.stub(IModelService, createMockModelService(instantiationService));
instantiationService.stub(IFileService, TestFileService);
instantiationService.stub(IBackupFileService, new TestBackupFileService());
instantiationService.stub(IBackupService, new TestBackupService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IMessageService, new TestMessageService());
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
......@@ -601,6 +605,92 @@ 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;
public isHotExitEnabled: boolean = false;
public backupBeforeShutdown(): boolean | TPromise<boolean> {
return false;
}
public cleanupBackupsBeforeShutdown(): boolean | TPromise<boolean> {
return false;
}
public doBackup(resource: URI, content: string, immediate?: boolean): TPromise<void> {
return TPromise.as(void 0);
}
}
export class TestBackupFileService implements IBackupFileService {
public _serviceBrand: any;
public getWorkspaceBackupPaths(): TPromise<string[]> {
return TPromise.as([]);
}
public getWorkspaceBackupPathsSync(): string[] {
return [];
}
public pushWorkspaceBackupPathsSync(workspaces: URI[]): void {
return null;
}
public getWorkspaceTextFilesWithBackupsSync(workspace: URI): string[] {
return [];
}
public getWorkspaceUntitledFileBackupsSync(workspace: URI): string[] {
return [];
}
public hasTextFileBackup(resource: URI): TPromise<boolean> {
return TPromise.as(false);
}
public registerResourceForBackup(resource: URI): TPromise<void> {
return TPromise.as(void 0);
}
public deregisterResourceForBackup(resource: URI): TPromise<void> {
return TPromise.as(void 0);
}
public getBackupResource(resource: URI): URI {
return null;
}
public backupResource(resource: URI, content: string): TPromise<void> {
return TPromise.as(void 0);
}
public discardResourceBackup(resource: URI): TPromise<void> {
return TPromise.as(void 0);
}
public discardAllWorkspaceBackups(): TPromise<void> {
return TPromise.as(void 0);
}
};
......@@ -609,6 +699,7 @@ export class TestLifecycleService implements ILifecycleService {
public _serviceBrand: any;
public willShutdown: boolean;
public quitRequested: boolean;
private _onWillShutdown = new Emitter<ShutdownEvent>();
private _onShutdown = new Emitter<void>();
......
......@@ -919,7 +919,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
return this.doOpenEditors(editors, activePosition, ratio);
}
public restoreEditors(): TPromise<BaseEditor[]> {
public restoreEditors(untitledToRestoreInputs?: EditorInput[]): TPromise<BaseEditor[]> {
const editors = this.stacks.groups.map((group, index) => {
return {
input: group.activeEditor,
......@@ -928,6 +928,17 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
};
});
// Add any untitled files to be restored from backup
if (untitledToRestoreInputs && untitledToRestoreInputs.length) {
editors.push(...untitledToRestoreInputs.map(input => {
return {
input: input,
position: Position.ONE,
options: null
};
}));
}
if (!editors.length) {
return TPromise.as<BaseEditor[]>([]);
}
......
......@@ -28,10 +28,12 @@ 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;
private _onDidModelChangeContent: Emitter<void>;
private _onDidModelChangeEncoding: Emitter<void>;
private toUnbind: IDisposable[];
......@@ -40,20 +42,26 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput {
resource: URI,
hasAssociatedFilePath: boolean,
modeId: string,
restoreResource: URI,
@IInstantiationService private instantiationService: IInstantiationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IModeService private modeService: IModeService,
@ITextFileService private textFileService: ITextFileService
) {
super();
this.resource = resource;
this.restoreResource = restoreResource;
this.hasAssociatedFilePath = hasAssociatedFilePath;
this.modeId = modeId;
this.toUnbind = [];
this._onDidModelChangeContent = new Emitter<void>();
this._onDidModelChangeEncoding = new Emitter<void>();
}
public get onDidModelChangeContent(): Event<void> {
return this._onDidModelChangeContent.event;
}
public get onDidModelChangeEncoding(): Event<void> {
return this._onDidModelChangeEncoding.event;
}
......@@ -79,7 +87,8 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput {
return this.cachedModel.isDirty();
}
return this.hasAssociatedFilePath; // untitled files with associated path are always dirty
// untitled files with an associated path or restore resource are always dirty
return this.hasAssociatedFilePath || !!this.restoreResource;
}
public confirmSave(): ConfirmResult {
......@@ -121,6 +130,14 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput {
return null;
}
public getValue(): string {
if (this.cachedModel) {
return this.cachedModel.getValue();
}
return null;
}
public setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void {
if (this.cachedModel) {
this.cachedModel.setEncoding(encoding);
......@@ -134,20 +151,29 @@ 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
this.toUnbind.push(model.onDidChangeContent(() => this._onDidModelChangeContent.fire()));
this.toUnbind.push(model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
this.toUnbind.push(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire()));
......@@ -170,6 +196,7 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput {
}
public dispose(): void {
this._onDidModelChangeContent.dispose();
this._onDidModelChangeEncoding.dispose();
// Listeners
......
......@@ -23,6 +23,7 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
private configurationChangeListener: IDisposable;
private dirty: boolean;
private _onDidChangeContent: Emitter<void>;
private _onDidChangeDirty: Emitter<void>;
private _onDidChangeEncoding: Emitter<void>;
......@@ -43,14 +44,19 @@ 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._onDidChangeContent = new Emitter<void>();
this._onDidChangeDirty = new Emitter<void>();
this._onDidChangeEncoding = new Emitter<void>();
this.registerListeners();
}
public get onDidChangeContent(): Event<void> {
return this._onDidChangeContent.event;
}
public get onDidChangeDirty(): Event<void> {
return this._onDidChangeDirty.event;
}
......@@ -111,6 +117,10 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
return this.dirty;
}
public getResource(): URI {
return this.resource;
}
public revert(): void {
this.dirty = false;
......@@ -146,7 +156,10 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
else if (!this.dirty) {
this.dirty = true;
this._onDidChangeDirty.fire();
}
this._onDidChangeContent.fire();
}
public dispose(): void {
......@@ -162,6 +175,7 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
this.configurationChangeListener = null;
}
this._onDidChangeContent.dispose();
this._onDidChangeDirty.dispose();
this._onDidChangeEncoding.dispose();
}
......
......@@ -22,4 +22,9 @@ export interface IOptions {
* Instructs the workbench to open a diff of the provided files right after startup.
*/
filesToDiff?: IResourceInput[];
/**
* Instructs the workbench to restore the provided untitled file from backup right after startup.
*/
untitledToRestore?: IResourceInput[];
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ export interface IOpenFileRequest {
filesToOpen?: IPath[];
filesToCreate?: IPath[];
filesToDiff?: IPath[];
untitledToRestore?: IPath[];
}
export interface IWindowConfiguration {
......
......@@ -58,10 +58,12 @@ export function startup(configuration: IWindowConfiguration): TPromise<void> {
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 untitledToRestore = configuration.untitledToRestore && configuration.untitledToRestore.length ? toInputs(configuration.untitledToRestore) : null;
const shellOptions: IOptions = {
filesToOpen,
filesToCreate,
filesToDiff
filesToDiff,
untitledToRestore
};
if (configuration.performance) {
......
......@@ -18,6 +18,10 @@ import * as browser from 'vs/base/browser/browser';
import assert = require('vs/base/common/assert');
import timer = require('vs/base/common/timer');
import errors = require('vs/base/common/errors');
import { BackupService } from 'vs/workbench/services/backup/node/backupService';
import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
import { BackupModelService } from 'vs/workbench/services/backup/node/backupModelService';
import { IBackupService, IBackupFileService, IBackupModelService } from 'vs/workbench/services/backup/common/backup';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Registry } from 'vs/platform/platform';
import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform';
......@@ -187,7 +191,10 @@ 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);
this.hasFilesToCreateOpenOrDiff =
(options.filesToCreate && options.filesToCreate.length > 0) ||
(options.filesToOpen && options.filesToOpen.length > 0) ||
(options.filesToDiff && options.filesToDiff.length > 0);
this.toDispose = [];
this.toShutdown = [];
......@@ -282,7 +289,14 @@ export class Workbench implements IPartService {
editorOpenPromise = this.editorPart.openEditors(editors);
} else {
editorOpenPromise = this.editorPart.restoreEditors();
if (this.workbenchParams.options.untitledToRestore && this.workbenchParams.options.untitledToRestore.length) {
const untitledToRestoreInputs = this.workbenchParams.options.untitledToRestore.map(resourceInput => {
return this.untitledEditorService.createOrGet(null, null, resourceInput.resource);
});
editorOpenPromise = this.editorPart.restoreEditors(untitledToRestoreInputs);
} else {
editorOpenPromise = this.editorPart.restoreEditors();
}
}
return editorOpenPromise.then(() => {
......@@ -343,7 +357,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<EditorInput>(filesToOpen.map(resourceInput => this.editorService.createInput(resourceInput))).then((inputsToOpen) => {
let filesToOpenInputPromise = filesToOpen.map(resourceInput => this.editorService.createInput(resourceInput));
return TPromise.join<EditorInput>(filesToOpenInputPromise).then((inputsToOpen) => {
inputs.push(...inputsToOpen);
options.push(...filesToOpen.map(resourceInput => TextEditorOptions.from(resourceInput)));
......@@ -430,9 +446,19 @@ export class Workbench implements IPartService {
// History
serviceCollection.set(IHistoryService, this.instantiationService.createInstance(HistoryService));
// Backup File Service
const workspace = this.contextService.getWorkspace();
serviceCollection.set(IBackupFileService, this.instantiationService.createInstance(BackupFileService, workspace ? workspace.resource : null));
// Backup Service
serviceCollection.set(IBackupService, this.instantiationService.createInstance(BackupService));
// Text File Service
serviceCollection.set(ITextFileService, this.instantiationService.createInstance(TextFileService));
// Backup Model Service
serviceCollection.set(IBackupModelService, this.instantiationService.createInstance(BackupModelService));
// Text Model Resolver Service
serviceCollection.set(ITextModelResolverService, this.instantiationService.createInstance(TextModelResolverService));
......@@ -441,7 +467,6 @@ export class Workbench implements IPartService {
serviceCollection.set(IConfigurationEditingService, this.configurationEditingService);
// Configuration Resolver
const workspace = this.contextService.getWorkspace();
const configurationResolverService = this.instantiationService.createInstance(ConfigurationResolverService, workspace ? workspace.resource : null, process.env);
serviceCollection.set(IConfigurationResolverService, configurationResolverService);
......
......@@ -218,6 +218,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,
......
/*---------------------------------------------------------------------------------------------
* 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';
import { ITextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textfiles';
export const IBackupService = createDecorator<IBackupService>('backupService');
export const IBackupFileService = createDecorator<IBackupFileService>('backupFileService');
export const IBackupModelService = createDecorator<IBackupModelService>('backupModelService');
/**
* A service that handles the lifecycle of backups, eg. listening for file changes and acting
* appropriately on shutdown.
*/
export interface IBackupService {
_serviceBrand: any;
isHotExitEnabled: boolean;
backupBeforeShutdown(dirtyToBackup: Uri[], textFileEditorModelManager: ITextFileEditorModelManager, quitRequested: boolean, confirmCallback: () => boolean | TPromise<boolean>): boolean | TPromise<boolean>;
cleanupBackupsBeforeShutdown(): boolean | TPromise<boolean>;
doBackup(resource: Uri, content: string, immediate?: boolean): TPromise<void>;
}
/**
* A service that handles any I/O and state associated with the backup system.
*/
export interface IBackupFileService {
_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 whether a text file has a backup to restore.
*
* @param resource The resource to check.
* @returns Whether the file has a backup.
*/
hasTextFileBackup(resource: Uri): TPromise<boolean>;
/**
* 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;
/**
* Backs up a resource.
*
* @param resource The resource to back up.
* @param content THe content of the resource.
*/
backupResource(resource: Uri, content: string): TPromise<void>;
/**
* Discards the backup associated with a resource if it exists..
*
* @param resource The resource whose backup is being discarded discard to back up.
*/
discardResourceBackup(resource: Uri): TPromise<void>;
/**
* Discards all backups associated with the current workspace.
*/
discardAllWorkspaceBackups(): TPromise<void>;
}
/**
* A service that handles the shutdown backup/hot exit process. This exists separately to
* IBackupService purely because BackupService has a hard dependency on ITextFileService which
* performs backup logic that must perform backup logic during shutdown.
*/
export interface IBackupModelService {
_serviceBrand: any;
}
/*---------------------------------------------------------------------------------------------
* 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 pfs = require('vs/base/node/pfs');
import Uri from 'vs/base/common/uri';
import { IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
export class BackupFileService implements IBackupFileService {
public _serviceBrand: any;
protected backupHome: string;
protected workspacesJsonPath: string;
constructor(
private currentWorkspace: Uri,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService private fileService: IFileService
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
}
public getWorkspaceBackupPaths(): TPromise<string[]> {
return this.loadWorkspaces().then(workspacesJsonContent => {
return workspacesJsonContent.folderWorkspaces;
});
}
public hasTextFileBackup(resource: Uri): TPromise<boolean> {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return TPromise.as(false);
}
return pfs.exists(this.getBackupResource(resource).fsPath);
}
public getBackupResource(resource: Uri): Uri {
// Hot exit is disabled for empty workspaces
if (!this.currentWorkspace) {
return null;
}
// Only hash the file path if the file is not untitled
const backupName = resource.scheme === 'untitled' ? resource.fsPath : crypto.createHash('md5').update(resource.fsPath).digest('hex');
const backupPath = path.join(this.getWorkspaceBackupDirectory(), resource.scheme, backupName);
return Uri.file(backupPath);
}
private getWorkspaceBackupDirectory(): string {
const workspaceHash = crypto.createHash('md5').update(this.currentWorkspace.fsPath).digest('hex');
return path.join(this.backupHome, workspaceHash);
}
public backupResource(resource: Uri, content: string): TPromise<void> {
const backupResource = this.getBackupResource(resource);
// Hot exit is disabled for empty workspaces
if (!backupResource) {
return TPromise.as(void 0);
}
return this.fileService.updateContent(backupResource, content).then(() => void 0);
}
public discardResourceBackup(resource: Uri): TPromise<void> {
const backupResource = this.getBackupResource(resource);
// Hot exit is disabled for empty workspaces
if (!backupResource) {
return TPromise.as(void 0);
}
return this.fileService.del(backupResource);
}
public discardAllWorkspaceBackups(): TPromise<void> {
return this.fileService.del(Uri.file(this.getWorkspaceBackupDirectory()));
}
private loadWorkspaces(): TPromise<IBackupWorkspacesFormat> {
return pfs.fileExists(this.workspacesJsonPath).then(exists => {
if (!exists) {
return { folderWorkspaces: [] };
}
return pfs.readFile(this.workspacesJsonPath, 'utf8').then(content => {
try {
return JSON.parse(content.toString());
} catch (ex) {
return [];
}
}).then(content => {
let result = content;
if (!result.folderWorkspaces) {
result.folderWorkspaces = [];
}
return result;
});
});
}
}
\ 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 Uri from 'vs/base/common/uri';
import { IBackupService, IBackupModelService, IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ITextFileService, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
export class BackupModelService implements IBackupModelService {
public _serviceBrand: any;
private toDispose: IDisposable[];
constructor(
@IBackupFileService private backupFileService: IBackupFileService,
@IBackupService private backupService: IBackupService,
@IFileService private fileService: IFileService,
@ITextFileService private textFileService: ITextFileService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
) {
this.toDispose = [];
this.registerListeners();
}
private registerListeners() {
// Listen for text file model changes
this.toDispose.push(this.textFileService.models.onModelContentChanged((e) => this.onTextFileModelChanged(e)));
this.toDispose.push(this.textFileService.models.onModelSaved((e) => this.discardBackup(e.resource)));
this.toDispose.push(this.textFileService.models.onModelReverted((e) => this.discardBackup(e.resource)));
this.toDispose.push(this.textFileService.models.onModelDisposed((e) => this.discardBackup(e)));
// Listen for untitled model changes
this.toDispose.push(this.untitledEditorService.onDidChangeContent((e) => this.onUntitledModelChanged(e)));
this.toDispose.push(this.untitledEditorService.onDidDisposeModel((e) => this.discardBackup(e)));
}
private onTextFileModelChanged(event: TextFileModelChangeEvent): void {
if (this.backupService.isHotExitEnabled) {
const model = this.textFileService.models.get(event.resource);
this.backupService.doBackup(model.getResource(), model.getValue());
}
}
private onUntitledModelChanged(resource: Uri): void {
if (this.backupService.isHotExitEnabled) {
const input = this.untitledEditorService.get(resource);
if (input.isDirty()) {
this.backupService.doBackup(resource, input.getValue());
} else {
this.backupFileService.discardResourceBackup(resource);
}
}
}
private discardBackup(resource: Uri): void {
this.backupFileService.discardResourceBackup(resource);
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}
\ 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 nls from 'vs/nls';
import platform = require('vs/base/common/platform');
import Uri from 'vs/base/common/uri';
import { IBackupService, IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ITextFileEditorModel, ITextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { TPromise } from 'vs/base/common/winjs.base';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
export class BackupService implements IBackupService {
public _serviceBrand: any;
protected backupHome: string;
protected backupWorkspacesPath: string;
private toDispose: IDisposable[];
private backupPromises: TPromise<void>[];
private configuredHotExit: boolean;
constructor(
@IBackupFileService private backupFileService: IBackupFileService,
@IConfigurationService private configurationService: IConfigurationService,
@IFileService private fileService: IFileService,
@IMessageService private messageService: IMessageService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
) {
this.toDispose = [];
this.backupPromises = [];
this.registerListeners();
}
private registerListeners() {
this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config)));
}
private onConfigurationChange(configuration: IFilesConfiguration): void {
// Hot exit is disabled for empty workspaces
this.configuredHotExit = this.contextService.getWorkspace() && configuration && configuration.files && configuration.files.hotExit;
}
private backupImmediately(resource: Uri, content: string): TPromise<void> {
if (!resource) {
return TPromise.as(void 0);
}
return this.doBackup(resource, content, true);
}
public doBackup(resource: Uri, content: string, immediate?: boolean): TPromise<void> {
// Cancel any currently running backups to make this the one that succeeds
this.cancelBackupPromises();
if (immediate) {
return this.backupFileService.backupResource(resource, content);
}
// Create new backup promise and keep it
const promise = TPromise.timeout(1000).then(() => {
this.backupFileService.backupResource(resource, content); // 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();
}
}
/**
* Performs an immedate backup of all dirty file and untitled models.
*/
private backupAll(dirtyToBackup: Uri[], textFileEditorModelManager: ITextFileEditorModelManager): TPromise<void> {
// split up between files and untitled
const filesToBackup: ITextFileEditorModel[] = [];
const untitledToBackup: Uri[] = [];
dirtyToBackup.forEach(s => {
if (s.scheme === 'file') {
filesToBackup.push(textFileEditorModelManager.get(s));
} else if (s.scheme === 'untitled') {
untitledToBackup.push(s);
}
});
return this.doBackupAll(filesToBackup, untitledToBackup);
}
private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: Uri[]): TPromise<void> {
// Handle file resources first
return TPromise.join(dirtyFileModels.map(model => {
return this.backupImmediately(model.getResource(), model.getValue()).then(() => void 0);
})).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 => {
return this.backupImmediately(model.getResource(), model.getValue());
});
return TPromise.join(untitledBackupPromises).then(() => void 0);
});
});
}
public get isHotExitEnabled(): boolean {
// If hot exit is enabled then save the dirty files in the workspace and then exit
// Hot exit is currently disabled for empty workspaces (#13733).
return this.configuredHotExit && !!this.contextService.getWorkspace();
}
public backupBeforeShutdown(dirtyToBackup: Uri[], textFileEditorModelManager: ITextFileEditorModelManager, quitRequested: boolean, confirmCallback: () => boolean | TPromise<boolean>): boolean | TPromise<boolean> {
// If there are no dirty files, clean up and exit
if (dirtyToBackup.length === 0) {
return this.cleanupBackupsBeforeShutdown();
}
return this.backupFileService.getWorkspaceBackupPaths().then(workspaceBackupPaths => {
// When quit is requested skip the confirm callback and attempt to backup all workspaces.
// When quit is not requested the confirm callback should be shown when the window being
// closed is the only VS Code window open, except for on Mac where hot exit is only
// ever activated when quit is requested.
if (!quitRequested && (workspaceBackupPaths.length > 1 || platform.isMacintosh)) {
return confirmCallback(); // confirm save
}
// Backup and hot exit
return this.backupAll(dirtyToBackup, textFileEditorModelManager).then(() => {
return false; // the backup went smoothly, no veto
}, errors => {
const firstError = errors[0];
this.messageService.show(Severity.Error, nls.localize('files.backup.failSave', "Files could not be backed up (Error: {0}), try saving your files to exit.", firstError.message));
return true; // veto, the backups failed
});
});
}
public cleanupBackupsBeforeShutdown(): boolean | TPromise<boolean> {
const workspace = this.contextService.getWorkspace();
if (!workspace) {
return false; // no backups to cleanup, no veto
}
return this.backupFileService.discardAllWorkspaceBackups().then(() => {
return false; // no veto
});
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
this.cancelBackupPromises();
}
}
\ 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 fs = require('fs');
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 { TestEnvironmentService } from 'vs/test/utils/servicesTestUtils';
import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
class TestBackupFileService extends BackupFileService {
constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
const fileService = new FileService(workspace.fsPath, { disableWatcher: true }, null);
super(workspace, TestEnvironmentService, fileService);
this.backupHome = backupHome;
this.workspacesJsonPath = workspacesJsonPath;
}
}
suite('BackupFileService', () => {
const parentDir = path.join(os.tmpdir(), 'vsctests', 'service');
const backupHome = path.join(parentDir, 'Backups');
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
const workspaceResource = Uri.file(platform.isWindows ? 'C:\\workspace' : '/workspace');
const workspaceBackupPath = path.join(backupHome, crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'));
const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo');
const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar');
const untitledFile = Uri.from({ scheme: 'untitled', path: 'Untitled-1' });
const fooBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(fooFile.fsPath).digest('hex'));
const barBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(barFile.fsPath).digest('hex'));
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', untitledFile.fsPath);
let service: BackupFileService;
setup(done => {
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
// Delete any existing backups completely and then re-create it.
extfs.del(backupHome, os.tmpdir(), () => {
pfs.mkdirp(backupHome).then(() => {
pfs.writeFileAndFlush(workspacesJsonPath, '').then(() => {
done();
});
});
});
});
teardown(done => {
extfs.del(backupHome, os.tmpdir(), done);
});
test('getBackupResource should get the correct backup path for text files', () => {
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
const backupResource = fooFile;
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(service.getBackupResource(backupResource).fsPath, expectedPath);
});
test('getBackupResource should get the correct backup path for untitled files', () => {
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePath>
const backupResource = Uri.from({ scheme: 'untitled', path: 'Untitled-1' });
const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex');
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', backupResource.fsPath)).fsPath;
assert.equal(service.getBackupResource(backupResource).fsPath, expectedPath);
});
test('doesTextFileHaveBackup should return whether a backup resource exists', done => {
service.hasTextFileBackup(fooFile).then(exists => {
assert.equal(exists, false);
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
service.hasTextFileBackup(fooFile).then(exists2 => {
assert.equal(exists2, true);
done();
});
});
});
});
test('backupResource - text file', function (done: () => void) {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
assert.equal(fs.existsSync(fooBackupPath), true);
assert.equal(fs.readFileSync(fooBackupPath), 'test');
done();
});
});
test('backupResource - untitled file', function (done: () => void) {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
assert.equal(fs.existsSync(untitledBackupPath), true);
assert.equal(fs.readFileSync(untitledBackupPath), 'test');
done();
});
});
test('discardResourceBackup - text file', function (done: () => void) {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
service.discardResourceBackup(fooFile).then(() => {
assert.equal(fs.existsSync(fooBackupPath), false);
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
done();
});
});
});
test('discardResourceBackup - untitled file', function (done: () => void) {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
service.discardResourceBackup(untitledFile).then(() => {
assert.equal(fs.existsSync(untitledBackupPath), false);
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
done();
});
});
});
test('discardAllWorkspaceBackups - text file', function (done: () => void) {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
service.backupResource(barFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
service.discardAllWorkspaceBackups().then(() => {
assert.equal(fs.existsSync(fooBackupPath), false);
assert.equal(fs.existsSync(barBackupPath), false);
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
done();
});
});
});
});
test('discardAllWorkspaceBackups - untitled file', function (done: () => void) {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
service.discardAllWorkspaceBackups().then(() => {
assert.equal(fs.existsSync(untitledBackupPath), false);
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
done();
});
});
});
});
\ No newline at end of file
......@@ -35,6 +35,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/workbench/services/backup/common/backup';
class SettingsTestEnvironmentService extends EnvironmentService {
......@@ -57,9 +58,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 {
......
......@@ -21,6 +21,7 @@ export class LifecycleService implements ILifecycleService {
private _onShutdown = new Emitter<void>();
private _willShutdown: boolean;
private _quitRequested: boolean;
constructor(
@IMessageService private messageService: IMessageService,
......@@ -33,6 +34,10 @@ export class LifecycleService implements ILifecycleService {
return this._willShutdown;
}
public get quitRequested(): boolean {
return this._quitRequested;
}
public get onWillShutdown(): Event<ShutdownEvent> {
return this._onWillShutdown.event;
}
......@@ -45,11 +50,13 @@ export class LifecycleService implements ILifecycleService {
const windowId = this.windowService.getWindowId();
// Main side indicates that window is about to unload, check for vetos
ipc.on('vscode:beforeUnload', (event, reply: { okChannel: string, cancelChannel: string }) => {
ipc.on('vscode:beforeUnload', (event, reply: { okChannel: string, cancelChannel: string, quitRequested: boolean }) => {
this._willShutdown = true;
this._quitRequested = reply.quitRequested;
// trigger onWillShutdown events and veto collecting
this.onBeforeUnload().done(veto => {
this.onBeforeUnload(reply.quitRequested).done(veto => {
this._quitRequested = false;
if (veto) {
this._willShutdown = false; // reset this flag since the shutdown has been vetoed!
ipc.send(reply.cancelChannel, windowId);
......@@ -61,13 +68,14 @@ export class LifecycleService implements ILifecycleService {
});
}
private onBeforeUnload(): TPromise<boolean> {
private onBeforeUnload(quitRequested: boolean): TPromise<boolean> {
const vetos: (boolean | TPromise<boolean>)[] = [];
this._onWillShutdown.fire({
veto(value) {
vetos.push(value);
}
},
quitRequested
});
if (vetos.length === 0) {
......
......@@ -24,6 +24,7 @@ import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/un
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IBackupService } from 'vs/workbench/services/backup/common/backup';
/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
......@@ -53,7 +54,8 @@ export abstract class TextFileService implements ITextFileService {
@IEditorGroupService private editorGroupService: IEditorGroupService,
@IFileService protected fileService: IFileService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IInstantiationService private instantiationService: IInstantiationService
@IInstantiationService private instantiationService: IInstantiationService,
@IBackupService private backupService: IBackupService
) {
this.toUnbind = [];
......@@ -96,7 +98,7 @@ export abstract class TextFileService implements ITextFileService {
private registerListeners(): void {
// Lifecycle
this.lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown()));
this.lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown(event.quitRequested)));
this.lifecycleService.onShutdown(this.dispose, this);
// Configuration changes
......@@ -108,11 +110,13 @@ export abstract class TextFileService implements ITextFileService {
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorFocusChanged()));
}
private beforeShutdown(): boolean | TPromise<boolean> {
private beforeShutdown(quitRequested: boolean): boolean | TPromise<boolean> {
if (this.backupService.isHotExitEnabled) {
return this.backupService.backupBeforeShutdown(this.getDirty(), this.models, quitRequested, this.confirmBeforeShutdown.bind(this));
}
// 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(() => {
......@@ -120,7 +124,7 @@ 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.backupService.cleanupBackupsBeforeShutdown(); // all good, no veto
});
}
......@@ -128,7 +132,7 @@ export abstract class TextFileService implements ITextFileService {
return this.confirmBeforeShutdown();
}
return false; // no veto
return this.backupService.cleanupBackupsBeforeShutdown(); // no veto
}
private confirmBeforeShutdown(): boolean | TPromise<boolean> {
......@@ -141,13 +145,13 @@ export abstract class TextFileService implements ITextFileService {
return true; // veto if some saves failed
}
return false; // no veto
return this.backupService.cleanupBackupsBeforeShutdown(); // no veto
});
}
// Don't Save
else if (confirm === ConfirmResult.DONT_SAVE) {
return false; // no veto
return this.backupService.cleanupBackupsBeforeShutdown(); // no veto
}
// Cancel
......
......@@ -16,12 +16,13 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import paths = require('vs/base/common/paths');
import diagnostics = require('vs/base/common/diagnostics');
import types = require('vs/base/common/types');
import { IModelContentChangedEvent } from 'vs/editor/common/editorCommon';
import { IModelContentChangedEvent, IRawText } from 'vs/editor/common/editorCommon';
import { IMode } from 'vs/editor/common/modes';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode, EditorModel } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IFileService, IFileStat, IFileOperationResult, FileOperationResult } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
......@@ -57,6 +58,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private inErrorMode: boolean;
private lastSaveAttemptTime: number;
private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
private _onDidContentChange: Emitter<void>;
private _onDidStateChange: Emitter<StateChange>;
constructor(
......@@ -69,7 +71,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
@ILifecycleService private lifecycleService: ILifecycleService,
@IInstantiationService private instantiationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService,
@ITextFileService private textFileService: ITextFileService
@ITextFileService private textFileService: ITextFileService,
@IBackupFileService private backupFileService: IBackupFileService
) {
super(modelService, modeService);
......@@ -77,7 +80,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.resource = resource;
this.toDispose = [];
this._onDidContentChange = new Emitter<void>();
this._onDidStateChange = new Emitter<StateChange>();
this.toDispose.push(this._onDidContentChange);
this.toDispose.push(this._onDidStateChange);
this.preferredEncoding = preferredEncoding;
this.dirty = false;
......@@ -110,6 +115,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.updateTextEditorModelMode();
}
public get onDidContentChange(): Event<void> {
return this._onDidContentChange.event;
}
public get onDidStateChange(): Event<StateChange> {
return this._onDidStateChange.event;
}
......@@ -257,20 +266,36 @@ 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;
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.backupFileService.hasTextFileBackup(this.resource).then(backupExists => {
let getContentPromise: TPromise<IRawText>;
if (backupExists) {
const restoreResource = this.backupFileService.getBackupResource(this.resource);
const restoreOptions = { acceptTextOnly: true, encoding: 'utf-8' };
// Try get restore content, if there is an issue fallback silently to the original file's content
getContentPromise = this.textFileService.resolveTextContent(restoreResource, restoreOptions).then(restoreContent => {
return restoreContent.value;
}, () => content.value);
} else {
getContentPromise = TPromise.as(content.value);
}
this.createTextEditorModelPromise = getContentPromise.then(fileContent => {
return this.createTextEditorModel(fileContent, content.resource).then(() => {
this.createTextEditorModelPromise = null;
this.setDirty(backupExists); // 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;
});
return this.createTextEditorModelPromise;
}
}, (error) => {
......@@ -334,6 +359,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();
}
private makeDirty(): void {
......
......@@ -24,6 +24,8 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
private toUnbind: IDisposable[];
private _onModelDisposed: Emitter<URI>;
private _onModelContentChanged: Emitter<TextFileModelChangeEvent>;
private _onModelDirty: Emitter<TextFileModelChangeEvent>;
private _onModelSaveError: Emitter<TextFileModelChangeEvent>;
private _onModelSaved: Emitter<TextFileModelChangeEvent>;
......@@ -32,6 +34,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
private mapResourceToDisposeListener: { [resource: string]: IDisposable; };
private mapResourceToStateChangeListener: { [resource: string]: IDisposable; };
private mapResourceToModelContentChangeListener: { [resource: string]: IDisposable; };
private mapResourceToModel: { [resource: string]: ITextFileEditorModel; };
private mapResourceToPendingModelLoaders: { [resource: string]: TPromise<ITextFileEditorModel> };
......@@ -43,12 +46,16 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
) {
this.toUnbind = [];
this._onModelDisposed = new Emitter<URI>();
this._onModelContentChanged = new Emitter<TextFileModelChangeEvent>();
this._onModelDirty = new Emitter<TextFileModelChangeEvent>();
this._onModelSaveError = new Emitter<TextFileModelChangeEvent>();
this._onModelSaved = new Emitter<TextFileModelChangeEvent>();
this._onModelReverted = new Emitter<TextFileModelChangeEvent>();
this._onModelEncodingChanged = new Emitter<TextFileModelChangeEvent>();
this.toUnbind.push(this._onModelDisposed);
this.toUnbind.push(this._onModelContentChanged);
this.toUnbind.push(this._onModelDirty);
this.toUnbind.push(this._onModelSaveError);
this.toUnbind.push(this._onModelSaved);
......@@ -58,6 +65,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
this.mapResourceToModel = Object.create(null);
this.mapResourceToDisposeListener = Object.create(null);
this.mapResourceToStateChangeListener = Object.create(null);
this.mapResourceToModelContentChangeListener = Object.create(null);
this.mapResourceToPendingModelLoaders = Object.create(null);
this.registerListeners();
......@@ -147,6 +155,14 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return true;
}
public get onModelDisposed(): Event<URI> {
return this._onModelDisposed.event;
}
public get onModelContentChanged(): Event<TextFileModelChangeEvent> {
return this._onModelContentChanged.event;
}
public get onModelDirty(): Event<TextFileModelChangeEvent> {
return this._onModelDirty.event;
}
......@@ -217,6 +233,12 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
break;
}
});
// Install model content change listener
this.mapResourceToModelContentChangeListener[resource.toString()] = model.onDidContentChange(() => {
const newEvent = new TextFileModelChangeEvent(model, StateChange.CONTENT_CHANGE);
this._onModelContentChanged.fire(newEvent);
});
}
// Store pending loads to avoid race conditions
......@@ -263,7 +285,10 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
// store in cache but remove when model gets disposed
this.mapResourceToModel[resource.toString()] = model;
this.mapResourceToDisposeListener[resource.toString()] = model.onDispose(() => this.remove(resource));
this.mapResourceToDisposeListener[resource.toString()] = model.onDispose(() => {
this.remove(resource);
this._onModelDisposed.fire(resource);
});
}
public remove(resource: URI): void {
......@@ -280,6 +305,12 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
dispose(stateChangeListener);
delete this.mapResourceToStateChangeListener[resource.toString()];
}
const modelContentCHangeListener = this.mapResourceToModelContentChangeListener[resource.toString()];
if (modelContentCHangeListener) {
dispose(modelContentCHangeListener);
delete this.mapResourceToModelContentChangeListener[resource.toString()];
}
}
public clear(): void {
......@@ -296,6 +327,11 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
keys = Object.keys(this.mapResourceToStateChangeListener);
dispose(keys.map(k => this.mapResourceToStateChangeListener[k]));
this.mapResourceToStateChangeListener = Object.create(null);
// dispose model content change listeners
keys = Object.keys(this.mapResourceToModelContentChangeListener);
dispose(keys.map(k => this.mapResourceToModelContentChangeListener[k]));
this.mapResourceToModelContentChangeListener = Object.create(null);
}
private disposeUnusedModels(): void {
......
......@@ -106,7 +106,8 @@ export enum StateChange {
SAVE_ERROR,
SAVED,
REVERTED,
ENCODING
ENCODING,
CONTENT_CHANGE
}
export class TextFileModelChangeEvent {
......@@ -182,6 +183,8 @@ export interface IRawTextContent extends IBaseStat {
export interface ITextFileEditorModelManager {
onModelDisposed: Event<URI>;
onModelContentChanged: Event<TextFileModelChangeEvent>;
onModelDirty: Event<TextFileModelChangeEvent>;
onModelSaveError: Event<TextFileModelChangeEvent>;
onModelSaved: Event<TextFileModelChangeEvent>;
......@@ -203,6 +206,7 @@ export interface IModelSaveOptions {
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport {
onDidContentChange: Event<void>;
onDidStateChange: Event<StateChange>;
getResource(): URI;
......
......@@ -28,6 +28,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/workbench/services/backup/common/backup';
export class TextFileService extends AbstractTextFileService {
......@@ -45,9 +46,10 @@ export class TextFileService extends AbstractTextFileService {
@IEditorGroupService editorGroupService: IEditorGroupService,
@IWindowIPCService private windowService: IWindowIPCService,
@IModelService private modelService: IModelService,
@IEnvironmentService private environmentService: IEnvironmentService
@IEnvironmentService private environmentService: IEnvironmentService,
@IBackupService backupService: IBackupService
) {
super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, fileService, untitledEditorService, instantiationService);
super(lifecycleService, contextService, configurationService, telemetryService, editorGroupService, fileService, untitledEditorService, instantiationService, backupService);
}
public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent> {
......
......@@ -28,6 +28,7 @@ class ServiceAccessor {
class ShutdownEventImpl implements ShutdownEvent {
public value: boolean | TPromise<boolean>;
public quitRequested: boolean = false;
veto(value: boolean | TPromise<boolean>): void {
this.value = value;
......
......@@ -16,6 +16,11 @@ export interface IUntitledEditorService {
_serviceBrand: any;
/**
* Events for when untitled editors content changes (e.g. any keystroke).
*/
onDidChangeContent: Event<URI>;
/**
* Events for when untitled editors change (e.g. getting dirty, saved or reverted).
*/
......@@ -26,6 +31,11 @@ export interface IUntitledEditorService {
*/
onDidChangeEncoding: Event<URI>;
/**
* Events for when untitled editors are disposed.
*/
onDidDisposeModel: Event<URI>;
/**
* Returns the untitled editor input matching the provided resource.
*/
......@@ -58,7 +68,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.
......@@ -73,12 +83,26 @@ 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<URI>;
private _onDidChangeDirty: Emitter<URI>;
private _onDidChangeEncoding: Emitter<URI>;
private _onDidDisposeModel: Emitter<URI>;
constructor( @IInstantiationService private instantiationService: IInstantiationService) {
constructor(
@IInstantiationService private instantiationService: IInstantiationService
) {
this._onDidChangeContent = new Emitter<URI>();
this._onDidChangeDirty = new Emitter<URI>();
this._onDidChangeEncoding = new Emitter<URI>();
this._onDidDisposeModel = new Emitter<URI>();
}
public get onDidDisposeModel(): Event<URI> {
return this._onDidDisposeModel.event;
}
public get onDidChangeContent(): Event<URI> {
return this._onDidChangeContent.event;
}
public get onDidChangeDirty(): Event<URI> {
......@@ -130,7 +154,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 +171,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
......@@ -161,13 +185,17 @@ export class UntitledEditorService implements IUntitledEditorService {
} while (Object.keys(UntitledEditorService.CACHE).indexOf(resource.toString()) >= 0);
}
const input = this.instantiationService.createInstance(UntitledEditorInput, resource, hasAssociatedFilePath, modeId);
const input = this.instantiationService.createInstance(UntitledEditorInput, resource, hasAssociatedFilePath, modeId, restoreResource);
if (input.isDirty()) {
setTimeout(() => {
this._onDidChangeDirty.fire(resource);
}, 0 /* prevent race condition between creating input and emitting dirty event */);
}
const contentListener = input.onDidModelChangeContent(() => {
this._onDidChangeContent.fire(resource);
});
const dirtyListener = input.onDidChangeDirty(() => {
this._onDidChangeDirty.fire(resource);
});
......@@ -176,13 +204,19 @@ export class UntitledEditorService implements IUntitledEditorService {
this._onDidChangeEncoding.fire(resource);
});
const disposeListener = input.onDispose(() => {
this._onDidDisposeModel.fire(resource);
});
// Remove from cache on dispose
const onceDispose = once(input.onDispose);
onceDispose(() => {
delete UntitledEditorService.CACHE[input.getResource().toString()];
delete UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[input.getResource().toString()];
contentListener.dispose();
dirtyListener.dispose();
encodingListener.dispose();
disposeListener.dispose();
});
// Add to cache
......@@ -204,7 +238,9 @@ export class UntitledEditorService implements IUntitledEditorService {
}
public dispose(): void {
this._onDidChangeContent.dispose();
this._onDidChangeDirty.dispose();
this._onDidChangeEncoding.dispose();
this._onDidDisposeModel.dispose();
}
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册