snippetSession.ts 18.1 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
	}

J
Johannes Rieken 已提交
146 147 148 149 150 151 152 153 154 155 156 157 158 159
	computePossibleSelections() {
		const result = new Map<number, Range[]>();
		for (const placeholdersWithEqualIndex of this._placeholderGroups) {
			let ranges: Range[];

			for (const placeholder of placeholdersWithEqualIndex) {
				if (placeholder.isFinalTabstop) {
					// ignore those
					break;
				}

				if (!ranges) {
					ranges = [];
					result.set(placeholder.index, ranges);
160
				}
J
Johannes Rieken 已提交
161 162 163 164

				const id = this._placeholderDecorations.get(placeholder);
				const range = this._editor.getModel().getDecorationRange(id);
				ranges.push(range);
165
			}
J
Johannes Rieken 已提交
166 167
		}
		return result;
J
Johannes Rieken 已提交
168
	}
169

170 171 172 173
	get choice(): Choice {
		return this._placeholderGroups[this._placeholderGroupsIdx][0].choice;
	}

174 175 176
	merge(others: OneSnippet[]): void {

		const model = this._editor.getModel();
J
Johannes Rieken 已提交
177
		this._nestingLevel *= 10;
178 179 180 181 182 183 184 185 186 187 188 189 190 191

		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
192
				for (const nestedPlaceholder of nested._snippet.placeholderInfo.all) {
193
					if (nestedPlaceholder.isFinalTabstop) {
194
						nestedPlaceholder.index = placeholder.index + ((nested._snippet.placeholderInfo.last.index + 1) / this._nestingLevel);
195
					} else {
J
Johannes Rieken 已提交
196
						nestedPlaceholder.index = placeholder.index + (nestedPlaceholder.index / this._nestingLevel);
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
					}
				}
				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 已提交
225
}
J
wip  
Johannes Rieken 已提交
226 227 228

export class SnippetSession {

J
Johannes Rieken 已提交
229
	static adjustWhitespace(model: IModel, position: IPosition, template: string): string {
J
Johannes Rieken 已提交
230 231 232 233 234

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

235
		for (let i = 1; i < templateLines.length; i++) {
J
Johannes Rieken 已提交
236
			let templateLeadingWhitespace = getLeadingWhitespace(templateLines[i]);
237
			templateLines[i] = model.normalizeIndentation(lineLeadingWhitespace + templateLeadingWhitespace) + templateLines[i].substr(templateLeadingWhitespace.length);
J
Johannes Rieken 已提交
238 239 240 241
		}
		return templateLines.join(model.getEOL());
	}

242
	static adjustSelection(model: IModel, selection: Selection, overwriteBefore: number, overwriteAfter: number): Selection {
243
		if (overwriteBefore !== 0 || overwriteAfter !== 0) {
J
Johannes Rieken 已提交
244 245 246 247 248 249 250 251 252 253 254 255
			// 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
			});
256 257 258 259 260 261

			selection = Selection.createWithDirection(
				range.startLineNumber, range.startColumn,
				range.endLineNumber, range.endColumn,
				selection.getDirection()
			);
262
		}
263
		return selection;
264 265
	}

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

268
		const model = editor.getModel();
269
		const edits: IIdentifiedSingleEditOperation[] = [];
270
		const snippets: OneSnippet[] = [];
271

J
Johannes Rieken 已提交
272 273
		let delta = 0;

274 275 276
		// 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
277 278
		let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));
		let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter));
279

280 281 282 283
		// 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
284
		const indexedSelection = editor.getSelections()
285 286 287 288
			.map((selection, idx) => ({ selection, idx }))
			.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));

		for (const { selection, idx } of indexedSelection) {
289 290 291

			// extend selection with the `overwriteBefore` and `overwriteAfter` and then
			// compare if this matches the extensions of the primary selection
292 293
			let extensionBefore = SnippetSession.adjustSelection(model, selection, overwriteBefore, 0);
			let extensionAfter = SnippetSession.adjustSelection(model, selection, 0, overwriteAfter);
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
			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();
309
			const adjustedTemplate = SnippetSession.adjustWhitespace(model, start, template);
J
Johannes Rieken 已提交
310

311 312
			const snippet = new SnippetParser()
				.parse(adjustedTemplate, true, enforceFinalTabstop)
J
Johannes Rieken 已提交
313
				.resolveVariables(new EditorSnippetVariableResolver(model, selection));
314

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

318 319 320
			// 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
321
			edits[idx] = EditOperation.replace(snippetSelection, snippet.toString());
322
			snippets[idx] = new OneSnippet(editor, snippet, offset);
323
		}
324

325 326 327
		return { edits, snippets };
	}

328
	private readonly _editor: ICodeEditor;
329
	private readonly _template: string;
330
	private readonly _templateMerges: [number, number, string][] = [];
331 332 333 334
	private readonly _overwriteBefore: number;
	private readonly _overwriteAfter: number;
	private _snippets: OneSnippet[] = [];

