/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as strings from 'vs/base/common/strings'; import { binarySearch } from 'vs/base/common/arrays'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { CommonEditorRegistry, commonEditorContribution, EditorCommand } from 'vs/editor/common/editorCommonExtensions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ISnippetVariableResolver, ICodeSnippet, CodeSnippet } from './snippet'; import { SnippetVariablesResolver } from './snippetVariables'; import EditorContextKeys = editorCommon.EditorContextKeys; export class InsertSnippetController { private editor: editorCommon.ICommonCodeEditor; private model: editorCommon.IModel; private finishPlaceHolderIndex: number; private listenersToRemove: IDisposable[]; private trackedPlaceHolders: ITrackedPlaceHolder[]; private placeHolderDecorations: string[]; private currentPlaceHolderIndex: number; private highlightDecorationId: string; private isFinished: boolean; private _onStop: () => void; private _initialAlternativeVersionId: number; constructor(editor: editorCommon.ICommonCodeEditor, adaptedSnippet: ICodeSnippet, startLineNumber: number, initialAlternativeVersionId: number, onStop: () => void) { this.editor = editor; this._onStop = onStop; this.model = editor.getModel(); this.finishPlaceHolderIndex = adaptedSnippet.finishPlaceHolderIndex; this.trackedPlaceHolders = []; this.placeHolderDecorations = []; this.currentPlaceHolderIndex = 0; this.highlightDecorationId = null; this.isFinished = false; this._initialAlternativeVersionId = initialAlternativeVersionId; this.initialize(adaptedSnippet, startLineNumber); } public dispose(): void { this.stopAll(); } private initialize(adaptedSnippet: ICodeSnippet, startLineNumber: number): void { // sorted list of all placeholder occurences for subsequent lockups const sortedOccurrences: editorCommon.IRange[] = []; for (const {occurences} of adaptedSnippet.placeHolders) { for (const range of occurences) { sortedOccurrences.push(range); } } sortedOccurrences.sort(Range.compareRangesUsingStarts); // track each occurence this.model.changeDecorations((changeAccessor) => { for (const {occurences} of adaptedSnippet.placeHolders) { const trackedRanges: string[] = []; for (const range of occurences) { let stickiness = editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges; // Check if the previous range ends exactly where this range starts // and iff so change the stickiness to avoid conflicts let idx = binarySearch(sortedOccurrences, range, Range.compareRangesUsingStarts); if (idx > 0 && sortedOccurrences[idx - 1].endLineNumber === range.startLineNumber && sortedOccurrences[idx - 1].endColumn === range.startColumn) { stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; } trackedRanges.push(changeAccessor.addDecoration(range, { stickiness: stickiness })); } this.trackedPlaceHolders.push({ ranges: trackedRanges }); } }); this.editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => { let newDecorations: editorCommon.IModelDeltaDecoration[] = []; let endLineNumber = startLineNumber + adaptedSnippet.lines.length - 1; let endLineNumberMaxColumn = this.model.getLineMaxColumn(endLineNumber); newDecorations.push({ range: new Range(startLineNumber, 1, endLineNumber, endLineNumberMaxColumn), options: { className: 'new-snippet', isWholeLine: true } }); for (let i = 0, len = this.trackedPlaceHolders.length; i < len; i++) { let className = (i === this.finishPlaceHolderIndex) ? 'finish-snippet-placeholder' : 'snippet-placeholder'; newDecorations.push({ range: this.model.getDecorationRange(this.trackedPlaceHolders[i].ranges[0]), options: { className: className } }); } let decorations = changeAccessor.deltaDecorations([], newDecorations); this.highlightDecorationId = decorations[0]; this.placeHolderDecorations = decorations.slice(1); }); this.listenersToRemove = []; this.listenersToRemove.push(this.editor.onDidChangeModelRawContent((e: editorCommon.IModelContentChangedEvent) => { if (this.isFinished) { return; } if (e.changeType === editorCommon.EventType.ModelRawContentChangedFlush) { // a model.setValue() was called this.stopAll(); } else if (e.changeType === editorCommon.EventType.ModelRawContentChangedLineChanged) { var changedLine = (e).lineNumber; var highlightRange = this.model.getDecorationRange(this.highlightDecorationId); if (changedLine < highlightRange.startLineNumber || changedLine > highlightRange.endLineNumber) { this.stopAll(); } } else if (e.changeType === editorCommon.EventType.ModelRawContentChangedLinesInserted) { var insertLine = (e).fromLineNumber; var highlightRange = this.model.getDecorationRange(this.highlightDecorationId); if (insertLine < highlightRange.startLineNumber || insertLine > highlightRange.endLineNumber) { this.stopAll(); } } else if (e.changeType === editorCommon.EventType.ModelRawContentChangedLinesDeleted) { var deleteLine1 = (e).fromLineNumber; var deleteLine2 = (e).toLineNumber; var highlightRange = this.model.getDecorationRange(this.highlightDecorationId); var deletedLinesAbove = (deleteLine2 < highlightRange.startLineNumber); var deletedLinesBelow = (deleteLine1 > highlightRange.endLineNumber); if (deletedLinesAbove || deletedLinesBelow) { this.stopAll(); } } var newAlternateVersionId = this.editor.getModel().getAlternativeVersionId(); if (this._initialAlternativeVersionId === newAlternateVersionId) { // We executed undo until we reached the same version we started with this.stopAll(); } })); this.listenersToRemove.push(this.editor.onDidChangeCursorPosition((e: editorCommon.ICursorPositionChangedEvent) => { if (this.isFinished) { return; } var highlightRange = this.model.getDecorationRange(this.highlightDecorationId); var lineNumber = e.position.lineNumber; if (lineNumber < highlightRange.startLineNumber || lineNumber > highlightRange.endLineNumber) { this.stopAll(); } })); this.listenersToRemove.push(this.editor.onDidChangeModel(() => { this.stopAll(); })); var blurTimeout = -1; this.listenersToRemove.push(this.editor.onDidBlurEditor(() => { // Blur if within 100ms we do not focus back blurTimeout = setTimeout(() => { this.stopAll(); }, 100); })); this.listenersToRemove.push(this.editor.onDidFocusEditor(() => { // Cancel the blur timeout (if any) if (blurTimeout !== -1) { clearTimeout(blurTimeout); blurTimeout = -1; } })); this.listenersToRemove.push(this.model.onDidChangeDecorations((e) => { if (this.isFinished) { return; } var modelEditableRange = this.model.getEditableRange(), previousRange: Range = null, allCollapsed = true, allEqualToEditableRange = true; for (var i = 0; (allCollapsed || allEqualToEditableRange) && i < this.trackedPlaceHolders.length; i++) { var ranges = this.trackedPlaceHolders[i].ranges; for (var j = 0; (allCollapsed || allEqualToEditableRange) && j < ranges.length; j++) { var range = this.model.getDecorationRange(ranges[j]); if (allCollapsed) { if (!range.isEmpty()) { allCollapsed = false; } else if (previousRange === null) { previousRange = range; } else if (!previousRange.equalsRange(range)) { allCollapsed = false; } } if (allEqualToEditableRange && !modelEditableRange.equalsRange(range)) { allEqualToEditableRange = false; } } } if (allCollapsed || allEqualToEditableRange) { this.stopAll(); } else { if (this.finishPlaceHolderIndex !== -1) { var finishPlaceHolderDecorationId = this.placeHolderDecorations[this.finishPlaceHolderIndex]; var finishPlaceHolderRange = this.model.getDecorationRange(finishPlaceHolderDecorationId); var finishPlaceHolderOptions = this.model.getDecorationOptions(finishPlaceHolderDecorationId); var finishPlaceHolderRangeIsEmpty = finishPlaceHolderRange.isEmpty(); var finishPlaceHolderClassNameIsForEmpty = (finishPlaceHolderOptions.className === 'finish-snippet-placeholder'); // Remember xor? :) var needsChanging = Number(finishPlaceHolderRangeIsEmpty) ^ Number(finishPlaceHolderClassNameIsForEmpty); if (needsChanging) { this.editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => { var className = finishPlaceHolderRangeIsEmpty ? 'finish-snippet-placeholder' : 'snippet-placeholder'; changeAccessor.changeDecorationOptions(finishPlaceHolderDecorationId, { className: className }); }); } } } })); this.doLinkEditing(); } public onNextPlaceHolder(): boolean { return this.changePlaceHolder(true); } public onPrevPlaceHolder(): boolean { return this.changePlaceHolder(false); } private changePlaceHolder(goToNext: boolean): boolean { if (this.isFinished) { return false; } var oldPlaceHolderIndex = this.currentPlaceHolderIndex; var oldRange = this.model.getDecorationRange(this.trackedPlaceHolders[oldPlaceHolderIndex].ranges[0]); var sameRange = true; do { if (goToNext) { this.currentPlaceHolderIndex = (this.currentPlaceHolderIndex + 1) % this.trackedPlaceHolders.length; } else { this.currentPlaceHolderIndex = (this.trackedPlaceHolders.length + this.currentPlaceHolderIndex - 1) % this.trackedPlaceHolders.length; } var newRange = this.model.getDecorationRange(this.trackedPlaceHolders[this.currentPlaceHolderIndex].ranges[0]); sameRange = oldRange.equalsRange(newRange); } while (this.currentPlaceHolderIndex !== oldPlaceHolderIndex && sameRange); this.doLinkEditing(); return true; } public onAccept(): boolean { if (this.isFinished) { return false; } if (this.finishPlaceHolderIndex !== -1) { var finishRange = this.model.getDecorationRange(this.trackedPlaceHolders[this.finishPlaceHolderIndex].ranges[0]); // Let's just position cursor at the end of the finish range this.editor.setPosition({ lineNumber: finishRange.endLineNumber, column: finishRange.endColumn }); } this.stopAll(); return true; } public onEscape(): boolean { if (this.isFinished) { return false; } this.stopAll(); // Cancel multi-cursor this.editor.setSelections([this.editor.getSelections()[0]]); return true; } private doLinkEditing(): void { var selections: editorCommon.ISelection[] = []; for (var i = 0, len = this.trackedPlaceHolders[this.currentPlaceHolderIndex].ranges.length; i < len; i++) { var range = this.model.getDecorationRange(this.trackedPlaceHolders[this.currentPlaceHolderIndex].ranges[i]); selections.push({ selectionStartLineNumber: range.startLineNumber, selectionStartColumn: range.startColumn, positionLineNumber: range.endLineNumber, positionColumn: range.endColumn }); } this.editor.setSelections(selections); } private stopAll(): void { if (this.isFinished) { return; } this.isFinished = true; this._onStop(); this.listenersToRemove = dispose(this.listenersToRemove); this.model.changeDecorations((changeAccessor) => { for (var i = 0; i < this.trackedPlaceHolders.length; i++) { var ranges = this.trackedPlaceHolders[i].ranges; for (var j = 0; j < ranges.length; j++) { changeAccessor.removeDecoration(ranges[j]); } } }); this.trackedPlaceHolders = []; this.editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => { let toRemove: string[] = []; toRemove.push(this.highlightDecorationId); for (let i = 0; i < this.placeHolderDecorations.length; i++) { toRemove.push(this.placeHolderDecorations[i]); } changeAccessor.deltaDecorations(toRemove, []); this.placeHolderDecorations = []; this.highlightDecorationId = null; }); } } export interface ITrackedPlaceHolder { ranges: string[]; } interface IPreparedSnippet { typeRange: Range; adaptedSnippet: ICodeSnippet; } class BeforeAfterData { static create(model: editorCommon.IModel, selection: Selection, overwriteBefore: number, overwriteAfter: number) { let contentBefore = ''; if (overwriteBefore > 0) { contentBefore = model.getLineContent(selection.startLineNumber).substring(selection.startColumn - 1 - overwriteBefore, selection.startColumn - 1); } let contentAfter = ''; if (overwriteAfter > 0) { contentAfter = model.getLineContent(selection.endLineNumber).substring(selection.endColumn - 1, selection.endColumn - 1 + overwriteAfter); } return new BeforeAfterData(model, contentBefore, contentAfter, overwriteBefore, overwriteAfter); } constructor(private readonly _model: editorCommon.IModel, private readonly _contentBefore: string, private readonly _contentAfter: string, public readonly overwriteBefore: number, public readonly overwriteAfter: number ) { // } next(selection: Selection) { const data = BeforeAfterData.create(this._model, selection, this.overwriteBefore, this.overwriteAfter); let {overwriteBefore, overwriteAfter} = data; if (data._contentBefore !== this._contentBefore) { overwriteBefore = 0; } if (data._contentAfter !== this._contentAfter) { overwriteAfter = 0; } return new BeforeAfterData(this._model, null, null, overwriteBefore, overwriteAfter); } } @commonEditorContribution export class SnippetController { private static ID = 'editor.contrib.snippetController'; public static get(editor: editorCommon.ICommonCodeEditor): SnippetController { return editor.getContribution(SnippetController.ID); } private _editor: editorCommon.ICommonCodeEditor; private _variableResolver: ISnippetVariableResolver; protected _currentController: InsertSnippetController; private _inSnippetMode: IContextKey; constructor(editor: editorCommon.ICommonCodeEditor, @IContextKeyService contextKeyService: IContextKeyService) { this._editor = editor; this._variableResolver = new SnippetVariablesResolver(editor); this._currentController = null; this._inSnippetMode = CONTEXT_SNIPPET_MODE.bindTo(contextKeyService); } public dispose(): void { if (this._currentController) { this._currentController.dispose(); this._currentController = null; } } public getId(): string { return SnippetController.ID; } public insertSnippet(template: string, overwriteBefore: number, overwriteAfter: number): void { const snippet = CodeSnippet.fromTextmate(template, this._variableResolver); this.run(snippet, overwriteBefore, overwriteAfter); } public run(snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix?: boolean): void { this._runAndRestoreController(() => { if (snippet.isInsertOnly || snippet.isSingleTabstopOnly) { // Only inserts text, not placeholders, tabstops etc // Only cursor endposition this._runForAllSelections(snippet, overwriteBefore, overwriteAfter, stripPrefix); } else { let prepared = SnippetController._prepareSnippet(this._editor, this._editor.getSelection(), snippet, overwriteBefore, overwriteAfter, stripPrefix); this._runPreparedSnippetForPrimarySelection(prepared, true); } }); } /** * Inserts once `snippet` at the start of `replaceRange`, after deleting `replaceRange`. */ public runWithReplaceRange(snippet: CodeSnippet, replaceRange: Range): void { this._runAndRestoreController(() => { this._runPreparedSnippetForPrimarySelection({ typeRange: replaceRange, adaptedSnippet: SnippetController._getAdaptedSnippet(this._editor.getModel(), snippet, replaceRange) }, false); }); } private _runAndRestoreController(callback: () => void): void { let prevController = this._currentController; this._currentController = null; callback(); if (!this._currentController) { // we didn't end up in snippet mode again => restore previous controller this._currentController = prevController; } else { // we ended up in snippet mode => dispose previous controller if necessary if (prevController) { prevController.dispose(); } } } private static _addCommandForSnippet(model: editorCommon.ITextModel, adaptedSnippet: ICodeSnippet, typeRange: Range, out: editorCommon.IIdentifiedSingleEditOperation[]): void { let insertText = adaptedSnippet.lines.join('\n'); let currentText = model.getValueInRange(typeRange, editorCommon.EndOfLinePreference.LF); if (insertText !== currentText) { out.push(EditOperation.replaceMove(typeRange, insertText)); } } private _runPreparedSnippetForPrimarySelection(prepared: IPreparedSnippet, undoStops: boolean): void { let initialAlternativeVersionId = this._editor.getModel().getAlternativeVersionId(); let edits: editorCommon.IIdentifiedSingleEditOperation[] = []; SnippetController._addCommandForSnippet(this._editor.getModel(), prepared.adaptedSnippet, prepared.typeRange, edits); if (edits.length > 0) { if (undoStops) { this._editor.pushUndoStop(); } this._editor.executeEdits('editor.contrib.insertSnippetHelper', edits); if (undoStops) { this._editor.pushUndoStop(); } } let cursorOnly = SnippetController._getSnippetCursorOnly(prepared.adaptedSnippet); if (cursorOnly) { this._editor.setSelection(new Selection(cursorOnly.lineNumber, cursorOnly.column, cursorOnly.lineNumber, cursorOnly.column)); } else if (prepared.adaptedSnippet.placeHolders.length > 0) { this._inSnippetMode.set(true); this._currentController = new InsertSnippetController(this._editor, prepared.adaptedSnippet, prepared.typeRange.startLineNumber, initialAlternativeVersionId, () => { this._inSnippetMode.reset(); if (this._currentController) { this._currentController.dispose(); this._currentController = null; } }); } } private _runForAllSelections(snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix?: boolean): void { const edits: editorCommon.IIdentifiedSingleEditOperation[] = []; const selections = this._editor.getSelections(); const model = this._editor.getModel(); const primaryBeforeAfter = BeforeAfterData.create(model, selections[0], overwriteBefore, overwriteAfter); let totalDelta = 0; const newSelections: { offset: number; i: number }[] = []; // sort selections by start position but remember where // each selection came from const selectionEntries = selections .map((selection, i) => ({ selection, i })) .sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection)); for (const {selection, i} of selectionEntries) { // only use overwrite[Before|After] for secondary cursors // when the same text as with the primary cursor is selected const beforeAfter = i !== 0 ? primaryBeforeAfter.next(selection) : primaryBeforeAfter; let {adaptedSnippet, typeRange} = SnippetController._prepareSnippet( this._editor, selection, snippet, beforeAfter.overwriteBefore, beforeAfter.overwriteAfter, stripPrefix ); SnippetController._addCommandForSnippet(this._editor.getModel(), adaptedSnippet, typeRange, edits); // compute new selection offset // * get current offset // * get length of snippet that we insert // * get final cursor position of snippet that we insert (might not exist) // * NEW selection offset is current + final cursor pos + inserts_until_here let offset = model.getOffsetAt(typeRange.getStartPosition()); // inserts until here offset += totalDelta; // each snippet has a different length (because of whitespace changes) let snippetLength = (adaptedSnippet.lines.length - 1) * model.getEOL().length; for (const line of adaptedSnippet.lines) { snippetLength += line.length; } // each snippet has a different cursor offset const finalCursorPos = SnippetController._getSnippetCursorOnly(adaptedSnippet); if (finalCursorPos) { let finalCursorOffset: number; if (finalCursorPos.lineNumber === typeRange.startLineNumber) { finalCursorOffset = finalCursorPos.column - typeRange.startColumn; } else { finalCursorOffset = finalCursorPos.column - 1; for (let i = 0, lineNumber = typeRange.startLineNumber; lineNumber < finalCursorPos.lineNumber; i++ , lineNumber++) { finalCursorOffset += adaptedSnippet.lines[i].length + model.getEOL().length; } } offset += finalCursorOffset; } else { offset += snippetLength; } newSelections.push({ offset, i }); totalDelta += (snippetLength - model.getValueLengthInRange(typeRange)); } if (edits.length === 0) { return; } const cursorStateComputer: editorCommon.ICursorStateComputer = function () { // create new selections from the new selection offsets // and restore the order we had at the beginning const result: Selection[] = []; for (const {offset, i} of newSelections) { const pos = model.getPositionAt(offset); result[i] = new Selection(pos.lineNumber, pos.column, pos.lineNumber, pos.column); } return result; }; model.pushStackElement(); this._editor.setSelections(model.pushEditOperations(selections, edits, cursorStateComputer)); model.pushStackElement(); } private static _prepareSnippet(editor: editorCommon.ICommonCodeEditor, selection: Selection, snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix = true): { typeRange: Range; adaptedSnippet: ICodeSnippet; } { var model = editor.getModel(); var typeRange = SnippetController._getTypeRangeForSelection(model, selection, overwriteBefore, overwriteAfter); if (snippet.lines.length === 1) { var nextTextOnLine = model.getLineContent(typeRange.endLineNumber).substr(typeRange.endColumn - 1); var nextInSnippet = snippet.lines[0].substr(overwriteBefore); var commonPrefix = strings.commonPrefixLength(nextTextOnLine, nextInSnippet); if (commonPrefix > 0 && stripPrefix) { typeRange = typeRange.setEndPosition(typeRange.endLineNumber, typeRange.endColumn + commonPrefix); } } var adaptedSnippet = SnippetController._getAdaptedSnippet(model, snippet, typeRange); return { typeRange: typeRange, adaptedSnippet: adaptedSnippet }; } private static _getTypeRangeForSelection(model: editorCommon.IModel, selection: Selection, overwriteBefore: number, overwriteAfter: number): Range { var typeRange: Range; if (overwriteBefore || overwriteAfter) { typeRange = model.validateRange(Range.plusRange(selection, { startLineNumber: selection.positionLineNumber, startColumn: selection.positionColumn - overwriteBefore, endLineNumber: selection.positionLineNumber, endColumn: selection.positionColumn + overwriteAfter })); } else { typeRange = selection; } return typeRange; } private static _getAdaptedSnippet(model: editorCommon.IModel, snippet: CodeSnippet, typeRange: Range): ICodeSnippet { return snippet.bind(model.getLineContent(typeRange.startLineNumber), typeRange.startLineNumber - 1, typeRange.startColumn - 1, model); } private static _getSnippetCursorOnly(snippet: ICodeSnippet): editorCommon.IPosition { if (snippet.placeHolders.length !== 1) { return null; } var placeHolder = snippet.placeHolders[0]; if (placeHolder.value !== '' || placeHolder.occurences.length !== 1) { return null; } var placeHolderRange = placeHolder.occurences[0]; if (!Range.isEmpty(placeHolderRange)) { return null; } return { lineNumber: placeHolderRange.startLineNumber, column: placeHolderRange.startColumn }; } public jumpToNextPlaceholder(): void { if (this._currentController) { this._currentController.onNextPlaceHolder(); } } public jumpToPrevPlaceholder(): void { if (this._currentController) { this._currentController.onPrevPlaceHolder(); } } public acceptSnippet(): void { if (this._currentController) { this._currentController.onAccept(); } } public leaveSnippet(): void { if (this._currentController) { this._currentController.onEscape(); } } } export var CONTEXT_SNIPPET_MODE = new RawContextKey('inSnippetMode', false); const SnippetCommand = EditorCommand.bindToContribution(SnippetController.get); CommonEditorRegistry.registerEditorCommand(new SnippetCommand({ id: 'jumpToNextSnippetPlaceholder', precondition: CONTEXT_SNIPPET_MODE, handler: x => x.jumpToNextPlaceholder(), kbOpts: { weight: CommonEditorRegistry.commandWeight(30), kbExpr: EditorContextKeys.TextFocus, primary: KeyCode.Tab } })); CommonEditorRegistry.registerEditorCommand(new SnippetCommand({ id: 'jumpToPrevSnippetPlaceholder', precondition: CONTEXT_SNIPPET_MODE, handler: x => x.jumpToPrevPlaceholder(), kbOpts: { weight: CommonEditorRegistry.commandWeight(30), kbExpr: EditorContextKeys.TextFocus, primary: KeyMod.Shift | KeyCode.Tab } })); CommonEditorRegistry.registerEditorCommand(new SnippetCommand({ id: 'acceptSnippet', precondition: CONTEXT_SNIPPET_MODE, handler: x => x.acceptSnippet(), kbOpts: { weight: CommonEditorRegistry.commandWeight(30), kbExpr: EditorContextKeys.TextFocus, primary: KeyCode.Enter } })); CommonEditorRegistry.registerEditorCommand(new SnippetCommand({ id: 'leaveSnippet', precondition: CONTEXT_SNIPPET_MODE, handler: x => x.leaveSnippet(), kbOpts: { weight: CommonEditorRegistry.commandWeight(30), kbExpr: EditorContextKeys.TextFocus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] } }));