saveErrorHandler.ts 11.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';

J
Johannes Rieken 已提交
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');
J
Johannes Rieken 已提交
10
import { toErrorMessage } from 'vs/base/common/errorMessage';
E
Erich Gamma 已提交
11
import paths = require('vs/base/common/paths');
J
Johannes Rieken 已提交
12
import { Action } from 'vs/base/common/actions';
13
import URI from 'vs/base/common/uri';
J
Johannes Rieken 已提交
14 15 16
import { EditorInputAction } from 'vs/workbench/browser/parts/editor/baseEditor';
import { SaveFileAsAction, RevertFileAction, SaveFileAction } from 'vs/workbench/parts/files/browser/fileActions';
import { IFileOperationResult, FileOperationResult } from 'vs/platform/files/common/files';
17
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
J
Johannes Rieken 已提交
18 19 20 21 22 23 24 25 26
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, IMessageWithAction, Severity, CancelAction } from 'vs/platform/message/common/message';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
27 28
import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
import { IModel } from 'vs/editor/common/editorCommon';
29
import { toResource } from 'vs/workbench/common/editor';
30 31

export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution';
E
Erich Gamma 已提交
32 33

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

	constructor(
		@IMessageService private messageService: IMessageService,
40
		@ITextFileService private textFileService: ITextFileService,
41 42 43
		@ITextModelResolverService private textModelResolverService: ITextModelResolverService,
		@IModelService private modelService: IModelService,
		@IModeService private modeService: IModeService,
E
Erich Gamma 已提交
44 45
		@IInstantiationService private instantiationService: IInstantiationService
	) {
46
		this.messages = Object.create(null);
B
Benjamin Pasero 已提交
47
		this.toUnbind = [];
48

49 50 51
		// Register as text model content provider that supports to load a resource as it actually
		// is stored on disk as opposed to using the file:// scheme that will return a dirty buffer
		// if there is one.
52
		this.textModelResolverService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, this);
B
Benjamin Pasero 已提交
53 54 55

		// Hook into model
		TextFileEditorModel.setSaveErrorHandler(this);
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

		this.registerListeners();
	}

	public provideTextContent(resource: URI): TPromise<IModel> {

		// Make sure our file from disk is resolved up to date
		return this.textFileService.resolveTextContent(URI.file(resource.fsPath)).then(content => {
			let codeEditorModel = this.modelService.getModel(resource);
			if (!codeEditorModel) {
				codeEditorModel = this.modelService.createModel(content.value, this.modeService.getOrCreateModeByFilenameOrFirstLine(resource.fsPath), resource);
			} else {
				codeEditorModel.setValueFromRawText(content.value);
			}

			return codeEditorModel;
		});
B
Benjamin Pasero 已提交
73 74 75 76
	}

	public getId(): string {
		return 'vs.files.saveerrorhandler';
77 78 79
	}

	private registerListeners(): void {
B
Benjamin Pasero 已提交
80 81
		this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource)));
		this.toUnbind.push(this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource)));
82 83 84 85 86 87 88 89
	}

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

92
	public onSaveError(error: any, model: ITextFileEditorModel): void {
E
Erich Gamma 已提交
93
		let message: IMessageWithAction;
94
		const resource = model.getResource();
E
Erich Gamma 已提交
95 96 97 98 99 100 101 102

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

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

106 107 108 109
			// 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);
110
				saveAsAction.run().done(() => saveAsAction.dispose(), errors.onUnexpectedError);
111

112
				return TPromise.as(true);
113 114 115 116 117 118
			}));

			// 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);
119
				revertFileAction.run().done(() => revertFileAction.dispose(), errors.onUnexpectedError);
120

121
				return TPromise.as(true);
122
			}));
E
Erich Gamma 已提交
123 124 125 126 127

			// Retry
			if (isReadonly) {
				actions.push(new Action('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"), null, true, () => {
					if (!model.isDisposed()) {
B
Benjamin Pasero 已提交
128
						model.save({ overwriteReadonly: true }).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
129 130
					}

A
Alex Dima 已提交
131
					return TPromise.as(true);
E
Erich Gamma 已提交
132 133 134
				}));
			} else {
				actions.push(new Action('workbench.files.action.retry', nls.localize('retry', "Retry"), null, true, () => {
135
					const saveFileAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL);
136
					saveFileAction.setResource(resource);
137
					saveFileAction.run().done(() => saveFileAction.dispose(), errors.onUnexpectedError);
E
Erich Gamma 已提交
138

139
					return TPromise.as(true);
E
Erich Gamma 已提交
140 141 142
				}));
			}

143 144
			// Cancel
			actions.push(CancelAction);
