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

M
Matt Bierner 已提交
6
import { IdleValue, sequence } from 'vs/base/common/async';
7
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
8
import * as strings from 'vs/base/common/strings';
9
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
M
Matt Bierner 已提交
10
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
11
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
M
Matt Bierner 已提交
12 13 14 15
import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand';
import { ICodeActionsOnSaveOptions } from 'vs/editor/common/config/editorOptions';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
16 17
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
18 19
import { ITextModel } from 'vs/editor/common/model';
import { CodeAction } from 'vs/editor/common/modes';
M
Matt Bierner 已提交
20
import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
21 22
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
M
Matt Bierner 已提交
23
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
24
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
M
Matt Bierner 已提交
25 26 27
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
28
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
M
Matt Bierner 已提交
29 30
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
31
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
32
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
33
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
34
import { ISaveParticipant, SaveReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
35
import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol';
E
Erich Gamma 已提交
36

37 38 39 40 41
export interface ISaveParticipantParticipant extends ISaveParticipant {
	// progressMessage: string;
}

class TrimWhitespaceParticipant implements ISaveParticipantParticipant {
E
Erich Gamma 已提交
42 43

	constructor(
44 45
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
E
Erich Gamma 已提交
46
	) {
J
Johannes Rieken 已提交
47
		// Nothing
E
Erich Gamma 已提交
48 49
	}

50
	async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
51
		if (this.configurationService.getValue('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.getResource() })) {
52
			this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO);
E
Erich Gamma 已提交
53 54 55
		}
	}

A
Alex Dima 已提交
56
	private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void {
57
		let prevSelection: Selection[] = [];
58
		let cursors: Position[] = [];
E
Erich Gamma 已提交
59

60
		const editor = findEditor(model, this.codeEditorService);
61 62 63 64 65
		if (editor) {
			// Find `prevSelection` in any case do ensure a good undo stack when pushing the edit
			// Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump
			prevSelection = editor.getSelections();
			if (isAutoSaved) {
66
				cursors = prevSelection.map(s => s.getPosition());
A
Alex Dima 已提交
67 68 69 70 71 72
				const snippetsRange = SnippetController2.get(editor).getSessionEnclosingRange();
				if (snippetsRange) {
					for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) {
						cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber)));
					}
				}
E
Erich Gamma 已提交
73 74 75
			}
		}

B
Benjamin Pasero 已提交
76
		const ops = trimTrailingWhitespace(model, cursors);
E
Erich Gamma 已提交
77
		if (!ops.length) {
78
			return; // Nothing to do
E
Erich Gamma 已提交
79 80 81 82
		}

		model.pushEditOperations(prevSelection, ops, (edits) => prevSelection);
	}
83
}
E
Erich Gamma 已提交
84

85 86
function findEditor(model: ITextModel, codeEditorService: ICodeEditorService): IActiveCodeEditor | null {
	let candidate: IActiveCodeEditor | null = null;
87

88 89
	if (model.isAttachedToEditor()) {
		for (const editor of codeEditorService.listCodeEditors()) {
90
			if (editor.hasModel() && editor.getModel() === model) {
A
Alex Dima 已提交
91
				if (editor.hasTextFocus()) {
92
					return editor; // favour focused editor if there are multiple
93
				}
94

95
				candidate = editor;
96 97 98 99
			}
		}
	}

100
	return candidate;
101 102
}

103
export class FinalNewLineParticipant implements ISaveParticipantParticipant {
104 105

	constructor(
106 107
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
108 109 110 111
	) {
		// Nothing
	}

112
	async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
113
		if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.getResource() })) {
114 115 116 117
			this.doInsertFinalNewLine(model.textEditorModel);
		}
	}

A
Alex Dima 已提交
118
	private doInsertFinalNewLine(model: ITextModel): void {
119 120 121 122
		const lineCount = model.getLineCount();
		const lastLine = model.getLineContent(lineCount);
		const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1;

B
Benjamin Pasero 已提交
123
		if (!lineCount || lastLineIsEmptyOrWhitespace) {
124 125 126
			return;
		}

127
		let prevSelection: Selection[] = [];
128 129 130 131 132
		const editor = findEditor(model, this.codeEditorService);
		if (editor) {
			prevSelection = editor.getSelections();
		}

133 134 135 136 137
		model.pushEditOperations(prevSelection, [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())], edits => prevSelection);

		if (editor) {
			editor.setSelections(prevSelection);
		}
138 139 140
	}
}

