mainThreadSaveParticipant.ts 16.7 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 7
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IdleValue, sequence } from 'vs/base/common/async';
8
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
9
import * as strings from 'vs/base/common/strings';
10
import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
M
Matt Bierner 已提交
11
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
12
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
M
Matt Bierner 已提交
13 14 15 16
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';
17 18
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
M
Matt Bierner 已提交
19 20
import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
import { CodeAction, TextEdit } from 'vs/editor/common/modes';
M
Matt Bierner 已提交
21 22
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
23 24
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
M
Matt Bierner 已提交
25
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
26
import { getDocumentFormattingEdits, FormatMode } from 'vs/editor/contrib/format/format';
27
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
M
Matt Bierner 已提交
28 29 30
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
31
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
M
Matt Bierner 已提交
32 33 34
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService2, ProgressLocation } from 'vs/platform/progress/common/progress';
35
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
36
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
37
import { ISaveParticipant, SaveReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
38
import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol';
J
Johannes Rieken 已提交
39
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
E
Erich Gamma 已提交
40

41 42 43 44 45
export interface ISaveParticipantParticipant extends ISaveParticipant {
	// progressMessage: string;
}

class TrimWhitespaceParticipant implements ISaveParticipantParticipant {
E
Erich Gamma 已提交
46 47

	constructor(
48 49
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
E
Erich Gamma 已提交
50
	) {
J
Johannes Rieken 已提交
51
		// Nothing
E
Erich Gamma 已提交
52 53
	}

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

A
Alex Dima 已提交
60
	private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void {
61
		let prevSelection: Selection[] = [];
62
		let cursors: Position[] = [];
E
Erich Gamma 已提交
63

64
		const editor = findEditor(model, this.codeEditorService);
65 66 67 68 69
		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) {
70
				cursors = prevSelection.map(s => s.getPosition());
A
Alex Dima 已提交
71 72 73 74 75 76
				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 已提交
77 78 79
			}
		}

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

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

89 90
function findEditor(model: ITextModel, codeEditorService: ICodeEditorService): IActiveCodeEditor | null {
	let candidate: IActiveCodeEditor | null = null;
91

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

99
				candidate = editor;
100 101 102 103
			}
		}
	}

104
	return candidate;
105 106
}

107
export class FinalNewLineParticipant implements ISaveParticipantParticipant {
108 109

	constructor(
110 111
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
112 113 114 115
	) {
		// Nothing
	}

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

A
Alex Dima 已提交
122
	private doInsertFinalNewLine(model: ITextModel): void {
123 124 125 126
		const lineCount = model.getLineCount();
		const lastLine = model.getLineContent(lineCount);
		const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1;

B
Benjamin Pasero 已提交
127
		if (!lineCount || lastLineIsEmptyOrWhitespace) {
128 129 130
			return;
		}

131
		let prevSelection: Selection[] = [];
132 133 134 135 136
		const editor = findEditor(model, this.codeEditorService);
		if (editor) {
			prevSelection = editor.getSelections();
		}

137 138 139 140 141
		model.pushEditOperations(prevSelection, [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())], edits => prevSelection);

		if (editor) {
			editor.setSelections(prevSelection);
		}
142 143 144
	}
}

