Skip to content

Commit e21a07f

Browse files
authored
Fix "from" / "instance" across splits (#1995)
It was incorrectly using the editor of the instance, rather than the "from" editor ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [-] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [-] I have not broken the cheatsheet
1 parent b2ab244 commit e21a07f

File tree

2 files changed

+173
-10
lines changed

2 files changed

+173
-10
lines changed

packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ export default class InstanceStage implements ModifierStage {
4646
}
4747

4848
private handleEveryScope(target: Target): Target[] {
49-
const { editor } = target;
50-
5149
return Array.from(
52-
flatmap(this.getEveryRanges(editor), (searchRange) =>
50+
flatmap(this.getEveryRanges(target), ([editor, searchRange]) =>
5351
this.getTargetIterable(target, editor, searchRange, "forward"),
5452
),
5553
);
@@ -59,9 +57,7 @@ export default class InstanceStage implements ModifierStage {
5957
target: Target,
6058
{ start, length }: OrdinalScopeModifier,
6159
): Target[] {
62-
const { editor } = target;
63-
64-
return this.getEveryRanges(editor).flatMap((searchRange) =>
60+
return this.getEveryRanges(target).flatMap(([editor, searchRange]) =>
6561
takeFromOffset(
6662
this.getTargetIterable(
6763
target,
@@ -79,13 +75,12 @@ export default class InstanceStage implements ModifierStage {
7975
target: Target,
8076
{ direction, offset, length }: RelativeScopeModifier,
8177
): Target[] {
82-
const { editor } = target;
83-
8478
const referenceTargets = this.storedTargets.get("instanceReference") ?? [
8579
target,
8680
];
8781

8882
return referenceTargets.flatMap((referenceTarget) => {
83+
const { editor } = referenceTarget;
8984
const iterationRange =
9085
direction === "forward"
9186
? new Range(
@@ -109,11 +104,14 @@ export default class InstanceStage implements ModifierStage {
109104
});
110105
}
111106

112-
private getEveryRanges(editor: TextEditor): Range[] {
107+
private getEveryRanges({
108+
editor: targetEditor,
109+
}: Target): readonly (readonly [TextEditor, Range])[] {
113110
return (
114111
this.storedTargets
115112
.get("instanceReference")
116-
?.map(({ contentRange }) => contentRange) ?? [editor.document.range]
113+
?.map(({ editor, contentRange }) => [editor, contentRange] as const) ??
114+
([[targetEditor, targetEditor.document.range]] as const)
117115
);
118116
}
119117

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
HatStability,
3+
Modifier,
4+
Range,
5+
SpyIDE,
6+
asyncSafety,
7+
} from "@cursorless/common";
8+
import {
9+
getCursorlessApi,
10+
openNewEditor,
11+
runCursorlessCommand,
12+
} from "@cursorless/vscode-common";
13+
import * as assert from "assert";
14+
import { Selection } from "vscode";
15+
import { endToEndTestSetup } from "../endToEndTestSetup";
16+
import { setupFake } from "./setupFake";
17+
18+
// Ensure that the "from" / "instance" work properly when "from"
19+
// is run in a different editor from "instance"
20+
suite("Instance across split", async function () {
21+
const { getSpy } = endToEndTestSetup(this);
22+
23+
suiteSetup(async () => {
24+
const { ide } = (await getCursorlessApi()).testHelpers!;
25+
setupFake(ide, HatStability.stable);
26+
});
27+
28+
test(
29+
"Every instance",
30+
asyncSafety(() =>
31+
runTest(
32+
getSpy()!,
33+
{
34+
type: "everyScope",
35+
scopeType: { type: "instance" },
36+
},
37+
true,
38+
" bbb ",
39+
),
40+
),
41+
);
42+
test(
43+
"Next instance",
44+
asyncSafety(() =>
45+
runTest(
46+
getSpy()!,
47+
{
48+
type: "relativeScope",
49+
scopeType: { type: "instance" },
50+
direction: "forward",
51+
length: 1,
52+
offset: 1,
53+
},
54+
false,
55+
" bbb aaa aaa",
56+
),
57+
),
58+
);
59+
test(
60+
"Two instances",
61+
asyncSafety(() =>
62+
runTest(
63+
getSpy()!,
64+
{
65+
type: "relativeScope",
66+
scopeType: { type: "instance" },
67+
direction: "forward",
68+
length: 2,
69+
offset: 0,
70+
},
71+
false,
72+
" bbb aaa",
73+
),
74+
),
75+
);
76+
test(
77+
"Second instance",
78+
asyncSafety(() =>
79+
runTest(
80+
getSpy()!,
81+
{
82+
type: "ordinalScope",
83+
scopeType: { type: "instance" },
84+
length: 1,
85+
start: 1,
86+
},
87+
true,
88+
" aaa bbb aaa",
89+
),
90+
),
91+
);
92+
});
93+
94+
async function runTest(
95+
spyIde: SpyIDE,
96+
modifier: Modifier,
97+
useWholeFile: boolean,
98+
expectedContents: string,
99+
) {
100+
const { hatTokenMap } = (await getCursorlessApi()).testHelpers!;
101+
102+
const { document: instanceDocument } = await openNewEditor("aaa");
103+
/** The editor containing the "instance" */
104+
const instanceEditor = spyIde.activeTextEditor!;
105+
/** The editor in which "from" is run */
106+
const fromEditor = await openNewEditor(" aaa bbb aaa aaa", {
107+
openBeside: true,
108+
});
109+
const { document: fromDocument } = fromEditor;
110+
fromEditor.selections = [new Selection(0, 0, 0, 0)];
111+
112+
await hatTokenMap.allocateHats([
113+
{
114+
grapheme: "a",
115+
hatStyle: "default",
116+
hatRange: new Range(0, 0, 0, 1),
117+
token: {
118+
editor: instanceEditor,
119+
offsets: { start: 0, end: 3 },
120+
range: new Range(0, 0, 0, 3),
121+
text: "aaa",
122+
},
123+
},
124+
]);
125+
126+
// "from this" / "from file this", depending on the value of `useWholeFile`
127+
await runCursorlessCommand({
128+
version: 6,
129+
action: {
130+
name: "experimental.setInstanceReference",
131+
target: {
132+
type: "primitive",
133+
mark: {
134+
type: "cursor",
135+
},
136+
modifiers: useWholeFile
137+
? [{ type: "containingScope", scopeType: { type: "document" } }]
138+
: [],
139+
},
140+
},
141+
usePrePhraseSnapshot: false,
142+
});
143+
144+
// "change <modifier> air", where <modifier> is some kind of "instance"
145+
// modifier
146+
await runCursorlessCommand({
147+
version: 6,
148+
action: {
149+
name: "clearAndSetSelection",
150+
target: {
151+
type: "primitive",
152+
mark: {
153+
type: "decoratedSymbol",
154+
symbolColor: "default",
155+
character: "a",
156+
},
157+
modifiers: [modifier],
158+
},
159+
},
160+
usePrePhraseSnapshot: false,
161+
});
162+
163+
assert.deepStrictEqual(instanceDocument.getText(), "aaa");
164+
assert.deepStrictEqual(fromDocument.getText(), expectedContents);
165+
}

0 commit comments

Comments
 (0)