saveErrorHandler.ts 14.7 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import {TPromise} from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
8 9
import nls = require('vs/nls');
import errors = require('vs/base/common/errors');
10
import {toErrorMessage} from 'vs/base/common/errorMessage';
E
Erich Gamma 已提交
11 12
import paths = require('vs/base/common/paths');
import {Action} from 'vs/base/common/actions';
13
import URI from 'vs/base/common/uri';
14
import product from 'vs/platform/product';
B
Benjamin Pasero 已提交
15
import {EditorModel} from 'vs/workbench/common/editor';
16
import {guessMimeTypes} from 'vs/base/common/mime';
17
import {EditorInputAction} from 'vs/workbench/browser/parts/editor/baseEditor';
18 19 20
import {ResourceEditorInput} from 'vs/workbench/common/editor/resourceEditorInput';
import {DiffEditorInput} from 'vs/workbench/common/editor/diffEditorInput';
import {DiffEditorModel} from 'vs/workbench/common/editor/diffEditorModel';
21
import {FileEditorInput} from 'vs/workbench/parts/files/common/editors/fileEditorInput';
E
Erich Gamma 已提交
22
import {SaveFileAsAction, RevertFileAction, SaveFileAction} from 'vs/workbench/parts/files/browser/fileActions';
23
import {IFileService, IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files';
E
Erich Gamma 已提交
24 25
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IEventService} from 'vs/platform/event/common/event';
26
import {ITextFileService, ISaveErrorHandler, ITextFileEditorModel} from 'vs/workbench/parts/files/common/files';
E
Erich Gamma 已提交
27 28
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IMessageService, IMessageWithAction, Severity, CancelAction} from 'vs/platform/message/common/message';
29 30
import {IModeService} from 'vs/editor/common/services/modeService';
import {IModelService} from 'vs/editor/common/services/modelService';
E
Erich Gamma 已提交
31 32 33

// A handler for save error happening with conflict resolution actions
export class SaveErrorHandler implements ISaveErrorHandler {
34
	private messages: { [resource: string]: () => void };
E
Erich Gamma 已提交
35 36 37

	constructor(
		@IMessageService private messageService: IMessageService,
38
		@IEventService private eventService: IEventService,
39
		@ITextFileService private textFileService: ITextFileService,
E
Erich Gamma 已提交
40 41
		@IInstantiationService private instantiationService: IInstantiationService
	) {
42 43 44 45 46 47
		this.messages = Object.create(null);

		this.registerListeners();
	}

	private registerListeners(): void {
48 49
		this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource));
		this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource));
50 51 52 53 54 55 56 57
	}

	private onFileSavedOrReverted(resource: URI): void {
		const hideMessage = this.messages[resource.toString()];
		if (hideMessage) {
			hideMessage();
			this.messages[resource.toString()] = void 0;
		}
E
Erich Gamma 已提交
58 59
	}

