monaco-editor-core
Version:
A browser based code editor
651 lines (650 loc) • 35.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var SnippetSession_1;
import { groupBy } from '../../../../base/common/arrays.js';
import { dispose } from '../../../../base/common/lifecycle.js';
import { getLeadingWhitespace } from '../../../../base/common/strings.js';
import './snippetSession.css';
import { EditOperation } from '../../../common/core/editOperation.js';
import { Range } from '../../../common/core/range.js';
import { Selection } from '../../../common/core/selection.js';
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { Choice, Placeholder, SnippetParser, Text, TextmateSnippet } from './snippetParser.js';
import { ClipboardBasedVariableResolver, CommentBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, RandomBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from './snippetVariables.js';
export class OneSnippet {
static { this._decor = {
active: ModelDecorationOptions.register({ description: 'snippet-placeholder-1', stickiness: 0 /* TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }),
inactive: ModelDecorationOptions.register({ description: 'snippet-placeholder-2', stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }),
activeFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-3', stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }),
inactiveFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-4', stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }),
}; }
constructor(_editor, _snippet, _snippetLineLeadingWhitespace) {
this._editor = _editor;
this._snippet = _snippet;
this._snippetLineLeadingWhitespace = _snippetLineLeadingWhitespace;
this._offset = -1;
this._nestingLevel = 1;
this._placeholderGroups = groupBy(_snippet.placeholders, Placeholder.compareByIndex);
this._placeholderGroupsIdx = -1;
}
initialize(textChange) {
this._offset = textChange.newPosition;
}
dispose() {
if (this._placeholderDecorations) {
this._editor.removeDecorations([...this._placeholderDecorations.values()]);
}
this._placeholderGroups.length = 0;
}
_initDecorations() {
if (this._offset === -1) {
throw new Error(`Snippet not initialized!`);
}
if (this._placeholderDecorations) {
// already initialized
return;
}
this._placeholderDecorations = new Map();
const model = this._editor.getModel();
this._editor.changeDecorations(accessor => {
// create a decoration for each placeholder
for (const placeholder of this._snippet.placeholders) {
const placeholderOffset = this._snippet.offset(placeholder);
const placeholderLen = this._snippet.fullLen(placeholder);
const range = Range.fromPositions(model.getPositionAt(this._offset + placeholderOffset), model.getPositionAt(this._offset + placeholderOffset + placeholderLen));
const options = placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive;
const handle = accessor.addDecoration(range, options);
this._placeholderDecorations.set(placeholder, handle);
}
});
}
move(fwd) {
if (!this._editor.hasModel()) {
return [];
}
this._initDecorations();
// Transform placeholder text if necessary
if (this._placeholderGroupsIdx >= 0) {
const operations = [];
for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
// Check if the placeholder has a transformation
if (placeholder.transform) {
const id = this._placeholderDecorations.get(placeholder);
const range = this._editor.getModel().getDecorationRange(id);
const currentValue = this._editor.getModel().getValueInRange(range);
const transformedValueLines = placeholder.transform.resolve(currentValue).split(/\r\n|\r|\n/);
// fix indentation for transformed lines
for (let i = 1; i < transformedValueLines.length; i++) {
transformedValueLines[i] = this._editor.getModel().normalizeIndentation(this._snippetLineLeadingWhitespace + transformedValueLines[i]);
}
operations.push(EditOperation.replace(range, transformedValueLines.join(this._editor.getModel().getEOL())));
}
}
if (operations.length > 0) {
this._editor.executeEdits('snippet.placeholderTransform', operations);
}
}
let couldSkipThisPlaceholder = false;
if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) {
this._placeholderGroupsIdx += 1;
couldSkipThisPlaceholder = true;
}
else if (fwd === false && this._placeholderGroupsIdx > 0) {
this._placeholderGroupsIdx -= 1;
couldSkipThisPlaceholder = true;
}
else {
// the selection of the current placeholder might
// not acurate any more -> simply restore it
}
const newSelections = this._editor.getModel().changeDecorations(accessor => {
const activePlaceholders = new Set();
// change stickiness to always grow when typing at its edges
// because these decorations represent the currently active
// tabstop.
// Special case #1: reaching the final tabstop
// Special case #2: placeholders enclosing active placeholders
const selections = [];
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));
// consider to skip this placeholder index when the decoration
// range is empty but when the placeholder wasn't. that's a strong
// hint that the placeholder has been deleted. (all placeholder must match this)
couldSkipThisPlaceholder = couldSkipThisPlaceholder && this._hasPlaceholderBeenCollapsed(placeholder);
accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);
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);
}
}
// change stickness to never grow when typing at its edges
// so that in-active tabstops never grow
for (const [placeholder, id] of this._placeholderDecorations) {
if (!activePlaceholders.has(placeholder)) {
accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive);
}
}
return selections;
});
return !couldSkipThisPlaceholder ? newSelections ?? [] : this.move(fwd);
}
_hasPlaceholderBeenCollapsed(placeholder) {
// A placeholder is empty when it wasn't empty when authored but
// when its tracking decoration is empty. This also applies to all
// potential parent placeholders
let marker = placeholder;
while (marker) {
if (marker instanceof Placeholder) {
const id = this._placeholderDecorations.get(marker);
const range = this._editor.getModel().getDecorationRange(id);
if (range.isEmpty() && marker.toString().length > 0) {
return true;
}
}
marker = marker.parent;
}
return false;
}
get isAtFirstPlaceholder() {
return this._placeholderGroupsIdx <= 0 || this._placeholderGroups.length === 0;
}
get isAtLastPlaceholder() {
return this._placeholderGroupsIdx === this._placeholderGroups.length - 1;
}
get hasPlaceholder() {
return this._snippet.placeholders.length > 0;
}
/**
* A snippet is trivial when it has no placeholder or only a final placeholder at
* its very end
*/
get isTrivialSnippet() {
if (this._snippet.placeholders.length === 0) {
return true;
}
if (this._snippet.placeholders.length === 1) {
const [placeholder] = this._snippet.placeholders;
if (placeholder.isFinalTabstop) {
if (this._snippet.rightMostDescendant === placeholder) {
return true;
}
}
}
return false;
}
computePossibleSelections() {
const result = new Map();
for (const placeholdersWithEqualIndex of this._placeholderGroups) {
let ranges;
for (const placeholder of placeholdersWithEqualIndex) {
if (placeholder.isFinalTabstop) {
// ignore those
break;
}
if (!ranges) {
ranges = [];
result.set(placeholder.index, ranges);
}
const id = this._placeholderDecorations.get(placeholder);
const range = this._editor.getModel().getDecorationRange(id);
if (!range) {
// one of the placeholder lost its decoration and
// therefore we bail out and pretend the placeholder
// (with its mirrors) doesn't exist anymore.
result.delete(placeholder.index);
break;
}
ranges.push(range);
}
}
return result;
}
get activeChoice() {
if (!this._placeholderDecorations) {
return undefined;
}
const placeholder = this._placeholderGroups[this._placeholderGroupsIdx][0];
if (!placeholder?.choice) {
return undefined;
}
const id = this._placeholderDecorations.get(placeholder);
if (!id) {
return undefined;
}
const range = this._editor.getModel().getDecorationRange(id);
if (!range) {
return undefined;
}
return { range, choice: placeholder.choice };
}
get hasChoice() {
let result = false;
this._snippet.walk(marker => {
result = marker instanceof Choice;
return !result;
});
return result;
}
merge(others) {
const model = this._editor.getModel();
this._nestingLevel *= 10;
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._offset !== -1);
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
const indexLastPlaceholder = nested._snippet.placeholderInfo.last.index;
for (const nestedPlaceholder of nested._snippet.placeholderInfo.all) {
if (nestedPlaceholder.isFinalTabstop) {
nestedPlaceholder.index = placeholder.index + ((indexLastPlaceholder + 1) / this._nestingLevel);
}
else {
nestedPlaceholder.index = placeholder.index + (nestedPlaceholder.index / this._nestingLevel);
}
}
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);
});
}
}
const _defaultOptions = {
overwriteBefore: 0,
overwriteAfter: 0,
adjustWhitespace: true,
clipboardText: undefined,
overtypingCapturer: undefined
};
let SnippetSession = SnippetSession_1 = class SnippetSession {
static adjustWhitespace(model, position, adjustIndentation, snippet, filter) {
const line = model.getLineContent(position.lineNumber);
const lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1);
// the snippet as inserted
let snippetTextString;
snippet.walk(marker => {
// all text elements that are not inside choice
if (!(marker instanceof Text) || marker.parent instanceof Choice) {
return true;
}
// check with filter (iff provided)
if (filter && !filter.has(marker)) {
return true;
}
const lines = marker.value.split(/\r\n|\r|\n/);
if (adjustIndentation) {
// adjust indentation of snippet test
// -the snippet-start doesn't get extra-indented (lineLeadingWhitespace), only normalized
// -all N+1 lines get extra-indented and normalized
// -the text start get extra-indented and normalized when following a linebreak
const offset = snippet.offset(marker);
if (offset === 0) {
// snippet start
lines[0] = model.normalizeIndentation(lines[0]);
}
else {
// check if text start is after a linebreak
snippetTextString = snippetTextString ?? snippet.toString();
const prevChar = snippetTextString.charCodeAt(offset - 1);
if (prevChar === 10 /* CharCode.LineFeed */ || prevChar === 13 /* CharCode.CarriageReturn */) {
lines[0] = model.normalizeIndentation(lineLeadingWhitespace + lines[0]);
}
}
for (let i = 1; i < lines.length; i++) {
lines[i] = model.normalizeIndentation(lineLeadingWhitespace + lines[i]);
}
}
const newValue = lines.join(model.getEOL());
if (newValue !== marker.value) {
marker.parent.replace(marker, [new Text(newValue)]);
snippetTextString = undefined;
}
return true;
});
return lineLeadingWhitespace;
}
static adjustSelection(model, selection, overwriteBefore, overwriteAfter) {
if (overwriteBefore !== 0 || overwriteAfter !== 0) {
// 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
});
selection = Selection.createWithDirection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn, selection.getDirection());
}
return selection;
}
static createEditsAndSnippetsFromSelections(editor, template, overwriteBefore, overwriteAfter, enforceFinalTabstop, adjustWhitespace, clipboardText, overtypingCapturer, languageConfigurationService) {
const edits = [];
const snippets = [];
if (!editor.hasModel()) {
return { edits, snippets };
}
const model = editor.getModel();
const workspaceService = editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService));
const modelBasedVariableResolver = editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model));
const readClipboardText = () => clipboardText;
// 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
const firstBeforeText = model.getValueInRange(SnippetSession_1.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));
const firstAfterText = model.getValueInRange(SnippetSession_1.adjustSelection(model, editor.getSelection(), 0, overwriteAfter));
// remember the first non-whitespace column to decide if
// `keepWhitespace` should be overruled for secondary selections
const firstLineFirstNonWhitespace = model.getLineFirstNonWhitespaceColumn(editor.getSelection().positionLineNumber);
// 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
const indexedSelections = editor.getSelections()
.map((selection, idx) => ({ selection, idx }))
.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
for (const { selection, idx } of indexedSelections) {
// extend selection with the `overwriteBefore` and `overwriteAfter` and then
// compare if this matches the extensions of the primary selection
let extensionBefore = SnippetSession_1.adjustSelection(model, selection, overwriteBefore, 0);
let extensionAfter = SnippetSession_1.adjustSelection(model, selection, 0, overwriteAfter);
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);
const snippet = new SnippetParser().parse(template, true, enforceFinalTabstop);
// adjust the template string to match the indentation and
// whitespace rules of this insert location (can be different for each cursor)
// happens when being asked for (default) or when this is a secondary
// cursor and the leading whitespace is different
const start = snippetSelection.getStartPosition();
const snippetLineLeadingWhitespace = SnippetSession_1.adjustWhitespace(model, start, adjustWhitespace || (idx > 0 && firstLineFirstNonWhitespace !== model.getLineFirstNonWhitespaceColumn(selection.positionLineNumber)), snippet);
snippet.resolveVariables(new CompositeSnippetVariableResolver([
modelBasedVariableResolver,
new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(79 /* EditorOption.multiCursorPaste */) === 'spread'),
new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer),
new CommentBasedVariableResolver(model, selection, languageConfigurationService),
new TimeBasedVariableResolver,
new WorkspaceBasedVariableResolver(workspaceService),
new RandomBasedVariableResolver,
]));
// 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
edits[idx] = EditOperation.replace(snippetSelection, snippet.toString());
edits[idx].identifier = { major: idx, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors
edits[idx]._isTracked = true;
snippets[idx] = new OneSnippet(editor, snippet, snippetLineLeadingWhitespace);
}
return { edits, snippets };
}
static createEditsAndSnippetsFromEdits(editor, snippetEdits, enforceFinalTabstop, adjustWhitespace, clipboardText, overtypingCapturer, languageConfigurationService) {
if (!editor.hasModel() || snippetEdits.length === 0) {
return { edits: [], snippets: [] };
}
const edits = [];
const model = editor.getModel();
const parser = new SnippetParser();
const snippet = new TextmateSnippet();
// snippet variables resolver
const resolver = new CompositeSnippetVariableResolver([
editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model)),
new ClipboardBasedVariableResolver(() => clipboardText, 0, editor.getSelections().length, editor.getOption(79 /* EditorOption.multiCursorPaste */) === 'spread'),
new SelectionBasedVariableResolver(model, editor.getSelection(), 0, overtypingCapturer),
new CommentBasedVariableResolver(model, editor.getSelection(), languageConfigurationService),
new TimeBasedVariableResolver,
new WorkspaceBasedVariableResolver(editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService))),
new RandomBasedVariableResolver,
]);
//
snippetEdits = snippetEdits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
let offset = 0;
for (let i = 0; i < snippetEdits.length; i++) {
const { range, template } = snippetEdits[i];
// gaps between snippet edits are appended as text nodes. this
// ensures placeholder-offsets are later correct
if (i > 0) {
const lastRange = snippetEdits[i - 1].range;
const textRange = Range.fromPositions(lastRange.getEndPosition(), range.getStartPosition());
const textNode = new Text(model.getValueInRange(textRange));
snippet.appendChild(textNode);
offset += textNode.value.length;
}
const newNodes = parser.parseFragment(template, snippet);
SnippetSession_1.adjustWhitespace(model, range.getStartPosition(), true, snippet, new Set(newNodes));
snippet.resolveVariables(resolver);
const snippetText = snippet.toString();
const snippetFragmentText = snippetText.slice(offset);
offset = snippetText.length;
// make edit
const edit = EditOperation.replace(range, snippetFragmentText);
edit.identifier = { major: i, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors
edit._isTracked = true;
edits.push(edit);
}
//
parser.ensureFinalTabstop(snippet, enforceFinalTabstop, true);
return {
edits,
snippets: [new OneSnippet(editor, snippet, '')]
};
}
constructor(_editor, _template, _options = _defaultOptions, _languageConfigurationService) {
this._editor = _editor;
this._template = _template;
this._options = _options;
this._languageConfigurationService = _languageConfigurationService;
this._templateMerges = [];
this._snippets = [];
}
dispose() {
dispose(this._snippets);
}
_logInfo() {
return `template="${this._template}", merged_templates="${this._templateMerges.join(' -> ')}"`;
}
insert() {
if (!this._editor.hasModel()) {
return;
}
// make insert edit and start with first selections
const { edits, snippets } = typeof this._template === 'string'
? SnippetSession_1.createEditsAndSnippetsFromSelections(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService)
: SnippetSession_1.createEditsAndSnippetsFromEdits(this._editor, this._template, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService);
this._snippets = snippets;
this._editor.executeEdits('snippet', edits, _undoEdits => {
// Sometimes, the text buffer will remove automatic whitespace when doing any edits,
// so we need to look only at the undo edits relevant for us.
// Our edits have an identifier set so that's how we can distinguish them
const undoEdits = _undoEdits.filter(edit => !!edit.identifier);
for (let idx = 0; idx < snippets.length; idx++) {
snippets[idx].initialize(undoEdits[idx].textChange);
}
if (this._snippets[0].hasPlaceholder) {
return this._move(true);
}
else {
return undoEdits
.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
}
});
this._editor.revealRange(this._editor.getSelections()[0]);
}
merge(template, options = _defaultOptions) {
if (!this._editor.hasModel()) {
return;
}
this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]);
const { edits, snippets } = SnippetSession_1.createEditsAndSnippetsFromSelections(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer, this._languageConfigurationService);
this._editor.executeEdits('snippet', edits, _undoEdits => {
// Sometimes, the text buffer will remove automatic whitespace when doing any edits,
// so we need to look only at the undo edits relevant for us.
// Our edits have an identifier set so that's how we can distinguish them
const undoEdits = _undoEdits.filter(edit => !!edit.identifier);
for (let idx = 0; idx < snippets.length; idx++) {
snippets[idx].initialize(undoEdits[idx].textChange);
}
// Trivial snippets have no placeholder or are just the final placeholder. That means they
// are just text insertions and we don't need to merge the nested snippet into the existing
// snippet
const isTrivialSnippet = snippets[0].isTrivialSnippet;
if (!isTrivialSnippet) {
for (const snippet of this._snippets) {
snippet.merge(snippets);
}
console.assert(snippets.length === 0);
}
if (this._snippets[0].hasPlaceholder && !isTrivialSnippet) {
return this._move(undefined);
}
else {
return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
}
});
}
next() {
const newSelections = this._move(true);
this._editor.setSelections(newSelections);
this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());
}
prev() {
const newSelections = this._move(false);
this._editor.setSelections(newSelections);
this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());
}
_move(fwd) {
const selections = [];
for (const snippet of this._snippets) {
const oneSelection = snippet.move(fwd);
selections.push(...oneSelection);
}
return selections;
}
get isAtFirstPlaceholder() {
return this._snippets[0].isAtFirstPlaceholder;
}
get isAtLastPlaceholder() {
return this._snippets[0].isAtLastPlaceholder;
}
get hasPlaceholder() {
return this._snippets[0].hasPlaceholder;
}
get hasChoice() {
return this._snippets[0].hasChoice;
}
get activeChoice() {
return this._snippets[0].activeChoice;
}
isSelectionWithinPlaceholders() {
if (!this.hasPlaceholder) {
return false;
}
const selections = this._editor.getSelections();
if (selections.length < this._snippets.length) {
// this means we started snippet mode with N
// selections and have M (N > M) selections.
// So one snippet is without selection -> cancel
return false;
}
const allPossibleSelections = new Map();
for (const snippet of this._snippets) {
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 (allPossibleSelections.size === 0) {
for (const [index, ranges] of possibleSelections) {
ranges.sort(Range.compareRangesUsingStarts);
for (const selection of selections) {
if (ranges[0].containsRange(selection)) {
allPossibleSelections.set(index, []);
break;
}
}
}
}
if (allPossibleSelections.size === 0) {
// return false if we couldn't associate a selection to
// this (the first) snippet
return false;
}
// add selections from 'this' snippet so that we know all
// selections for this placeholder
allPossibleSelections.forEach((array, index) => {
array.push(...possibleSelections.get(index));
});
}
// sort selections (and later placeholder-ranges). then walk both
// arrays and make sure the placeholder-ranges contain the corresponding
// selection
selections.sort(Range.compareRangesUsingStarts);
for (const [index, ranges] of allPossibleSelections) {
if (ranges.length !== selections.length) {
allPossibleSelections.delete(index);
continue;
}
ranges.sort(Range.compareRangesUsingStarts);
for (let i = 0; i < ranges.length; i++) {
if (!ranges[i].containsRange(selections[i])) {
allPossibleSelections.delete(index);
continue;
}
}
}
// from all possible selections we have deleted those
// that don't match with the current selection. if we don't
// have any left, we don't have a selection anymore
return allPossibleSelections.size > 0;
}
};
SnippetSession = SnippetSession_1 = __decorate([
__param(3, ILanguageConfigurationService)
], SnippetSession);
export { SnippetSession };