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

6
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
7
import { toErrorMessage } from 'vs/base/common/errorMessage';
B
Benjamin Pasero 已提交
8
import { basename } from 'vs/base/common/resources';
J
Johannes Rieken 已提交
9
import { Action } from 'vs/base/common/actions';
10
import { URI } from 'vs/base/common/uri';
11
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
M
Matt Bierner 已提交
12
import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
13
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
B
Benjamin Pasero 已提交
14
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
15 16
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
17
import { ITextModelService } from 'vs/editor/common/services/resolverService';
B
Benjamin Pasero 已提交
18
import { ResourceMap } from 'vs/base/common/map';
19 20 21
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
22 23
import { FileOnDiskContentProvider } from 'vs/workbench/contrib/files/common/files';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
24
import { IModelService } from 'vs/editor/common/services/modelService';
25
import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands';
B
Benjamin Pasero 已提交
26
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
27
import { INotificationService, INotificationHandle, INotificationActions, Severity } from 'vs/platform/notification/common/notification';
28
import { IOpenerService } from 'vs/platform/opener/common/opener';
B
Benjamin Pasero 已提交
29
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
30 31
import { ExecuteCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
J
Joao Moreno 已提交
32
import { Event } from 'vs/base/common/event';
33
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
34
import { isWindows } from 'vs/base/common/platform';
35

36
export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext';
37
export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution';
E
Erich Gamma 已提交
38

39 40 41
const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError';

const conflictEditorHelp = 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.");
42

E
Erich Gamma 已提交
43
// A handler for save error happening with conflict resolution actions
B
Benjamin Pasero 已提交
44
export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution {
45
	private messages: ResourceMap<INotificationHandle>;
46
	private conflictResolutionContext: IContextKey<boolean>;
47
	private activeConflictResolutionResource?: URI;
E
Erich Gamma 已提交
48 49

	constructor(
50 51
		@INotificationService private readonly notificationService: INotificationService,
		@ITextFileService private readonly textFileService: ITextFileService,
52
		@IContextKeyService contextKeyService: IContextKeyService,
53
		@IEditorService private readonly editorService: IEditorService,
54
		@ITextModelService textModelService: ITextModelService,
55 56
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IStorageService private readonly storageService: IStorageService
E
Erich Gamma 已提交
57
	) {
B
Benjamin Pasero 已提交
58 59
		super();

60
		this.messages = new ResourceMap<INotificationHandle>();
61
		this.conflictResolutionContext = new RawContextKey<boolean>(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(contextKeyService);
62

B
Benjamin Pasero 已提交
63 64
		const provider = this._register(instantiationService.createInstance(FileOnDiskContentProvider));
		this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider));
B
Benjamin Pasero 已提交
65 66 67

		// Hook into model
		TextFileEditorModel.setSaveErrorHandler(this);
68 69 70 71

		this.registerListeners();
	}

72
	private registerListeners(): void {
B
Benjamin Pasero 已提交
73 74 75
		this._register(this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource)));
		this._register(this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource)));
		this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged()));
76 77
	}

B
Benjamin Pasero 已提交
78
	private onActiveEditorChanged(): void {
79
		let isActiveEditorSaveConflictResolution = false;
80
		let activeConflictResolutionResource: URI | undefined;
81

82 83 84
		const activeInput = this.editorService.activeEditor;
		if (activeInput instanceof DiffEditorInput && activeInput.originalInput instanceof ResourceEditorInput && activeInput.modifiedInput instanceof FileEditorInput) {
			const resource = activeInput.originalInput.getResource();
85 86
			if (resource && resource.scheme === CONFLICT_RESOLUTION_SCHEME) {
				isActiveEditorSaveConflictResolution = true;
87
				activeConflictResolutionResource = activeInput.modifiedInput.getResource();
88
			}
89 90 91
		}

		this.conflictResolutionContext.set(isActiveEditorSaveConflictResolution);
92
		this.activeConflictResolutionResource = activeConflictResolutionResource;
93 94 95
	}

	private onFileSavedOrReverted(resource: URI): void {
96 97
		const messageHandle = this.messages.get(resource);
		if (messageHandle) {
98
			messageHandle.close();
B
Benjamin Pasero 已提交
99
			this.messages.delete(resource);
100
		}
E
Erich Gamma 已提交
101 102
	}

B
Benjamin Pasero 已提交
103
	onSaveError(error: any, model: ITextFileEditorModel): void {
104
		const fileOperationError = error as FileOperationError;
105
		const resource = model.getResource();
E
Erich Gamma 已提交
106

107 108 109
		let message: string;
		const actions: INotificationActions = { primary: [], secondary: [] };

E
Erich Gamma 已提交
110
		// Dirty write prevention
111
		if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
112 113 114

			// If the user tried to save from the opened conflict editor, show its message again
			if (this.activeConflictResolutionResource && this.activeConflictResolutionResource.toString() === model.getResource().toString()) {
115
				if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
116 117 118
					return; // return if this message is ignored
				}

119
				message = conflictEditorHelp;
120

121 122
				actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction));
				actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction));