60
	public onSaveError(error: any, model: ITextFileEditorModel): void {
E
Erich Gamma 已提交
61
		let message: IMessageWithAction;
62
		const resource = model.getResource();
E
Erich Gamma 已提交
63 64 65 66 67 68 69 70

		// Dirty write prevention
		if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
			message = this.instantiationService.createInstance(ResolveSaveConflictMessage, model, null);
		}

		// Any other save error
		else {
71 72
			const isReadonly = (<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_READ_ONLY;
			const actions: Action[] = [];
E
Erich Gamma 已提交
73

74 75 76 77
			// Save As
			actions.push(new Action('workbench.files.action.saveAs', SaveFileAsAction.LABEL, null, true, () => {
				const saveAsAction = this.instantiationService.createInstance(SaveFileAsAction, SaveFileAsAction.ID, SaveFileAsAction.LABEL);
				saveAsAction.setResource(resource);
78
				saveAsAction.run().done(() => saveAsAction.dispose(), errors.onUnexpectedError);
79

80
				return TPromise.as(true);
81 82 83 84 85 86
			}));

			// Discard
			actions.push(new Action('workbench.files.action.discard', nls.localize('discard', "Discard"), null, true, () => {
				const revertFileAction = this.instantiationService.createInstance(RevertFileAction, RevertFileAction.ID, RevertFileAction.LABEL);
				revertFileAction.setResource(resource);
87
				revertFileAction.run().done(() => revertFileAction.dispose(), errors.onUnexpectedError);
88

89
				return TPromise.as(true);
90
			}));
E
Erich Gamma 已提交
91 92 93 94 95

			// Retry
			if (isReadonly) {
				actions.push(new Action('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"), null, true, () => {
					if (!model.isDisposed()) {
96
						model.save(true /* overwrite readonly */).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
97 98
					}

A
Alex Dima 已提交
99
					return TPromise.as(true);
E
Erich Gamma 已提交
100 101 102
				}));
			} else {
				actions.push(new Action('workbench.files.action.retry', nls.localize('retry', "Retry"), null, true, () => {
103
					const saveFileAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL);
104
					saveFileAction.setResource(resource);
105
					saveFileAction.run().done(() => saveFileAction.dispose(), errors.onUnexpectedError);
E
Erich Gamma 已提交
106

107
					return TPromise.as(true);
E
Erich Gamma 已提交
108 109 110
				}));
			}

111 112
			// Cancel
			actions.push(CancelAction);
E
Erich Gamma 已提交
113 114 115

			let errorMessage: string;
			if (isReadonly) {
116
				errorMessage = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to remove protection.", paths.basename(resource.fsPath));
E
Erich Gamma 已提交
117
			} else {
118
				errorMessage = nls.localize('genericSaveError', "Failed to save '{0}': {1}", paths.basename(resource.fsPath), toErrorMessage(error, false));
E
Erich Gamma 已提交
119 120 121 122
			}

			message = {
				message: errorMessage,
123
				actions
E
Erich Gamma 已提交
124 125 126
			};
		}

127 128
		// Show message and keep function to hide in case the file gets saved/reverted
		this.messages[model.getResource().toString()] = this.messageService.show(Severity.Error, message);
E
Erich Gamma 已提交
129 130 131 132 133 134 135 136
	}
}

// Save conflict resolution editor input
export class ConflictResolutionDiffEditorInput extends DiffEditorInput {

	public static ID = 'workbench.editors.files.conflictResolutionDiffEditorInput';

137
	private model: ITextFileEditorModel;
E
Erich Gamma 已提交
138 139

	constructor(
140
		model: ITextFileEditorModel,
E
Erich Gamma 已提交
141 142
		name: string,
		description: string,
143
		originalInput: FileOnDiskEditorInput,
144
		modifiedInput: FileEditorInput
E
Erich Gamma 已提交
145 146 147 148 149 150
	) {
		super(name, description, originalInput, modifiedInput);

		this.model = model;
	}

151
	public getModel(): ITextFileEditorModel {
E
Erich Gamma 已提交
152 153 154
		return this.model;
	}

155
	public getTypeId(): string {
E
Erich Gamma 已提交
156 157 158 159
		return ConflictResolutionDiffEditorInput.ID;
	}
}

160
export class FileOnDiskEditorInput extends ResourceEditorInput {
161 162 163
	private fileResource: URI;
	private lastModified: number;
	private mime: string;
164
	private createdEditorModel: boolean;
165 166 167 168 169 170 171

	constructor(
		fileResource: URI,
		mime: string,
		name: string,
		description: string,
		@IModelService modelService: IModelService,
J
ops  
Johannes Rieken 已提交
172
		@IModeService private modeService: IModeService,
173
		@IInstantiationService instantiationService: IInstantiationService,
A
Alex Dima 已提交
174 175
		@IFileService private fileService: IFileService,
		@ITextFileService private textFileService: ITextFileService
176 177 178 179
	) {
		// We create a new resource URI here that is different from the file resource because we represent the state of
		// the file as it is on disk and not as it is (potentially cached) in Code. That allows us to have a different
		// model for the left-hand comparision compared to the conflicting one in Code to the right.
180
		super(name, description, URI.from({ scheme: 'disk', path: fileResource.fsPath }), modelService, instantiationService);
181 182 183 184 185 186 187 188 189 190 191 192

		this.fileResource = fileResource;
		this.mime = mime;
	}