141
export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant {
142 143

	constructor(
144 145
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
146 147 148 149
	) {
		// Nothing
	}

150
	async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
151
		if (this.configurationService.getValue('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.getResource() })) {
152
			this.doTrimFinalNewLines(model.textEditorModel, env.reason === SaveReason.AUTO);
153 154 155
		}
	}

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
	/**
	 * returns 0 if the entire file is empty or whitespace only
	 */
	private findLastLineWithContent(model: ITextModel): number {
		for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) {
			const lineContent = model.getLineContent(lineNumber);
			if (strings.lastNonWhitespaceIndex(lineContent) !== -1) {
				// this line has content
				return lineNumber;
			}
		}
		// no line has content
		return 0;
	}

	private doTrimFinalNewLines(model: ITextModel, isAutoSaved: boolean): void {
172 173 174
		const lineCount = model.getLineCount();

		// Do not insert new line if file does not end with new line
175
		if (lineCount === 1) {
176 177 178
			return;
		}

179
		let prevSelection: Selection[] = [];
180
		let cannotTouchLineNumber = 0;
181 182 183
		const editor = findEditor(model, this.codeEditorService);
		if (editor) {
			prevSelection = editor.getSelections();
184 185 186 187 188 189 190 191
			if (isAutoSaved) {
				for (let i = 0, len = prevSelection.length; i < len; i++) {
					const positionLineNumber = prevSelection[i].positionLineNumber;
					if (positionLineNumber > cannotTouchLineNumber) {
						cannotTouchLineNumber = positionLineNumber;
					}
				}
			}
192 193
		}

194 195 196
		const lastLineNumberWithContent = this.findLastLineWithContent(model);
		const deleteFromLineNumber = Math.max(lastLineNumberWithContent + 1, cannotTouchLineNumber + 1);
		const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, lineCount, model.getLineMaxColumn(lineCount)));
A
AiryShift 已提交
197

198 199
		if (deletionRange.isEmpty()) {
			return;
A
AiryShift 已提交
200
		}
201

202 203
		model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], edits => prevSelection);

204 205 206 207 208 209
		if (editor) {
			editor.setSelections(prevSelection);
		}
	}
}

210
class FormatOnSaveParticipant implements ISaveParticipantParticipant {
J
Johannes Rieken 已提交
211 212

	constructor(
J
Johannes Rieken 已提交
213
		@IConfigurationService private readonly _configurationService: IConfigurationService,
214
		@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
215
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
J
Johannes Rieken 已提交
216 217 218 219
	) {
		// Nothing
	}

220
	async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
221

222
		const model = editorModel.textEditorModel;
223 224 225
		const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri };

		if (env.reason === SaveReason.AUTO || !this._configurationService.getValue('editor.formatOnSave', overrides)) {
M
Matt Bierner 已提交
226
			return undefined;
J
Johannes Rieken 已提交
227
		}
228

229
		return new Promise<any>((resolve, reject) => {
230
			const source = new CancellationTokenSource();
231
			const editorOrModel = findEditor(model, this._codeEditorService) || model;
232
			const timeout = this._configurationService.getValue<number>('editor.formatOnSaveTimeout', overrides);
233
			const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, source.token);
J
Johannes Rieken 已提交
234

235 236 237 238
			setTimeout(() => {
				reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout));
				source.cancel();
			}, timeout);
239

240
			request.then(resolve, reject);
241 242
		});
	}
J
Johannes Rieken 已提交
243 244
}

M
Matt Bierner 已提交
245
class CodeActionOnSaveParticipant implements ISaveParticipant {
246 247

	constructor(
248
		@IBulkEditService private readonly _bulkEditService: IBulkEditService,
249 250 251 252
		@ICommandService private readonly _commandService: ICommandService,
		@IConfigurationService private readonly _configurationService: IConfigurationService
	) { }

253
	async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
254 255 256 257 258 259 260 261 262 263 264 265
		if (env.reason === SaveReason.AUTO) {
			return undefined;
		}

		const model = editorModel.textEditorModel;

		const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() };
		const setting = this._configurationService.getValue<ICodeActionsOnSaveOptions>('editor.codeActionsOnSave', settingsOverrides);
		if (!setting) {
			return undefined;
		}

266 267 268
		const codeActionsOnSave = Object.keys(setting)
			.filter(x => setting[x]).map(x => new CodeActionKind(x))
			.sort((a, b) => {
269 270 271 272
				if (CodeActionKind.SourceFixAll.contains(a)) {
					if (CodeActionKind.SourceFixAll.contains(b)) {
						return 0;
					}
273
					return -1;
274
				}
275
				if (CodeActionKind.SourceFixAll.contains(b)) {
276
					return 1;
277
				}
278 279
				return 0;
			});
280

281 282 283 284
		if (!codeActionsOnSave.length) {
			return undefined;
		}

