mainThreadSaveParticipant.ts 9.5 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

E
Erich Gamma 已提交
6 7
'use strict';

8 9
import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async';
10
import * as strings from 'vs/base/common/strings';
11 12
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
13
import { ISaveParticipant, ITextFileEditorModel, SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
14
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
15
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
16
import { IModel, ICommonCodeEditor, ISingleEditOperation, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon';
17 18
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
19
import { Position } from 'vs/editor/common/core/position';
20
import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand';
21
import { getDocumentFormattingEdits } from 'vs/editor/contrib/format/common/format';
22 23
import { EditOperationsCommand } from 'vs/editor/contrib/format/common/formatCommand';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
24
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
25
import { ExtHostContext, ExtHostDocumentSaveParticipantShape } from '../node/extHost.protocol';
26
import { EditOperation } from 'vs/editor/common/core/editOperation';
E
Erich Gamma 已提交
27

B
Benjamin Pasero 已提交
28
export interface INamedSaveParticpant extends ISaveParticipant {
29 30 31 32 33 34
	readonly name: string;
}

class TrimWhitespaceParticipant implements INamedSaveParticpant {

	readonly name = 'TrimWhitespaceParticipant';
E
Erich Gamma 已提交
35 36 37 38 39

	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
		@ICodeEditorService private codeEditorService: ICodeEditorService
	) {
J
Johannes Rieken 已提交
40
		// Nothing
E
Erich Gamma 已提交
41 42
	}

B
Benjamin Pasero 已提交
43
	public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): void {
44
		if (this.configurationService.lookup('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.getResource() }).value) {
45
			this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO);
E
Erich Gamma 已提交
46 47 48
		}
	}

49
	private doTrimTrailingWhitespace(model: IModel, isAutoSaved: boolean): void {
A
Alex Dima 已提交
50
		let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
51
		const cursors: Position[] = [];
E
Erich Gamma 已提交
52

53 54 55 56 57 58
		let editor = findEditor(model, this.codeEditorService);
		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) {
59
				cursors.push(...prevSelection.map(s => new Position(s.positionLineNumber, s.positionColumn)));
E
Erich Gamma 已提交
60 61 62
			}
		}

B
Benjamin Pasero 已提交
63
		const ops = trimTrailingWhitespace(model, cursors);
E
Erich Gamma 已提交
64
		if (!ops.length) {
65
			return; // Nothing to do
E
Erich Gamma 已提交
66 67 68 69
		}

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

72
function findEditor(model: IModel, codeEditorService: ICodeEditorService): ICommonCodeEditor {
73
	let candidate: ICommonCodeEditor = null;
74

75 76 77 78 79 80
	if (model.isAttachedToEditor()) {
		for (const editor of codeEditorService.listCodeEditors()) {
			if (editor.getModel() === model) {
				if (editor.isFocused()) {
					return editor; // favour focussed editor if there are multiple
				}
81

82
				candidate = editor;
83 84 85 86
			}
		}
	}

87
	return candidate;
88 89
}

B
Benjamin Pasero 已提交
90
export class FinalNewLineParticipant implements INamedSaveParticpant {
91 92 93 94 95 96 97 98 99 100

	readonly name = 'FinalNewLineParticipant';

	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
		@ICodeEditorService private codeEditorService: ICodeEditorService
	) {
		// Nothing
	}

B
Benjamin Pasero 已提交
101
	public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): void {
102
		if (this.configurationService.lookup('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.getResource() }).value) {
103 104 105 106 107 108 109 110 111
			this.doInsertFinalNewLine(model.textEditorModel);
		}
	}

	private doInsertFinalNewLine(model: IModel): void {
		const lineCount = model.getLineCount();
		const lastLine = model.getLineContent(lineCount);
		const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1;

B
Benjamin Pasero 已提交
112
		if (!lineCount || lastLineIsEmptyOrWhitespace) {
113 114 115 116 117 118 119 120 121
			return;
		}

		let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
		const editor = findEditor(model, this.codeEditorService);
		if (editor) {
			prevSelection = editor.getSelections();
		}

122 123 124 125 126
		model.pushEditOperations(prevSelection, [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())], edits => prevSelection);

		if (editor) {
			editor.setSelections(prevSelection);
		}
127 128 129
	}
}

130 131 132
class FormatOnSaveParticipant implements INamedSaveParticpant {

	readonly name = 'FormatOnSaveParticipant';
J
Johannes Rieken 已提交
133 134 135 136 137 138 139 140

