snippetSession.ts 16.7 KB
Newer Older
J
wip  
Johannes Rieken 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import 'vs/css!./snippetSession';
J
Johannes Rieken 已提交
9
import { getLeadingWhitespace } from 'vs/base/common/strings';
10
import { IModel, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
11
import { EditOperation } from 'vs/editor/common/core/editOperation';
12
import { TextmateSnippet, Placeholder, Choice, SnippetParser } from './snippetParser';
J
Johannes Rieken 已提交
13 14
import { Selection } from 'vs/editor/common/core/selection';
import { Range } from 'vs/editor/common/core/range';
J
Johannes Rieken 已提交
15
import { IPosition } from 'vs/editor/common/core/position';
J
Johannes Rieken 已提交
16
import { groupBy } from 'vs/base/common/arrays';
J
Johannes Rieken 已提交
17
import { dispose } from 'vs/base/common/lifecycle';
18
import { EditorSnippetVariableResolver } from './snippetVariables';
19
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
20
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
J
wip  
Johannes Rieken 已提交
21

22
export class OneSnippet {
J
Johannes Rieken 已提交
23

24
	private readonly _editor: ICodeEditor;
J
Johannes Rieken 已提交
25 26
	private readonly _snippet: TextmateSnippet;
	private readonly _offset: number;
J
Johannes Rieken 已提交
27

J
Johannes Rieken 已提交
28 29
	private _placeholderDecorations: Map<Placeholder, string>;
	private _placeholderGroups: Placeholder[][];
30 31
	_placeholderGroupsIdx: number;
	_nestingLevel: number = 1;
J
Johannes Rieken 已提交
32

J
Johannes Rieken 已提交
33
	private static readonly _decor = {
34 35 36 37
		active: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }),
		inactive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }),
		activeFinal: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }),
		inactiveFinal: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }),
38
	};
39

40
	constructor(editor: ICodeEditor, snippet: TextmateSnippet, offset: number) {
J
Johannes Rieken 已提交
41
		this._editor = editor;
J
Johannes Rieken 已提交
42 43
		this._snippet = snippet;
		this._offset = offset;
J
Johannes Rieken 已提交
44 45 46

		this._placeholderGroups = groupBy(snippet.placeholders, Placeholder.compareByIndex);
		this._placeholderGroupsIdx = -1;
J
Johannes Rieken 已提交
47
	}
J
Johannes Rieken 已提交
48

J
Johannes Rieken 已提交
49
	dispose(): void {
J
Johannes Rieken 已提交
50 51
		if (this._placeholderDecorations) {
			this._editor.changeDecorations(accessor => this._placeholderDecorations.forEach(handle => accessor.removeDecoration(handle)));
J
Johannes Rieken 已提交
52
		}
53
		this._placeholderGroups.length = 0;
J
Johannes Rieken 已提交
54
	}
J
Johannes Rieken 已提交
55

J
Johannes Rieken 已提交
56
	private _initDecorations(): void {
J
Johannes Rieken 已提交
57

J
Johannes Rieken 已提交
58 59 60 61 62 63 64
		if (this._placeholderDecorations) {
			// already initialized
			return;
		}

		this._placeholderDecorations = new Map<Placeholder, string>();
		const model = this._editor.getModel();
J
Johannes Rieken 已提交
65 66

		this._editor.changeDecorations(accessor => {
J
Johannes Rieken 已提交
67
			// create a decoration for each placeholder
68
			for (const placeholder of this._snippet.placeholders) {
J
Johannes Rieken 已提交
69
				const placeholderOffset = this._snippet.offset(placeholder);
70
				const placeholderLen = this._snippet.fullLen(placeholder);
J
Johannes Rieken 已提交
71 72 73 74
				const range = Range.fromPositions(
					model.getPositionAt(this._offset + placeholderOffset),
					model.getPositionAt(this._offset + placeholderOffset + placeholderLen)
				);
75 76
				const options = placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive;
				const handle = accessor.addDecoration(range, options);
J
Johannes Rieken 已提交
77
				this._placeholderDecorations.set(placeholder, handle);
J
Johannes Rieken 已提交
78 79 80 81
			}
		});
	}

82
	move(fwd: boolean | undefined): Selection[] {
J
Johannes Rieken 已提交
83

J
Johannes Rieken 已提交
84
		this._initDecorations();
J
Johannes Rieken 已提交
85

86
		if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) {
J
Johannes Rieken 已提交
87 88
			this._placeholderGroupsIdx += 1;

89
		} else if (fwd === false && this._placeholderGroupsIdx > 0) {
J
Johannes Rieken 已提交
90
			this._placeholderGroupsIdx -= 1;
91 92 93 94

		} else {
			// the selection of the current placeholder might
			// not acurate any more -> simply restore it
J
Johannes Rieken 已提交
95
		}
