Skip to content

Commit ca57264

Browse files
authored
Support "inside head" (#2442)
## 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 7968fc3 commit ca57264

File tree

6 files changed

+129
-52
lines changed

6 files changed

+129
-52
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
languageId: plaintext
2+
command:
3+
version: 7
4+
spokenForm: change inside tail curly
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- {type: interiorOnly}
11+
- type: extendThroughEndOf
12+
modifiers:
13+
- type: containingScope
14+
scopeType: {type: surroundingPair, delimiter: curlyBrackets}
15+
usePrePhraseSnapshot: true
16+
initialState:
17+
documentContents: "{(\"hello\")}"
18+
selections:
19+
- anchor: {line: 0, character: 5}
20+
active: {line: 0, character: 5}
21+
marks: {}
22+
finalState:
23+
documentContents: "{(\"he}"
24+
selections:
25+
- anchor: {line: 0, character: 5}
26+
active: {line: 0, character: 5}

packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
KeepContentFilterStage,
1717
KeepEmptyFilterStage,
1818
} from "./modifiers/FilterStages";
19-
import { HeadStage, TailStage } from "./modifiers/HeadTailStage";
19+
import { HeadTailStage } from "./modifiers/HeadTailStage";
2020
import { InstanceStage } from "./modifiers/InstanceStage";
2121
import {
2222
ExcludeInteriorStage,
@@ -56,9 +56,8 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory {
5656
case "endOf":
5757
return new EndOfStage();
5858
case "extendThroughStartOf":
59-
return new HeadStage(this, modifier);
6059
case "extendThroughEndOf":
61-
return new TailStage(this, modifier);
60+
return new HeadTailStage(this, modifier);
6261
case "toRawSelection":
6362
return new RawSelectionStage(modifier);
6463
case "interiorOnly":
Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
1-
import {
2-
HeadModifier,
3-
Modifier,
4-
Range,
5-
TailModifier,
6-
} from "@cursorless/common";
1+
import { HeadModifier, TailModifier } from "@cursorless/common";
72
import { Target } from "../../typings/target.types";
83
import { ModifierStageFactory } from "../ModifierStageFactory";
94
import { ModifierStage } from "../PipelineStages.types";
105
import {
116
getModifierStagesFromTargetModifiers,
127
processModifierStages,
138
} from "../TargetPipelineRunner";
14-
import { TokenTarget } from "../targets";
9+
import { HeadTailTarget } from "../targets";
1510

16-
abstract class HeadTailStage implements ModifierStage {
11+
export class HeadTailStage implements ModifierStage {
1712
constructor(
1813
private modifierStageFactory: ModifierStageFactory,
19-
private isReversed: boolean,
20-
private modifiers?: Modifier[],
14+
private modifier: HeadModifier | TailModifier,
2115
) {}
2216

2317
run(target: Target): Target[] {
24-
const modifiers = this.modifiers ?? [
18+
const modifiers = this.modifier.modifiers ?? [
2519
{
2620
type: "containingScope",
2721
scopeType: { type: "line" },
@@ -35,47 +29,15 @@ abstract class HeadTailStage implements ModifierStage {
3529
const modifiedTargets = processModifierStages(modifierStages, [target]);
3630

3731
return modifiedTargets.map((modifiedTarget) => {
38-
const contentRange = this.constructContentRange(
39-
target.contentRange,
40-
modifiedTarget.contentRange,
41-
);
32+
const isHead = this.modifier.type === "extendThroughStartOf";
4233

43-
return new TokenTarget({
34+
return new HeadTailTarget({
4435
editor: target.editor,
45-
isReversed: this.isReversed,
46-
contentRange,
36+
isReversed: isHead,
37+
inputTarget: target,
38+
modifiedTarget,
39+
isHead,
4740
});
4841
});
4942
}
50-
51-
protected abstract constructContentRange(
52-
originalRange: Range,
53-
modifiedRange: Range,
54-
): Range;
55-
}
56-
57-
export class HeadStage extends HeadTailStage {
58-
constructor(
59-
modifierStageFactory: ModifierStageFactory,
60-
modifier: HeadModifier,
61-
) {
62-
super(modifierStageFactory, true, modifier.modifiers);
63-
}
64-
65-
protected constructContentRange(originalRange: Range, modifiedRange: Range) {
66-
return new Range(modifiedRange.start, originalRange.end);
67-
}
68-
}
69-
70-
export class TailStage extends HeadTailStage {
71-
constructor(
72-
modifierStageFactory: ModifierStageFactory,
73-
modifier: TailModifier,
74-
) {
75-
super(modifierStageFactory, false, modifier.modifiers);
76-
}
77-
78-
protected constructContentRange(originalRange: Range, modifiedRange: Range) {
79-
return new Range(originalRange.start, modifiedRange.end);
80-
}
8143
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Range } from "@cursorless/common";
2+
import { BaseTarget, MinimumTargetParameters } from "./BaseTarget";
3+
import { Target } from "../../typings/target.types";
4+
import {
5+
getTokenLeadingDelimiterTarget,
6+
getTokenRemovalRange,
7+
getTokenTrailingDelimiterTarget,
8+
} from "./util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior";
9+
import { PlainTarget } from "./PlainTarget";
10+
11+
export interface HeadTailTargetParameters extends MinimumTargetParameters {
12+
inputTarget: Target;
13+
modifiedTarget: Target;
14+
isHead: boolean;
15+
}
16+
17+
export class HeadTailTarget extends BaseTarget<HeadTailTargetParameters> {
18+
type = "HeadTailTarget";
19+
insertionDelimiter = " ";
20+
private readonly inputTarget: Target;
21+
private readonly modifiedTarget: Target;
22+
private readonly isHead: boolean;
23+
24+
constructor(parameters: HeadTailTargetParameters) {
25+
const { inputTarget, modifiedTarget, isHead } = parameters;
26+
super({
27+
...parameters,
28+
contentRange: constructRange(
29+
inputTarget.contentRange,
30+
modifiedTarget.contentRange,
31+
isHead,
32+
),
33+
});
34+
this.inputTarget = inputTarget;
35+
this.modifiedTarget = modifiedTarget;
36+
this.isHead = isHead;
37+
}
38+
getLeadingDelimiterTarget(): Target | undefined {
39+
return getTokenLeadingDelimiterTarget(this);
40+
}
41+
getTrailingDelimiterTarget(): Target | undefined {
42+
return getTokenTrailingDelimiterTarget(this);
43+
}
44+
getRemovalRange(): Range {
45+
return getTokenRemovalRange(this);
46+
}
47+
getInterior(): Target[] | undefined {
48+
const modifiedInterior = this.modifiedTarget.getInterior();
49+
if (modifiedInterior == null) {
50+
return undefined;
51+
}
52+
return modifiedInterior.map((target) => {
53+
return new PlainTarget({
54+
editor: this.editor,
55+
contentRange: constructRange(
56+
this.inputTarget.contentRange,
57+
target.contentRange,
58+
this.isHead,
59+
),
60+
isReversed: this.isReversed,
61+
});
62+
});
63+
}
64+
65+
protected getCloneParameters() {
66+
return {
67+
...this.state,
68+
inputTarget: this.inputTarget,
69+
modifiedTarget: this.modifiedTarget,
70+
isHead: this.isHead,
71+
};
72+
}
73+
}
74+
75+
function constructRange(
76+
originalRange: Range,
77+
modifiedRange: Range,
78+
isHead: boolean,
79+
): Range {
80+
return isHead
81+
? new Range(modifiedRange.start, originalRange.end)
82+
: new Range(originalRange.start, modifiedRange.end);
83+
}

packages/cursorless-engine/src/processTargets/targets/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from "./TokenTarget";
1414
export * from "./UntypedTarget";
1515
export * from "./ImplicitTarget";
1616
export * from "./InteriorTarget";
17+
export * from "./HeadTailTarget";

packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ const testCases: TestCase[] = [
6969
keySequence: ["da", "mi", "c"],
7070
finalContent: "()",
7171
},
72+
{
73+
name: "inside tail curly bat",
74+
initialContent: "{(aaa bbb ccc)}",
75+
keySequence: ["db", "mt", "wb", "mi", "c"],
76+
finalContent: "{(aaa }",
77+
},
7278
{
7379
name: "wrap",
7480
initialContent: "a",

0 commit comments

Comments
 (0)