123
			}
124

125 126
			// Otherwise show the message that will lead the user into the save conflict editor.
			else {
B
Benjamin Pasero 已提交
127
				message = nls.localize('staleSaveError', "Failed to save '{0}': The content on disk is newer. Please compare your version with the one on disk.", basename(resource));
128

129
				actions.primary!.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model));
130
			}
E
Erich Gamma 已提交
131 132 133 134
		}

		// Any other save error
		else {
135 136 137 138
			const isReadonly = fileOperationError.fileOperationResult === FileOperationResult.FILE_READ_ONLY;
			const triedToMakeWriteable = isReadonly && fileOperationError.options && fileOperationError.options.overwriteReadonly;
			const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;

139
			// Save Elevated
140
			if (isPermissionDenied || triedToMakeWriteable) {
141
				actions.primary!.push(this.instantiationService.createInstance(SaveElevatedAction, model, triedToMakeWriteable));
142 143 144 145
			}

			// Overwrite
			else if (isReadonly) {
146
				actions.primary!.push(this.instantiationService.createInstance(OverwriteReadonlyAction, model));
147 148
			}

149 150
			// Retry
			else {
151
				actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, SAVE_FILE_COMMAND_ID, nls.localize('retry', "Retry")));
152 153
			}

154
			// Save As
155
			actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL));
156 157

			// Discard
158
			actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, REVERT_FILE_COMMAND_ID, nls.localize('discard', "Discard")));
E
Erich Gamma 已提交
159 160

			if (isReadonly) {
161
				if (triedToMakeWriteable) {
B
Benjamin Pasero 已提交
162
					message = isWindows ? nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is write protected. Select 'Overwrite as Admin' to retry as administrator.", basename(resource)) : nls.localize('readonlySaveErrorSudo', "Failed to save '{0}': File is write protected. Select 'Overwrite as Sudo' to retry as superuser.", basename(resource));
163
				} else {
B
Benjamin Pasero 已提交
164
					message = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", basename(resource));
165 166
				}
			} else if (isPermissionDenied) {
B
Benjamin Pasero 已提交
167
				message = isWindows ? nls.localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", basename(resource)) : nls.localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", basename(resource));
E
Erich Gamma 已提交
168
			} else {
B
Benjamin Pasero 已提交
169
				message = nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(resource), toErrorMessage(error, false));
E
Erich Gamma 已提交
170 171 172
			}
		}

173
		// Show message and keep function to hide in case the file gets saved/reverted
174
		const handle = this.notificationService.notify({ severity: Severity.Error, message, actions });
175
		Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
176
		this.messages.set(model.getResource(), handle);
E
Erich Gamma 已提交
177
	}
B
Benjamin Pasero 已提交
178

B
Benjamin Pasero 已提交
179 180
	dispose(): void {
		super.dispose();
B
Benjamin Pasero 已提交
181 182

		this.messages.clear();
B
Benjamin Pasero 已提交
183
	}
E
Erich Gamma 已提交
184 185
}

186
const pendingResolveSaveConflictMessages: INotificationHandle[] = [];
187 188
function clearPendingResolveSaveConflictMessages(): void {
	while (pendingResolveSaveConflictMessages.length > 0) {
189 190 191 192
		const item = pendingResolveSaveConflictMessages.pop();
		if (item) {
			item.close();
		}
E
Erich Gamma 已提交
193 194 195
	}
}

196 197 198
class ResolveConflictLearnMoreAction extends Action {

	constructor(
199
		@IOpenerService private readonly openerService: IOpenerService
200 201 202 203
	) {
		super('workbench.files.action.resolveConflictLearnMore', nls.localize('learnMore', "Learn More"));
	}

B
Benjamin Pasero 已提交
204
	run(): Promise<any> {
205 206 207 208 209 210 211
		return this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=868264'));
	}
}

class DoNotShowResolveConflictLearnMoreAction extends Action {

	constructor(
212
		@IStorageService private readonly storageService: IStorageService
213 214 215 216
	) {
		super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', nls.localize('dontShowAgain', "Don't Show Again"));
	}

B
Benjamin Pasero 已提交
217
	run(notification: IDisposable): Promise<any> {
B
Benjamin Pasero 已提交
218
		this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true, StorageScope.GLOBAL);
219

220 221 222
		// Hide notification
		notification.dispose();

B
Benjamin Pasero 已提交
223
		return Promise.resolve();
224 225 226 227 228 229 230
	}
}

class ResolveSaveConflictAction extends Action {

