Skip to content

Commit

Permalink
Fallback to Talon actions when focus is not on the text editor (#2235)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
3 people authored and cursorless-bot committed Mar 19, 2024
1 parent 067392c commit 3b9df05
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
16 changes: 13 additions & 3 deletions src/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

from talon import Module, actions, speech_system

from .fallback import perform_fallback
from .versions import COMMAND_VERSION


@dataclasses.dataclass
class CursorlessCommand:
version = 6
version = COMMAND_VERSION
spokenForm: str
usePrePhraseSnapshot: bool
action: dict
Expand All @@ -30,10 +33,12 @@ def on_phrase(d):
class Actions:
def private_cursorless_command_and_wait(action: dict):
"""Execute cursorless command and wait for it to finish"""
actions.user.private_cursorless_run_rpc_command_and_wait(
response = actions.user.private_cursorless_run_rpc_command_get(
CURSORLESS_COMMAND_ID,
construct_cursorless_command(action),
)
if "fallback" in response:
perform_fallback(response["fallback"])

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

def private_cursorless_command_get(action: dict):
"""Execute cursorless command and return result"""
return actions.user.private_cursorless_run_rpc_command_get(
response = actions.user.private_cursorless_run_rpc_command_get(
CURSORLESS_COMMAND_ID,
construct_cursorless_command(action),
)
if "fallback" in response:
return perform_fallback(response["fallback"])
if "returnValue" in response:
return response["returnValue"]
return None


def construct_cursorless_command(action: dict) -> dict:
Expand Down
107 changes: 107 additions & 0 deletions src/fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from typing import Callable

from talon import actions

from .versions import COMMAND_VERSION

# This ensures that we remember to update fallback if the response payload changes
assert COMMAND_VERSION == 7

action_callbacks = {
"getText": lambda: [actions.edit.selected_text()],
"setSelection": actions.skip,
"setSelectionBefore": actions.edit.left,
"setSelectionAfter": actions.edit.right,
"copyToClipboard": actions.edit.copy,
"cutToClipboard": actions.edit.cut,
"pasteFromClipboard": actions.edit.paste,
"clearAndSetSelection": actions.edit.delete,
"remove": actions.edit.delete,
"editNewLineBefore": actions.edit.line_insert_up,
"editNewLineAfter": actions.edit.line_insert_down,
}

modifier_callbacks = {
"extendThroughStartOf.line": actions.user.select_line_start,
"extendThroughEndOf.line": actions.user.select_line_end,
"containingScope.document": actions.edit.select_all,
"containingScope.paragraph": actions.edit.select_paragraph,
"containingScope.line": actions.edit.select_line,
"containingScope.token": actions.edit.select_word,
}


def call_as_function(callee: str):
wrap_with_paired_delimiter(f"{callee}(", ")")


def wrap_with_paired_delimiter(left: str, right: str):
selected = actions.edit.selected_text()
actions.insert(f"{left}{selected}{right}")
for _ in right:
actions.edit.left()


def containing_token_if_empty():
if actions.edit.selected_text() == "":
actions.edit.select_word()


def perform_fallback(fallback: dict):
try:
modifier_callbacks = get_modifier_callbacks(fallback)
action_callback = get_action_callback(fallback)
for callback in reversed(modifier_callbacks):
callback()
return action_callback()
except ValueError as ex:
actions.app.notify(str(ex))


def get_action_callback(fallback: dict) -> Callable:
action = fallback["action"]

if action in action_callbacks:
return action_callbacks[action]

match action:
case "insert":
return lambda: actions.insert(fallback["text"])
case "callAsFunction":
return lambda: call_as_function(fallback["callee"])
case "wrapWithPairedDelimiter":
return lambda: wrap_with_paired_delimiter(
fallback["left"], fallback["right"]
)

raise ValueError(f"Unknown Cursorless fallback action: {action}")


def get_modifier_callbacks(fallback: dict) -> list[Callable]:
return [get_modifier_callback(modifier) for modifier in fallback["modifiers"]]


def get_modifier_callback(modifier: dict) -> Callable:
modifier_type = modifier["type"]

match modifier_type:
case "containingTokenIfEmpty":
return containing_token_if_empty
case "containingScope":
scope_type_type = modifier["scopeType"]["type"]
return get_simple_modifier_callback(f"{modifier_type}.{scope_type_type}")
case "extendThroughStartOf":
if "modifiers" not in modifier:
return get_simple_modifier_callback(f"{modifier_type}.line")
case "extendThroughEndOf":
if "modifiers" not in modifier:
return get_simple_modifier_callback(f"{modifier_type}.line")

raise ValueError(f"Unknown Cursorless fallback modifier: {modifier_type}")


def get_simple_modifier_callback(key: str) -> Callable:
try:
return modifier_callbacks[key]
except KeyError:
raise ValueError(f"Unknown Cursorless fallback modifier: {key}")
1 change: 1 addition & 0 deletions src/versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMAND_VERSION = 7

0 comments on commit 3b9df05

Please sign in to comment.