	public getLastModified(): number {
		return this.lastModified;
	}

	public resolve(refresh?: boolean): TPromise<EditorModel> {

		// Make sure our file from disk is resolved up to date
A
Alex Dima 已提交
193
		return this.textFileService.resolveTextContent(this.fileResource).then(content => {
194 195
			this.lastModified = content.mtime;

196
			const codeEditorModel = this.modelService.getModel(this.resource);
197
			if (!codeEditorModel) {
J
Johannes Rieken 已提交
198
				this.modelService.createModel(content.value, this.modeService.getOrCreateMode(this.mime), this.resource);
199
				this.createdEditorModel = true;
200
			} else {
A
Alex Dima 已提交
201
				codeEditorModel.setValueFromRawText(content.value);
202 203 204 205 206
			}

			return super.resolve(refresh);
		});
	}
207 208 209 210 211 212 213 214 215

	public dispose(): void {
		if (this.createdEditorModel) {
			this.modelService.destroyModel(this.resource);
			this.createdEditorModel = false;
		}

		super.dispose();
	}
216 217
}

218 219 220 221 222 223 224 225
const pendingResolveSaveConflictMessages: Function[] = [];
function clearPendingResolveSaveConflictMessages(): void {
	while (pendingResolveSaveConflictMessages.length > 0) {
		pendingResolveSaveConflictMessages.pop()();
	}
}

// A message with action to resolve a save conflict
E
Erich Gamma 已提交
226 227 228 229
class ResolveSaveConflictMessage implements IMessageWithAction {
	public message: string;
	public actions: Action[];

230
	private model: ITextFileEditorModel;
E
Erich Gamma 已提交
231 232

	constructor(
233
		model: ITextFileEditorModel,
E
Erich Gamma 已提交
234 235 236 237 238 239 240
		message: string,
		@IMessageService private messageService: IMessageService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService
	) {
		this.model = model;

241
		const resource = model.getResource();
E
Erich Gamma 已提交
242 243 244
		if (message) {
			this.message = message;
		} else {
245
			this.message = nls.localize('staleSaveError', "Failed to save '{0}': The content on disk is newer. Click on **Compare** to compare your version with the one on disk.", paths.basename(resource.fsPath));
E
Erich Gamma 已提交
246 247 248 249 250
		}

		this.actions = [
			new Action('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"), null, true, () => {
				if (!this.model.isDisposed()) {
251 252 253
					const mime = guessMimeTypes(resource.fsPath).join(', ');
					const originalInput = this.instantiationService.createInstance(FileOnDiskEditorInput, resource, mime, paths.basename(resource.fsPath), resource.fsPath);
					const modifiedInput = this.instantiationService.createInstance(FileEditorInput, resource, mime, void 0);
254
					const conflictInput = this.instantiationService.createInstance(ConflictResolutionDiffEditorInput, this.model, nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2})", modifiedInput.getName(), modifiedInput.getName(), product.nameLong), nls.localize('resolveSaveConflict', "Resolve save conflict"), originalInput, modifiedInput);
E
Erich Gamma 已提交
255

256
					return this.editorService.openEditor(conflictInput).then(() => {
E
Erich Gamma 已提交
257 258 259 260 261

						// We have to bring the model into conflict resolution mode to prevent subsequent save erros when the user makes edits
						this.model.setConflictResolutionMode();

						// Inform user
262
						pendingResolveSaveConflictMessages.push(this.messageService.show(Severity.Info, nls.localize('userGuide', "Use the actions in the editor tool bar to either **undo** your changes or **overwrite** the content on disk with your changes")));
E
Erich Gamma 已提交
263 264 265
					});
				}

A
Alex Dima 已提交
266
				return TPromise.as(true);
E
Erich Gamma 已提交
267 268 269 270 271 272
			})
		];
	}
}

