diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts index 279645e2be..ceb17e6c9c 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts @@ -46,10 +46,8 @@ export default class InstanceStage implements ModifierStage { } private handleEveryScope(target: Target): Target[] { - const { editor } = target; - return Array.from( - flatmap(this.getEveryRanges(editor), (searchRange) => + flatmap(this.getEveryRanges(target), ([editor, searchRange]) => this.getTargetIterable(target, editor, searchRange, "forward"), ), ); @@ -59,9 +57,7 @@ export default class InstanceStage implements ModifierStage { target: Target, { start, length }: OrdinalScopeModifier, ): Target[] { - const { editor } = target; - - return this.getEveryRanges(editor).flatMap((searchRange) => + return this.getEveryRanges(target).flatMap(([editor, searchRange]) => takeFromOffset( this.getTargetIterable( target, @@ -79,13 +75,12 @@ export default class InstanceStage implements ModifierStage { target: Target, { direction, offset, length }: RelativeScopeModifier, ): Target[] { - const { editor } = target; - const referenceTargets = this.storedTargets.get("instanceReference") ?? [ target, ]; return referenceTargets.flatMap((referenceTarget) => { + const { editor } = referenceTarget; const iterationRange = direction === "forward" ? new Range( @@ -109,11 +104,14 @@ export default class InstanceStage implements ModifierStage { }); } - private getEveryRanges(editor: TextEditor): Range[] { + private getEveryRanges({ + editor: targetEditor, + }: Target): readonly (readonly [TextEditor, Range])[] { return ( this.storedTargets .get("instanceReference") - ?.map(({ contentRange }) => contentRange) ?? [editor.document.range] + ?.map(({ editor, contentRange }) => [editor, contentRange] as const) ?? + ([[targetEditor, targetEditor.document.range]] as const) ); } diff --git a/packages/cursorless-vscode-e2e/src/suite/instanceAcrossSplit.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/instanceAcrossSplit.vscode.test.ts new file mode 100644 index 0000000000..54b0be801d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/instanceAcrossSplit.vscode.test.ts @@ -0,0 +1,165 @@ +import { + HatStability, + Modifier, + Range, + SpyIDE, + asyncSafety, +} from "@cursorless/common"; +import { + getCursorlessApi, + openNewEditor, + runCursorlessCommand, +} from "@cursorless/vscode-common"; +import * as assert from "assert"; +import { Selection } from "vscode"; +import { endToEndTestSetup } from "../endToEndTestSetup"; +import { setupFake } from "./setupFake"; + +// Ensure that the "from" / "instance" work properly when "from" +// is run in a different editor from "instance" +suite("Instance across split", async function () { + const { getSpy } = endToEndTestSetup(this); + + suiteSetup(async () => { + const { ide } = (await getCursorlessApi()).testHelpers!; + setupFake(ide, HatStability.stable); + }); + + test( + "Every instance", + asyncSafety(() => + runTest( + getSpy()!, + { + type: "everyScope", + scopeType: { type: "instance" }, + }, + true, + " bbb ", + ), + ), + ); + test( + "Next instance", + asyncSafety(() => + runTest( + getSpy()!, + { + type: "relativeScope", + scopeType: { type: "instance" }, + direction: "forward", + length: 1, + offset: 1, + }, + false, + " bbb aaa aaa", + ), + ), + ); + test( + "Two instances", + asyncSafety(() => + runTest( + getSpy()!, + { + type: "relativeScope", + scopeType: { type: "instance" }, + direction: "forward", + length: 2, + offset: 0, + }, + false, + " bbb aaa", + ), + ), + ); + test( + "Second instance", + asyncSafety(() => + runTest( + getSpy()!, + { + type: "ordinalScope", + scopeType: { type: "instance" }, + length: 1, + start: 1, + }, + true, + " aaa bbb aaa", + ), + ), + ); +}); + +async function runTest( + spyIde: SpyIDE, + modifier: Modifier, + useWholeFile: boolean, + expectedContents: string, +) { + const { hatTokenMap } = (await getCursorlessApi()).testHelpers!; + + const { document: instanceDocument } = await openNewEditor("aaa"); + /** The editor containing the "instance" */ + const instanceEditor = spyIde.activeTextEditor!; + /** The editor in which "from" is run */ + const fromEditor = await openNewEditor(" aaa bbb aaa aaa", { + openBeside: true, + }); + const { document: fromDocument } = fromEditor; + fromEditor.selections = [new Selection(0, 0, 0, 0)]; + + await hatTokenMap.allocateHats([ + { + grapheme: "a", + hatStyle: "default", + hatRange: new Range(0, 0, 0, 1), + token: { + editor: instanceEditor, + offsets: { start: 0, end: 3 }, + range: new Range(0, 0, 0, 3), + text: "aaa", + }, + }, + ]); + + // "from this" / "from file this", depending on the value of `useWholeFile` + await runCursorlessCommand({ + version: 6, + action: { + name: "experimental.setInstanceReference", + target: { + type: "primitive", + mark: { + type: "cursor", + }, + modifiers: useWholeFile + ? [{ type: "containingScope", scopeType: { type: "document" } }] + : [], + }, + }, + usePrePhraseSnapshot: false, + }); + + // "change air", where is some kind of "instance" + // modifier + await runCursorlessCommand({ + version: 6, + action: { + name: "clearAndSetSelection", + target: { + type: "primitive", + mark: { + type: "decoratedSymbol", + symbolColor: "default", + character: "a", + }, + modifiers: [modifier], + }, + }, + usePrePhraseSnapshot: false, + }); + + assert.deepStrictEqual(instanceDocument.getText(), "aaa"); + assert.deepStrictEqual(fromDocument.getText(), expectedContents); +}