E
Erich Gamma 已提交
145 146 147

			let errorMessage: string;
			if (isReadonly) {
148
				errorMessage = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to remove protection.", paths.basename(resource.fsPath));
E
Erich Gamma 已提交
149
			} else {
150
				errorMessage = nls.localize('genericSaveError', "Failed to save '{0}': {1}", paths.basename(resource.fsPath), toErrorMessage(error, false));
E
Erich Gamma 已提交
151 152 153 154
			}

			message = {
				message: errorMessage,
155
				actions
E
Erich Gamma 已提交
156 157 158
			};
		}

159 160
		// 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 已提交
161
	}
B
Benjamin Pasero 已提交
162 163 164 165

	public dispose(): void {
		this.toUnbind = dispose(this.toUnbind);
	}
E
Erich Gamma 已提交
166 167
}

168 169 170 171 172 173 174 175
const pendingResolveSaveConflictMessages: Function[] = [];
function clearPendingResolveSaveConflictMessages(): void {
	while (pendingResolveSaveConflictMessages.length > 0) {
		pendingResolveSaveConflictMessages.pop()();
	}
}

// A message with action to resolve a save conflict
E
Erich Gamma 已提交
176 177 178 179
class ResolveSaveConflictMessage implements IMessageWithAction {
	public message: string;
	public actions: Action[];

180
	private model: ITextFileEditorModel;
E
Erich Gamma 已提交
181 182

	constructor(
183
		model: ITextFileEditorModel,
E
Erich Gamma 已提交
184 185 186
		message: string,
		@IMessageService private messageService: IMessageService,
		@IInstantiationService private instantiationService: IInstantiationService,
187 188
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IEnvironmentService private environmentService: IEnvironmentService
E
Erich Gamma 已提交
189 190 191
	) {
		this.model = model;

192
		const resource = model.getResource();
E
Erich Gamma 已提交
193 194 195
		if (message) {
			this.message = message;
		} else {
196
			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 已提交
197 198 199 200 201
		}

		this.actions = [
			new Action('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"), null, true, () => {
				if (!this.model.isDisposed()) {
202 203
					const name = paths.basename(resource.fsPath);
					const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);
E
Erich Gamma 已提交
204

205
					return this.editorService.openEditor({ leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }), rightResource: resource, label: editorLabel }).then(() => {
E
Erich Gamma 已提交
206 207 208 209 210

						// 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
211
						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 已提交
212 213 214
					});
				}

A
Alex Dima 已提交
215
				return TPromise.as(true);
E
Erich Gamma 已提交
216 217 218 219 220 221
			})
		];
	}
}

// Accept changes to resolve a conflicting edit
222
export class AcceptLocalChangesAction extends EditorInputAction {
E
Erich Gamma 已提交
223 224

	constructor(
225 226
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@ITextModelResolverService private resolverService: ITextModelResolverService
E
Erich Gamma 已提交
227
	) {
228
		super('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use local changes and overwrite disk contents"), 'conflict-editor-action accept-changes');
E
Erich Gamma 已提交
229 230
	}

231
	public run(): TPromise<void> {
232
		return this.resolverService.createModelReference(toResource(this.input, { supportSideBySide: true })).then(reference => {
233 234
			const model = reference.object as ITextFileEditorModel;
			const localModelValue = model.getValue();
E
Erich Gamma 已提交
235

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

238 239
			// revert to be able to save
			return model.revert().then(() => {
E
Erich Gamma 已提交
240

241 242
				// Restore user value
				model.textEditorModel.setValue(localModelValue);
E
Erich Gamma 已提交
243

244 245
				// Trigger save
				return model.save().then(() => {
E
Erich Gamma 已提交
246

247 248
					// Reopen file input
					return this.editorService.openEditor({ resource: model.getResource() }, this.position).then(() => {
E
Erich Gamma 已提交
249

250 251 252 253
						// Clean up
						this.input.dispose();
						reference.dispose();
					});
254
				});
E
Erich Gamma 已提交
255 256 257 258 259 260
			});
		});
	}
}

// Revert changes to resolve a conflicting edit
261
export class RevertLocalChangesAction extends EditorInputAction {
E
Erich Gamma 已提交
262 263

	constructor(
264 265
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@ITextModelResolverService private resolverService: ITextModelResolverService
E
Erich Gamma 已提交
266
	) {
267
		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 已提交
268 269
	}

270
	public run(): TPromise<void> {
271
		return this.resolverService.createModelReference(toResource(this.input, { supportSideBySide: true })).then(reference => {
272
			const model = reference.object as ITextFileEditorModel;
E
Erich Gamma 已提交
273

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

276 277
			// Revert on model
			return model.revert().then(() => {
E
Erich Gamma 已提交
278

279 280
				// Reopen file input
				return this.editorService.openEditor({ resource: model.getResource() }, this.position).then(() => {
E
Erich Gamma 已提交
281

282 283 284 285
					// Clean up
					this.input.dispose();
					reference.dispose();
				});
E
Erich Gamma 已提交
286 287 288 289
			});
		});
	}
}