96 97 98

		return this._editor.getModel().changeDecorations(accessor => {

99
			const activePlaceholders = new Set<Placeholder>();
100 101 102

			// change stickiness to always grow when typing at its edges
			// because these decorations represent the currently active
103 104 105
			// tabstop.
			// Special case #1: reaching the final tabstop
			// Special case #2: placeholders enclosing active placeholders
106 107 108 109 110 111
			const selections: Selection[] = [];
			for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
				const id = this._placeholderDecorations.get(placeholder);
				const range = this._editor.getModel().getDecorationRange(id);
				selections.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn));

J
Johannes Rieken 已提交
112
				accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);
113 114 115 116 117 118 119
				activePlaceholders.add(placeholder);

				for (const enclosingPlaceholder of this._snippet.enclosingPlaceholders(placeholder)) {
					const id = this._placeholderDecorations.get(enclosingPlaceholder);
					accessor.changeDecorationOptions(id, enclosingPlaceholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);
					activePlaceholders.add(enclosingPlaceholder);
				}
120
			}
121 122 123 124 125

			// change stickness to never grow when typing at its edges
			// so that in-active tabstops never grow
			this._placeholderDecorations.forEach((id, placeholder) => {
				if (!activePlaceholders.has(placeholder)) {
J
Johannes Rieken 已提交
126
					accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive);
127 128 129
				}
			});

130 131
			return selections;
		});
J
Johannes Rieken 已提交
132 133
	}

134
	get isAtFirstPlaceholder() {
135
		return this._placeholderGroupsIdx <= 0 || this._placeholderGroups.length === 0;
136 137
	}

138 139
	get isAtLastPlaceholder() {
		return this._placeholderGroupsIdx === this._placeholderGroups.length - 1;
J
Johannes Rieken 已提交
140
	}
J
Johannes Rieken 已提交
141

142
	get hasPlaceholder() {
143
		return this._snippet.placeholders.length > 0;
144 145
	}

146 147 148 149 150 151 152 153 154 155 156
	get placeholderRanges() {
		const ret: Range[] = [];
		this._placeholderDecorations.forEach((id, placeholder) => {
			if (!placeholder.isFinalTabstop) {
				const range = this._editor.getModel().getDecorationRange(id);
				if (range) {
					ret.push(range);
				}
			}
		});
		return ret;
J
Johannes Rieken 已提交
157
	}
158

159 160 161 162
	get choice(): Choice {
		return this._placeholderGroups[this._placeholderGroupsIdx][0].choice;
	}

163 164 165
	merge(others: OneSnippet[]): void {

		const model = this._editor.getModel();
J
Johannes Rieken 已提交
166
		this._nestingLevel *= 10;
167 168 169 170 171 172 173 174 175 176 177 178 179 180

		this._editor.changeDecorations(accessor => {

			// For each active placeholder take one snippet and merge it
			// in that the placeholder (can be many for `$1foo$1foo`). Because
			// everything is sorted by editor selection we can simply remove
			// elements from the beginning of the array
			for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
				const nested = others.shift();
				console.assert(!nested._placeholderDecorations);

				// Massage placeholder-indicies of the nested snippet to be
				// sorted right after the insertion point. This ensures we move
				// through the placeholders in the correct order
181
				for (const nestedPlaceholder of nested._snippet.placeholderInfo.all) {
182
					if (nestedPlaceholder.isFinalTabstop) {
183
						nestedPlaceholder.index = placeholder.index + ((nested._snippet.placeholderInfo.last.index + 1) / this._nestingLevel);
184
					} else {
J
Johannes Rieken 已提交
185
						nestedPlaceholder.index = placeholder.index + (nestedPlaceholder.index / this._nestingLevel);
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
					}
				}
				this._snippet.replace(placeholder, nested._snippet.children);

				// Remove the placeholder at which position are inserting
				// the snippet and also remove its decoration.
				const id = this._placeholderDecorations.get(placeholder);
				accessor.removeDecoration(id);
				this._placeholderDecorations.delete(placeholder);

				// For each *new* placeholder we create decoration to monitor
				// how and if it grows/shrinks.
				for (const placeholder of nested._snippet.placeholders) {
					const placeholderOffset = nested._snippet.offset(placeholder);
					const placeholderLen = nested._snippet.fullLen(placeholder);
					const range = Range.fromPositions(
						model.getPositionAt(nested._offset + placeholderOffset),
						model.getPositionAt(nested._offset + placeholderOffset + placeholderLen)
					);
					const handle = accessor.addDecoration(range, OneSnippet._decor.inactive);
					this._placeholderDecorations.set(placeholder, handle);
				}
			}

			// Last, re-create the placeholder groups by sorting placeholders by their index.
			this._placeholderGroups = groupBy(this._snippet.placeholders, Placeholder.compareByIndex);
		});
	}