// Accept changes to resolve a conflicting edit
273
export class AcceptLocalChangesAction extends EditorInputAction {
E
Erich Gamma 已提交
274 275 276 277 278 279 280
	private messagesToHide: { (): void; }[];

	constructor(
		@IMessageService private messageService: IMessageService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService
	) {
281
		super('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use local changes and overwrite disk contents"), 'conflict-editor-action accept-changes');
E
Erich Gamma 已提交
282 283 284 285

		this.messagesToHide = [];
	}

286
	public run(): TPromise<void> {
287
		const conflictInput = <ConflictResolutionDiffEditorInput>this.input;
288 289
		const model = conflictInput.getModel();
		const localModelValue = model.getValue();
E
Erich Gamma 已提交
290

291 292
		clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions

E
Erich Gamma 已提交
293 294
		// 1.) Get the diff editor model from cache (resolve(false)) to have access to the mtime of the file we currently show to the left
		return conflictInput.resolve(false).then((diffModel: DiffEditorModel) => {
295
			const knownLastModified = (<FileOnDiskEditorInput>conflictInput.originalInput).getLastModified();
E
Erich Gamma 已提交
296 297 298

			// 2.) Revert the model to get the latest copy from disk and to have access to the mtime of the file now
			return model.revert().then(() => {
299
				const diskLastModified = model.getLastModifiedTime();
E
Erich Gamma 已提交
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

				// 3. a) If we know that the file on the left hand side was not modified meanwhile, restore the user value and trigger a save
				if (diskLastModified <= knownLastModified) {

					// Restore user value
					model.textEditorModel.setValue(localModelValue);

					// Trigger save
					return model.save().then(() => {

						// Hide any previously shown messages
						while (this.messagesToHide.length) {
							this.messagesToHide.pop()();
						}

						// Reopen file input
316
						const input = this.instantiationService.createInstance(FileEditorInput, model.getResource(), guessMimeTypes(model.getResource().fsPath).join(', '), void 0);
E
Erich Gamma 已提交
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
						return this.editorService.openEditor(input, null, this.position).then(() => {

							// Dispose conflict input
							conflictInput.dispose();
						});
					});
				}

				// 3. b) The file was changed on disk while it was shown in the conflict editor
				else {

					// Again, we have to bring the model into conflict resolution because revert() would have cleared it
					model.setConflictResolutionMode();

					// Restore user value
					model.textEditorModel.setValue(localModelValue);

					// Reload the left hand side of the diff editor to show the up to date version and inform the user that he has to redo the action
335
					return conflictInput.originalInput.resolve(true).then(() => {
E
Erich Gamma 已提交
336 337 338 339 340 341 342 343 344
						this.messagesToHide.push(this.messageService.show(Severity.Info, nls.localize('conflictingFileHasChanged', "The content of the file on disk has changed and the left hand side of the compare editor was refreshed. Please review and resolve again.")));
					});
				}
			});
		});
	}
}

// Revert changes to resolve a conflicting edit
345
export class RevertLocalChangesAction extends EditorInputAction {
E
Erich Gamma 已提交
346 347 348 349 350

	constructor(
		@IInstantiationService private instantiationService: IInstantiationService,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService
	) {
351
		super('workbench.action.files.revert', nls.localize('revertLocalChanges', "Discard local changes and revert to content on disk"), 'conflict-editor-action revert-changes');
E
Erich Gamma 已提交
352 353
	}

354
	public run(): TPromise<void> {
355
		const conflictInput = <ConflictResolutionDiffEditorInput>this.input;
356
		const model = conflictInput.getModel();
E
Erich Gamma 已提交
357

358 359
		clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions

E
Erich Gamma 已提交
360 361 362 363
		// Revert on model
		return model.revert().then(() => {

			// Reopen file input
364
			const input = this.instantiationService.createInstance(FileEditorInput, model.getResource(), guessMimeTypes(model.getResource().fsPath).join(', '), void 0);
E
Erich Gamma 已提交
365 366 367 368 369 370 371 372
			return this.editorService.openEditor(input, null, this.position).then(() => {

				// Dispose conflict input
				conflictInput.dispose();
			});
		});
	}
}