Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetSession.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { groupBy } from '../../../../base/common/arrays.js';6import { CharCode } from '../../../../base/common/charCode.js';7import { dispose } from '../../../../base/common/lifecycle.js';8import { getLeadingWhitespace } from '../../../../base/common/strings.js';9import './snippetSession.css';10import { IActiveCodeEditor } from '../../../browser/editorBrowser.js';11import { EditorOption } from '../../../common/config/editorOptions.js';12import { EditOperation, ISingleEditOperation } from '../../../common/core/editOperation.js';13import { IPosition } from '../../../common/core/position.js';14import { Range } from '../../../common/core/range.js';15import { Selection } from '../../../common/core/selection.js';16import { TextChange } from '../../../common/core/textChange.js';17import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';18import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness } from '../../../common/model.js';19import { ModelDecorationOptions } from '../../../common/model/textModel.js';20import { OvertypingCapturer } from '../../suggest/browser/suggestOvertypingCapturer.js';21import { ILabelService } from '../../../../platform/label/common/label.js';22import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';23import { Choice, Marker, Placeholder, SnippetParser, Text, TextmateSnippet } from './snippetParser.js';24import { ClipboardBasedVariableResolver, CommentBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, RandomBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from './snippetVariables.js';25import { EditSources, TextModelEditSource } from '../../../common/textModelEditSource.js';2627export class OneSnippet {2829private _placeholderDecorations?: Map<Placeholder, string>;30private _placeholderGroups: Placeholder[][];31private _offset: number = -1;32_placeholderGroupsIdx: number;33_nestingLevel: number = 1;3435private static readonly _decor = {36active: ModelDecorationOptions.register({ description: 'snippet-placeholder-1', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }),37inactive: ModelDecorationOptions.register({ description: 'snippet-placeholder-2', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }),38activeFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-3', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }),39inactiveFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-4', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }),40};4142constructor(43private readonly _editor: IActiveCodeEditor,44private readonly _snippet: TextmateSnippet,45private readonly _snippetLineLeadingWhitespace: string46) {47this._placeholderGroups = groupBy(_snippet.placeholders, Placeholder.compareByIndex);48this._placeholderGroupsIdx = -1;49}5051initialize(textChange: TextChange): void {52this._offset = textChange.newPosition;53}5455dispose(): void {56if (this._placeholderDecorations) {57this._editor.removeDecorations([...this._placeholderDecorations.values()]);58}59this._placeholderGroups.length = 0;60}6162private _initDecorations(): void {6364if (this._offset === -1) {65throw new Error(`Snippet not initialized!`);66}6768if (this._placeholderDecorations) {69// already initialized70return;71}7273this._placeholderDecorations = new Map<Placeholder, string>();74const model = this._editor.getModel();7576this._editor.changeDecorations(accessor => {77// create a decoration for each placeholder78for (const placeholder of this._snippet.placeholders) {79const placeholderOffset = this._snippet.offset(placeholder);80const placeholderLen = this._snippet.fullLen(placeholder);81const range = Range.fromPositions(82model.getPositionAt(this._offset + placeholderOffset),83model.getPositionAt(this._offset + placeholderOffset + placeholderLen)84);85const options = placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive;86const handle = accessor.addDecoration(range, options);87this._placeholderDecorations!.set(placeholder, handle);88}89});90}9192move(fwd: boolean | undefined): Selection[] {93if (!this._editor.hasModel()) {94return [];95}9697this._initDecorations();9899// Transform placeholder text if necessary100if (this._placeholderGroupsIdx >= 0) {101const operations: ISingleEditOperation[] = [];102103for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {104// Check if the placeholder has a transformation105if (placeholder.transform) {106const id = this._placeholderDecorations!.get(placeholder)!;107const range = this._editor.getModel().getDecorationRange(id)!;108const currentValue = this._editor.getModel().getValueInRange(range);109const transformedValueLines = placeholder.transform.resolve(currentValue).split(/\r\n|\r|\n/);110// fix indentation for transformed lines111for (let i = 1; i < transformedValueLines.length; i++) {112transformedValueLines[i] = this._editor.getModel().normalizeIndentation(this._snippetLineLeadingWhitespace + transformedValueLines[i]);113}114operations.push(EditOperation.replace(range, transformedValueLines.join(this._editor.getModel().getEOL())));115}116}117if (operations.length > 0) {118this._editor.executeEdits('snippet.placeholderTransform', operations);119}120}121122let couldSkipThisPlaceholder = false;123if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) {124this._placeholderGroupsIdx += 1;125couldSkipThisPlaceholder = true;126127} else if (fwd === false && this._placeholderGroupsIdx > 0) {128this._placeholderGroupsIdx -= 1;129couldSkipThisPlaceholder = true;130131} else {132// the selection of the current placeholder might133// not acurate any more -> simply restore it134}135136const newSelections = this._editor.getModel().changeDecorations(accessor => {137138const activePlaceholders = new Set<Placeholder>();139140// change stickiness to always grow when typing at its edges141// because these decorations represent the currently active142// tabstop.143// Special case #1: reaching the final tabstop144// Special case #2: placeholders enclosing active placeholders145const selections: Selection[] = [];146for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {147const id = this._placeholderDecorations!.get(placeholder)!;148const range = this._editor.getModel().getDecorationRange(id)!;149selections.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn));150151// consider to skip this placeholder index when the decoration152// range is empty but when the placeholder wasn't. that's a strong153// hint that the placeholder has been deleted. (all placeholder must match this)154couldSkipThisPlaceholder = couldSkipThisPlaceholder && this._hasPlaceholderBeenCollapsed(placeholder);155156accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);157activePlaceholders.add(placeholder);158159for (const enclosingPlaceholder of this._snippet.enclosingPlaceholders(placeholder)) {160const id = this._placeholderDecorations!.get(enclosingPlaceholder)!;161accessor.changeDecorationOptions(id, enclosingPlaceholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);162activePlaceholders.add(enclosingPlaceholder);163}164}165166// change stickness to never grow when typing at its edges167// so that in-active tabstops never grow168for (const [placeholder, id] of this._placeholderDecorations!) {169if (!activePlaceholders.has(placeholder)) {170accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive);171}172}173174return selections;175});176177return !couldSkipThisPlaceholder ? newSelections ?? [] : this.move(fwd);178}179180private _hasPlaceholderBeenCollapsed(placeholder: Placeholder): boolean {181// A placeholder is empty when it wasn't empty when authored but182// when its tracking decoration is empty. This also applies to all183// potential parent placeholders184let marker: Marker | undefined = placeholder;185while (marker) {186if (marker instanceof Placeholder) {187const id = this._placeholderDecorations!.get(marker)!;188const range = this._editor.getModel().getDecorationRange(id)!;189if (range.isEmpty() && marker.toString().length > 0) {190return true;191}192}193marker = marker.parent;194}195return false;196}197198get isAtFirstPlaceholder() {199return this._placeholderGroupsIdx <= 0 || this._placeholderGroups.length === 0;200}201202get isAtLastPlaceholder() {203return this._placeholderGroupsIdx === this._placeholderGroups.length - 1;204}205206get hasPlaceholder() {207return this._snippet.placeholders.length > 0;208}209210/**211* A snippet is trivial when it has no placeholder or only a final placeholder at212* its very end213*/214get isTrivialSnippet(): boolean {215if (this._snippet.placeholders.length === 0) {216return true;217}218if (this._snippet.placeholders.length === 1) {219const [placeholder] = this._snippet.placeholders;220if (placeholder.isFinalTabstop) {221if (this._snippet.rightMostDescendant === placeholder) {222return true;223}224}225}226return false;227}228229computePossibleSelections() {230const result = new Map<number, Range[]>();231for (const placeholdersWithEqualIndex of this._placeholderGroups) {232let ranges: Range[] | undefined;233234for (const placeholder of placeholdersWithEqualIndex) {235if (placeholder.isFinalTabstop) {236// ignore those237break;238}239240if (!ranges) {241ranges = [];242result.set(placeholder.index, ranges);243}244245const id = this._placeholderDecorations!.get(placeholder)!;246const range = this._editor.getModel().getDecorationRange(id);247if (!range) {248// one of the placeholder lost its decoration and249// therefore we bail out and pretend the placeholder250// (with its mirrors) doesn't exist anymore.251result.delete(placeholder.index);252break;253}254255ranges.push(range);256}257}258return result;259}260261get activeChoice(): { choice: Choice; range: Range } | undefined {262if (!this._placeholderDecorations) {263return undefined;264}265const placeholder = this._placeholderGroups[this._placeholderGroupsIdx][0];266if (!placeholder?.choice) {267return undefined;268}269const id = this._placeholderDecorations.get(placeholder);270if (!id) {271return undefined;272}273const range = this._editor.getModel().getDecorationRange(id);274if (!range) {275return undefined;276}277return { range, choice: placeholder.choice };278}279280get hasChoice(): boolean {281let result = false;282this._snippet.walk(marker => {283result = marker instanceof Choice;284return !result;285});286return result;287}288289merge(others: OneSnippet[]): void {290291const model = this._editor.getModel();292this._nestingLevel *= 10;293294this._editor.changeDecorations(accessor => {295296// For each active placeholder take one snippet and merge it297// in that the placeholder (can be many for `$1foo$1foo`). Because298// everything is sorted by editor selection we can simply remove299// elements from the beginning of the array300for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {301const nested = others.shift()!;302console.assert(nested._offset !== -1);303console.assert(!nested._placeholderDecorations);304305// Massage placeholder-indicies of the nested snippet to be306// sorted right after the insertion point. This ensures we move307// through the placeholders in the correct order308const indexLastPlaceholder = nested._snippet.placeholderInfo.last!.index;309310for (const nestedPlaceholder of nested._snippet.placeholderInfo.all) {311if (nestedPlaceholder.isFinalTabstop) {312nestedPlaceholder.index = placeholder.index + ((indexLastPlaceholder + 1) / this._nestingLevel);313} else {314nestedPlaceholder.index = placeholder.index + (nestedPlaceholder.index / this._nestingLevel);315}316}317this._snippet.replace(placeholder, nested._snippet.children);318319// Remove the placeholder at which position are inserting320// the snippet and also remove its decoration.321const id = this._placeholderDecorations!.get(placeholder)!;322accessor.removeDecoration(id);323this._placeholderDecorations!.delete(placeholder);324325// For each *new* placeholder we create decoration to monitor326// how and if it grows/shrinks.327for (const placeholder of nested._snippet.placeholders) {328const placeholderOffset = nested._snippet.offset(placeholder);329const placeholderLen = nested._snippet.fullLen(placeholder);330const range = Range.fromPositions(331model.getPositionAt(nested._offset + placeholderOffset),332model.getPositionAt(nested._offset + placeholderOffset + placeholderLen)333);334const handle = accessor.addDecoration(range, OneSnippet._decor.inactive);335this._placeholderDecorations!.set(placeholder, handle);336}337}338339// Last, re-create the placeholder groups by sorting placeholders by their index.340this._placeholderGroups = groupBy(this._snippet.placeholders, Placeholder.compareByIndex);341});342}343344getEnclosingRange(): Range | undefined {345let result: Range | undefined;346const model = this._editor.getModel();347for (const decorationId of this._placeholderDecorations!.values()) {348const placeholderRange = model.getDecorationRange(decorationId) ?? undefined;349if (!result) {350result = placeholderRange;351} else {352result = result.plusRange(placeholderRange!);353}354}355return result;356}357}358359export interface ISnippetSessionInsertOptions {360overwriteBefore: number;361overwriteAfter: number;362adjustWhitespace: boolean;363clipboardText: string | undefined;364overtypingCapturer: OvertypingCapturer | undefined;365}366367const _defaultOptions: ISnippetSessionInsertOptions = {368overwriteBefore: 0,369overwriteAfter: 0,370adjustWhitespace: true,371clipboardText: undefined,372overtypingCapturer: undefined373};374375export interface ISnippetEdit {376range: Range;377template: string;378keepWhitespace?: boolean;379}380381export class SnippetSession {382383static adjustWhitespace(model: ITextModel, position: IPosition, adjustIndentation: boolean, snippet: TextmateSnippet, filter?: Set<Marker>): string {384const line = model.getLineContent(position.lineNumber);385const lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1);386387// the snippet as inserted388let snippetTextString: string | undefined;389390snippet.walk(marker => {391// all text elements that are not inside choice392if (!(marker instanceof Text) || marker.parent instanceof Choice) {393return true;394}395396// check with filter (iff provided)397if (filter && !filter.has(marker)) {398return true;399}400401const lines = marker.value.split(/\r\n|\r|\n/);402403if (adjustIndentation) {404// adjust indentation of snippet test405// -the snippet-start doesn't get extra-indented (lineLeadingWhitespace), only normalized406// -all N+1 lines get extra-indented and normalized407// -the text start get extra-indented and normalized when following a linebreak408const offset = snippet.offset(marker);409if (offset === 0) {410// snippet start411lines[0] = model.normalizeIndentation(lines[0]);412413} else {414// check if text start is after a linebreak415snippetTextString = snippetTextString ?? snippet.toString();416const prevChar = snippetTextString.charCodeAt(offset - 1);417if (prevChar === CharCode.LineFeed || prevChar === CharCode.CarriageReturn) {418lines[0] = model.normalizeIndentation(lineLeadingWhitespace + lines[0]);419}420}421for (let i = 1; i < lines.length; i++) {422lines[i] = model.normalizeIndentation(lineLeadingWhitespace + lines[i]);423}424}425426const newValue = lines.join(model.getEOL());427if (newValue !== marker.value) {428marker.parent.replace(marker, [new Text(newValue)]);429snippetTextString = undefined;430}431return true;432});433434return lineLeadingWhitespace;435}436437static adjustSelection(model: ITextModel, selection: Selection, overwriteBefore: number, overwriteAfter: number): Selection {438if (overwriteBefore !== 0 || overwriteAfter !== 0) {439// overwrite[Before|After] is compute using the position, not the whole440// selection. therefore we adjust the selection around that position441const { positionLineNumber, positionColumn } = selection;442const positionColumnBefore = positionColumn - overwriteBefore;443const positionColumnAfter = positionColumn + overwriteAfter;444445const range = model.validateRange({446startLineNumber: positionLineNumber,447startColumn: positionColumnBefore,448endLineNumber: positionLineNumber,449endColumn: positionColumnAfter450});451452selection = Selection.createWithDirection(453range.startLineNumber, range.startColumn,454range.endLineNumber, range.endColumn,455selection.getDirection()456);457}458return selection;459}460461static createEditsAndSnippetsFromSelections(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined, languageConfigurationService: ILanguageConfigurationService): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } {462const edits: IIdentifiedSingleEditOperation[] = [];463const snippets: OneSnippet[] = [];464465if (!editor.hasModel()) {466return { edits, snippets };467}468const model = editor.getModel();469470const workspaceService = editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService));471const modelBasedVariableResolver = editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model));472const readClipboardText = () => clipboardText;473474// know what text the overwrite[Before|After] extensions475// of the primary cursor have selected because only when476// secondary selections extend to the same text we can grow them477const firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));478const firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter));479480// remember the first non-whitespace column to decide if481// `keepWhitespace` should be overruled for secondary selections482const firstLineFirstNonWhitespace = model.getLineFirstNonWhitespaceColumn(editor.getSelection().positionLineNumber);483484// sort selections by their start position but remeber485// the original index. that allows you to create correct486// offset-based selection logic without changing the487// primary selection488const indexedSelections = editor.getSelections()489.map((selection, idx) => ({ selection, idx }))490.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));491492for (const { selection, idx } of indexedSelections) {493494// extend selection with the `overwriteBefore` and `overwriteAfter` and then495// compare if this matches the extensions of the primary selection496let extensionBefore = SnippetSession.adjustSelection(model, selection, overwriteBefore, 0);497let extensionAfter = SnippetSession.adjustSelection(model, selection, 0, overwriteAfter);498if (firstBeforeText !== model.getValueInRange(extensionBefore)) {499extensionBefore = selection;500}501if (firstAfterText !== model.getValueInRange(extensionAfter)) {502extensionAfter = selection;503}504505// merge the before and after selection into one506const snippetSelection = selection507.setStartPosition(extensionBefore.startLineNumber, extensionBefore.startColumn)508.setEndPosition(extensionAfter.endLineNumber, extensionAfter.endColumn);509510const snippet = new SnippetParser().parse(template, true, enforceFinalTabstop);511512// adjust the template string to match the indentation and513// whitespace rules of this insert location (can be different for each cursor)514// happens when being asked for (default) or when this is a secondary515// cursor and the leading whitespace is different516const start = snippetSelection.getStartPosition();517const snippetLineLeadingWhitespace = SnippetSession.adjustWhitespace(518model, start,519adjustWhitespace || (idx > 0 && firstLineFirstNonWhitespace !== model.getLineFirstNonWhitespaceColumn(selection.positionLineNumber)),520snippet,521);522523snippet.resolveVariables(new CompositeSnippetVariableResolver([524modelBasedVariableResolver,525new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'),526new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer),527new CommentBasedVariableResolver(model, selection, languageConfigurationService),528new TimeBasedVariableResolver,529new WorkspaceBasedVariableResolver(workspaceService),530new RandomBasedVariableResolver,531]));532533// store snippets with the index of their originating selection.534// that ensures the primary cursor stays primary despite not being535// the one with lowest start position536edits[idx] = EditOperation.replace(snippetSelection, snippet.toString());537edits[idx].identifier = { major: idx, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors538edits[idx]._isTracked = true;539snippets[idx] = new OneSnippet(editor, snippet, snippetLineLeadingWhitespace);540}541542return { edits, snippets };543}544545static createEditsAndSnippetsFromEdits(editor: IActiveCodeEditor, snippetEdits: ISnippetEdit[], enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined, languageConfigurationService: ILanguageConfigurationService): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } {546547if (!editor.hasModel() || snippetEdits.length === 0) {548return { edits: [], snippets: [] };549}550551const edits: IIdentifiedSingleEditOperation[] = [];552const model = editor.getModel();553554const parser = new SnippetParser();555const snippet = new TextmateSnippet();556557// snippet variables resolver558const resolver = new CompositeSnippetVariableResolver([559editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model)),560new ClipboardBasedVariableResolver(() => clipboardText, 0, editor.getSelections().length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'),561new SelectionBasedVariableResolver(model, editor.getSelection(), 0, overtypingCapturer),562new CommentBasedVariableResolver(model, editor.getSelection(), languageConfigurationService),563new TimeBasedVariableResolver,564new WorkspaceBasedVariableResolver(editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService))),565new RandomBasedVariableResolver,566]);567568//569snippetEdits = snippetEdits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));570let offset = 0;571for (let i = 0; i < snippetEdits.length; i++) {572573const { range, template, keepWhitespace } = snippetEdits[i];574575// gaps between snippet edits are appended as text nodes. this576// ensures placeholder-offsets are later correct577if (i > 0) {578const lastRange = snippetEdits[i - 1].range;579const textRange = Range.fromPositions(lastRange.getEndPosition(), range.getStartPosition());580const textNode = new Text(model.getValueInRange(textRange));581snippet.appendChild(textNode);582offset += textNode.value.length;583}584585const newNodes = parser.parseFragment(template, snippet);586SnippetSession.adjustWhitespace(model, range.getStartPosition(), keepWhitespace !== undefined ? !keepWhitespace : adjustWhitespace, snippet, new Set(newNodes));587snippet.resolveVariables(resolver);588589const snippetText = snippet.toString();590const snippetFragmentText = snippetText.slice(offset);591offset = snippetText.length;592593// make edit594const edit: IIdentifiedSingleEditOperation = EditOperation.replace(range, snippetFragmentText);595edit.identifier = { major: i, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors596edit._isTracked = true;597edits.push(edit);598}599600//601parser.ensureFinalTabstop(snippet, enforceFinalTabstop, true);602603return {604edits,605snippets: [new OneSnippet(editor, snippet, '')]606};607}608609private readonly _templateMerges: [number, number, string | ISnippetEdit[]][] = [];610private _snippets: OneSnippet[] = [];611612constructor(613private readonly _editor: IActiveCodeEditor,614private readonly _template: string | ISnippetEdit[],615private readonly _options: ISnippetSessionInsertOptions = _defaultOptions,616@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService617) { }618619dispose(): void {620dispose(this._snippets);621}622623_logInfo(): string {624return `template="${this._template}", merged_templates="${this._templateMerges.join(' -> ')}"`;625}626627insert(editReason?: TextModelEditSource): void {628if (!this._editor.hasModel()) {629return;630}631632// make insert edit and start with first selections633const { edits, snippets } = typeof this._template === 'string'634? SnippetSession.createEditsAndSnippetsFromSelections(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService)635: SnippetSession.createEditsAndSnippetsFromEdits(this._editor, this._template, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService);636637this._snippets = snippets;638639this._editor.executeEdits(editReason ?? EditSources.snippet(), edits, _undoEdits => {640// Sometimes, the text buffer will remove automatic whitespace when doing any edits,641// so we need to look only at the undo edits relevant for us.642// Our edits have an identifier set so that's how we can distinguish them643const undoEdits = _undoEdits.filter(edit => !!edit.identifier);644for (let idx = 0; idx < snippets.length; idx++) {645snippets[idx].initialize(undoEdits[idx].textChange);646}647648if (this._snippets[0].hasPlaceholder) {649return this._move(true);650} else {651return undoEdits652.map(edit => Selection.fromPositions(edit.range.getEndPosition()));653}654});655this._editor.revealRange(this._editor.getSelections()[0]);656}657658merge(template: string, options: ISnippetSessionInsertOptions = _defaultOptions): void {659if (!this._editor.hasModel()) {660return;661}662this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]);663const { edits, snippets } = SnippetSession.createEditsAndSnippetsFromSelections(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer, this._languageConfigurationService);664665this._editor.executeEdits('snippet', edits, _undoEdits => {666// Sometimes, the text buffer will remove automatic whitespace when doing any edits,667// so we need to look only at the undo edits relevant for us.668// Our edits have an identifier set so that's how we can distinguish them669const undoEdits = _undoEdits.filter(edit => !!edit.identifier);670for (let idx = 0; idx < snippets.length; idx++) {671snippets[idx].initialize(undoEdits[idx].textChange);672}673674// Trivial snippets have no placeholder or are just the final placeholder. That means they675// are just text insertions and we don't need to merge the nested snippet into the existing676// snippet677const isTrivialSnippet = snippets[0].isTrivialSnippet;678if (!isTrivialSnippet) {679for (const snippet of this._snippets) {680snippet.merge(snippets);681}682console.assert(snippets.length === 0);683}684685if (this._snippets[0].hasPlaceholder && !isTrivialSnippet) {686return this._move(undefined);687} else {688return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));689}690});691}692693next(): void {694const newSelections = this._move(true);695this._editor.setSelections(newSelections);696this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());697}698699prev(): void {700const newSelections = this._move(false);701this._editor.setSelections(newSelections);702this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());703}704705private _move(fwd: boolean | undefined): Selection[] {706const selections: Selection[] = [];707for (const snippet of this._snippets) {708const oneSelection = snippet.move(fwd);709selections.push(...oneSelection);710}711return selections;712}713714get isAtFirstPlaceholder() {715return this._snippets[0].isAtFirstPlaceholder;716}717718get isAtLastPlaceholder() {719return this._snippets[0].isAtLastPlaceholder;720}721722get hasPlaceholder() {723return this._snippets[0].hasPlaceholder;724}725726get hasChoice(): boolean {727return this._snippets[0].hasChoice;728}729730get activeChoice(): { choice: Choice; range: Range } | undefined {731return this._snippets[0].activeChoice;732}733734isSelectionWithinPlaceholders(): boolean {735736if (!this.hasPlaceholder) {737return false;738}739740const selections = this._editor.getSelections();741if (selections.length < this._snippets.length) {742// this means we started snippet mode with N743// selections and have M (N > M) selections.744// So one snippet is without selection -> cancel745return false;746}747748const allPossibleSelections = new Map<number, Range[]>();749for (const snippet of this._snippets) {750751const possibleSelections = snippet.computePossibleSelections();752753// for the first snippet find the placeholder (and its ranges)754// that contain at least one selection. for all remaining snippets755// the same placeholder (and their ranges) must be used.756if (allPossibleSelections.size === 0) {757for (const [index, ranges] of possibleSelections) {758ranges.sort(Range.compareRangesUsingStarts);759for (const selection of selections) {760if (ranges[0].containsRange(selection)) {761allPossibleSelections.set(index, []);762break;763}764}765}766}767768if (allPossibleSelections.size === 0) {769// return false if we couldn't associate a selection to770// this (the first) snippet771return false;772}773774// add selections from 'this' snippet so that we know all775// selections for this placeholder776allPossibleSelections.forEach((array, index) => {777array.push(...possibleSelections.get(index)!);778});779}780781// sort selections (and later placeholder-ranges). then walk both782// arrays and make sure the placeholder-ranges contain the corresponding783// selection784selections.sort(Range.compareRangesUsingStarts);785786for (const [index, ranges] of allPossibleSelections) {787if (ranges.length !== selections.length) {788allPossibleSelections.delete(index);789continue;790}791792ranges.sort(Range.compareRangesUsingStarts);793794for (let i = 0; i < ranges.length; i++) {795if (!ranges[i].containsRange(selections[i])) {796allPossibleSelections.delete(index);797continue;798}799}800}801802// from all possible selections we have deleted those803// that don't match with the current selection. if we don't804// have any left, we don't have a selection anymore805return allPossibleSelections.size > 0;806}807808public getEnclosingRange(): Range | undefined {809let result: Range | undefined;810for (const snippet of this._snippets) {811const snippetRange = snippet.getEnclosingRange();812if (!result) {813result = snippetRange;814} else {815result = result.plusRange(snippetRange!);816}817}818return result;819}820}821822823