Skip to content

Commit 463a00f

Browse files
Added spread support for relative and ordinal scopes (#2254)
`"change every two tokens"` Fixes #1514 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] I have updated the [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [x] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) - [x] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]>
1 parent aa76859 commit 463a00f

29 files changed

+721
-274
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
tags: [enhancement]
3+
pullRequest: 2254
4+
---
5+
6+
- Added every/spread ordinal/relative modifier. Turns relative and ordinal range modifiers into multiple target selections instead of contiguous range.
7+
8+
- `"take every two tokens"` selects two tokens as separate selections
9+
- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors)

cursorless-talon/src/cheatsheet/sections/modifiers.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from itertools import chain
2+
from typing import TypedDict
3+
14
from ..get_list import get_raw_list, make_dict_readable
25

36
MODIFIER_LIST_NAMES = [
47
"simple_modifier",
58
"interior_modifier",
69
"head_tail_modifier",
7-
"simple_scope_modifier",
10+
"every_scope_modifier",
11+
"ancestor_scope_modifier",
812
"first_modifier",
913
"last_modifier",
1014
"previous_next_modifier",
@@ -132,10 +136,6 @@ def get_modifiers():
132136
"spokenForm": f"<ordinal> {complex_modifiers['next']} <scope>",
133137
"description": "<ordinal> instance of <scope> after target",
134138
},
135-
{
136-
"spokenForm": f"{complex_modifiers['previous']} <number> <scope>s",
137-
"description": "previous <number> instances of <scope>",
138-
},
139139
{
140140
"spokenForm": f"<scope> {complex_modifiers['backward']}",
141141
"description": "single instance of <scope> including target, going backwards",
@@ -144,18 +144,25 @@ def get_modifiers():
144144
"spokenForm": f"<scope> {complex_modifiers['forward']}",
145145
"description": "single instance of <scope> including target, going forwards",
146146
},
147-
{
148-
"spokenForm": f"<number> <scope>s {complex_modifiers['backward']}",
149-
"description": "<number> instances of <scope> including target, going backwards",
150-
},
151-
{
152-
"spokenForm": "<number> <scope>s",
153-
"description": "<number> instances of <scope> including target, going forwards",
154-
},
155-
{
156-
"spokenForm": f"{complex_modifiers['next']} <number> <scope>s",
157-
"description": "next <number> instances of <scope>",
158-
},
147+
*generateOptionalEvery(
148+
complex_modifiers["every"],
149+
{
150+
"spokenForm": f"<number> <scope>s {complex_modifiers['backward']}",
151+
"description": "<number> instances of <scope> including target, going backwards",
152+
},
153+
{
154+
"spokenForm": "<number> <scope>s",
155+
"description": "<number> instances of <scope> including target, going forwards",
156+
},
157+
{
158+
"spokenForm": f"{complex_modifiers['previous']} <number> <scope>s",
159+
"description": "previous <number> instances of <scope>",
160+
},
161+
{
162+
"spokenForm": f"{complex_modifiers['next']} <number> <scope>s",
163+
"description": "next <number> instances of <scope>",
164+
},
165+
),
159166
],
160167
},
161168
{
@@ -170,14 +177,40 @@ def get_modifiers():
170177
"spokenForm": f"<ordinal> {complex_modifiers['last']} <scope>",
171178
"description": "<ordinal>-to-last instance of <scope> in iteration scope",
172179
},
180+
*generateOptionalEvery(
181+
complex_modifiers["every"],
182+
{
183+
"spokenForm": f"{complex_modifiers['first']} <number> <scope>s",
184+
"description": "first <number> instances of <scope> in iteration scope",
185+
},
186+
{
187+
"spokenForm": f"{complex_modifiers['last']} <number> <scope>s",
188+
"description": "last <number> instances of <scope> in iteration scope",
189+
},
190+
),
191+
],
192+
},
193+
]
194+
195+
196+
class Entry(TypedDict):
197+
spokenForm: str
198+
description: str
199+
200+
201+
def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]:
202+
return list(
203+
chain.from_iterable(
204+
[
173205
{
174-
"spokenForm": f"{complex_modifiers['first']} <number> <scope>s",
175-
"description": "First <number> instances of <scope> in iteration scope",
206+
"spokenForm": entry["spokenForm"],
207+
"description": f"{entry['description']}, as contiguous range",
176208
},
177209
{
178-
"spokenForm": f"{complex_modifiers['last']} <number> <scope>s",
179-
"description": "Last <number> instances of <scope> in iteration scope",
210+
"spokenForm": f"{every} {entry['spokenForm']}",
211+
"description": f"{entry['description']}, as individual targets",
180212
},
181-
],
182-
},
183-
]
213+
]
214+
for entry in entries
215+
)
216+
)

