Skip to content

Commit

Permalink
Merge branch 'main' into pr/saidelike/2131
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Mar 27, 2024
2 parents 0dbbfcd + de37ebf commit 32ab372
Show file tree
Hide file tree
Showing 144 changed files with 3,553 additions and 480 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"trailingComma": "all"
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
}
6 changes: 6 additions & 0 deletions changelog/2024-03-addLuaSupport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
tags: [enhancement]
pullRequest: 1962
---

- Add support for the Lua programming language
9 changes: 9 additions & 0 deletions changelog/2024-03-addedSpreadModifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
tags: [enhancement]
pullRequest: 2254
---

- Added every/spread ordinal/relative modifier. Turns relative and ordinal range modifiers into multiple target selections instead of contiguous range.

- `"take every two tokens"` selects two tokens as separate selections
- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors)
6 changes: 6 additions & 0 deletions changelog/2024-03-fallBackToTalonActions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
tags: [enhancement]
pullRequest: 2235
---

- Fall back to text-based Talon actions when editor is not focused. This allows you to say things like "take token", "bring air", etc, when in the terminal, search bar, etc.
81 changes: 57 additions & 24 deletions cursorless-talon/src/cheatsheet/sections/modifiers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from itertools import chain
from typing import TypedDict

from ..get_list import get_raw_list, make_dict_readable

MODIFIER_LIST_NAMES = [
"simple_modifier",
"interior_modifier",
"head_tail_modifier",
"simple_scope_modifier",
"every_scope_modifier",
"ancestor_scope_modifier",
"first_modifier",
"last_modifier",
"previous_next_modifier",
Expand Down Expand Up @@ -132,10 +136,6 @@ def get_modifiers():
"spokenForm": f"<ordinal> {complex_modifiers['next']} <scope>",
"description": "<ordinal> instance of <scope> after target",
},
{
"spokenForm": f"{complex_modifiers['previous']} <number> <scope>s",
"description": "previous <number> instances of <scope>",
},
{
"spokenForm": f"<scope> {complex_modifiers['backward']}",
"description": "single instance of <scope> including target, going backwards",
Expand All @@ -144,18 +144,25 @@ def get_modifiers():
"spokenForm": f"<scope> {complex_modifiers['forward']}",
"description": "single instance of <scope> including target, going forwards",
},
{
"spokenForm": f"<number> <scope>s {complex_modifiers['backward']}",
"description": "<number> instances of <scope> including target, going backwards",
},
{
"spokenForm": "<number> <scope>s",
"description": "<number> instances of <scope> including target, going forwards",
},
{
"spokenForm": f"{complex_modifiers['next']} <number> <scope>s",
"description": "next <number> instances of <scope>",
},
*generateOptionalEvery(
complex_modifiers["every"],
{
"spokenForm": f"<number> <scope>s {complex_modifiers['backward']}",
"description": "<number> instances of <scope> including target, going backwards",
},
{
"spokenForm": "<number> <scope>s",
"description": "<number> instances of <scope> including target, going forwards",
},
{
"spokenForm": f"{complex_modifiers['previous']} <number> <scope>s",
"description": "previous <number> instances of <scope>",
},
{
"spokenForm": f"{complex_modifiers['next']} <number> <scope>s",
"description": "next <number> instances of <scope>",
},
),
],
},
{
Expand All @@ -170,14 +177,40 @@ def get_modifiers():
"spokenForm": f"<ordinal> {complex_modifiers['last']} <scope>",
"description": "<ordinal>-to-last instance of <scope> in iteration scope",
},
*generateOptionalEvery(
complex_modifiers["every"],
{
"spokenForm": f"{complex_modifiers['first']} <number> <scope>s",
"description": "first <number> instances of <scope> in iteration scope",
},
{
"spokenForm": f"{complex_modifiers['last']} <number> <scope>s",
"description": "last <number> instances of <scope> in iteration scope",
},
),
],
},
]


class Entry(TypedDict):
spokenForm: str
description: str


def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]:
return list(
chain.from_iterable(
[
{
"spokenForm": f"{complex_modifiers['first']} <number> <scope>s",
"description": "First <number> instances of <scope> in iteration scope",
"spokenForm": entry["spokenForm"],
"description": f"{entry['description']}, as contiguous range",
},
{
"spokenForm": f"{complex_modifiers['last']} <number> <scope>s",
"description": "Last <number> instances of <scope> in iteration scope",
"spokenForm": f"{every} {entry['spokenForm']}",
"description": f"{entry['description']}, as individual targets",
},
],
},
]
]
for entry in entries
)
)
16 changes: 13 additions & 3 deletions cursorless-talon/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 cursorless-talon/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}")
27 changes: 22 additions & 5 deletions cursorless-talon/src/modifiers/ordinal_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,27 @@ def cursorless_ordinal_range(m) -> dict[str, Any]:


@mod.capture(
rule="({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
rule=(
"[{user.cursorless_every_scope_modifier}] "
"({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) "
"<user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
),
)
def cursorless_first_last(m) -> dict[str, Any]:
"""First/last `n` scopes; eg "first three funks"""
if m[0] == "first":
is_every = hasattr(m, "cursorless_every_scope_modifier")
if hasattr(m, "cursorless_first_modifier"):
return create_ordinal_scope_modifier(
m.cursorless_scope_type_plural, 0, m.private_cursorless_number_small
m.cursorless_scope_type_plural,
0,
m.private_cursorless_number_small,
is_every,
)
return create_ordinal_scope_modifier(
m.cursorless_scope_type_plural,
-m.private_cursorless_number_small,
m.private_cursorless_number_small,
is_every,
)


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


def create_ordinal_scope_modifier(scope_type: dict, start: int, length: int = 1):
return {
def create_ordinal_scope_modifier(
scope_type: dict,
start: int,
length: int = 1,
is_every: bool = False,
):
res = {
"type": "ordinalScope",
"scopeType": scope_type,
"start": start,
"length": length,
}
if is_every:
res["isEvery"] = True
return res
Loading

0 comments on commit 32ab372

Please sign in to comment.