Skip to content

Commit 3b9df05

Browse files
AndreasArvidssonpre-commit-ci-lite[bot]pokey
authored andcommitted
Fallback to Talon actions when focus is not on the text editor (#2235)
Edit operations supported by community will now work in vscode outside of the text editor. eg the search widget `take line` `chuck token` Everything appears to be working when I have tested it. With that said I have not tested on community and we should probably have a discussion about some of the finer details of this. ## 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) - [x] I have not broken the cheatsheet --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Pokey Rule <[email protected]>
1 parent 067392c commit 3b9df05

File tree

3 files changed

+121
-3
lines changed

3 files changed

+121
-3
lines changed

src/command.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
from talon import Module, actions, speech_system
55

6+
from .fallback import perform_fallback
7+
from .versions import COMMAND_VERSION
8+
69

710
@dataclasses.dataclass
811
class CursorlessCommand:
9-
version = 6
12+
version = COMMAND_VERSION
1013
spokenForm: str
1114
usePrePhraseSnapshot: bool
1215
action: dict
@@ -30,10 +33,12 @@ def on_phrase(d):
3033
class Actions:
3134
def private_cursorless_command_and_wait(action: dict):
3235
"""Execute cursorless command and wait for it to finish"""
33-
actions.user.private_cursorless_run_rpc_command_and_wait(
36+
response = actions.user.private_cursorless_run_rpc_command_get(
3437
CURSORLESS_COMMAND_ID,
3538
construct_cursorless_command(action),
3639
)
40+
if "fallback" in response:
41+
perform_fallback(response["fallback"])
3742

3843
def private_cursorless_command_no_wait(action: dict):
3944
"""Execute cursorless command without waiting"""
@@ -44,10 +49,15 @@ def private_cursorless_command_no_wait(action: dict):
4449

4550
def private_cursorless_command_get(action: dict):
4651
"""Execute cursorless command and return result"""
47-
return actions.user.private_cursorless_run_rpc_command_get(
52+
response = actions.user.private_cursorless_run_rpc_command_get(
4853
CURSORLESS_COMMAND_ID,
4954
construct_cursorless_command(action),
5055
)
56+
if "fallback" in response:
57+
return perform_fallback(response["fallback"])
58+
if "returnValue" in response:
59+
return response["returnValue"]
60+
return None
5161

5262

5363
def construct_cursorless_command(action: dict) -> dict:

src/fallback.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from typing import Callable
2+
3+
from talon import actions
4+
5+
from .versions import COMMAND_VERSION
6+
7+
# This ensures that we remember to update fallback if the response payload changes
8+
assert COMMAND_VERSION == 7
9+
10+
action_callbacks = {
11+
"getText": lambda: [actions.edit.selected_text()],
12+
"setSelection": actions.skip,
13+
"setSelectionBefore": actions.edit.left,
14+
"setSelectionAfter": actions.edit.right,
15+
"copyToClipboard": actions.edit.copy,
16+
"cutToClipboard": actions.edit.cut,
17+
"pasteFromClipboard": actions.edit.paste,
18+
"clearAndSetSelection": actions.edit.delete,
19+
"remove": actions.edit.delete,
20+
"editNewLineBefore": actions.edit.line_insert_up,
21+
"editNewLineAfter": actions.edit.line_insert_down,
22+
}
23+
24+
modifier_callbacks = {
25+
"extendThroughStartOf.line": actions.user.select_line_start,
26+
"extendThroughEndOf.line": actions.user.select_line_end,
27+
"containingScope.document": actions.edit.select_all,
28+
"containingScope.paragraph": actions.edit.select_paragraph,
29+
"containingScope.line": actions.edit.select_line,
30+
"containingScope.token": actions.edit.select_word,
31+
}
32+
33+
34+
def call_as_function(callee: str):
35+
wrap_with_paired_delimiter(f"{callee}(", ")")
36+
37+
38+
def wrap_with_paired_delimiter(left: str, right: str):
39+
selected = actions.edit.selected_text()
40+
actions.insert(f"{left}{selected}{right}")
41+
for _ in right:
42+
actions.edit.left()
43+
44+
45+
def containing_token_if_empty():
46+
if actions.edit.selected_text() == "":
47+
actions.edit.select_word()
48+
49+
50+
def perform_fallback(fallback: dict):
51+
try:
52+
modifier_callbacks = get_modifier_callbacks(fallback)
53+
action_callback = get_action_callback(fallback)
54+
for callback in reversed(modifier_callbacks):
55+
callback()
56+
return action_callback()
57+
except ValueError as ex:
58+
actions.app.notify(str(ex))
59+
60+
61+
def get_action_callback(fallback: dict) -> Callable:
62+
action = fallback["action"]
63+
64+
if action in action_callbacks:
65+
return action_callbacks[action]
66+
67+
match action:
68+
case "insert":
69+
return lambda: actions.insert(fallback["text"])
70+
case "callAsFunction":
71+
return lambda: call_as_function(fallback["callee"])
72+
case "wrapWithPairedDelimiter":
73+
return lambda: wrap_with_paired_delimiter(
74+
fallback["left"], fallback["right"]
75+
)
76+
77+
raise ValueError(f"Unknown Cursorless fallback action: {action}")
78+
79+
80+
def get_modifier_callbacks(fallback: dict) -> list[Callable]:
81+
return [get_modifier_callback(modifier) for modifier in fallback["modifiers"]]
82+
83+
84+
def get_modifier_callback(modifier: dict) -> Callable:
85+
modifier_type = modifier["type"]
86+
87+
match modifier_type:
88+
case "containingTokenIfEmpty":
89+
return containing_token_if_empty
90+
case "containingScope":
91+
scope_type_type = modifier["scopeType"]["type"]
92+
return get_simple_modifier_callback(f"{modifier_type}.{scope_type_type}")
93+
case "extendThroughStartOf":
94+
if "modifiers" not in modifier:
95+
return get_simple_modifier_callback(f"{modifier_type}.line")
96+
case "extendThroughEndOf":
97+
if "modifiers" not in modifier:
98+
return get_simple_modifier_callback(f"{modifier_type}.line")
99+
100+
raise ValueError(f"Unknown Cursorless fallback modifier: {modifier_type}")
101+
102+
103+
def get_simple_modifier_callback(key: str) -> Callable:
104+
try:
105+
return modifier_callbacks[key]
106+
except KeyError:
107+
raise ValueError(f"Unknown Cursorless fallback modifier: {key}")

src/versions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
COMMAND_VERSION = 7

0 commit comments

Comments
 (0)