	constructor(
		private model: ITextFileEditorModel,
231 232 233 234 235
		@IEditorService private readonly editorService: IEditorService,
		@INotificationService private readonly notificationService: INotificationService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IStorageService private readonly storageService: IStorageService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService
236 237 238 239
	) {
		super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"));
	}

J
Johannes Rieken 已提交
240
	run(): Promise<any> {
241 242
		if (!this.model.isDisposed()) {
			const resource = this.model.getResource();
B
Benjamin Pasero 已提交
243
			const name = basename(resource);
244 245 246 247 248 249 250 251 252 253
			const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);

			return this.editorService.openEditor(
				{
					leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }),
					rightResource: resource,
					label: editorLabel,
					options: { pinned: true }
				}
			).then(() => {
254
				if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
255 256 257 258 259
					return; // return if this message is ignored
				}

				// Show additional help how to resolve the save conflict
				const actions: INotificationActions = { primary: [], secondary: [] };
260 261
				actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction));
				actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction));
262 263

				const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions });
264
				Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
265 266 267 268
				pendingResolveSaveConflictMessages.push(handle);
			});
		}

I
isidor 已提交
269
		return Promise.resolve(true);
270 271 272 273 274 275 276 277 278
	}
}

class SaveElevatedAction extends Action {

	constructor(
		private model: ITextFileEditorModel,
		private triedToMakeWriteable: boolean
	) {
279
		super('workbench.files.action.saveElevated', triedToMakeWriteable ? isWindows ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? nls.localize('saveElevated', "Retry as Admin...") : nls.localize('saveElevatedSudo', "Retry as Sudo..."));
280 281
	}

B
Benjamin Pasero 已提交
282
	run(): Promise<any> {
283 284 285 286
		if (!this.model.isDisposed()) {
			this.model.save({
				writeElevated: true,
				overwriteReadonly: this.triedToMakeWriteable
287
			});
288 289
		}

I
isidor 已提交
290
		return Promise.resolve(true);
291 292 293 294 295 296 297 298 299 300 301
	}
}

class OverwriteReadonlyAction extends Action {

	constructor(
		private model: ITextFileEditorModel
	) {
		super('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"));
	}

B
Benjamin Pasero 已提交
302
	run(): Promise<any> {
303
		if (!this.model.isDisposed()) {
304
			this.model.save({ overwriteReadonly: true });
305 306
		}

I
isidor 已提交
307
		return Promise.resolve(true);
308 309 310
	}
}

311
export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: URI) => {
312
	const editorService = accessor.get(IEditorService);
313
	const resolverService = accessor.get(ITextModelService);
314
	const modelService = accessor.get(IModelService);
E
Erich Gamma 已提交
315

316
	const control = editorService.activeControl;
M
Matt Bierner 已提交
317 318 319
	if (!control) {
		return;
	}
320 321
	const editor = control.input;
	const group = control.group;
E
Erich Gamma 已提交
322

323
	resolverService.createModelReference(resource).then(reference => {
M
Matt Bierner 已提交
324
		const model = reference.object as IResolvedTextFileEditorModel;
B
Benjamin Pasero 已提交
325
		const localModelSnapshot = model.createSnapshot();
E
Erich Gamma 已提交
326

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

329
		// Revert to be able to save
330
		return model.revert().then(() => {
E
Erich Gamma 已提交
331

332
			// Restore user value (without loosing undo stack)
B
Benjamin Pasero 已提交
333
			modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot));
E
Erich Gamma 已提交
334

335 336
			// Trigger save
			return model.save().then(() => {
E
Erich Gamma 已提交
337

338
				// Reopen file input
B
Benjamin Pasero 已提交
339
				return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
E
Erich Gamma 已提交
340

341
					// Clean up
342
					group.closeEditor(editor);
343
					editor.dispose();
344
					reference.dispose();
345
				});
E
Erich Gamma 已提交
346 347
			});
		});
348 349
	});
};
E
Erich Gamma 已提交
350

351
export const revertLocalChangesCommand = (accessor: ServicesAccessor, resource: URI) => {
352
	const editorService = accessor.get(IEditorService);
353
	const resolverService = accessor.get(ITextModelService);
E
Erich Gamma 已提交
354

355
	const control = editorService.activeControl;
M
Matt Bierner 已提交
356 357 358
	if (!control) {
		return;
	}
359 360
	const editor = control.input;
	const group = control.group;
E
Erich Gamma 已提交
361

362 363
	resolverService.createModelReference(resource).then(reference => {
		const model = reference.object as ITextFileEditorModel;
E
Erich Gamma 已提交
364

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

367 368
		// Revert on model
		return model.revert().then(() => {
E
Erich Gamma 已提交
369

370
			// Reopen file input
B
Benjamin Pasero 已提交
371
			return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
E
Erich Gamma 已提交
372

373
				// Clean up
374
				group.closeEditor(editor);
375
				editor.dispose();
376
				reference.dispose();
E
Erich Gamma 已提交
377 378
			});
		});
379
	});
380
};