	constructor(
		@ICodeEditorService private _editorService: ICodeEditorService,
		@IConfigurationService private _configurationService: IConfigurationService
	) {
		// Nothing
	}

B
Benjamin Pasero 已提交
141
	participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): TPromise<void> {
142

143
		const model = editorModel.textEditorModel;
144
		if (env.reason === SaveReason.AUTO
145
			|| !this._configurationService.lookup('editor.formatOnSave', { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() }).value) {
M
Matt Bierner 已提交
146
			return undefined;
J
Johannes Rieken 已提交
147
		}
148

149
		const versionNow = model.getVersionId();
J
Johannes Rieken 已提交
150
		const { tabSize, insertSpaces } = model.getOptions();
151

152
		return new TPromise<ISingleEditOperation[]>((resolve, reject) => {
153
			setTimeout(reject, 750);
154
			getDocumentFormattingEdits(model, { tabSize, insertSpaces }).then(resolve, reject);
155 156

		}).then(edits => {
157
			if (edits && versionNow === model.getVersionId()) {
158
				const editor = findEditor(model, this._editorService);
159
				if (editor) {
160
					this._editsWithEditor(editor, edits);
161 162 163
				} else {
					this._editWithModel(model, edits);
				}
164
			}
165 166
		});
	}
167

168
	private _editsWithEditor(editor: ICommonCodeEditor, edits: ISingleEditOperation[]): void {
J
Johannes Rieken 已提交
169
		EditOperationsCommand.execute(editor, edits);
170 171 172
	}

	private _editWithModel(model: IModel, edits: ISingleEditOperation[]): void {
173

J
Johannes Rieken 已提交
174
		const [{ range }] = edits;
175 176 177
		const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);

		model.pushEditOperations([initialSelection], edits.map(FormatOnSaveParticipant._asIdentEdit), undoEdits => {
J
Johannes Rieken 已提交
178
			for (const { range } of undoEdits) {
179 180 181 182
				if (Range.areIntersectingOrTouching(range, initialSelection)) {
					return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
				}
			}
M
Matt Bierner 已提交
183
			return undefined;
184 185 186
		});
	}

J
Johannes Rieken 已提交
187
	private static _asIdentEdit({ text, range }: ISingleEditOperation): IIdentifiedSingleEditOperation {
188
		return {
189 190 191 192
			text,
			range: Range.lift(range),
			identifier: undefined,
			forceMoveMarkers: true
193
		};
J
Johannes Rieken 已提交
194 195 196
	}
}

197
class ExtHostSaveParticipant implements INamedSaveParticpant {
J
Johannes Rieken 已提交
198 199 200

	private _proxy: ExtHostDocumentSaveParticipantShape;

201 202
	readonly name = 'ExtHostSaveParticipant';

J
Johannes Rieken 已提交
203 204 205 206
	constructor( @IThreadService threadService: IThreadService) {
		this._proxy = threadService.get(ExtHostContext.ExtHostDocumentSaveParticipant);
	}

B
Benjamin Pasero 已提交
207
	participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): TPromise<void> {
208 209 210 211 212
		return new TPromise<any>((resolve, reject) => {
			setTimeout(reject, 1750);
			this._proxy.$participateInSave(editorModel.getResource(), env.reason).then(values => {
				for (const success of values) {
					if (!success) {
213
						return TPromise.wrapError(new Error('listener failed'));
214
					}
215
				}
M
Matt Bierner 已提交
216
				return undefined;
217
			}).then(resolve, reject);
218
		});
J
Johannes Rieken 已提交
219 220 221
	}
}

222 223 224
// The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace
export class SaveParticipant implements ISaveParticipant {

225
	private _saveParticipants: INamedSaveParticpant[];
226 227

	constructor(
228
		@IThreadService threadService: IThreadService,
229
		@ITelemetryService private _telemetryService: ITelemetryService,
230
		@IInstantiationService instantiationService: IInstantiationService
231
	) {
J
Johannes Rieken 已提交
232 233 234 235

		this._saveParticipants = [
			instantiationService.createInstance(TrimWhitespaceParticipant),
			instantiationService.createInstance(FormatOnSaveParticipant),
236
			instantiationService.createInstance(FinalNewLineParticipant),
J
Johannes Rieken 已提交
237 238
			instantiationService.createInstance(ExtHostSaveParticipant)
		];
239 240 241 242

		// Hook into model
		TextFileEditorModel.setSaveParticipant(this);
	}
243 244 245 246 247

	dispose(): void {
		TextFileEditorModel.setSaveParticipant(undefined);
	}

B
Benjamin Pasero 已提交
248
	participate(model: ITextFileEditorModel, env: { reason: SaveReason }): TPromise<void> {
249 250 251

		const stats: { [name: string]: number } = Object.create(null);

J
Johannes Rieken 已提交
252
		const promiseFactory = this._saveParticipants.map(p => () => {
253

J
Johannes Rieken 已提交
254
			const { name } = p;
255 256 257 258 259 260
			const t1 = Date.now();

			return TPromise.as(p.participate(model, env)).then(() => {
				stats[`Success-${name}`] = Date.now() - t1;
			}, err => {
				stats[`Failure-${name}`] = Date.now() - t1;
J
Johannes Rieken 已提交
261 262 263
				// console.error(err);
			});
		});
264 265

		return sequence(promiseFactory).then(() => {
266
			this._telemetryService.publicLog('saveParticipantStats', stats);
267
		});
E
Erich Gamma 已提交
268
	}
J
Johannes Rieken 已提交
269
}