diff --git a/packages/cursorless-engine/src/actions/BreakLine.ts b/packages/cursorless-engine/src/actions/BreakLine.ts index c8bd79126e..32eedd4877 100644 --- a/packages/cursorless-engine/src/actions/BreakLine.ts +++ b/packages/cursorless-engine/src/actions/BreakLine.ts @@ -7,7 +7,7 @@ import { } from "@cursorless/common"; import { flatten, zip } from "lodash-es"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; @@ -25,13 +25,17 @@ export class BreakLine { await runOnTargetsForEachEditor(targets, async (editor, targets) => { const contentRanges = targets.map(({ contentRange }) => contentRange); const edits = getEdits(editor, contentRanges); + const editableEditor = ide().getEditableTextEditor(editor); - const [updatedRanges] = await performEditsAndUpdateRanges( - this.rangeUpdater, - ide().getEditableTextEditor(editor), - edits, - [contentRanges], - ); + const { contentRanges: updatedRanges } = + await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: editableEditor, + edits, + selections: { + contentRanges, + }, + }); return zip(targets, updatedRanges).map(([target, range]) => ({ editor: target!.editor, diff --git a/packages/cursorless-engine/src/actions/BringMoveSwap.ts b/packages/cursorless-engine/src/actions/BringMoveSwap.ts index 5b55e8ff3b..d2de8599c5 100644 --- a/packages/cursorless-engine/src/actions/BringMoveSwap.ts +++ b/packages/cursorless-engine/src/actions/BringMoveSwap.ts @@ -7,10 +7,7 @@ import { } from "@cursorless/common"; import { flatten } from "lodash-es"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { - getSelectionInfo, - performEditsAndUpdateFullSelectionInfos, -} from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { EditWithRangeUpdater } from "../typings/Types"; import { Destination, Target } from "../typings/target.types"; @@ -158,64 +155,35 @@ abstract class BringMoveSwap { ? edits : edits.filter(({ isSource }) => !isSource); - // Sources should be closedClosed, because they should be logically - // the same as the original source. - const sourceEditSelectionInfos = sourceEdits.map( - ({ edit: { range }, originalTarget }) => - getSelectionInfo( - editor.document, - range.toSelection(originalTarget.isReversed), - RangeExpansionBehavior.closedClosed, - ), - ); - - // Destinations should be openOpen, because they should grow to contain - // the new text. - const destinationEditSelectionInfos = destinationEdits.map( - ({ edit: { range }, originalTarget }) => - getSelectionInfo( - editor.document, - range.toSelection(originalTarget.isReversed), - RangeExpansionBehavior.openOpen, - ), - ); - - const cursorSelectionInfos = editor.selections.map((selection) => - getSelectionInfo( - editor.document, - selection, - RangeExpansionBehavior.closedClosed, - ), + const sourceEditRanges = sourceEdits.map(({ edit }) => edit.range); + const destinationEditRanges = destinationEdits.map( + ({ edit }) => edit.range, ); - const editableEditor = ide().getEditableTextEditor(editor); - const [ - updatedSourceEditSelections, - updatedDestinationEditSelections, - cursorSelections, - ]: Selection[][] = await performEditsAndUpdateFullSelectionInfos( - this.rangeUpdater, - editableEditor, - filteredEdits.map(({ edit }) => edit), - [ - sourceEditSelectionInfos, - destinationEditSelectionInfos, - cursorSelectionInfos, - ], - ); - - // NB: We set the selections here because we don't trust vscode to - // properly move the cursor on a bring. Sometimes it will smear an - // empty selection - await editableEditor.setSelections(cursorSelections); + const { + sourceEditRanges: updatedSourceEditRanges, + destinationEditRanges: updatedDestinationEditRanges, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: editableEditor, + edits: filteredEdits.map(({ edit }) => edit), + selections: { + // Sources should be closedClosed, because they should be logically + // the same as the original source. + sourceEditRanges, + // Destinations should be openOpen, because they should grow to contain + // the new text. + destinationEditRanges: { + selections: destinationEditRanges, + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); const marks = [ - ...this.getMarks(sourceEdits, updatedSourceEditSelections), - ...this.getMarks( - destinationEdits, - updatedDestinationEditSelections, - ), + ...this.getMarks(sourceEdits, updatedSourceEditRanges), + ...this.getMarks(destinationEdits, updatedDestinationEditRanges), ]; // Restore original order before split into source and destination @@ -231,13 +199,10 @@ abstract class BringMoveSwap { ); } - private getMarks( - edits: ExtendedEdit[], - selections: Selection[], - ): MarkEntry[] { + private getMarks(edits: ExtendedEdit[], ranges: Range[]): MarkEntry[] { return edits.map((edit, index): MarkEntry => { - const selection = selections[index]; - const range = edit.edit.updateRange(selection); + const originalRange = ranges[index]; + const range = edit.edit.updateRange(originalRange); const target = edit.originalTarget; return { editor: edit.editor, diff --git a/packages/cursorless-engine/src/actions/CallbackAction.ts b/packages/cursorless-engine/src/actions/CallbackAction.ts index 63f3276321..883867699a 100644 --- a/packages/cursorless-engine/src/actions/CallbackAction.ts +++ b/packages/cursorless-engine/src/actions/CallbackAction.ts @@ -2,7 +2,7 @@ import { EditableTextEditor, FlashStyle, TextEditor } from "@cursorless/common"; import { flatten } from "lodash-es"; import { selectionToStoredTarget } from "../core/commandRunner/selectionToStoredTarget"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { @@ -97,13 +97,19 @@ export class CallbackAction { }); } - const [updatedOriginalSelections, updatedTargetSelections] = - await callFunctionAndUpdateSelections( - this.rangeUpdater, - () => options.callback(editableEditor, targets), - editor.document, - [originalSelections, targetSelections], - ); + const { + originalSelections: updatedOriginalSelections, + targetSelections: updatedTargetSelections, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: editableEditor, + callback: () => options.callback(editableEditor, targets), + preserveCursorSelections: true, + selections: { + originalSelections, + targetSelections, + }, + }); // Reset original selections if (options.setSelection && options.restoreSelection) { diff --git a/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts b/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts index 438c04c045..64366e33cd 100644 --- a/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts +++ b/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts @@ -1,11 +1,7 @@ -import { - EditableTextEditor, - RangeExpansionBehavior, - Selection, -} from "@cursorless/common"; +import { EditableTextEditor, RangeExpansionBehavior } from "@cursorless/common"; import { zip } from "lodash-es"; import { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateSelectionsWithBehavior } from "../../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../../core/updateSelections/updateSelections"; import { EditDestination, State } from "./EditNew.types"; /** @@ -46,10 +42,6 @@ export async function runEditTargets( destination.destination.constructChangeEdit(""), ); - const thatSelections = { - selections: state.thatRanges.map((r) => r.toSelection(false)), - }; - // We need to remove undefined cursor locations. Note that these undefined // locations will be the locations where our edit targets will go. The only // cursor positions defined at this point will have come from command targets @@ -59,44 +51,45 @@ export async function runEditTargets( .filter(({ range }) => range != null); const cursorIndices = cursorInfos.map(({ index }) => index); + const cursorRanges = cursorInfos.map(({ range }) => range!); + const editRanges = edits.map((edit) => edit.range); - const cursorSelections = { - selections: cursorInfos.map(({ range }) => range!.toSelection(false)), - }; - - const editSelections = { - selections: edits.map((edit) => edit.range.toSelection(false)), - rangeBehavior: RangeExpansionBehavior.openOpen, - }; - - const [ - updatedThatSelections, - updatedCursorSelections, - updatedEditSelections, - ]: Selection[][] = await performEditsAndUpdateSelectionsWithBehavior( + const { + thatRanges: updatedThatRanges, + cursorRanges: updatedCursorRanges, + editRanges: updatedEditRanges, + } = await performEditsAndUpdateSelections({ rangeUpdater, editor, edits, - [thatSelections, cursorSelections, editSelections], - ); + preserveCursorSelections: true, + selections: { + thatRanges: state.thatRanges, + cursorRanges, + editRanges: { + selections: editRanges, + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); - const updatedCursorRanges = [...state.cursorRanges]; + const finalCursorRanges = [...state.cursorRanges]; // Update the cursor positions for the command targets - zip(cursorIndices, updatedCursorSelections).forEach(([index, selection]) => { - updatedCursorRanges[index!] = selection; + zip(cursorIndices, updatedCursorRanges).forEach(([index, range]) => { + finalCursorRanges[index!] = range; }); // Add cursor positions for our edit targets. destinations.forEach((delimiterTarget, index) => { const edit = edits[index]; - const range = edit.updateRange(updatedEditSelections[index]); - updatedCursorRanges[delimiterTarget.index] = range; + const range = edit.updateRange(updatedEditRanges[index]); + finalCursorRanges[delimiterTarget.index] = range; }); return { destinations: state.destinations, - thatRanges: updatedThatSelections, - cursorRanges: updatedCursorRanges, + thatRanges: updatedThatRanges, + cursorRanges: finalCursorRanges, }; } diff --git a/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts b/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts index d088d5cf48..38a29b9883 100644 --- a/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts +++ b/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts @@ -1,6 +1,6 @@ import { CommandCapabilities, EditableTextEditor } from "@cursorless/common"; import { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; -import { callFunctionAndUpdateRanges } from "../../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../../core/updateSelections/updateSelections"; import { EditDestination, State } from "./EditNew.types"; /** @@ -39,27 +39,33 @@ export async function runInsertLineAfterTargets( const contentRanges = destinations.map( ({ destination }) => destination.contentRange, ); + const targetRanges = state.destinations.map( + ({ contentRange }) => contentRange, + ); + + const callback = async () => { + if (acceptsLocation) { + await editor.insertLineAfter(contentRanges); + } else { + await editor.setSelections( + contentRanges.map((range) => range.toSelection(false)), + ); + await editor.focus(); + await editor.insertLineAfter(); + } + }; - const [updatedTargetRanges, updatedThatRanges] = - await callFunctionAndUpdateRanges( + const { targetRanges: updatedTargetRanges, thatRanges: updatedThatRanges } = + await performEditsAndUpdateSelections({ rangeUpdater, - async () => { - if (acceptsLocation) { - await editor.insertLineAfter(contentRanges); - } else { - await editor.setSelections( - contentRanges.map((range) => range.toSelection(false)), - ); - await editor.focus(); - await editor.insertLineAfter(); - } + editor, + callback, + preserveCursorSelections: true, + selections: { + targetRanges, + thatRanges: state.thatRanges, }, - editor.document, - [ - state.destinations.map(({ contentRange }) => contentRange), - state.thatRanges, - ], - ); + }); // For each of the given command targets, the cursor will go where it ended // up after running the command. We add it to the state so that any diff --git a/packages/cursorless-engine/src/actions/InsertCopy.ts b/packages/cursorless-engine/src/actions/InsertCopy.ts index 25528b1e62..7440dc1b77 100644 --- a/packages/cursorless-engine/src/actions/InsertCopy.ts +++ b/packages/cursorless-engine/src/actions/InsertCopy.ts @@ -1,19 +1,18 @@ import { FlashStyle, RangeExpansionBehavior, - Selection, TextEditor, toCharacterRange, } from "@cursorless/common"; import { flatten, zip } from "lodash-es"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateSelectionsWithBehavior } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { containingLineIfUntypedModifier } from "../processTargets/modifiers/commonContainingScopeIfUntypedModifiers"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; -import { SimpleAction, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue, SimpleAction } from "./actions.types"; class InsertCopy implements SimpleAction { getFinalStages = () => [ @@ -56,36 +55,32 @@ class InsertCopy implements SimpleAction { const edits = targets.flatMap((target) => target.toDestination(position).constructChangeEdit(target.contentText), ); - - const cursorSelections = { selections: editor.selections }; - const contentSelections = { - selections: targets.map(({ contentSelection }) => contentSelection), - }; - const editSelections = { - selections: edits.map( - ({ range }) => new Selection(range.start, range.end), - ), - rangeBehavior: RangeExpansionBehavior.openOpen, - }; - + const contentSelections = targets.map( + ({ contentSelection }) => contentSelection, + ); + const editRanges = edits.map(({ range }) => range); const editableEditor = ide().getEditableTextEditor(editor); - const [ - updatedCursorSelections, - updatedContentSelections, - updatedEditSelections, - ]: Selection[][] = await performEditsAndUpdateSelectionsWithBehavior( - this.rangeUpdater, - editableEditor, + const { + contentSelections: updatedContentSelections, + editRanges: updatedEditRanges, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: editableEditor, edits, - [cursorSelections, contentSelections, editSelections], - ); + selections: { + contentSelections, + editRanges: { + selections: editRanges, + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); - const insertionRanges = zip(edits, updatedEditSelections).map( - ([edit, selection]) => edit!.updateRange(selection!), + const insertionRanges = zip(edits, updatedEditRanges).map(([edit, range]) => + edit!.updateRange(range!), ); - await editableEditor.setSelections(updatedCursorSelections); const primarySelection = editor.selections[0]; if ( diff --git a/packages/cursorless-engine/src/actions/InsertEmptyLines.ts b/packages/cursorless-engine/src/actions/InsertEmptyLines.ts index a29bbbbc25..f9ae3f8f39 100644 --- a/packages/cursorless-engine/src/actions/InsertEmptyLines.ts +++ b/packages/cursorless-engine/src/actions/InsertEmptyLines.ts @@ -1,11 +1,11 @@ -import { FlashStyle, Range, Selection, toLineRange } from "@cursorless/common"; +import { FlashStyle, Range, toLineRange } from "@cursorless/common"; import { flatten } from "lodash-es"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import { SimpleAction, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue, SimpleAction } from "./actions.types"; class InsertEmptyLines implements SimpleAction { constructor( @@ -45,22 +45,23 @@ class InsertEmptyLines implements SimpleAction { await runOnTargetsForEachEditor(targets, async (editor, targets) => { const ranges = this.getRanges(targets); const edits = this.getEdits(ranges); - + const contentSelections = targets.map( + (target) => target.thatTarget.contentSelection, + ); const editableEditor = ide().getEditableTextEditor(editor); - const [updatedThatSelections, lineSelections, updatedCursorSelections] = - await performEditsAndUpdateSelections( - this.rangeUpdater, - editableEditor, - edits, - [ - targets.map((target) => target.thatTarget.contentSelection), - ranges.map((range) => new Selection(range.start, range.end)), - editor.selections, - ], - ); - - await editableEditor.setSelections(updatedCursorSelections); + const { + contentSelections: updatedThatSelections, + ranges: lineSelections, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: editableEditor, + edits, + selections: { + contentSelections, + ranges, + }, + }); return { thatMark: updatedThatSelections.map((selection) => ({ diff --git a/packages/cursorless-engine/src/actions/InsertSnippet.ts b/packages/cursorless-engine/src/actions/InsertSnippet.ts index 0ae8701c9c..375a9e0b3b 100644 --- a/packages/cursorless-engine/src/actions/InsertSnippet.ts +++ b/packages/cursorless-engine/src/actions/InsertSnippet.ts @@ -8,10 +8,7 @@ import { } from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { - callFunctionAndUpdateSelectionInfos, - getSelectionInfo, -} from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedExplicitStage } from "../processTargets/modifiers/ConditionalModifierStages"; import { UntypedTarget } from "../processTargets/targets"; @@ -116,13 +113,6 @@ export default class InsertSnippet { await this.actions.editNew.run(destinations); - const targetSelectionInfos = editor.selections.map((selection) => - getSelectionInfo( - editor.document, - selection, - RangeExpansionBehavior.openOpen, - ), - ); const { body, formatSubstitutions } = this.getSnippetInfo( snippetDescription, // Use new selection locations instead of original targets because @@ -148,17 +138,22 @@ export default class InsertSnippet { const snippetString = parsedSnippet.toTextmateString(); - // NB: We used the command "editor.action.insertSnippet" instead of calling editor.insertSnippet - // because the latter doesn't support special variables like CLIPBOARD - const [updatedTargetSelections] = await callFunctionAndUpdateSelectionInfos( - this.rangeUpdater, - () => editor.insertSnippet(snippetString), - editor.document, - [targetSelectionInfos], - ); + const { editorSelections: updatedThatSelections } = + await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor, + callback: () => editor.insertSnippet(snippetString), + preserveCursorSelections: true, + selections: { + editorSelections: { + selections: editor.selections, + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); return { - thatSelections: updatedTargetSelections.map((selection) => ({ + thatSelections: updatedThatSelections.map((selection) => ({ editor, selection, })), diff --git a/packages/cursorless-engine/src/actions/JoinLines.ts b/packages/cursorless-engine/src/actions/JoinLines.ts index 7d8acb0c0e..ab52112485 100644 --- a/packages/cursorless-engine/src/actions/JoinLines.ts +++ b/packages/cursorless-engine/src/actions/JoinLines.ts @@ -2,7 +2,7 @@ import { Edit, FlashStyle, Range, TextEditor } from "@cursorless/common"; import { range as iterRange, map, pairwise } from "itertools"; import { flatten, zip } from "lodash-es"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; @@ -19,14 +19,16 @@ export default class JoinLines { const thatSelections = flatten( await runOnTargetsForEachEditor(targets, async (editor, targets) => { const contentRanges = targets.map(({ contentRange }) => contentRange); - const edits = getEdits(editor, contentRanges); - const [updatedRanges] = await performEditsAndUpdateRanges( - this.rangeUpdater, - ide().getEditableTextEditor(editor), - edits, - [contentRanges], - ); + const { contentRanges: updatedRanges } = + await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: ide().getEditableTextEditor(editor), + edits: getEdits(editor, contentRanges), + selections: { + contentRanges, + }, + }); return zip(targets, updatedRanges).map(([target, range]) => ({ editor: target!.editor, diff --git a/packages/cursorless-engine/src/actions/PasteFromClipboard.ts b/packages/cursorless-engine/src/actions/PasteFromClipboard.ts index a7cd932e07..1b148a2f3e 100644 --- a/packages/cursorless-engine/src/actions/PasteFromClipboard.ts +++ b/packages/cursorless-engine/src/actions/PasteFromClipboard.ts @@ -4,10 +4,7 @@ import { toCharacterRange, } from "@cursorless/common"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { - callFunctionAndUpdateSelections, - callFunctionAndUpdateSelectionsWithBehavior, -} from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { Destination } from "../typings/target.types"; import { ensureSingleEditor } from "../util/targetUtils"; @@ -29,32 +26,38 @@ export class PasteFromClipboard { // First call editNew in order to insert delimiters if necessary and leave // the cursor in the right position. Note that this action will focus the // editor containing the targets - const [originalCursorSelections] = await callFunctionAndUpdateSelections( - this.rangeUpdater, - async () => { - await this.actions.editNew.run(destinations); - }, - editor.document, - [editor.selections], - ); + const callbackEdit = async () => { + await this.actions.editNew.run(destinations); + }; + + const { cursorSelections: originalCursorSelections } = + await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor, + preserveCursorSelections: true, + callback: callbackEdit, + selections: { + cursorSelections: editor.selections, + }, + }); // Then use VSCode paste command, using open ranges at the place where we // paste in order to capture the pasted text for highlights and `that` mark - const [updatedCursorSelections, updatedTargetSelections] = - await callFunctionAndUpdateSelectionsWithBehavior( - this.rangeUpdater, - () => editor.clipboardPaste(), - editor.document, - [ - { - selections: originalCursorSelections, - }, - { - selections: editor.selections, - rangeBehavior: RangeExpansionBehavior.openOpen, - }, - ], - ); + const { + originalCursorSelections: updatedCursorSelections, + editorSelections: updatedTargetSelections, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor, + callback: () => editor.clipboardPaste(), + selections: { + originalCursorSelections, + editorSelections: { + selections: editor.selections, + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); // Reset cursors on the editor where the edits took place. // NB: We don't focus the editor here because we want to focus the original diff --git a/packages/cursorless-engine/src/actions/Remove.ts b/packages/cursorless-engine/src/actions/Remove.ts index ff1dfc887f..017ae56d71 100644 --- a/packages/cursorless-engine/src/actions/Remove.ts +++ b/packages/cursorless-engine/src/actions/Remove.ts @@ -1,4 +1,4 @@ -import { FlashStyle, Selection, TextEditor } from "@cursorless/common"; +import { FlashStyle, TextEditor } from "@cursorless/common"; import { flatten, zip } from "lodash-es"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; @@ -7,7 +7,7 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; import { unifyRemovalTargets } from "../util/unifyRanges"; -import { SimpleAction, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue, SimpleAction } from "./actions.types"; export default class Delete implements SimpleAction { constructor(private rangeUpdater: RangeUpdater) { @@ -37,22 +37,19 @@ export default class Delete implements SimpleAction { private async runForEditor(editor: TextEditor, targets: Target[]) { const edits = targets.map((target) => target.constructRemovalEdit()); - - const cursorSelections = editor.selections; - const editSelections = edits.map(({ range }) => range.toSelection(false)); const editableEditor = ide().getEditableTextEditor(editor); - const [updatedCursorSelections, updatedEditSelections]: Selection[][] = - await performEditsAndUpdateSelections( - this.rangeUpdater, - editableEditor, + const { editRanges: updatedEditRanges } = + await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: editableEditor, edits, - [cursorSelections, editSelections], - ); - - await editableEditor.setSelections(updatedCursorSelections); + selections: { + editRanges: edits.map(({ range }) => range), + }, + }); - return zip(targets, updatedEditSelections).map( + return zip(targets, updatedEditRanges).map( ([target, range]) => new RawSelectionTarget({ editor: target!.editor, diff --git a/packages/cursorless-engine/src/actions/Replace.ts b/packages/cursorless-engine/src/actions/Replace.ts index 8ea474b385..7e6ca746b2 100644 --- a/packages/cursorless-engine/src/actions/Replace.ts +++ b/packages/cursorless-engine/src/actions/Replace.ts @@ -5,7 +5,7 @@ import { } from "@cursorless/common"; import { zip } from "lodash-es"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateSelectionsWithBehavior } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { SelectionWithEditor } from "../typings/Types"; import { Destination, Target } from "../typings/target.types"; @@ -63,33 +63,38 @@ export default class Replace { await runForEachEditor( edits, (edit) => edit.editor, - async (editor, edits) => { - const contentSelections = { - selections: edits.map(({ target }) => target.contentSelection), - }; - const editSelections = { - selections: edits.map(({ edit }) => edit.range.toSelection(false)), - rangeBehavior: RangeExpansionBehavior.openOpen, - }; + async (editor, editWrappers) => { + const edits = editWrappers.map(({ edit }) => edit); - const [updatedContentSelections, updatedEditSelections] = - await performEditsAndUpdateSelectionsWithBehavior( - this.rangeUpdater, - ide().getEditableTextEditor(editor), - edits.map(({ edit }) => edit), - [contentSelections, editSelections], - ); + const { + contentSelections: updatedContentSelections, + editRanges: updatedEditRanges, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: ide().getEditableTextEditor(editor), + edits, + selections: { + contentSelections: editWrappers.map( + ({ target }) => target.contentSelection, + ), + editRanges: { + selections: edits.map(({ range }) => range), + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); - for (const [edit, selection] of zip(edits, updatedContentSelections)) { - sourceTargets.push(edit!.target.withContentRange(selection!)); + for (const [wrapper, selection] of zip( + editWrappers, + updatedContentSelections, + )) { + sourceTargets.push(wrapper!.target.withContentRange(selection!)); } - for (const [edit, selection] of zip(edits, updatedEditSelections)) { + for (const [wrapper, range] of zip(editWrappers, updatedEditRanges)) { thatSelections.push({ editor, - selection: edit!.edit - .updateRange(selection!) - .toSelection(selection!.isReversed), + selection: wrapper!.edit.updateRange(range!).toSelection(false), }); } }, diff --git a/packages/cursorless-engine/src/actions/Rewrap.ts b/packages/cursorless-engine/src/actions/Rewrap.ts index dd057e655c..5eb2a8d2ce 100644 --- a/packages/cursorless-engine/src/actions/Rewrap.ts +++ b/packages/cursorless-engine/src/actions/Rewrap.ts @@ -1,6 +1,7 @@ import { FlashStyle } from "@cursorless/common"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { getContainingSurroundingPairIfNoBoundaryStage } from "../processTargets/modifiers/InteriorStage"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; @@ -10,7 +11,6 @@ import { runOnTargetsForEachEditor, } from "../util/targetUtils"; import { ActionReturnValue } from "./actions.types"; -import { getContainingSurroundingPairIfNoBoundaryStage } from "../processTargets/modifiers/InteriorStage"; export default class Rewrap { getFinalStages = () => [ @@ -50,16 +50,20 @@ export default class Rewrap { text: i % 2 === 0 ? left : right, })); - const [updatedSourceRanges, updatedThatRanges] = - await performEditsAndUpdateRanges( - this.rangeUpdater, - ide().getEditableTextEditor(editor), - edits, - [ - targets.map((target) => target.thatTarget.contentRange), - targets.map((target) => target.contentRange), - ], - ); + const { + sourceRanges: updatedSourceRanges, + thatRanges: updatedThatRanges, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: ide().getEditableTextEditor(editor), + edits, + selections: { + sourceRanges: targets.map( + (target) => target.thatTarget.contentRange, + ), + thatRanges: targets.map((target) => target.contentRange), + }, + }); return { sourceMark: createThatMark(targets, updatedSourceRanges), diff --git a/packages/cursorless-engine/src/actions/Wrap.ts b/packages/cursorless-engine/src/actions/Wrap.ts index 76e01f055c..d7c1b76c33 100644 --- a/packages/cursorless-engine/src/actions/Wrap.ts +++ b/packages/cursorless-engine/src/actions/Wrap.ts @@ -6,13 +6,9 @@ import { toCharacterRange, } from "@cursorless/common"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { - getSelectionInfo, - performEditsAndUpdateFullSelectionInfos, -} from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; -import { FullSelectionInfo } from "../typings/updateSelections"; import { runOnTargetsForEachEditor } from "../util/targetUtils"; import { ActionReturnValue } from "./actions.types"; @@ -29,7 +25,6 @@ export default class Wrap { const results = await runOnTargetsForEachEditor( targets, async (editor, targets) => { - const { document } = editor; const boundaries = targets.map((target) => ({ start: new Selection( target.contentRange.start, @@ -50,67 +45,43 @@ export default class Wrap { }, ]); - const delimiterSelectionInfos: FullSelectionInfo[] = boundaries.flatMap( - ({ start, end }) => { - return [ - getSelectionInfo( - document, - start, - RangeExpansionBehavior.openClosed, - ), - getSelectionInfo( - document, - end, - RangeExpansionBehavior.closedOpen, - ), - ]; - }, - ); - - const cursorSelectionInfos = editor.selections.map((selection) => - getSelectionInfo( - document, - selection, - RangeExpansionBehavior.closedClosed, - ), + const contentSelections = targets.map( + (target) => target.contentSelection, ); - const sourceMarkSelectionInfos = targets.map((target) => - getSelectionInfo( - document, - target.contentSelection, - RangeExpansionBehavior.closedClosed, - ), - ); - - const thatMarkSelectionInfos = targets.map((target) => - getSelectionInfo( - document, - target.contentSelection, - RangeExpansionBehavior.openOpen, - ), - ); - - const editableEditor = ide().getEditableTextEditor(editor); - - const [ - delimiterSelections, - cursorSelections, - sourceMarkSelections, - thatMarkSelections, - ] = await performEditsAndUpdateFullSelectionInfos( - this.rangeUpdater, - editableEditor, + const { + boundariesStartSelections: delimiterStartSelections, + boundariesEndSelections: delimiterEndSelections, + sourceSelections: sourceMarkSelections, + thatSelections: thatMarkSelections, + } = await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor: ide().getEditableTextEditor(editor), edits, - [ - delimiterSelectionInfos, - cursorSelectionInfos, - sourceMarkSelectionInfos, - thatMarkSelectionInfos, - ], - ); + selections: { + boundariesStartSelections: { + selections: boundaries.map(({ start }) => start), + behavior: RangeExpansionBehavior.openClosed, + }, + boundariesEndSelections: { + selections: boundaries.map(({ end }) => end), + behavior: RangeExpansionBehavior.closedOpen, + }, + sourceSelections: { + selections: contentSelections, + behavior: RangeExpansionBehavior.closedClosed, + }, + thatSelections: { + selections: contentSelections, + behavior: RangeExpansionBehavior.openOpen, + }, + }, + }); - await editableEditor.setSelections(cursorSelections); + const delimiterSelections = [ + ...delimiterStartSelections, + ...delimiterEndSelections, + ]; await ide().flashRanges( delimiterSelections.map((selection) => ({ diff --git a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts index 56ca6942ae..c7588d4a52 100644 --- a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts +++ b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts @@ -1,7 +1,7 @@ import { FlashStyle, ScopeType, WrapWithSnippetArg } from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedStage } from "../processTargets/modifiers/ConditionalModifierStages"; import { ide } from "../singletons/ide.singleton"; @@ -102,14 +102,19 @@ export default class WrapWithSnippet { const targetSelections = targets.map((target) => target.contentSelection); - // NB: We used the command "editor.action.insertSnippet" instead of calling editor.insertSnippet - // because the latter doesn't support special variables like CLIPBOARD - const [updatedTargetSelections] = await callFunctionAndUpdateSelections( - this.rangeUpdater, - () => editor.insertSnippet(snippetString, targetSelections), - editor.document, - [targetSelections], - ); + const callback = () => + editor.insertSnippet(snippetString, targetSelections); + + const { targetSelections: updatedTargetSelections } = + await performEditsAndUpdateSelections({ + rangeUpdater: this.rangeUpdater, + editor, + callback, + preserveCursorSelections: true, + selections: { + targetSelections, + }, + }); return { thatSelections: updatedTargetSelections.map((selection) => ({ diff --git a/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts b/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts index 9a1893a815..d5cfbe6c64 100644 --- a/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts +++ b/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts @@ -5,6 +5,7 @@ import { RangeExpansionBehavior, Selection, TextDocument, + unsafeKeys, } from "@cursorless/common"; import { flatten } from "lodash-es"; import { @@ -14,33 +15,131 @@ import { import { performDocumentEdits } from "../../util/performDocumentEdits"; import { RangeUpdater } from "./RangeUpdater"; +/** Selections OR ranges */ +type SelectionsOrRanges = readonly Selection[] | readonly Range[]; + +/** Selections to be updated with specified expansion behavior */ interface SelectionsWithBehavior { - selections: readonly Selection[]; - rangeBehavior?: RangeExpansionBehavior; + /** Selections or ranges to be updated */ + selections: SelectionsOrRanges; + /** The behavior to use expanding ranges */ + behavior: RangeExpansionBehavior; +} + +/** Base properties for updating selections */ +interface BaseProps { + /** A RangeUpdate instance that will perform actual range updating */ + rangeUpdater: RangeUpdater; + /** The editor containing the selections */ + editor: EditableTextEditor; + /** Whether to preserve the editor's current cursor selections */ + preserveCursorSelections?: boolean; + /** The selections to update */ + selections: Record; +} + +/** Updater properties for editor edits */ +interface EditsProps extends BaseProps { + edits: Edit[]; } +/** Updater properties for callback */ +interface CallbackProps extends BaseProps { + callback: () => Promise; +} + +/** Updater properties. Can contain edits OR a callback */ +type UpdaterProps = EditsProps | CallbackProps; + /** - * Given a selection, this function creates a `SelectionInfo` object that can - * be passed in to any of the commands that update selections. - * - * @param document The document containing the selection - * @param selection The selection - * @param rangeBehavior How selection should behave with respect to insertions on either end - * @returns An object that can be used for selection tracking + * Perform editor edits or call callback and update the selections based on the + * changes that occurred. + * @returns The initial selections updated based upon the changes that occurred. */ -export function getSelectionInfo( +export async function performEditsAndUpdateSelections({ + rangeUpdater, + editor, + selections, + preserveCursorSelections: preserveEditorSelections, + ...rest +}: UpdaterProps): Promise> { + const keys = unsafeKeys(selections); + + const selectionInfos = keys.map((key) => { + const selectionValue = selections[key]; + const selectionsWithBehavior = getSelectionsWithBehavior(selectionValue); + return getFullSelectionInfos( + editor.document, + selectionsWithBehavior.selections, + selectionsWithBehavior.behavior, + ); + }); + + if (!preserveEditorSelections) { + selectionInfos.push( + getFullSelectionInfos( + editor.document, + editor.selections, + RangeExpansionBehavior.closedClosed, + ), + ); + } + + const updatedSelectionsMatrix = await (() => { + if ("edits" in rest) { + return performEditsAndUpdateFullSelectionInfos( + rangeUpdater, + editor, + rest.edits, + selectionInfos, + ); + } + return callFunctionAndUpdateFullSelectionInfos( + rangeUpdater, + rest.callback, + editor.document, + selectionInfos, + ); + })(); + + if (!preserveEditorSelections) { + await editor.setSelections(updatedSelectionsMatrix.pop()!); + } + + const result = Object.fromEntries( + keys.map((key, index) => [key, updatedSelectionsMatrix[index]]), + ); + + return result as Record; +} + +function getFullSelectionInfos( document: TextDocument, - selection: Selection, + selections: readonly Selection[] | readonly Range[], rangeBehavior: RangeExpansionBehavior, -): FullSelectionInfo { - return getSelectionInfoInternal( - document, - selection, - !selection.isReversed, - rangeBehavior, +): FullSelectionInfo[] { + return selections.map((selection) => + getSelectionInfoInternal( + document, + selection, + selection instanceof Selection ? !selection.isReversed : true, + rangeBehavior, + ), ); } +function getSelectionsWithBehavior( + selections: SelectionsOrRanges | SelectionsWithBehavior, +): SelectionsWithBehavior { + if ("selections" in selections) { + return selections; + } + return { + selections, + behavior: RangeExpansionBehavior.closedClosed, + }; +} + function getSelectionInfoInternal( document: TextDocument, range: Range, @@ -74,57 +173,6 @@ function getSelectionInfoInternal( }; } -/** - * Creates SelectionInfo objects for all selections in a list of lists. - * - * @param document The document containing the selections - * @param selectionMatrix A list of lists of selections - * @param rangeBehavior How selections should behave with respect to insertions on either end - * @returns A list of lists of selection info objects - */ -function selectionsToSelectionInfos( - document: TextDocument, - selectionMatrix: (readonly Selection[])[], - rangeBehavior: RangeExpansionBehavior = RangeExpansionBehavior.closedClosed, -): FullSelectionInfo[][] { - return selectionMatrix.map((selections) => - selections.map((selection) => - getSelectionInfo(document, selection, rangeBehavior), - ), - ); -} - -function rangesToSelectionInfos( - document: TextDocument, - rangeMatrix: (readonly Range[])[], - rangeBehavior: RangeExpansionBehavior = RangeExpansionBehavior.closedClosed, -): FullSelectionInfo[][] { - return rangeMatrix.map((ranges) => - ranges.map((range) => - getSelectionInfoInternal(document, range, false, rangeBehavior), - ), - ); -} - -function fillOutSelectionInfos( - document: TextDocument, - selectionInfoMatrix: SelectionInfo[][], -): selectionInfoMatrix is FullSelectionInfo[][] { - selectionInfoMatrix.forEach((selectionInfos) => - selectionInfos.map((selectionInfo) => { - const { range } = selectionInfo; - Object.assign(selectionInfo, { - offsets: { - start: document.offsetAt(range.start), - end: document.offsetAt(range.end), - }, - text: document.getText(range), - }); - }), - ); - return true; -} - function selectionInfosToSelections( selectionInfoMatrix: SelectionInfo[][], ): Selection[][] { @@ -141,195 +189,25 @@ function selectionInfosToSelections( * @param rangeUpdater A RangeUpdate instance that will perform actual range updating * @param func The function to call * @param document The document containing the selections - * @param selectionMatrix A matrix of selections to update - * @returns The initial selections updated based upon what happened in the function - */ -export async function callFunctionAndUpdateSelections( - rangeUpdater: RangeUpdater, - func: () => Promise, - document: TextDocument, - selectionMatrix: (readonly Selection[])[], -): Promise { - const selectionInfoMatrix = selectionsToSelectionInfos( - document, - selectionMatrix, - ); - - return await callFunctionAndUpdateSelectionInfos( - rangeUpdater, - func, - document, - selectionInfoMatrix, - ); -} - -export async function callFunctionAndUpdateRanges( - rangeUpdater: RangeUpdater, - func: () => Promise, - document: TextDocument, - rangeMatrix: (readonly Range[])[], -): Promise { - const selectionInfoMatrix = rangesToSelectionInfos(document, rangeMatrix); - - return await callFunctionAndUpdateSelectionInfos( - rangeUpdater, - func, - document, - selectionInfoMatrix, - ); -} - -/** - * Calls the given function and updates the given selections based on the - * changes that occurred as a result of calling function. - * @param rangeUpdater A RangeUpdate instance that will perform actual range updating - * @param func The function to call - * @param document The document containing the selections - * @param selectionInfoMatrix A matrix of selection info objects to update - * @returns The initial selections updated based upon what happened in the function + * @param originalSelectionInfos The selection info objects to update + * @returns The updated selections */ -export async function callFunctionAndUpdateSelectionInfos( +async function callFunctionAndUpdateFullSelectionInfos( rangeUpdater: RangeUpdater, func: () => Promise, document: TextDocument, - selectionInfoMatrix: FullSelectionInfo[][], + originalSelectionInfos: FullSelectionInfo[][], ) { const unsubscribe = rangeUpdater.registerRangeInfoList( document, - flatten(selectionInfoMatrix), + flatten(originalSelectionInfos), ); await func(); unsubscribe(); - return selectionInfosToSelections(selectionInfoMatrix); -} - -/** - * Performs a list of edits and returns the given selections updated based on - * the applied edits - * @param rangeUpdater A RangeUpdate instance that will perform actual range updating - * @param func The function to call - * @param document The document containing the selections - * @param originalSelections The selections to update - * @returns The updated selections - */ -export function callFunctionAndUpdateSelectionsWithBehavior( - rangeUpdater: RangeUpdater, - func: () => Promise, - document: TextDocument, - originalSelections: SelectionsWithBehavior[], -) { - return callFunctionAndUpdateSelectionInfos( - rangeUpdater, - func, - document, - originalSelections.map((selectionsWithBehavior) => - selectionsWithBehavior.selections.map((selection) => - getSelectionInfo( - document, - selection, - selectionsWithBehavior.rangeBehavior ?? - RangeExpansionBehavior.closedClosed, - ), - ), - ), - ); -} - -/** - * Performs a list of edits and returns the given selections updated based on - * the applied edits - * @param rangeUpdater A RangeUpdate instance that will perform actual range updating - * @param editor The editor containing the selections - * @param edits A list of edits to apply - * @param originalSelections The selections to update - * @returns The updated selections - */ -export async function performEditsAndUpdateSelections( - rangeUpdater: RangeUpdater, - editor: EditableTextEditor, - edits: Edit[], - originalSelections: (readonly Selection[])[], -) { - const document = editor.document; - const selectionInfoMatrix = selectionsToSelectionInfos( - document, - originalSelections, - ); - return performEditsAndUpdateFullSelectionInfos( - rangeUpdater, - editor, - edits, - selectionInfoMatrix, - ); -} - -/** - * Performs a list of edits and returns the given selections updated based on - * the applied edits - * @param rangeUpdater A RangeUpdate instance that will perform actual range updating - * @param editor The editor containing the selections - * @param edits A list of edits to apply - * @param originalSelections The selections to update - * @param rangeBehavior How selections should behave with respect to insertions on either end - * @returns The updated selections - */ -export function performEditsAndUpdateSelectionsWithBehavior( - rangeUpdater: RangeUpdater, - editor: EditableTextEditor, - edits: Edit[], - originalSelections: SelectionsWithBehavior[], -) { - return performEditsAndUpdateFullSelectionInfos( - rangeUpdater, - editor, - edits, - originalSelections.map((selectionsWithBehavior) => - selectionsWithBehavior.selections.map((selection) => - getSelectionInfo( - editor.document, - selection, - selectionsWithBehavior.rangeBehavior ?? - RangeExpansionBehavior.closedClosed, - ), - ), - ), - ); -} - -export async function performEditsAndUpdateRanges( - rangeUpdater: RangeUpdater, - editor: EditableTextEditor, - edits: Edit[], - originalRanges: (readonly Range[])[], -): Promise { - const document = editor.document; - const selectionInfoMatrix = rangesToSelectionInfos(document, originalRanges); - return performEditsAndUpdateFullSelectionInfos( - rangeUpdater, - editor, - edits, - selectionInfoMatrix, - ); -} - -// FIXME: Remove this function if we don't end up using it for the next couple use cases, eg `that` mark and cursor history -export async function performEditsAndUpdateSelectionInfos( - rangeUpdater: RangeUpdater, - editor: EditableTextEditor, - edits: Edit[], - originalSelectionInfos: SelectionInfo[][], -): Promise { - fillOutSelectionInfos(editor.document, originalSelectionInfos); - - return await performEditsAndUpdateFullSelectionInfos( - rangeUpdater, - editor, - edits, - originalSelectionInfos as FullSelectionInfo[][], - ); + return selectionInfosToSelections(originalSelectionInfos); } /** @@ -341,7 +219,7 @@ export async function performEditsAndUpdateSelectionInfos( * @param originalSelectionInfos The selection info objects to update * @returns The updated selections */ -export async function performEditsAndUpdateFullSelectionInfos( +async function performEditsAndUpdateFullSelectionInfos( rangeUpdater: RangeUpdater, editor: EditableTextEditor, edits: Edit[], @@ -382,7 +260,7 @@ export async function performEditsAndUpdateFullSelectionInfos( } }; - return await callFunctionAndUpdateSelectionInfos( + return await callFunctionAndUpdateFullSelectionInfos( rangeUpdater, func, editor.document, diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeInsertSnippets.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeInsertSnippets.ts index 14a4933ea0..0f8c5e6030 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeInsertSnippets.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeInsertSnippets.ts @@ -12,6 +12,9 @@ export async function vscodeInsertSnippet( } await editor.focus(); + + // NB: We use the command "editor.action.insertSnippet" instead of calling editor.insertSnippet + // because the latter doesn't support special variables like CLIPBOARD await vscode.commands.executeCommand("editor.action.insertSnippet", { snippet, });