285 286
		const tokenSource = new CancellationTokenSource();

287 288
		const timeout = this._configurationService.getValue<number>('editor.codeActionsOnSaveTimeout', settingsOverrides);

289 290
		return Promise.race([
			new Promise<void>((_resolve, reject) =>
291 292 293 294 295
				setTimeout(() => {
					tokenSource.cancel();
					reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout));
				}, timeout)),
			this.applyOnSaveActions(model, codeActionsOnSave, tokenSource.token)
M
Matt Bierner 已提交
296
		]).finally(() => {
297 298
			tokenSource.cancel();
		});
299 300
	}

301
	private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: CodeActionKind[], token: CancellationToken): Promise<void> {
302
		for (const codeActionKind of codeActionsOnSave) {
303
			const actionsToRun = await this.getActionsToRun(model, codeActionKind, token);
304
			try {
M
Matt Bierner 已提交
305
				await this.applyCodeActions(actionsToRun.actions);
306 307
			} catch {
				// Failure to apply a code action should not block other on save actions
308 309
			} finally {
				actionsToRun.dispose();
310 311
			}
		}
312 313
	}

314
	private async applyCodeActions(actionsToRun: readonly CodeAction[]) {
315
		for (const action of actionsToRun) {
316
			await applyCodeAction(action, this._bulkEditService, this._commandService);
317 318 319
		}
	}

320
	private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, token: CancellationToken) {
321
		return getCodeActions(model, model.getFullModelRange(), {
322
			type: 'auto',
323
			filter: { kind: codeActionKind, includeSourceActions: true },
324
		}, token);
325 326 327
	}
}

328
class ExtHostSaveParticipant implements ISaveParticipantParticipant {
J
Johannes Rieken 已提交
329

330
	private readonly _proxy: ExtHostDocumentSaveParticipantShape;
J
Johannes Rieken 已提交
331

332
	constructor(extHostContext: IExtHostContext) {
333
		this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentSaveParticipant);
J
Johannes Rieken 已提交
334 335
	}

336
	async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
J
Johannes Rieken 已提交
337

338
		if (!shouldSynchronizeModel(editorModel.textEditorModel)) {
J
Johannes Rieken 已提交
339 340 341 342 343
			// the model never made it to the extension
			// host meaning we cannot participate in its save
			return undefined;
		}

344
		return new Promise<any>((resolve, reject) => {
345
			setTimeout(() => reject(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms")), 1750);
346 347 348
			this._proxy.$participateInSave(editorModel.getResource(), env.reason).then(values => {
				for (const success of values) {
					if (!success) {
349
						return Promise.reject(new Error('listener failed'));
350
					}
351
				}
M
Matt Bierner 已提交
352
				return undefined;
353
			}).then(resolve, reject);
354
		});
J
Johannes Rieken 已提交
355 356 357
	}
}

358
// The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace
359
@extHostCustomer
360 361
export class SaveParticipant implements ISaveParticipant {

362
	private readonly _saveParticipants: IdleValue<ISaveParticipantParticipant[]>;
363 364

	constructor(
365
		extHostContext: IExtHostContext,
366
		@IInstantiationService instantiationService: IInstantiationService,
367
		@IProgressService private readonly _progressService: IProgressService,
368
		@ILogService private readonly _logService: ILogService
369
	) {
370
		this._saveParticipants = new IdleValue(() => [
371
			instantiationService.createInstance(TrimWhitespaceParticipant),
M
Matt Bierner 已提交
372
			instantiationService.createInstance(CodeActionOnSaveParticipant),
373 374 375 376
			instantiationService.createInstance(FormatOnSaveParticipant),
			instantiationService.createInstance(FinalNewLineParticipant),
			instantiationService.createInstance(TrimFinalNewLinesParticipant),
			instantiationService.createInstance(ExtHostSaveParticipant, extHostContext),
377
		]);
378 379 380
		// Hook into model
		TextFileEditorModel.setSaveParticipant(this);
	}
381 382

	dispose(): void {
383
		TextFileEditorModel.setSaveParticipant(null);
384
		this._saveParticipants.dispose();
385 386
	}

387
	async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
388 389
		return this._progressService.withProgress({ location: ProgressLocation.Window }, progress => {
			progress.report({ message: localize('saveParticipants', "Running Save Participants...") });
390
			const promiseFactory = this._saveParticipants.getValue().map(p => () => {
B
Benjamin Pasero 已提交
391
				return p.participate(model, env);
392
			});
393
			return sequence(promiseFactory).then(() => { }, err => this._logService.warn(err));
J
Johannes Rieken 已提交
394
		});
E
Erich Gamma 已提交
395
	}
J
Johannes Rieken 已提交
396
}