145
export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant {
146 147

	constructor(
148 149
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
150 151 152 153
	) {
		// Nothing
	}

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

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
	/**
	 * 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 {
176 177 178
		const lineCount = model.getLineCount();

		// Do not insert new line if file does not end with new line
179
		if (lineCount === 1) {
180 181 182
			return;
		}

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

198 199 200
		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 已提交
201

202 203
		if (deletionRange.isEmpty()) {
			return;
A
AiryShift 已提交
204
		}
205

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

208 209 210 211 212 213
		if (editor) {
			editor.setSelections(prevSelection);
		}
	}
}

214
class FormatOnSaveParticipant implements ISaveParticipantParticipant {
J
Johannes Rieken 已提交
215 216

	constructor(
217 218
		@ICodeEditorService private readonly _editorService: ICodeEditorService,
		@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
J
Johannes Rieken 已提交
219 220
		@IConfigurationService private readonly _configurationService: IConfigurationService,
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
J
Johannes Rieken 已提交
221 222 223 224
	) {
		// Nothing
	}

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

227
		const model = editorModel.textEditorModel;
228
		if (env.reason === SaveReason.AUTO
229
			|| !this._configurationService.getValue('editor.formatOnSave', { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() })) {
M
Matt Bierner 已提交
230
			return undefined;
J
Johannes Rieken 已提交
231
		}
232

233
		const versionNow = model.getVersionId();
234

235
		const timeout = this._configurationService.getValue<number>('editor.formatOnSaveTimeout', { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() });
236

237
		return new Promise<TextEdit[] | null>((resolve, reject) => {
238 239
			const source = new CancellationTokenSource();
			const request = getDocumentFormattingEdits(this._telemetryService, this._editorWorkerService, model, model.getFormattingOptions(), FormatMode.Auto, source.token);
J
Johannes Rieken 已提交
240 241 242

			setTimeout(() => {
				reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout));
243
				source.cancel();
J
Johannes Rieken 已提交
244 245
			}, timeout);

246
			request.then(resolve, reject);
247 248

		}).then(edits => {
249
			if (isNonEmptyArray(edits) && versionNow === model.getVersionId()) {
250
				const editor = findEditor(model, this._editorService);
251
				if (editor) {
252
					this._editsWithEditor(editor, edits);
253 254 255
				} else {
					this._editWithModel(model, edits);
				}
256
			}
257 258
		});
	}
259

M
Matt Bierner 已提交
260
	private _editsWithEditor(editor: ICodeEditor, edits: TextEdit[]): void {
261
		FormattingEdit.execute(editor, edits);
262 263
	}

M
Matt Bierner 已提交
264
	private _editWithModel(model: ITextModel, edits: TextEdit[]): void {
265

J
Johannes Rieken 已提交
266
		const [{ range }] = edits;
267 268 269
		const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);

		model.pushEditOperations([initialSelection], edits.map(FormatOnSaveParticipant._asIdentEdit), undoEdits => {
J
Johannes Rieken 已提交
270
			for (const { range } of undoEdits) {
271 272 273 274
				if (Range.areIntersectingOrTouching(range, initialSelection)) {
					return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
				}
			}
275
			return null;
276 277 278
		});
	}

M
Matt Bierner 已提交
279
	private static _asIdentEdit({ text, range }: TextEdit): IIdentifiedSingleEditOperation {
280
		return {
281 282 283
			text,
			range: Range.lift(range),
			forceMoveMarkers: true
284
		};
J
Johannes Rieken 已提交
285 286 287
	}
}

M
Matt Bierner 已提交
288
class CodeActionOnSaveParticipant implements ISaveParticipant {
289 290

	constructor(
291
		@IBulkEditService private readonly _bulkEditService: IBulkEditService,
292 293 294 295
		@ICommandService private readonly _commandService: ICommandService,
		@IConfigurationService private readonly _configurationService: IConfigurationService
	) { }

296
	async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
297 298 299 300 301 302 303 304 305 306 307 308
		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;
		}

309 310 311 312 313 314 315 316 317 318 319
		const codeActionsOnSave = Object.keys(setting)
			.filter(x => setting[x]).map(x => new CodeActionKind(x))
			.sort((a, b) => {
				if (a.value === CodeActionKind.SourceFixAll.value) {
					return -1;
				}
				if (b.value === CodeActionKind.SourceFixAll.value) {
					return 1;
				}
				return 0;
			});
320 321 322 323
		if (!codeActionsOnSave.length) {
			return undefined;
		}

324 325
		const tokenSource = new CancellationTokenSource();

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

328 329
		return Promise.race([
			new Promise<void>((_resolve, reject) =>
330 331 332 333 334 335 336 337 338 339 340
				setTimeout(() => {
					tokenSource.cancel();
					reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout));
				}, timeout)),
			this.applyOnSaveActions(model, codeActionsOnSave, tokenSource.token)
		]).then(() => {
			tokenSource.cancel();
		}, (e) => {
			tokenSource.cancel();
			return Promise.reject(e);
		});
341 342
	}

343
	private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: CodeActionKind[], token: CancellationToken): Promise<void> {
344
		for (const codeActionKind of codeActionsOnSave) {
345
			const actionsToRun = await this.getActionsToRun(model, codeActionKind, token);
346
			try {
M
Matt Bierner 已提交
347
				await this.applyCodeActions(actionsToRun.actions);
348 349 350 351
			} catch {
				// Failure to apply a code action should not block other on save actions
			}
		}
352 353
	}

M
Matt Bierner 已提交
354
	private async applyCodeActions(actionsToRun: ReadonlyArray<CodeAction>) {
355
		for (const action of actionsToRun) {
356
			await applyCodeAction(action, this._bulkEditService, this._commandService);
357 358 359
		}
	}

360
	private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, token: CancellationToken) {
361
		return getCodeActions(model, model.getFullModelRange(), {
362
			type: 'auto',
363
			filter: { kind: codeActionKind, includeSourceActions: true },
364
		}, token);
365 366 367
	}
}

368
class ExtHostSaveParticipant implements ISaveParticipantParticipant {
J
Johannes Rieken 已提交
369

370
	private readonly _proxy: ExtHostDocumentSaveParticipantShape;
J
Johannes Rieken 已提交
371

372
	constructor(extHostContext: IExtHostContext) {
373
		this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentSaveParticipant);
J
Johannes Rieken 已提交
374 375
	}

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

378
		if (!shouldSynchronizeModel(editorModel.textEditorModel)) {
J
Johannes Rieken 已提交
379 380 381 382 383
			// the model never made it to the extension
			// host meaning we cannot participate in its save
			return undefined;
		}

384
		return new Promise<any>((resolve, reject) => {
385
			setTimeout(() => reject(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms")), 1750);
386 387 388
			this._proxy.$participateInSave(editorModel.getResource(), env.reason).then(values => {
				for (const success of values) {
					if (!success) {
389
						return Promise.reject(new Error('listener failed'));
390
					}
391
				}
M
Matt Bierner 已提交
392
				return undefined;
393
			}).then(resolve, reject);
394
		});
J
Johannes Rieken 已提交
395 396 397
	}
}

398
// The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace
399
@extHostCustomer
400 401
export class SaveParticipant implements ISaveParticipant {

402
	private readonly _saveParticipants: IdleValue<ISaveParticipantParticipant[]>;
403 404

	constructor(
405
		extHostContext: IExtHostContext,
406
		@IInstantiationService instantiationService: IInstantiationService,
407 408
		@IProgressService2 private readonly _progressService: IProgressService2,
		@ILogService private readonly _logService: ILogService
409
	) {
410
		this._saveParticipants = new IdleValue(() => [
411
			instantiationService.createInstance(TrimWhitespaceParticipant),
M
Matt Bierner 已提交
412
			instantiationService.createInstance(CodeActionOnSaveParticipant),
413 414 415 416
			instantiationService.createInstance(FormatOnSaveParticipant),
			instantiationService.createInstance(FinalNewLineParticipant),
			instantiationService.createInstance(TrimFinalNewLinesParticipant),
			instantiationService.createInstance(ExtHostSaveParticipant, extHostContext),
417
		]);
418 419 420
		// Hook into model
		TextFileEditorModel.setSaveParticipant(this);
	}
421 422

	dispose(): void {
423
		TextFileEditorModel.setSaveParticipant(null);
424
		this._saveParticipants.dispose();
425 426
	}

427
	async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
428 429
		return this._progressService.withProgress({ location: ProgressLocation.Window }, progress => {
			progress.report({ message: localize('saveParticipants', "Running Save Participants...") });
430
			const promiseFactory = this._saveParticipants.getValue().map(p => () => {
B
Benjamin Pasero 已提交
431
				return p.participate(model, env);
432
			});
433
			return sequence(promiseFactory).then(() => { }, err => this._logService.warn(err));
J
Johannes Rieken 已提交
434
		});
E
Erich Gamma 已提交
435
	}
J
Johannes Rieken 已提交
436
}