UNPKG

monaco-editor

Version:
522 lines (521 loc) • 29.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 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 { ModelDecorationOptions } from '../../common/model/textModel.js'; import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js'; import { optional } from '../../../platform/instantiation/common/instantiation.js'; import { Choice, Placeholder, SnippetParser, Text } from './snippetParser.js'; import { ClipboardBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver } from './snippetVariables.js'; import { registerThemingParticipant } from '../../../platform/theme/common/themeService.js'; import * as colors from '../../../platform/theme/common/colorRegistry.js'; registerThemingParticipant(function (theme, collector) { function getColorGraceful(name) { var color = theme.getColor(name); return color ? color.toString() : 'transparent'; } collector.addRule(".monaco-editor .snippet-placeholder { background-color: " + getColorGraceful(colors.snippetTabstopHighlightBackground) + "; outline-color: " + getColorGraceful(colors.snippetTabstopHighlightBorder) + "; }"); collector.addRule(".monaco-editor .finish-snippet-placeholder { background-color: " + getColorGraceful(colors.snippetFinalTabstopHighlightBackground) + "; outline-color: " + getColorGraceful(colors.snippetFinalTabstopHighlightBorder) + "; }"); }); var OneSnippet = /** @class */ (function () { function OneSnippet(editor, snippet, offset) { this._nestingLevel = 1; this._editor = editor; this._snippet = snippet; this._offset = offset; this._placeholderGroups = groupBy(snippet.placeholders, Placeholder.compareByIndex); this._placeholderGroupsIdx = -1; } OneSnippet.prototype.dispose = function () { if (this._placeholderDecorations) { var toRemove_1 = []; this._placeholderDecorations.forEach(function (handle) { return toRemove_1.push(handle); }); this._editor.deltaDecorations(toRemove_1, []); } this._placeholderGroups.length = 0; }; OneSnippet.prototype._initDecorations = function () { var _this = this; if (this._placeholderDecorations) { // already initialized return; } this._placeholderDecorations = new Map(); var model = this._editor.getModel(); this._editor.changeDecorations(function (accessor) { // create a decoration for each placeholder for (var _i = 0, _a = _this._snippet.placeholders; _i < _a.length; _i++) { var placeholder = _a[_i]; var placeholderOffset = _this._snippet.offset(placeholder); var placeholderLen = _this._snippet.fullLen(placeholder); var range = Range.fromPositions(model.getPositionAt(_this._offset + placeholderOffset), model.getPositionAt(_this._offset + placeholderOffset + placeholderLen)); var options = placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive; var handle = accessor.addDecoration(range, options); _this._placeholderDecorations.set(placeholder, handle); } }); }; OneSnippet.prototype.move = function (fwd) { var _this = this; this._initDecorations(); // Transform placeholder text if necessary if (this._placeholderGroupsIdx >= 0) { var operations = []; for (var _i = 0, _a = this._placeholderGroups[this._placeholderGroupsIdx]; _i < _a.length; _i++) { var placeholder = _a[_i]; // Check if the placeholder has a transformation if (placeholder.transform) { var id = this._placeholderDecorations.get(placeholder); var range = this._editor.getModel().getDecorationRange(id); var currentValue = this._editor.getModel().getValueInRange(range); operations.push(EditOperation.replaceMove(range, placeholder.transform.resolve(currentValue))); } } if (operations.length > 0) { this._editor.executeEdits('snippet.placeholderTransform', operations); } } var skipThisPlaceholder = false; if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) { this._placeholderGroupsIdx += 1; skipThisPlaceholder = true; } else if (fwd === false && this._placeholderGroupsIdx > 0) { this._placeholderGroupsIdx -= 1; skipThisPlaceholder = true; } else { // the selection of the current placeholder might // not acurate any more -> simply restore it } var newSelections = this._editor.getModel().changeDecorations(function (accessor) { var 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 var selections = []; for (var _i = 0, _a = _this._placeholderGroups[_this._placeholderGroupsIdx]; _i < _a.length; _i++) { var placeholder = _a[_i]; var id = _this._placeholderDecorations.get(placeholder); var 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) skipThisPlaceholder = skipThisPlaceholder && (range.isEmpty() && placeholder.toString().length > 0); accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active); activePlaceholders.add(placeholder); for (var _b = 0, _c = _this._snippet.enclosingPlaceholders(placeholder); _b < _c.length; _b++) { var enclosingPlaceholder = _c[_b]; var id_1 = _this._placeholderDecorations.get(enclosingPlaceholder); accessor.changeDecorationOptions(id_1, 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 _this._placeholderDecorations.forEach(function (id, placeholder) { if (!activePlaceholders.has(placeholder)) { accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive); } }); return selections; }); return !skipThisPlaceholder ? newSelections : this.move(fwd); }; Object.defineProperty(OneSnippet.prototype, "isAtFirstPlaceholder", { get: function () { return this._placeholderGroupsIdx <= 0 || this._placeholderGroups.length === 0; }, enumerable: true, configurable: true }); Object.defineProperty(OneSnippet.prototype, "isAtLastPlaceholder", { get: function () { return this._placeholderGroupsIdx === this._placeholderGroups.length - 1; }, enumerable: true, configurable: true }); Object.defineProperty(OneSnippet.prototype, "hasPlaceholder", { get: function () { return this._snippet.placeholders.length > 0; }, enumerable: true, configurable: true }); OneSnippet.prototype.computePossibleSelections = function () { var result = new Map(); for (var _i = 0, _a = this._placeholderGroups; _i < _a.length; _i++) { var placeholdersWithEqualIndex = _a[_i]; var ranges = void 0; for (var _b = 0, placeholdersWithEqualIndex_1 = placeholdersWithEqualIndex; _b < placeholdersWithEqualIndex_1.length; _b++) { var placeholder = placeholdersWithEqualIndex_1[_b]; if (placeholder.isFinalTabstop) { // ignore those break; } if (!ranges) { ranges = []; result.set(placeholder.index, ranges); } var id = this._placeholderDecorations.get(placeholder); var 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; }; Object.defineProperty(OneSnippet.prototype, "choice", { get: function () { return this._placeholderGroups[this._placeholderGroupsIdx][0].choice; }, enumerable: true, configurable: true }); OneSnippet.prototype.merge = function (others) { var _this = this; var model = this._editor.getModel(); this._nestingLevel *= 10; this._editor.changeDecorations(function (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 (var _i = 0, _a = _this._placeholderGroups[_this._placeholderGroupsIdx]; _i < _a.length; _i++) { var placeholder = _a[_i]; var 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 var indexLastPlaceholder = nested._snippet.placeholderInfo.last.index; for (var _b = 0, _c = nested._snippet.placeholderInfo.all; _b < _c.length; _b++) { var nestedPlaceholder = _c[_b]; 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. var 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 (var _d = 0, _e = nested._snippet.placeholders; _d < _e.length; _d++) { var placeholder_1 = _e[_d]; var placeholderOffset = nested._snippet.offset(placeholder_1); var placeholderLen = nested._snippet.fullLen(placeholder_1); var range = Range.fromPositions(model.getPositionAt(nested._offset + placeholderOffset), model.getPositionAt(nested._offset + placeholderOffset + placeholderLen)); var handle = accessor.addDecoration(range, OneSnippet._decor.inactive); _this._placeholderDecorations.set(placeholder_1, handle); } } // Last, re-create the placeholder groups by sorting placeholders by their index. _this._placeholderGroups = groupBy(_this._snippet.placeholders, Placeholder.compareByIndex); }); }; OneSnippet._decor = { active: ModelDecorationOptions.register({ stickiness: 0 /* AlwaysGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }), inactive: ModelDecorationOptions.register({ stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }), activeFinal: ModelDecorationOptions.register({ stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }), inactiveFinal: ModelDecorationOptions.register({ stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }), }; return OneSnippet; }()); export { OneSnippet }; var SnippetSession = /** @class */ (function () { function SnippetSession(editor, template, overwriteBefore, overwriteAfter, adjustWhitespace) { if (overwriteBefore === void 0) { overwriteBefore = 0; } if (overwriteAfter === void 0) { overwriteAfter = 0; } if (adjustWhitespace === void 0) { adjustWhitespace = true; } this._templateMerges = []; this._snippets = []; this._editor = editor; this._template = template; this._overwriteBefore = overwriteBefore; this._overwriteAfter = overwriteAfter; this._adjustWhitespace = adjustWhitespace; } SnippetSession.adjustWhitespace = function (model, position, snippet) { var line = model.getLineContent(position.lineNumber); var lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1); snippet.walk(function (marker) { if (marker instanceof Text && !(marker.parent instanceof Choice)) { // adjust indentation of text markers, except for choise elements // which get adjusted when being selected var lines = marker.value.split(/\r\n|\r|\n/); for (var i = 1; i < lines.length; i++) { var templateLeadingWhitespace = getLeadingWhitespace(lines[i]); lines[i] = model.normalizeIndentation(lineLeadingWhitespace + templateLeadingWhitespace) + lines[i].substr(templateLeadingWhitespace.length); } var newValue = lines.join(model.getEOL()); if (newValue !== marker.value) { marker.parent.replace(marker, [new Text(newValue)]); } } return true; }); }; SnippetSession.adjustSelection = function (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 var positionLineNumber = selection.positionLineNumber, positionColumn = selection.positionColumn; var positionColumnBefore = positionColumn - overwriteBefore; var positionColumnAfter = positionColumn + overwriteAfter; var 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; }; SnippetSession.createEditsAndSnippets = function (editor, template, overwriteBefore, overwriteAfter, enforceFinalTabstop, adjustWhitespace) { var model = editor.getModel(); var edits = []; var snippets = []; var modelBasedVariableResolver = new ModelBasedVariableResolver(model); var clipboardService = editor.invokeWithinContext(function (accessor) { return accessor.get(IClipboardService, optional); }); var delta = 0; // 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 var firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0)); var firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter)); // remember the first non-whitespace column to decide if // `keepWhitespace` should be overruled for secondary selections var 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 var indexedSelections = editor.getSelections() .map(function (selection, idx) { return ({ selection: selection, idx: idx }); }) .sort(function (a, b) { return Range.compareRangesUsingStarts(a.selection, b.selection); }); for (var _i = 0, indexedSelections_1 = indexedSelections; _i < indexedSelections_1.length; _i++) { var _a = indexedSelections_1[_i], selection = _a.selection, idx = _a.idx; // extend selection with the `overwriteBefore` and `overwriteAfter` and then // compare if this matches the extensions of the primary selection var extensionBefore = SnippetSession.adjustSelection(model, selection, overwriteBefore, 0); var extensionAfter = SnippetSession.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 var snippetSelection = selection .setStartPosition(extensionBefore.startLineNumber, extensionBefore.startColumn) .setEndPosition(extensionAfter.endLineNumber, extensionAfter.endColumn); var 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 var start = snippetSelection.getStartPosition(); if (adjustWhitespace || (idx > 0 && firstLineFirstNonWhitespace !== model.getLineFirstNonWhitespaceColumn(selection.positionLineNumber))) { SnippetSession.adjustWhitespace(model, start, snippet); } snippet.resolveVariables(new CompositeSnippetVariableResolver([ modelBasedVariableResolver, new ClipboardBasedVariableResolver(clipboardService, idx, indexedSelections.length), new SelectionBasedVariableResolver(model, selection), new TimeBasedVariableResolver ])); var offset = model.getOffsetAt(start) + delta; delta += snippet.toString().length - model.getValueLengthInRange(snippetSelection); // 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()); snippets[idx] = new OneSnippet(editor, snippet, offset); } return { edits: edits, snippets: snippets }; }; SnippetSession.prototype.dispose = function () { dispose(this._snippets); }; SnippetSession.prototype._logInfo = function () { return "template=\"" + this._template + "\", merged_templates=\"" + this._templateMerges.join(' -> ') + "\""; }; SnippetSession.prototype.insert = function () { var _this = this; var model = this._editor.getModel(); // make insert edit and start with first selections var _a = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._overwriteBefore, this._overwriteAfter, false, this._adjustWhitespace), edits = _a.edits, snippets = _a.snippets; this._snippets = snippets; var selections = model.pushEditOperations(this._editor.getSelections(), edits, function (undoEdits) { if (_this._snippets[0].hasPlaceholder) { return _this._move(true); } else { return undoEdits.map(function (edit) { return Selection.fromPositions(edit.range.getEndPosition()); }); } }); this._editor.setSelections(selections); this._editor.revealRange(selections[0]); }; SnippetSession.prototype.merge = function (template, overwriteBefore, overwriteAfter, adjustWhitespace) { var _this = this; if (overwriteBefore === void 0) { overwriteBefore = 0; } if (overwriteAfter === void 0) { overwriteAfter = 0; } if (adjustWhitespace === void 0) { adjustWhitespace = true; } this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); var _a = SnippetSession.createEditsAndSnippets(this._editor, template, overwriteBefore, overwriteAfter, true, adjustWhitespace), edits = _a.edits, snippets = _a.snippets; this._editor.setSelections(this._editor.getModel().pushEditOperations(this._editor.getSelections(), edits, function (undoEdits) { for (var _i = 0, _a = _this._snippets; _i < _a.length; _i++) { var snippet = _a[_i]; snippet.merge(snippets); } console.assert(snippets.length === 0); if (_this._snippets[0].hasPlaceholder) { return _this._move(undefined); } else { return undoEdits.map(function (edit) { return Selection.fromPositions(edit.range.getEndPosition()); }); } })); }; SnippetSession.prototype.next = function () { var newSelections = this._move(true); this._editor.setSelections(newSelections); this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition()); }; SnippetSession.prototype.prev = function () { var newSelections = this._move(false); this._editor.setSelections(newSelections); this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition()); }; SnippetSession.prototype._move = function (fwd) { var selections = []; for (var _i = 0, _a = this._snippets; _i < _a.length; _i++) { var snippet = _a[_i]; var oneSelection = snippet.move(fwd); selections.push.apply(selections, oneSelection); } return selections; }; Object.defineProperty(SnippetSession.prototype, "isAtFirstPlaceholder", { get: function () { return this._snippets[0].isAtFirstPlaceholder; }, enumerable: true, configurable: true }); Object.defineProperty(SnippetSession.prototype, "isAtLastPlaceholder", { get: function () { return this._snippets[0].isAtLastPlaceholder; }, enumerable: true, configurable: true }); Object.defineProperty(SnippetSession.prototype, "hasPlaceholder", { get: function () { return this._snippets[0].hasPlaceholder; }, enumerable: true, configurable: true }); Object.defineProperty(SnippetSession.prototype, "choice", { get: function () { return this._snippets[0].choice; }, enumerable: true, configurable: true }); SnippetSession.prototype.isSelectionWithinPlaceholders = function () { if (!this.hasPlaceholder) { return false; } var 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; } var allPossibleSelections; var _loop_1 = function (snippet) { var 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) { allPossibleSelections = new Map(); possibleSelections.forEach(function (ranges, index) { ranges.sort(Range.compareRangesUsingStarts); for (var _i = 0, selections_1 = selections; _i < selections_1.length; _i++) { var selection = selections_1[_i]; if (ranges[0].containsRange(selection)) { allPossibleSelections.set(index, []); break; } } }); } if (allPossibleSelections.size === 0) { return { value: false }; } // add selections from 'this' snippet so that we know all // selections for this placeholder allPossibleSelections.forEach(function (array, index) { array.push.apply(array, possibleSelections.get(index)); }); }; for (var _i = 0, _a = this._snippets; _i < _a.length; _i++) { var snippet = _a[_i]; var state_1 = _loop_1(snippet); if (typeof state_1 === "object") return state_1.value; } // sort selections (and later placeholder-ranges). then walk both // arrays and make sure the placeholder-ranges contain the corresponding // selection selections.sort(Range.compareRangesUsingStarts); allPossibleSelections.forEach(function (ranges, index) { if (ranges.length !== selections.length) { allPossibleSelections.delete(index); return; } ranges.sort(Range.compareRangesUsingStarts); for (var i = 0; i < ranges.length; i++) { if (!ranges[i].containsRange(selections[i])) { allPossibleSelections.delete(index); return; } } }); // 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; }; return SnippetSession; }()); export { SnippetSession };