335
	constructor(editor: ICodeEditor, template: string, overwriteBefore: number = 0, overwriteAfter: number = 0) {
336 337 338 339 340 341 342 343 344 345
		this._editor = editor;
		this._template = template;
		this._overwriteBefore = overwriteBefore;
		this._overwriteAfter = overwriteAfter;
	}

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

346 347 348 349
	_logInfo(): string {
		return `template="${this._template}", merged_templates="${this._templateMerges.join(' -> ')}"`;
	}

350 351 352 353
	insert(): void {

		const model = this._editor.getModel();

J
Johannes Rieken 已提交
354
		// make insert edit and start with first selections
J
Johannes Rieken 已提交
355
		const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._overwriteBefore, this._overwriteAfter, false);
356
		this._snippets = snippets;
357 358 359

		this._editor.setSelections(model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => {
			if (this._snippets[0].hasPlaceholder) {
360 361 362 363
				return this._move(true);
			} else {
				return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
			}
364
		}));
J
Johannes Rieken 已提交
365 366
	}

367
	merge(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): void {
368
		this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]);
J
Johannes Rieken 已提交
369
		const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, overwriteBefore, overwriteAfter, true);
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385

		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 已提交
386
	next(): void {
J
Johannes Rieken 已提交
387
		const newSelections = this._move(true);
J
Johannes Rieken 已提交
388
		this._editor.setSelections(newSelections);
J
Johannes Rieken 已提交
389
		this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());
J
Johannes Rieken 已提交
390 391
	}

J
Johannes Rieken 已提交
392
	prev(): void {
J
Johannes Rieken 已提交
393
		const newSelections = this._move(false);
J
Johannes Rieken 已提交
394
		this._editor.setSelections(newSelections);
J
Johannes Rieken 已提交
395
		this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());
J
Johannes Rieken 已提交
396 397
	}

398
	private _move(fwd: boolean | undefined): Selection[] {
J
Johannes Rieken 已提交
399 400
		const selections: Selection[] = [];
		for (const snippet of this._snippets) {
J
Johannes Rieken 已提交
401 402
			const oneSelection = snippet.move(fwd);
			selections.push(...oneSelection);
J
wip  
Johannes Rieken 已提交
403
		}
J
Johannes Rieken 已提交
404 405 406
		return selections;
	}

407 408 409 410
	get isAtFirstPlaceholder() {
		return this._snippets[0].isAtFirstPlaceholder;
	}

411 412
	get isAtLastPlaceholder() {
		return this._snippets[0].isAtLastPlaceholder;
J
wip  
Johannes Rieken 已提交
413
	}
J
Johannes Rieken 已提交
414

415 416 417 418
	get hasPlaceholder() {
		return this._snippets[0].hasPlaceholder;
	}

419 420 421 422
	get choice(): Choice {
		return this._snippets[0].choice;
	}

J
Johannes Rieken 已提交
423
	isSelectionWithinPlaceholders(): boolean {
424 425 426 427 428

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

J
Johannes Rieken 已提交
429 430
		const selections = this._editor.getSelections();
		if (selections.length < this._snippets.length) {
431 432 433
			// 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 已提交
434 435 436
			return false;
		}

J
Johannes Rieken 已提交
437 438
		let ranges: Range[] = [];
		let placeholderIndex: number = -1;
439
		for (const snippet of this._snippets) {
J
Johannes Rieken 已提交
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467

			const possibleSelections = snippet.computePossibleSelections();

			// for the first snippet find the placeholder (and its ranges)
			// that contain at least one selection. for all remaining snippets
			// the same placeholder (and their ranges) must be used.
			if (placeholderIndex < 0) {
				possibleSelections.forEach((ranges, index) => {
					if (placeholderIndex >= 0) {
						return;
					}
					ranges.sort(Range.compareRangesUsingStarts);
					for (const selection of selections) {
						if (ranges[0].containsRange(selection)) {
							placeholderIndex = index;
							break;
						}
					}
				});
			}

			if (placeholderIndex < 0) {
				// return false if we couldn't associate a selection to
				// this (the first) snippet
				return false;
			}

			ranges.push(...possibleSelections.get(placeholderIndex));
468 469
		}

J
Johannes Rieken 已提交
470 471 472 473
		if (selections.length !== ranges.length) {
			// this means we started at a placeholder with N
			// ranges and new have M (N > M) selections.
			// So (at least) one placeholder is without selection -> cancel
474 475 476
			return false;
		}

J
Johannes Rieken 已提交
477 478 479
		// also sort (placeholder)-ranges. then walk both arrays and
		// make sure the placeholder-ranges contain the corresponding
		// selection
480 481 482
		selections.sort(Range.compareRangesUsingStarts);
		ranges.sort(Range.compareRangesUsingStarts);

J
Johannes Rieken 已提交
483 484 485
		for (let i = 0; i < ranges.length; i++) {
			if (!ranges[i].containsRange(selections[i])) {
				return false;
J
Johannes Rieken 已提交
486 487 488 489 490
			}
		}

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