text: /*---------------------------------------------------------------------------------------------
   *  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 { ILabelService } from '../../../platform/label/common/label.js';
  import * as colors from '../../../platform/theme/common/colorRegistry.js';
  import { registerThemingParticipant } from '../../../platform/theme/common/themeService.js';
  import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js';
  import { Choice, Placeholder, SnippetParser, Text } from './snippetParser.js';
  import { ClipboardBasedVariableResolver, CommentBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, RandomBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from './snippetVariables.js';
  registerThemingParticipant((theme, collector) => {
      function getColorGraceful(name) {
          const 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)}; }`);
  });
  export class OneSnippet {
      constructor(_editor, _snippet, _offset, _snippetLineLeadingWhitespace) {
          this._editor = _editor;
          this._snippet = _snippet;
          this._offset = _offset;
          this._snippetLineLeadingWhitespace = _snippetLineLeadingWhitespace;
          this._nestingLevel = 1;
          this._placeholderGroups = groupBy(_snippet.placeholders, Placeholder.compareByIndex);
          this._placeholderGroupsIdx = -1;
      }
      dispose() {
          if (this._placeholderDecorations) {
              this._editor.deltaDecorations([...this._placeholderDecorations.values()], []);
          }
          this._placeholderGroups.length = 0;
      }
      _initDecorations() {
          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) {
              let 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 !== null && newSelections !== void 0 ? 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;
      }
      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 choice() {
          return this._placeholderGroups[this._placeholderGroupsIdx][0].choice;
      }
      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._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);
          });
      }
  }
  OneSnippet._decor = {
      active: ModelDecorationOptions.register({ description: 'snippet-placeholder-1', stickiness: 0 /* AlwaysGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }),
      inactive: ModelDecorationOptions.register({ description: 'snippet-placeholder-2', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'snippet-placeholder' }),
      activeFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-3', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }),
      inactiveFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-4', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */, className: 'finish-snippet-placeholder' }),
  };
  const _defaultOptions = {
      overwriteBefore: 0,
      overwriteAfter: 0,
      adjustWhitespace: true,
      clipboardText: undefined,
      overtypingCapturer: undefined
  };
  export class SnippetSession {
      constructor(editor, template, options = _defaultOptions) {
          this._templateMerges = [];
          this._snippets = [];
          this._editor = editor;
          this._template = template;
          this._options = options;
      }
      static adjustWhitespace(model, position, snippet, adjustIndentation, adjustNewlines) {
          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;
              }
              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 !== null && snippetTextString !== void 0 ? snippetTextString : snippet.toString();
                      let prevChar = snippetTextString.charCodeAt(offset - 1);
                      if (prevChar === 10 /* LineFeed */ || prevChar === 13 /* 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 createEditsAndSnippets(editor, template, overwriteBefore, overwriteAfter, enforceFinalTabstop, adjustWhitespace, clipboardText, overtypingCapturer) {
          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;
          let 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
          let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));
          let 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
          let 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.adjustSelection(model, selection, overwriteBefore, 0);
              let 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
              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.adjustWhitespace(model, start, snippet, adjustWhitespace || (idx > 0 && firstLineFirstNonWhitespace !== model.getLineFirstNonWhitespaceColumn(selection.positionLineNumber)), true);
              snippet.resolveVariables(new CompositeSnippetVariableResolver([
                  modelBasedVariableResolver,
                  new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(70 /* multiCursorPaste */) === 'spread'),
                  new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer),
                  new CommentBasedVariableResolver(model, selection),
                  new TimeBasedVariableResolver,
                  new WorkspaceBasedVariableResolver(workspaceService),
                  new RandomBasedVariableResolver,
              ]));
              const 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());
              edits[idx].identifier = { major: idx, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors
              snippets[idx] = new OneSnippet(editor, snippet, offset, snippetLineLeadingWhitespace);
          }
          return { edits, 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 } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer);
          this._snippets = snippets;
          this._editor.executeEdits('snippet', edits, undoEdits => {
              if (this._snippets[0].hasPlaceholder) {
                  return this._move(true);
              }
              else {
                  return undoEdits
                      .filter(edit => !!edit.identifier) // only use our undo edits
                      .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.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer);
          this._editor.executeEdits('snippet', edits, undoEdits => {
              for (const snippet of this._snippets) {
                  snippet.merge(snippets);
              }
              console.assert(snippets.length === 0);
              if (this._snippets[0].hasPlaceholder) {
                  return this._move(undefined);
              }
              else {
                  return (undoEdits
                      .filter(edit => !!edit.identifier) // only use our undo edits
                      .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 choice() {
          return this._snippets[0].choice;
      }
      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;
          }
          let 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 (let [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;
      }
  }
  