J
Johannes Rieken 已提交
214
}
J
wip  
Johannes Rieken 已提交
215 216 217

export class SnippetSession {

J
Johannes Rieken 已提交
218
	static adjustWhitespace(model: IModel, position: IPosition, template: string): string {
J
Johannes Rieken 已提交
219 220 221 222 223

		const line = model.getLineContent(position.lineNumber);
		const lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1);
		const templateLines = template.split(/\r\n|\r|\n/);

224
		for (let i = 1; i < templateLines.length; i++) {
J
Johannes Rieken 已提交
225
			let templateLeadingWhitespace = getLeadingWhitespace(templateLines[i]);
226
			templateLines[i] = model.normalizeIndentation(lineLeadingWhitespace + templateLeadingWhitespace) + templateLines[i].substr(templateLeadingWhitespace.length);
J
Johannes Rieken 已提交
227 228 229 230
		}
		return templateLines.join(model.getEOL());
	}

231
	static adjustSelection(model: IModel, selection: Selection, overwriteBefore: number, overwriteAfter: number): Selection {
232
		if (overwriteBefore !== 0 || overwriteAfter !== 0) {
J
Johannes Rieken 已提交
233 234 235 236 237 238 239 240 241 242 243 244
			// overwrite[Before|After] is compute using the position, not the whole
			// selection. therefore we adjust the selection around that position
			const { positionLineNumber, positionColumn } = selection;
			const positionColumnBefore = positionColumn - overwriteBefore;
			const positionColumnAfter = positionColumn + overwriteAfter;

			const range = model.validateRange({
				startLineNumber: positionLineNumber,
				startColumn: positionColumnBefore,
				endLineNumber: positionLineNumber,
				endColumn: positionColumnAfter
			});
245 246 247 248 249 250

			selection = Selection.createWithDirection(
				range.startLineNumber, range.startColumn,
				range.endLineNumber, range.endColumn,
				selection.getDirection()
			);
251
		}
252
		return selection;
253 254
	}

255
	static createEditsAndSnippets(editor: ICodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } {
256

257
		const model = editor.getModel();
258
		const edits: IIdentifiedSingleEditOperation[] = [];
259
		const snippets: OneSnippet[] = [];
260

J
Johannes Rieken 已提交
261 262
		let delta = 0;

263 264 265
		// know what text the overwrite[Before|After] extensions
		// of the primary curser have selected because only when
		// secondary selections extend to the same text we can grow them
266 267
		let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));
		let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter));
268

269 270 271 272
		// sort selections by their start position but remeber
		// the original index. that allows you to create correct
		// offset-based selection logic without changing the
		// primary selection
273
		const indexedSelection = editor.getSelections()
274 275 276 277
			.map((selection, idx) => ({ selection, idx }))
			.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));

		for (const { selection, idx } of indexedSelection) {
278 279 280

			// extend selection with the `overwriteBefore` and `overwriteAfter` and then
			// compare if this matches the extensions of the primary selection
281 282
			let extensionBefore = SnippetSession.adjustSelection(model, selection, overwriteBefore, 0);
			let extensionAfter = SnippetSession.adjustSelection(model, selection, 0, overwriteAfter);
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
			if (firstBeforeText !== model.getValueInRange(extensionBefore)) {
				extensionBefore = selection;
			}
			if (firstAfterText !== model.getValueInRange(extensionAfter)) {
				extensionAfter = selection;
			}

			// merge the before and after selection into one
			const snippetSelection = selection
				.setStartPosition(extensionBefore.startLineNumber, extensionBefore.startColumn)
				.setEndPosition(extensionAfter.endLineNumber, extensionAfter.endColumn);

			// adjust the template string to match the indentation and
			// whitespace rules of this insert location (can be different for each cursor)
			const start = snippetSelection.getStartPosition();
298
			const adjustedTemplate = SnippetSession.adjustWhitespace(model, start, template);
J
Johannes Rieken 已提交
299

300 301
			const snippet = new SnippetParser()
				.parse(adjustedTemplate, true, enforceFinalTabstop)
J
Johannes Rieken 已提交
302
				.resolveVariables(new EditorSnippetVariableResolver(model, selection));
303

J
Johannes Rieken 已提交
304
			const offset = model.getOffsetAt(start) + delta;
305
			delta += snippet.toString().length - model.getValueLengthInRange(snippetSelection);
J
Johannes Rieken 已提交
306

307 308 309
			// store snippets with the index of their originating selection.
			// that ensures the primiary cursor stays primary despite not being
			// the one with lowest start position
310
			edits[idx] = EditOperation.replace(snippetSelection, snippet.toString());
311
			snippets[idx] = new OneSnippet(editor, snippet, offset);
312
		}
313

314 315 316
		return { edits, snippets };
	}

