snippetSession.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { groupBy } from '../../../base/common/arrays.js';
  6. import { dispose } from '../../../base/common/lifecycle.js';
  7. import { getLeadingWhitespace } from '../../../base/common/strings.js';
  8. import './snippetSession.css';
  9. import { EditOperation } from '../../common/core/editOperation.js';
  10. import { Range } from '../../common/core/range.js';
  11. import { Selection } from '../../common/core/selection.js';
  12. import { ModelDecorationOptions } from '../../common/model/textModel.js';
  13. import { ILabelService } from '../../../platform/label/common/label.js';
  14. import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js';
  15. import { Choice, Placeholder, SnippetParser, Text } from './snippetParser.js';
  16. import { ClipboardBasedVariableResolver, CommentBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, RandomBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from './snippetVariables.js';
  17. export class OneSnippet {
  18. constructor(_editor, _snippet, _offset, _snippetLineLeadingWhitespace) {
  19. this._editor = _editor;
  20. this._snippet = _snippet;
  21. this._offset = _offset;
  22. this._snippetLineLeadingWhitespace = _snippetLineLeadingWhitespace;
  23. this._nestingLevel = 1;
  24. this._placeholderGroups = groupBy(_snippet.placeholders, Placeholder.compareByIndex);
  25. this._placeholderGroupsIdx = -1;
  26. }
  27. dispose() {
  28. if (this._placeholderDecorations) {
  29. this._editor.deltaDecorations([...this._placeholderDecorations.values()], []);
  30. }
  31. this._placeholderGroups.length = 0;
  32. }
  33. _initDecorations() {
  34. if (this._placeholderDecorations) {
  35. // already initialized
  36. return;
  37. }
  38. this._placeholderDecorations = new Map();
  39. const model = this._editor.getModel();
  40. this._editor.changeDecorations(accessor => {
  41. // create a decoration for each placeholder
  42. for (const placeholder of this._snippet.placeholders) {
  43. const placeholderOffset = this._snippet.offset(placeholder);
  44. const placeholderLen = this._snippet.fullLen(placeholder);
  45. const range = Range.fromPositions(model.getPositionAt(this._offset + placeholderOffset), model.getPositionAt(this._offset + placeholderOffset + placeholderLen));
  46. const options = placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive;
  47. const handle = accessor.addDecoration(range, options);
  48. this._placeholderDecorations.set(placeholder, handle);
  49. }
  50. });
  51. }
  52. move(fwd) {
  53. if (!this._editor.hasModel()) {
  54. return [];
  55. }
  56. this._initDecorations();
  57. // Transform placeholder text if necessary
  58. if (this._placeholderGroupsIdx >= 0) {
  59. let operations = [];
  60. for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
  61. // Check if the placeholder has a transformation
  62. if (placeholder.transform) {
  63. const id = this._placeholderDecorations.get(placeholder);
  64. const range = this._editor.getModel().getDecorationRange(id);
  65. const currentValue = this._editor.getModel().getValueInRange(range);
  66. const transformedValueLines = placeholder.transform.resolve(currentValue).split(/\r\n|\r|\n/);
  67. // fix indentation for transformed lines
  68. for (let i = 1; i < transformedValueLines.length; i++) {
  69. transformedValueLines[i] = this._editor.getModel().normalizeIndentation(this._snippetLineLeadingWhitespace + transformedValueLines[i]);
  70. }
  71. operations.push(EditOperation.replace(range, transformedValueLines.join(this._editor.getModel().getEOL())));
  72. }
  73. }
  74. if (operations.length > 0) {
  75. this._editor.executeEdits('snippet.placeholderTransform', operations);
  76. }
  77. }
  78. let couldSkipThisPlaceholder = false;
  79. if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) {
  80. this._placeholderGroupsIdx += 1;
  81. couldSkipThisPlaceholder = true;
  82. }
  83. else if (fwd === false && this._placeholderGroupsIdx > 0) {
  84. this._placeholderGroupsIdx -= 1;
  85. couldSkipThisPlaceholder = true;
  86. }
  87. else {
  88. // the selection of the current placeholder might
  89. // not acurate any more -> simply restore it
  90. }
  91. const newSelections = this._editor.getModel().changeDecorations(accessor => {
  92. const activePlaceholders = new Set();
  93. // change stickiness to always grow when typing at its edges
  94. // because these decorations represent the currently active
  95. // tabstop.
  96. // Special case #1: reaching the final tabstop
  97. // Special case #2: placeholders enclosing active placeholders
  98. const selections = [];
  99. for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
  100. const id = this._placeholderDecorations.get(placeholder);
  101. const range = this._editor.getModel().getDecorationRange(id);
  102. selections.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn));
  103. // consider to skip this placeholder index when the decoration
  104. // range is empty but when the placeholder wasn't. that's a strong
  105. // hint that the placeholder has been deleted. (all placeholder must match this)
  106. couldSkipThisPlaceholder = couldSkipThisPlaceholder && this._hasPlaceholderBeenCollapsed(placeholder);
  107. accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);
  108. activePlaceholders.add(placeholder);
  109. for (const enclosingPlaceholder of this._snippet.enclosingPlaceholders(placeholder)) {
  110. const id = this._placeholderDecorations.get(enclosingPlaceholder);
  111. accessor.changeDecorationOptions(id, enclosingPlaceholder.isFinalTabstop ? OneSnippet._decor.activeFinal : OneSnippet._decor.active);
  112. activePlaceholders.add(enclosingPlaceholder);
  113. }
  114. }
  115. // change stickness to never grow when typing at its edges
  116. // so that in-active tabstops never grow
  117. for (const [placeholder, id] of this._placeholderDecorations) {
  118. if (!activePlaceholders.has(placeholder)) {
  119. accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._decor.inactiveFinal : OneSnippet._decor.inactive);
  120. }
  121. }
  122. return selections;
  123. });
  124. return !couldSkipThisPlaceholder ? newSelections !== null && newSelections !== void 0 ? newSelections : [] : this.move(fwd);
  125. }
  126. _hasPlaceholderBeenCollapsed(placeholder) {
  127. // A placeholder is empty when it wasn't empty when authored but
  128. // when its tracking decoration is empty. This also applies to all
  129. // potential parent placeholders
  130. let marker = placeholder;
  131. while (marker) {
  132. if (marker instanceof Placeholder) {
  133. const id = this._placeholderDecorations.get(marker);
  134. const range = this._editor.getModel().getDecorationRange(id);
  135. if (range.isEmpty() && marker.toString().length > 0) {
  136. return true;
  137. }
  138. }
  139. marker = marker.parent;
  140. }
  141. return false;
  142. }
  143. get isAtFirstPlaceholder() {
  144. return this._placeholderGroupsIdx <= 0 || this._placeholderGroups.length === 0;
  145. }
  146. get isAtLastPlaceholder() {
  147. return this._placeholderGroupsIdx === this._placeholderGroups.length - 1;
  148. }
  149. get hasPlaceholder() {
  150. return this._snippet.placeholders.length > 0;
  151. }
  152. computePossibleSelections() {
  153. const result = new Map();
  154. for (const placeholdersWithEqualIndex of this._placeholderGroups) {
  155. let ranges;
  156. for (const placeholder of placeholdersWithEqualIndex) {
  157. if (placeholder.isFinalTabstop) {
  158. // ignore those
  159. break;
  160. }
  161. if (!ranges) {
  162. ranges = [];
  163. result.set(placeholder.index, ranges);
  164. }
  165. const id = this._placeholderDecorations.get(placeholder);
  166. const range = this._editor.getModel().getDecorationRange(id);
  167. if (!range) {
  168. // one of the placeholder lost its decoration and
  169. // therefore we bail out and pretend the placeholder
  170. // (with its mirrors) doesn't exist anymore.
  171. result.delete(placeholder.index);
  172. break;
  173. }
  174. ranges.push(range);
  175. }
  176. }
  177. return result;
  178. }
  179. get choice() {
  180. return this._placeholderGroups[this._placeholderGroupsIdx][0].choice;
  181. }
  182. merge(others) {
  183. const model = this._editor.getModel();
  184. this._nestingLevel *= 10;
  185. this._editor.changeDecorations(accessor => {
  186. // For each active placeholder take one snippet and merge it
  187. // in that the placeholder (can be many for `$1foo$1foo`). Because
  188. // everything is sorted by editor selection we can simply remove
  189. // elements from the beginning of the array
  190. for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
  191. const nested = others.shift();
  192. console.assert(!nested._placeholderDecorations);
  193. // Massage placeholder-indicies of the nested snippet to be
  194. // sorted right after the insertion point. This ensures we move
  195. // through the placeholders in the correct order
  196. const indexLastPlaceholder = nested._snippet.placeholderInfo.last.index;
  197. for (const nestedPlaceholder of nested._snippet.placeholderInfo.all) {
  198. if (nestedPlaceholder.isFinalTabstop) {
  199. nestedPlaceholder.index = placeholder.index + ((indexLastPlaceholder + 1) / this._nestingLevel);
  200. }
  201. else {
  202. nestedPlaceholder.index = placeholder.index + (nestedPlaceholder.index / this._nestingLevel);
  203. }
  204. }
  205. this._snippet.replace(placeholder, nested._snippet.children);
  206. // Remove the placeholder at which position are inserting
  207. // the snippet and also remove its decoration.
  208. const id = this._placeholderDecorations.get(placeholder);
  209. accessor.removeDecoration(id);
  210. this._placeholderDecorations.delete(placeholder);
  211. // For each *new* placeholder we create decoration to monitor
  212. // how and if it grows/shrinks.
  213. for (const placeholder of nested._snippet.placeholders) {
  214. const placeholderOffset = nested._snippet.offset(placeholder);
  215. const placeholderLen = nested._snippet.fullLen(placeholder);
  216. const range = Range.fromPositions(model.getPositionAt(nested._offset + placeholderOffset), model.getPositionAt(nested._offset + placeholderOffset + placeholderLen));
  217. const handle = accessor.addDecoration(range, OneSnippet._decor.inactive);
  218. this._placeholderDecorations.set(placeholder, handle);
  219. }
  220. }
  221. // Last, re-create the placeholder groups by sorting placeholders by their index.
  222. this._placeholderGroups = groupBy(this._snippet.placeholders, Placeholder.compareByIndex);
  223. });
  224. }
  225. }
  226. OneSnippet._decor = {
  227. active: ModelDecorationOptions.register({ description: 'snippet-placeholder-1', stickiness: 0 /* AlwaysGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }),
  228. inactive: ModelDecorationOptions.register({ description: 'snippet-placeholder-2', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }),
  229. activeFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-3', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }),
  230. inactiveFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-4', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }),
  231. };
  232. const _defaultOptions = {
  233. overwriteBefore: 0,
  234. overwriteAfter: 0,
  235. adjustWhitespace: true,
  236. clipboardText: undefined,
  237. overtypingCapturer: undefined
  238. };
  239. export class SnippetSession {
  240. constructor(editor, template, options = _defaultOptions) {
  241. this._templateMerges = [];
  242. this._snippets = [];
  243. this._editor = editor;
  244. this._template = template;
  245. this._options = options;
  246. }
  247. static adjustWhitespace(model, position, snippet, adjustIndentation, adjustNewlines) {
  248. const line = model.getLineContent(position.lineNumber);
  249. const lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1);
  250. // the snippet as inserted
  251. let snippetTextString;
  252. snippet.walk(marker => {
  253. // all text elements that are not inside choice
  254. if (!(marker instanceof Text) || marker.parent instanceof Choice) {
  255. return true;
  256. }
  257. const lines = marker.value.split(/\r\n|\r|\n/);
  258. if (adjustIndentation) {
  259. // adjust indentation of snippet test
  260. // -the snippet-start doesn't get extra-indented (lineLeadingWhitespace), only normalized
  261. // -all N+1 lines get extra-indented and normalized
  262. // -the text start get extra-indented and normalized when following a linebreak
  263. const offset = snippet.offset(marker);
  264. if (offset === 0) {
  265. // snippet start
  266. lines[0] = model.normalizeIndentation(lines[0]);
  267. }
  268. else {
  269. // check if text start is after a linebreak
  270. snippetTextString = snippetTextString !== null && snippetTextString !== void 0 ? snippetTextString : snippet.toString();
  271. let prevChar = snippetTextString.charCodeAt(offset - 1);
  272. if (prevChar === 10 /* LineFeed */ || prevChar === 13 /* CarriageReturn */) {
  273. lines[0] = model.normalizeIndentation(lineLeadingWhitespace + lines[0]);
  274. }
  275. }
  276. for (let i = 1; i < lines.length; i++) {
  277. lines[i] = model.normalizeIndentation(lineLeadingWhitespace + lines[i]);
  278. }
  279. }
  280. const newValue = lines.join(model.getEOL());
  281. if (newValue !== marker.value) {
  282. marker.parent.replace(marker, [new Text(newValue)]);
  283. snippetTextString = undefined;
  284. }
  285. return true;
  286. });
  287. return lineLeadingWhitespace;
  288. }
  289. static adjustSelection(model, selection, overwriteBefore, overwriteAfter) {
  290. if (overwriteBefore !== 0 || overwriteAfter !== 0) {
  291. // overwrite[Before|After] is compute using the position, not the whole
  292. // selection. therefore we adjust the selection around that position
  293. const { positionLineNumber, positionColumn } = selection;
  294. const positionColumnBefore = positionColumn - overwriteBefore;
  295. const positionColumnAfter = positionColumn + overwriteAfter;
  296. const range = model.validateRange({
  297. startLineNumber: positionLineNumber,
  298. startColumn: positionColumnBefore,
  299. endLineNumber: positionLineNumber,
  300. endColumn: positionColumnAfter
  301. });
  302. selection = Selection.createWithDirection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn, selection.getDirection());
  303. }
  304. return selection;
  305. }
  306. static createEditsAndSnippets(editor, template, overwriteBefore, overwriteAfter, enforceFinalTabstop, adjustWhitespace, clipboardText, overtypingCapturer) {
  307. const edits = [];
  308. const snippets = [];
  309. if (!editor.hasModel()) {
  310. return { edits, snippets };
  311. }
  312. const model = editor.getModel();
  313. const workspaceService = editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService));
  314. const modelBasedVariableResolver = editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model));
  315. const readClipboardText = () => clipboardText;
  316. let delta = 0;
  317. // know what text the overwrite[Before|After] extensions
  318. // of the primary curser have selected because only when
  319. // secondary selections extend to the same text we can grow them
  320. let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));
  321. let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter));
  322. // remember the first non-whitespace column to decide if
  323. // `keepWhitespace` should be overruled for secondary selections
  324. let firstLineFirstNonWhitespace = model.getLineFirstNonWhitespaceColumn(editor.getSelection().positionLineNumber);
  325. // sort selections by their start position but remeber
  326. // the original index. that allows you to create correct
  327. // offset-based selection logic without changing the
  328. // primary selection
  329. const indexedSelections = editor.getSelections()
  330. .map((selection, idx) => ({ selection, idx }))
  331. .sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
  332. for (const { selection, idx } of indexedSelections) {
  333. // extend selection with the `overwriteBefore` and `overwriteAfter` and then
  334. // compare if this matches the extensions of the primary selection
  335. let extensionBefore = SnippetSession.adjustSelection(model, selection, overwriteBefore, 0);
  336. let extensionAfter = SnippetSession.adjustSelection(model, selection, 0, overwriteAfter);
  337. if (firstBeforeText !== model.getValueInRange(extensionBefore)) {
  338. extensionBefore = selection;
  339. }
  340. if (firstAfterText !== model.getValueInRange(extensionAfter)) {
  341. extensionAfter = selection;
  342. }
  343. // merge the before and after selection into one
  344. const snippetSelection = selection
  345. .setStartPosition(extensionBefore.startLineNumber, extensionBefore.startColumn)
  346. .setEndPosition(extensionAfter.endLineNumber, extensionAfter.endColumn);
  347. const snippet = new SnippetParser().parse(template, true, enforceFinalTabstop);
  348. // adjust the template string to match the indentation and
  349. // whitespace rules of this insert location (can be different for each cursor)
  350. // happens when being asked for (default) or when this is a secondary
  351. // cursor and the leading whitespace is different
  352. const start = snippetSelection.getStartPosition();
  353. const snippetLineLeadingWhitespace = SnippetSession.adjustWhitespace(model, start, snippet, adjustWhitespace || (idx > 0 && firstLineFirstNonWhitespace !== model.getLineFirstNonWhitespaceColumn(selection.positionLineNumber)), true);
  354. snippet.resolveVariables(new CompositeSnippetVariableResolver([
  355. modelBasedVariableResolver,
  356. new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(70 /* multiCursorPaste */) === 'spread'),
  357. new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer),
  358. new CommentBasedVariableResolver(model, selection),
  359. new TimeBasedVariableResolver,
  360. new WorkspaceBasedVariableResolver(workspaceService),
  361. new RandomBasedVariableResolver,
  362. ]));
  363. const offset = model.getOffsetAt(start) + delta;
  364. delta += snippet.toString().length - model.getValueLengthInRange(snippetSelection);
  365. // store snippets with the index of their originating selection.
  366. // that ensures the primiary cursor stays primary despite not being
  367. // the one with lowest start position
  368. edits[idx] = EditOperation.replace(snippetSelection, snippet.toString());
  369. edits[idx].identifier = { major: idx, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors
  370. snippets[idx] = new OneSnippet(editor, snippet, offset, snippetLineLeadingWhitespace);
  371. }
  372. return { edits, snippets };
  373. }
  374. dispose() {
  375. dispose(this._snippets);
  376. }
  377. _logInfo() {
  378. return `template="${this._template}", merged_templates="${this._templateMerges.join(' -> ')}"`;
  379. }
  380. insert() {
  381. if (!this._editor.hasModel()) {
  382. return;
  383. }
  384. // make insert edit and start with first selections
  385. const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer);
  386. this._snippets = snippets;
  387. this._editor.executeEdits('snippet', edits, undoEdits => {
  388. if (this._snippets[0].hasPlaceholder) {
  389. return this._move(true);
  390. }
  391. else {
  392. return undoEdits
  393. .filter(edit => !!edit.identifier) // only use our undo edits
  394. .map(edit => Selection.fromPositions(edit.range.getEndPosition()));
  395. }
  396. });
  397. this._editor.revealRange(this._editor.getSelections()[0]);
  398. }
  399. merge(template, options = _defaultOptions) {
  400. if (!this._editor.hasModel()) {
  401. return;
  402. }
  403. this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]);
  404. const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer);
  405. this._editor.executeEdits('snippet', edits, undoEdits => {
  406. for (const snippet of this._snippets) {
  407. snippet.merge(snippets);
  408. }
  409. console.assert(snippets.length === 0);
  410. if (this._snippets[0].hasPlaceholder) {
  411. return this._move(undefined);
  412. }
  413. else {
  414. return (undoEdits
  415. .filter(edit => !!edit.identifier) // only use our undo edits
  416. .map(edit => Selection.fromPositions(edit.range.getEndPosition())));
  417. }
  418. });
  419. }
  420. next() {
  421. const newSelections = this._move(true);
  422. this._editor.setSelections(newSelections);
  423. this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());
  424. }
  425. prev() {
  426. const newSelections = this._move(false);
  427. this._editor.setSelections(newSelections);
  428. this._editor.revealPositionInCenterIfOutsideViewport(newSelections[0].getPosition());
  429. }
  430. _move(fwd) {
  431. const selections = [];
  432. for (const snippet of this._snippets) {
  433. const oneSelection = snippet.move(fwd);
  434. selections.push(...oneSelection);
  435. }
  436. return selections;
  437. }
  438. get isAtFirstPlaceholder() {
  439. return this._snippets[0].isAtFirstPlaceholder;
  440. }
  441. get isAtLastPlaceholder() {
  442. return this._snippets[0].isAtLastPlaceholder;
  443. }
  444. get hasPlaceholder() {
  445. return this._snippets[0].hasPlaceholder;
  446. }
  447. get choice() {
  448. return this._snippets[0].choice;
  449. }
  450. isSelectionWithinPlaceholders() {
  451. if (!this.hasPlaceholder) {
  452. return false;
  453. }
  454. const selections = this._editor.getSelections();
  455. if (selections.length < this._snippets.length) {
  456. // this means we started snippet mode with N
  457. // selections and have M (N > M) selections.
  458. // So one snippet is without selection -> cancel
  459. return false;
  460. }
  461. let allPossibleSelections = new Map();
  462. for (const snippet of this._snippets) {
  463. const possibleSelections = snippet.computePossibleSelections();
  464. // for the first snippet find the placeholder (and its ranges)
  465. // that contain at least one selection. for all remaining snippets
  466. // the same placeholder (and their ranges) must be used.
  467. if (allPossibleSelections.size === 0) {
  468. for (const [index, ranges] of possibleSelections) {
  469. ranges.sort(Range.compareRangesUsingStarts);
  470. for (const selection of selections) {
  471. if (ranges[0].containsRange(selection)) {
  472. allPossibleSelections.set(index, []);
  473. break;
  474. }
  475. }
  476. }
  477. }
  478. if (allPossibleSelections.size === 0) {
  479. // return false if we couldn't associate a selection to
  480. // this (the first) snippet
  481. return false;
  482. }
  483. // add selections from 'this' snippet so that we know all
  484. // selections for this placeholder
  485. allPossibleSelections.forEach((array, index) => {
  486. array.push(...possibleSelections.get(index));
  487. });
  488. }
  489. // sort selections (and later placeholder-ranges). then walk both
  490. // arrays and make sure the placeholder-ranges contain the corresponding
  491. // selection
  492. selections.sort(Range.compareRangesUsingStarts);
  493. for (let [index, ranges] of allPossibleSelections) {
  494. if (ranges.length !== selections.length) {
  495. allPossibleSelections.delete(index);
  496. continue;
  497. }
  498. ranges.sort(Range.compareRangesUsingStarts);
  499. for (let i = 0; i < ranges.length; i++) {
  500. if (!ranges[i].containsRange(selections[i])) {
  501. allPossibleSelections.delete(index);
  502. continue;
  503. }
  504. }
  505. }
  506. // from all possible selections we have deleted those
  507. // that don't match with the current selection. if we don't
  508. // have any left, we don't have a selection anymore
  509. return allPossibleSelections.size > 0;
  510. }
  511. }