cursorless-talon/src/modifiers/ordinal_scope.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,27 @@ def cursorless_ordinal_range(m) -> dict[str, Any]:
4444

4545

4646
@mod.capture(
47-
rule="({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
47+
rule=(
48+
"[{user.cursorless_every_scope_modifier}] "
49+
"({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) "
50+
"<user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
51+
),
4852
)
4953
def cursorless_first_last(m) -> dict[str, Any]:
5054
"""First/last `n` scopes; eg "first three funks"""
51-
if m[0] == "first":
55+
is_every = hasattr(m, "cursorless_every_scope_modifier")
56+
if hasattr(m, "cursorless_first_modifier"):
5257
return create_ordinal_scope_modifier(
53-
m.cursorless_scope_type_plural, 0, m.private_cursorless_number_small
58+
m.cursorless_scope_type_plural,
59+
0,
60+
m.private_cursorless_number_small,
61+
is_every,
5462
)
5563
return create_ordinal_scope_modifier(
5664
m.cursorless_scope_type_plural,
5765
-m.private_cursorless_number_small,
5866
m.private_cursorless_number_small,
67+
is_every,
5968
)
6069

6170

@@ -65,10 +74,18 @@ def cursorless_ordinal_scope(m) -> dict[str, Any]:
6574
return m[0]
6675

6776

68-
def create_ordinal_scope_modifier(scope_type: dict, start: int, length: int = 1):
69-
return {
77+
def create_ordinal_scope_modifier(
78+
scope_type: dict,
79+
start: int,
80+
length: int = 1,
81+
is_every: bool = False,
82+
):
83+
res = {
7084
"type": "ordinalScope",
7185
"scopeType": scope_type,
7286
"start": start,
7387
"length": length,
7488
}
89+
if is_every:
90+
res["isEvery"] = True
91+
return res

cursorless-talon/src/modifiers/relative_scope.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ def cursorless_relative_scope_singular(m) -> dict[str, Any]:
2626
getattr(m, "ordinals_small", 1),
2727
1,
2828
m.cursorless_relative_direction,
29+
False,
2930
)
3031

3132

3233
@mod.capture(
33-
rule="<user.cursorless_relative_direction> <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
34+
rule="[{user.cursorless_every_scope_modifier}] <user.cursorless_relative_direction> <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
3435
)
3536
def cursorless_relative_scope_plural(m) -> dict[str, Any]:
3637
"""Relative previous/next plural scope. `next three funks`"""
@@ -39,11 +40,12 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]:
3940
1,
4041
m.private_cursorless_number_small,
4142
m.cursorless_relative_direction,
43+
hasattr(m, "cursorless_every_scope_modifier"),
4244
)
4345

4446

4547
@mod.capture(
46-
rule="<user.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{user.cursorless_forward_backward_modifier}]"
48+
rule="[{user.cursorless_every_scope_modifier}] <user.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{user.cursorless_forward_backward_modifier}]"
4749
)
4850
def cursorless_relative_scope_count(m) -> dict[str, Any]:
4951
"""Relative count scope. `three funks`"""
@@ -52,6 +54,7 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]:
5254
0,
5355
m.private_cursorless_number_small,
5456
getattr(m, "cursorless_forward_backward_modifier", "forward"),
57+
hasattr(m, "cursorless_every_scope_modifier"),
5558
)
5659

5760

@@ -65,6 +68,7 @@ def cursorless_relative_scope_one_backward(m) -> dict[str, Any]:
6568
0,
6669
1,
6770
m.cursorless_forward_backward_modifier,
71+
False,
6872
)
6973

7074

@@ -82,12 +86,19 @@ def cursorless_relative_scope(m) -> dict[str, Any]:
8286

