Skip to content

Commit 21715e4

Browse files
AndreasArvidssonpre-commit-ci-lite[bot]phillco
authored
Added literal mark (#2593)
I'm not sure this is a direction we want to go or if this is a good implementation, but for editors where we don't have hats it would be nice if you could just speak the literal text instead. `"take hello world"` `"chuck state get value"` In this implementation I'm only looking in the viewport and I'm solving ambiguity by selecting the match closest to a cursor selection. ## 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 - [x] Add list for literal prefix/keyword. Empty by default. test that this behaves properly - [x] Tag to remove prefix and have literal as a replacement for hats. Check dfa compilation - [x] Tag to default to preferred scope instead of containing scope --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Phil Cohen <[email protected]>
1 parent 0e01381 commit 21715e4

24 files changed

+516
-7
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from talon import Context, Module
2+
3+
from .mark_types import LiteralMark
4+
5+
mod = Module()
6+
7+
mod.list("private_cursorless_literal_mark", desc="Cursorless literal mark")
8+
9+
# This is a private tag and should not be used by non Cursorless developers
10+
mod.tag(
11+
"private_cursorless_literal_mark_no_prefix",
12+
desc="Tag for enabling literal mark without prefix",
13+
)
14+
15+
ctx_no_prefix = Context()
16+
ctx_no_prefix.matches = r"""
17+
tag: user.private_cursorless_literal_mark_no_prefix
18+
"""
19+
20+
21+
# NB: <phrase> is used over <user.text> for DFA performance reasons
22+
# (we intend to replace this with a dynamic list of document contents eventually)
23+
@mod.capture(rule="{user.private_cursorless_literal_mark} <phrase>")
24+
def cursorless_literal_mark(m) -> LiteralMark:
25+
return construct_mark(str(m.phrase))
26+
27+
28+
@ctx_no_prefix.capture("user.cursorless_literal_mark", rule="<phrase>")
29+
def cursorless_literal_mark_no_prefix(m) -> LiteralMark:
30+
return construct_mark(str(m.phrase))
31+
32+
33+
def construct_mark(text: str) -> LiteralMark:
34+
return {
35+
"type": "literal",
36+
"modifier": {
37+
"type": "preferredScope",
38+
"scopeType": {
39+
"type": "customRegex",
40+
"regex": construct_fuzzy_regex(text),
41+
"flags": "gui",
42+
},
43+
},
44+
}
45+
46+
47+
def construct_fuzzy_regex(text: str) -> str:
48+
parts = text.split(" ")
49+
# Between each word there can be any number of non-alpha symbols (including escape characters: \t\r\n). No separator at all is also valid -- for example, when searching for a camelCase identifier.
50+
return r"([^a-zA-Z]|\\[trn])*".join(parts)

cursorless-talon/src/marks/mark.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
@mod.capture(
99
rule=(
1010
"<user.cursorless_decorated_symbol> | "
11+
"<user.cursorless_literal_mark> | "
1112
"<user.cursorless_simple_mark> |"
1213
"<user.cursorless_line_number>" # row (ie absolute mod 100), up, down
1314
)

cursorless-talon/src/marks/mark_types.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ class DecoratedSymbol(TypedDict):
77
character: str
88

99

10+
class LiteralMark(TypedDict):
11+
type: Literal["literal"]
12+
modifier: dict
13+
14+
1015
SimpleMark = dict[Literal["type"], str]
1116

1217
LineNumberType = Literal["modulo100", "relative"]
@@ -28,4 +33,4 @@ class LineNumberRange(TypedDict):
2833

2934
LineNumber = Union[LineNumberMark, LineNumberRange]
3035

31-
Mark = Union[DecoratedSymbol, SimpleMark, LineNumber]
36+
Mark = Union[DecoratedSymbol, LiteralMark, SimpleMark, LineNumber]

cursorless-talon/src/modifiers/simple_scope_modifier.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Any
22

3-
from talon import Module
3+
from talon import Module, settings
44

55
mod = Module()
66

@@ -13,6 +13,13 @@
1313
desc="Cursorless ancestor scope modifiers",
1414
)
1515

16+
# This is a private setting and should not be used by non Cursorless developers
17+
mod.setting(
18+
"private_cursorless_use_preferred_scope",
19+
bool,
20+
desc="Use preferred scope instead of containing scope for all scopes by default (EXPERIMENTAL)",
21+
)
22+
1623

1724
@mod.capture(
1825
rule=(
@@ -35,6 +42,12 @@ def cursorless_simple_scope_modifier(m) -> dict[str, Any]:
3542
"ancestorIndex": 1,
3643
}
3744

45+
if settings.get("user.private_cursorless_use_preferred_scope", False):
46+
return {
47+
"type": "preferredScope",
48+
"scopeType": m.cursorless_scope_type,
49+
}
50+
3851
return {
3952
"type": "containingScope",
4053
"scopeType": m.cursorless_scope_type,

cursorless-talon/src/targets/primitive_target.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@
1111
)
1212
)
1313
def cursorless_primitive_target(m) -> PrimitiveTarget:
14-
return PrimitiveTarget(
15-
getattr(m, "cursorless_mark", None),
16-
getattr(m, "cursorless_modifier_list", None),
17-
)
14+
mark = getattr(m, "cursorless_mark", None)
15+
modifiers = getattr(m, "cursorless_modifier_list", None)
16+
17+
# for grammar performance reasons, the literal modifier is exposed to Talon as a mark,
18+
# but is converted to a modifier in the engine.
19+
if mark is not None and mark["type"] == "literal":
20+
if modifiers is None:
21+
modifiers = []
22+
modifiers.append(mark["modifier"])
23+
mark = None
24+
25+
return PrimitiveTarget(mark, modifiers)
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 good pair
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: preferredScope
11+
scopeType: {type: surroundingPair, delimiter: any}
12+
usePrePhraseSnapshot: true
13+
spokenFormError: Modifier 'preferredScope'
14+
initialState:
15+
documentContents: |
16+
()
17+
selections:
18+
- anchor: {line: 1, character: 0}
19+
active: {line: 1, character: 0}
20+
marks: {}
21+
finalState:
22+
documentContents: |+
23+
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
languageId: plaintext
2+
command:
3+
version: 7
4+
spokenForm: change good pair
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: preferredScope
11+
scopeType: {type: surroundingPair, delimiter: any}
12+
usePrePhraseSnapshot: true
13+
spokenFormError: Modifier 'preferredScope'
14+
initialState:
15+
documentContents: |-
16+
() ()
17+
18+
selections:
19+
- anchor: {line: 1, character: 9}
20+
active: {line: 1, character: 9}
21+
marks: {}
22+
finalState:
23+
documentContents: |-
24+
()
25+
26+
selections:
27+
- anchor: {line: 0, character: 8}
28+
active: {line: 0, character: 8}
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 good pair
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: preferredScope
11+
scopeType: {type: surroundingPair, delimiter: any}
12+
usePrePhraseSnapshot: true
13+
spokenFormError: Modifier 'preferredScope'
14+
initialState:
15+
documentContents: |
16+
() ()
17+
selections:
18+
- anchor: {line: 1, character: 0}
19+
active: {line: 1, character: 0}
20+
marks: {}
21+
finalState:
22+
documentContents: |2
23+
()
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
languageId: plaintext
2+
command:
3+
version: 7
4+
spokenForm: change good pair
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: preferredScope
11+
scopeType: {type: surroundingPair, delimiter: any}
12+
usePrePhraseSnapshot: true
13+
spokenFormError: Modifier 'preferredScope'
14+
initialState:
15+
documentContents: () ()
16+
selections:
17+
- anchor: {line: 0, character: 3}
18+
active: {line: 0, character: 3}
19+
marks: {}
20+
finalState:
21+
documentContents: "() "
22+
selections:
23+
- anchor: {line: 0, character: 4}
24+
active: {line: 0, character: 4}
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: clear literal hello
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: preferredScope
11+
scopeType: {type: customRegex, regex: hello, flags: gui}
12+
usePrePhraseSnapshot: true
13+
spokenFormError: Modifier 'preferredScope'
14+
initialState:
15+
documentContents: |
16+
hello
17+
selections:
18+
- anchor: {line: 1, character: 0}
19+
active: {line: 1, character: 0}
20+
marks: {}
21+
finalState:
22+
documentContents: |+
23+
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}

0 commit comments

Comments
 (0)