317
	private readonly _editor: ICodeEditor;
318
	private readonly _template: string;
319
	private readonly _templateMerges: [number, number, string][] = [];
320 321 322 323
	private readonly _overwriteBefore: number;
	private readonly _overwriteAfter: number;
	private _snippets: OneSnippet[] = [];

324
	constructor(editor: ICodeEditor, template: string, overwriteBefore: number = 0, overwriteAfter: number = 0) {
325 326 327 328 329 330 331 332 333 334
		this._editor = editor;
		this._template = template;
		this._overwriteBefore = overwriteBefore;
		this._overwriteAfter = overwriteAfter;
	}

	dispose(): void {
		dispose(this._snippets);
	}

335 336 337 338
	_logInfo(): string {
		return `template="${this._template}", merged_templates="${this._templateMerges.join(' -> ')}"`;
	}

339 340 341 342
	insert(): void {

		const model = this._editor.getModel();

J
Johannes Rieken 已提交
343
		// make insert edit and start with first selections
J
Johannes Rieken 已提交
344
		const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._overwriteBefore, this._overwriteAfter, false);
345
		this._snippets = snippets;
346 347 348

		this._editor.setSelections(model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => {
			if (this._snippets[0].hasPlaceholder) {
349 350 351 352
				return this._move(true);
			} else {
				return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
			}
353
		}));
J
Johannes Rieken 已提交
354 355
	}

356
	merge(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): void {
357
		this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]);
J
Johannes Rieken 已提交
358
		const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, overwriteBefore, overwriteAfter, true);
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374

		this._editor.setSelections(this._editor.getModel().pushEditOperations(this._editor.getSelections(), edits, undoEdits => {

			for (const snippet of this._snippets) {
				snippet.merge(snippets);
			}
			console.assert(snippets.length === 0);

			if (this._snippets[0].hasPlaceholder) {
				return this._move(undefined);
			} else {
				return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
			}
		}));
	}

J
Johannes Rieken 已提交
375
	next(): void {
J
Johannes Rieken 已提交
376
		const newSelections = this._move(true);
J
Johannes Rieken 已提交
377
		this._editor.setSelections(newSelections);
J
Johannes Rieken 已提交
378 379
	}

J
Johannes Rieken 已提交
380
	prev(): void {
J
Johannes Rieken 已提交
381
		const newSelections = this._move(false);
J
Johannes Rieken 已提交
382
		this._editor.setSelections(newSelections);
J
Johannes Rieken 已提交
383 384
	}

385
	private _move(fwd: boolean | undefined): Selection[] {
J
Johannes Rieken 已提交
386 387
		const selections: Selection[] = [];
		for (const snippet of this._snippets) {
J
Johannes Rieken 已提交
388 389
			const oneSelection = snippet.move(fwd);
			selections.push(...oneSelection);
J
wip  
Johannes Rieken 已提交
390
		}
J
Johannes Rieken 已提交
391 392 393
		return selections;
	}

394 395 396 397
	get isAtFirstPlaceholder() {
		return this._snippets[0].isAtFirstPlaceholder;
	}

398 399
	get isAtLastPlaceholder() {
		return this._snippets[0].isAtLastPlaceholder;
J
wip  
Johannes Rieken 已提交
400
	}
J
Johannes Rieken 已提交
401

402 403 404 405
	get hasPlaceholder() {
		return this._snippets[0].hasPlaceholder;
	}

406 407 408 409
	get choice(): Choice {
		return this._snippets[0].choice;
	}

J
Johannes Rieken 已提交
410
	isSelectionWithinPlaceholders(): boolean {
411 412 413 414 415

		if (!this.hasPlaceholder) {
			return false;
		}

J
Johannes Rieken 已提交
416 417
		const selections = this._editor.getSelections();
		if (selections.length < this._snippets.length) {
418 419 420
			// this means we started snippet mode with N
			// selections and have M (N > M) selections.
			// So one snippet is without selection -> cancel
J
Johannes Rieken 已提交
421 422 423
			return false;
		}

424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
		const ranges: Range[] = [];
		for (const snippet of this._snippets) {
			ranges.push(...snippet.placeholderRanges);
		}

		if (selections.length > ranges.length) {
			return false;
		}

		// sort selections and ranges by their start position
		// and then make sure each selection is contained by
		// a placeholder range
		selections.sort(Range.compareRangesUsingStarts);
		ranges.sort(Range.compareRangesUsingStarts);

		outer: for (const selection of selections) {
			let range: Range;
			while (range = ranges.shift()) {
442
				if (range.containsRange(selection)) {
443
					continue outer;
J
Johannes Rieken 已提交
444 445
				}
			}
446
			return false;
J
Johannes Rieken 已提交
447 448 449 450
		}

		return true;
	}
J
wip  
Johannes Rieken 已提交
451
}