8387

8488
def create_relative_scope_modifier(
85-
scope_type: dict, offset: int, length: int, direction: str
89+
scope_type: dict,
90+
offset: int,
91+
length: int,
92+
direction: str,
93+
is_every: bool,
8694
) -> dict[str, Any]:
87-
return {
95+
res = {
8896
"type": "relativeScope",
8997
"scopeType": scope_type,
9098
"offset": offset,
9199
"length": length,
92100
"direction": direction,
93101
}
102+
if is_every:
103+
res["isEvery"] = True
104+
return res

cursorless-talon/src/modifiers/simple_scope_modifier.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,35 @@
55
mod = Module()
66

77
mod.list(
8-
"cursorless_simple_scope_modifier",
9-
desc='Cursorless simple scope modifiers, eg "every"',
8+
"cursorless_every_scope_modifier",
9+
desc="Cursorless every scope modifiers",
10+
)
11+
mod.list(
12+
"cursorless_ancestor_scope_modifier",
13+
desc="Cursorless ancestor scope modifiers",
1014
)
1115

1216

1317
@mod.capture(
14-
rule="[{user.cursorless_simple_scope_modifier}] <user.cursorless_scope_type>"
18+
rule=(
19+
"[{user.cursorless_every_scope_modifier} | {user.cursorless_ancestor_scope_modifier}] "
20+
"<user.cursorless_scope_type>"
21+
),
1522
)
1623
def cursorless_simple_scope_modifier(m) -> dict[str, Any]:
1724
"""Containing scope, every scope, etc"""
18-
if hasattr(m, "cursorless_simple_scope_modifier"):
19-
modifier = m.cursorless_simple_scope_modifier
20-
21-
if modifier == "every":
22-
return {
23-
"type": "everyScope",
24-
"scopeType": m.cursorless_scope_type,
25-
}
26-
27-
if modifier == "ancestor":
28-
return {
29-
"type": "containingScope",
30-
"scopeType": m.cursorless_scope_type,
31-
"ancestorIndex": 1,
32-
}
25+
if hasattr(m, "cursorless_every_scope_modifier"):
26+
return {
27+
"type": "everyScope",
28+
"scopeType": m.cursorless_scope_type,
29+
}
30+
31+
if hasattr(m, "cursorless_ancestor_scope_modifier"):
32+
return {
33+
"type": "containingScope",
34+
"scopeType": m.cursorless_scope_type,
35+
"ancestorIndex": 1,
36+
}
3337

3438
return {
3539
"type": "containingScope",

cursorless-talon/src/spoken_forms.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@
8383
"its": "inferPreviousMark",
8484
"visible": "visible"
8585
},
86-
"simple_scope_modifier": { "every": "every", "grand": "ancestor" },
86+
"every_scope_modifier": { "every": "every" },
87+
"ancestor_scope_modifier": { "grand": "ancestor" },
8788
"interior_modifier": {
8889
"inside": "interiorOnly"
8990
},

docs/user/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ And here is a table of the spoken forms:
207207
| `"previous [number] [scope]s"` | previous `[number]` instances of `[scope]` | `"take previous three funks"` |
208208
| `"previous [scope]"` | Previous instance of `[scope]` | `"take previous funk"` |
209209

210+
You can prefix the modifier with `"every"` to yield multiple targets rather than a range. For example, `"take every two tokens"` selects two tokens as separate selections.
211+
210212
##### `"every"`
211213

212214
The modifier `"every"` can be used to select a syntactic element and all of its matching siblings.
@@ -217,6 +219,13 @@ The modifier `"every"` can be used to select a syntactic element and all of its
217219

218220
For example, the command `"take every key [blue] air"` will select every key in the map/object/dict including the token with a blue hat over the letter 'a'.
219221

222+
###### Use with relative / ordinal modifiers
223+
224+
The modifier `every` can also be used to cause [relative / ordinal modifiers](#previous--next--ordinal--number) to yield multiple targets rather than a range:
225+
226+
- `"take every two tokens"` selects two tokens as separate selections
227+
- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors)
228+
220229
##### `"grand"`
221230

222231
The modifier `"grand"` can be used to select the parent of the containing syntactic element.

0 commit comments

Comments
 (0)