diff --git a/cursorless-talon/.github/CODEOWNERS b/cursorless-talon/.github/CODEOWNERS deleted file mode 100644 index 33f266adbb..0000000000 --- a/cursorless-talon/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @pokey diff --git a/cursorless-talon/.github/ISSUE_TEMPLATE/config.yml b/cursorless-talon/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index d371b3a3c3..0000000000 --- a/cursorless-talon/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Create issue on cursorless-dev/cursorless - url: https://github.com/cursorless-dev/cursorless/issues/new - about: Please file issues on the main Cursorless repository diff --git a/cursorless-talon/.gitignore b/cursorless-talon/.gitignore deleted file mode 100644 index 97c530fa22..0000000000 --- a/cursorless-talon/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.flac -data/ -.vscode/settings.json diff --git a/cursorless-talon/LICENSE b/cursorless-talon/LICENSE deleted file mode 100644 index ec2b623613..0000000000 --- a/cursorless-talon/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Brandon Virgil Rule - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/cursorless-talon/README.md b/cursorless-talon/README.md deleted file mode 100644 index dc4ac1c302..0000000000 --- a/cursorless-talon/README.md +++ /dev/null @@ -1,32 +0,0 @@ -

Welcome to Cursorless!

-

- - Rating - - - Documentation - - - Tests - - - Maintenance - - - License: MIT - -

- -Cursorless is a spoken language for structural code editing, enabling developers to code by voice at speeds not possible with a keyboard. Cursorless decorates every token on the screen and defines a spoken language for rapid, high-level semantic manipulation of structured text. - -This repository holds the Talon side of Cursorless. If you've arrived here as part of the [Cursorless installation process](https://www.cursorless.org/docs/user/installation/), then you're in the right place! - -# Contributing - -If you're looking to improve Cursorless, note that Cursorless is now maintained as a monorepo, so if you're here in a browser, and your address bar points to https://github.com/cursorless-dev/cursorless-talon, then you're probably in the wrong place. The monorepo is hosted at [`cursorless`](https://github.com/cursorless-dev/cursorless), and the source of truth for these talon files is in the [`cursorless-talon`](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon) subdirectory. - -See [the contributor docs](https://www.cursorless.org/docs/contributing/) to get started. - -# Cursorless talon - -This directory contains the talon side of [Cursorless](https://marketplace.visualstudio.com/items?itemName=pokey.cursorless). diff --git a/cursorless-talon/docs/README.md b/cursorless-talon/docs/README.md deleted file mode 100644 index 6af3830dcc..0000000000 --- a/cursorless-talon/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -Cursorless is now monorepo 🙌. The docs now live at https://www.cursorless.org/docs/. diff --git a/cursorless-talon/docs/customization.md b/cursorless-talon/docs/customization.md deleted file mode 100644 index 7fed001ae2..0000000000 --- a/cursorless-talon/docs/customization.md +++ /dev/null @@ -1 +0,0 @@ -Cursorless is now monorepo 🙌. This document now lives at https://www.cursorless.org/docs/user/customization/. diff --git a/cursorless-talon/docs/experimental.md b/cursorless-talon/docs/experimental.md deleted file mode 100644 index f579d28476..0000000000 --- a/cursorless-talon/docs/experimental.md +++ /dev/null @@ -1 +0,0 @@ -Cursorless is now monorepo 🙌. This document now lives at https://www.cursorless.org/docs/user/experimental/ diff --git a/cursorless-talon/src/actions/actions.py b/cursorless-talon/src/actions/actions.py deleted file mode 100644 index c9e75cf750..0000000000 --- a/cursorless-talon/src/actions/actions.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import Callable, Union - -from talon import Module, actions - -from ..targets.target_types import ( - CursorlessDestination, - CursorlessExplicitTarget, - CursorlessTarget, - ImplicitDestination, -) -from .bring_move import BringMoveTargets -from .execute_command import cursorless_execute_command_action -from .homophones import cursorless_homophones_action -from .replace import cursorless_replace_action - -mod = Module() - -mod.list( - "cursorless_simple_action", - desc="Cursorless internal: simple actions", -) - -mod.list( - "cursorless_callback_action", - desc="Cursorless internal: actions implemented via a callback function", -) - -mod.list( - "cursorless_custom_action", - desc="Cursorless internal: user-defined custom actions", -) - -mod.list( - "cursorless_experimental_action", - desc="Cursorless internal: experimental actions", -) - -ACTION_LIST_NAMES = [ - "simple_action", - "callback_action", - "paste_action", - "bring_move_action", - "swap_action", - "wrap_action", - "insert_snippet_action", - "reformat_action", - "call_action", - "experimental_action", - "custom_action", -] - -callback_actions: dict[str, Callable[[CursorlessExplicitTarget], None]] = { - "nextHomophone": cursorless_homophones_action, -} - -# Don't wait for these actions to finish, usually because they hang on some kind of user interaction -no_wait_actions = [ - "generateSnippet", - "rename", -] - -# These are actions that we don't wait for, but still want to have a post action sleep -no_wait_actions_post_sleep = { - "rename": 0.3, -} - - -@mod.capture( - rule=( - "{user.cursorless_simple_action} |" - "{user.cursorless_experimental_action} |" - "{user.cursorless_callback_action} |" - "{user.cursorless_call_action} |" - "{user.cursorless_custom_action}" - ) -) -def cursorless_action_or_ide_command(m) -> dict[str, str]: - try: - value = m.cursorless_custom_action - type = "ide_command" - except AttributeError: - value = m[0] - type = "cursorless_action" - return { - "value": value, - "type": type, - } - - -@mod.action_class -class Actions: - def cursorless_command(action_name: str, target: CursorlessExplicitTarget): # pyright: ignore [reportGeneralTypeIssues] - """Perform cursorless command on target""" - if action_name in callback_actions: - callback_actions[action_name](target) - elif action_name in ["replaceWithTarget", "moveToTarget"]: - actions.user.private_cursorless_bring_move( - action_name, BringMoveTargets(target, ImplicitDestination()) - ) - elif action_name == "callAsFunction": - actions.user.private_cursorless_call(target) - elif action_name in no_wait_actions: - action = {"name": action_name, "target": target} - actions.user.private_cursorless_command_no_wait(action) - if action_name in no_wait_actions_post_sleep: - actions.sleep(no_wait_actions_post_sleep[action_name]) - else: - action = {"name": action_name, "target": target} - actions.user.private_cursorless_command_and_wait(action) - - def cursorless_vscode_command(command_id: str, target: CursorlessTarget): # pyright: ignore [reportGeneralTypeIssues] - """ - Perform vscode command on cursorless target - - Deprecated: prefer `cursorless_ide_command` - """ - return actions.user.cursorless_ide_command(command_id, target) - - def cursorless_ide_command(command_id: str, target: CursorlessTarget): # pyright: ignore [reportGeneralTypeIssues] - """Perform ide command on cursorless target""" - return cursorless_execute_command_action(command_id, target) - - def cursorless_insert( - destination: CursorlessDestination, # pyright: ignore [reportGeneralTypeIssues] - text: Union[str, list[str]], - ): - """Perform text insertion on Cursorless destination""" - if isinstance(text, str): - text = [text] - cursorless_replace_action(destination, text) - - def private_cursorless_action_or_ide_command( - instruction: dict[str, str], # pyright: ignore [reportGeneralTypeIssues] - target: CursorlessTarget, - ): - """Perform cursorless action or ide command on target (internal use only)""" - type = instruction["type"] - value = instruction["value"] - if type == "cursorless_action": - actions.user.cursorless_command(value, target) - elif type == "ide_command": - actions.user.cursorless_ide_command(value, target) diff --git a/cursorless-talon/src/actions/bring_move.py b/cursorless-talon/src/actions/bring_move.py deleted file mode 100644 index bb13497ada..0000000000 --- a/cursorless-talon/src/actions/bring_move.py +++ /dev/null @@ -1,46 +0,0 @@ -from dataclasses import dataclass - -from talon import Module, actions - -from ..targets.target_types import ( - CursorlessDestination, - CursorlessTarget, - ImplicitDestination, -) - - -@dataclass -class BringMoveTargets: - source: CursorlessTarget - destination: CursorlessDestination - - -mod = Module() - - -mod.list("cursorless_bring_move_action", desc="Cursorless bring or move actions") - - -@mod.capture(rule=" []") -def cursorless_bring_move_targets(m) -> BringMoveTargets: - source = m.cursorless_target - - try: - destination = m.cursorless_destination - except AttributeError: - destination = ImplicitDestination() - - return BringMoveTargets(source, destination) - - -@mod.action_class -class Actions: - def private_cursorless_bring_move(action_name: str, targets: BringMoveTargets): # pyright: ignore [reportGeneralTypeIssues] - """Execute Cursorless move/bring action""" - actions.user.private_cursorless_command_and_wait( - { - "name": action_name, - "source": targets.source, - "destination": targets.destination, - } - ) diff --git a/cursorless-talon/src/actions/call.py b/cursorless-talon/src/actions/call.py deleted file mode 100644 index 7bfe75cb78..0000000000 --- a/cursorless-talon/src/actions/call.py +++ /dev/null @@ -1,22 +0,0 @@ -from talon import Module, actions - -from ..targets.target_types import CursorlessTarget, ImplicitTarget - -mod = Module() -mod.list("cursorless_call_action", desc="Cursorless call action") - - -@mod.action_class -class Actions: - def private_cursorless_call( - callee: CursorlessTarget, # pyright: ignore [reportGeneralTypeIssues] - argument: CursorlessTarget = ImplicitTarget(), - ): - """Execute Cursorless call action""" - actions.user.private_cursorless_command_and_wait( - { - "name": "callAsFunction", - "callee": callee, - "argument": argument, - } - ) diff --git a/cursorless-talon/src/actions/execute_command.py b/cursorless-talon/src/actions/execute_command.py deleted file mode 100644 index 067fc14488..0000000000 --- a/cursorless-talon/src/actions/execute_command.py +++ /dev/null @@ -1,17 +0,0 @@ -from talon import actions - -from ..targets.target_types import CursorlessTarget - - -def cursorless_execute_command_action( - command_id: str, target: CursorlessTarget, command_options: dict = {} -): - """Execute Cursorless execute command action""" - actions.user.private_cursorless_command_and_wait( - { - "name": "executeCommand", - "commandId": command_id, - "options": command_options, - "target": target, - } - ) diff --git a/cursorless-talon/src/actions/get_text.py b/cursorless-talon/src/actions/get_text.py deleted file mode 100644 index 6f1770ad85..0000000000 --- a/cursorless-talon/src/actions/get_text.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Optional - -from talon import Module, actions - -from ..targets.target_types import CursorlessTarget - -mod = Module() - - -@mod.action_class -class Actions: - def cursorless_get_text( - target: CursorlessTarget, # pyright: ignore [reportGeneralTypeIssues] - hide_decorations: bool = False, - ) -> str: - """Get target text. If hide_decorations is True, don't show decorations""" - return cursorless_get_text_action( - target, - show_decorations=not hide_decorations, - ensure_single_target=True, - )[0] - - def cursorless_get_text_list( - target: CursorlessTarget, # pyright: ignore [reportGeneralTypeIssues] - hide_decorations: bool = False, - ) -> list[str]: - """Get texts for multiple targets. If hide_decorations is True, don't show decorations""" - return cursorless_get_text_action( - target, - show_decorations=not hide_decorations, - ensure_single_target=False, - ) - - -def cursorless_get_text_action( - target: CursorlessTarget, - *, - show_decorations: Optional[bool] = None, - ensure_single_target: Optional[bool] = None, -) -> list[str]: - """Get target texts""" - options: dict[str, bool] = {} - - if show_decorations is not None: - options["showDecorations"] = show_decorations - - if ensure_single_target is not None: - options["ensureSingleTarget"] = ensure_single_target - - return actions.user.private_cursorless_command_get( - { - "name": "getText", - "options": options, - "target": target, - } - ) diff --git a/cursorless-talon/src/actions/homophones.py b/cursorless-talon/src/actions/homophones.py deleted file mode 100644 index 4b3b7f3280..0000000000 --- a/cursorless-talon/src/actions/homophones.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Optional - -from talon import actions, app - -from ..targets.target_types import ( - CursorlessExplicitTarget, - PrimitiveDestination, -) -from .get_text import cursorless_get_text_action -from .replace import cursorless_replace_action - - -def cursorless_homophones_action(target: CursorlessExplicitTarget): - """Replaced target with next homophone""" - texts = cursorless_get_text_action(target, show_decorations=False) - try: - updated_texts = list(map(get_next_homophone, texts)) - except LookupError as e: - app.notify(str(e)) - return - destination = PrimitiveDestination("to", target) - cursorless_replace_action(destination, updated_texts) - - -def get_next_homophone(word: str) -> str: - homophones: Optional[list[str]] = actions.user.homophones_get(word) - if not homophones: - raise LookupError(f"Found no homophones for '{word}'") - index = (homophones.index(word.lower()) + 1) % len(homophones) - homophone = homophones[index] - return format_homophone(word, homophone) - - -def format_homophone(word: str, homophone: str) -> str: - if word.isupper(): - return homophone.upper() - if word == word.capitalize(): - return homophone.capitalize() - return homophone diff --git a/cursorless-talon/src/actions/paste.py b/cursorless-talon/src/actions/paste.py deleted file mode 100644 index 5ee32b97bc..0000000000 --- a/cursorless-talon/src/actions/paste.py +++ /dev/null @@ -1,21 +0,0 @@ -from talon import Module, actions - -from ..targets.target_types import CursorlessDestination - -mod = Module() - -mod.list("cursorless_paste_action", desc="Cursorless paste action") - - -@mod.action_class -class Actions: - def private_cursorless_paste( - destination: CursorlessDestination, # pyright: ignore [reportGeneralTypeIssues] - ): - """Execute Cursorless paste action""" - actions.user.private_cursorless_command_and_wait( - { - "name": "pasteFromClipboard", - "destination": destination, - } - ) diff --git a/cursorless-talon/src/actions/reformat.py b/cursorless-talon/src/actions/reformat.py deleted file mode 100644 index 1b60bee875..0000000000 --- a/cursorless-talon/src/actions/reformat.py +++ /dev/null @@ -1,25 +0,0 @@ -from talon import Module, actions - -from ..targets.target_types import ( - CursorlessExplicitTarget, - PrimitiveDestination, -) -from .get_text import cursorless_get_text_action -from .replace import cursorless_replace_action - -mod = Module() - -mod.list("cursorless_reformat_action", desc="Cursorless reformat action") - - -@mod.action_class -class Actions: - def private_cursorless_reformat( - target: CursorlessExplicitTarget, # pyright: ignore [reportGeneralTypeIssues] - formatters: str, - ): - """Execute Cursorless reformat action. Reformat target with formatter""" - texts = cursorless_get_text_action(target, show_decorations=False) - updated_texts = [actions.user.reformat_text(text, formatters) for text in texts] - destination = PrimitiveDestination("to", target) - cursorless_replace_action(destination, updated_texts) diff --git a/cursorless-talon/src/actions/replace.py b/cursorless-talon/src/actions/replace.py deleted file mode 100644 index 4067eb1f2b..0000000000 --- a/cursorless-talon/src/actions/replace.py +++ /dev/null @@ -1,16 +0,0 @@ -from talon import actions - -from ..targets.target_types import CursorlessDestination - - -def cursorless_replace_action( - destination: CursorlessDestination, replace_with: list[str] -): - """Execute Cursorless replace action. Replace targets with texts""" - actions.user.private_cursorless_command_and_wait( - { - "name": "replace", - "replaceWith": replace_with, - "destination": destination, - } - ) diff --git a/cursorless-talon/src/actions/swap.py b/cursorless-talon/src/actions/swap.py deleted file mode 100644 index 862f7dcaba..0000000000 --- a/cursorless-talon/src/actions/swap.py +++ /dev/null @@ -1,49 +0,0 @@ -from dataclasses import dataclass - -from talon import Module, actions - -from ..targets.target_types import CursorlessTarget, ImplicitTarget - - -@dataclass -class SwapTargets: - target1: CursorlessTarget - target2: CursorlessTarget - - -mod = Module() - -mod.list("cursorless_swap_action", desc="Cursorless swap action") -mod.list( - "cursorless_swap_connective", - desc="The connective used to separate swap targets", -) - - -@mod.capture( - rule=( - "[] {user.cursorless_swap_connective} " - ) -) -def cursorless_swap_targets(m) -> SwapTargets: - targets = m.cursorless_target_list - - return SwapTargets( - ImplicitTarget() if len(targets) == 1 else targets[0], - targets[-1], - ) - - -@mod.action_class -class Actions: - def private_cursorless_swap( - targets: SwapTargets, # pyright: ignore [reportGeneralTypeIssues] - ): - """Execute Cursorless swap action""" - actions.user.private_cursorless_command_and_wait( - { - "name": "swapTargets", - "target1": targets.target1, - "target2": targets.target2, - } - ) diff --git a/cursorless-talon/src/actions/wrap.py b/cursorless-talon/src/actions/wrap.py deleted file mode 100644 index f9e846fc56..0000000000 --- a/cursorless-talon/src/actions/wrap.py +++ /dev/null @@ -1,60 +0,0 @@ -from talon import Module, actions - -from ..targets.target_types import CursorlessTarget - -mod = Module() - -mod.list("cursorless_wrap_action", desc="Cursorless wrap action") - - -@mod.action_class -class Actions: - def private_cursorless_wrap_with_paired_delimiter( - action_name: str, # pyright: ignore [reportGeneralTypeIssues] - target: CursorlessTarget, - paired_delimiter: list[str], - ): - """Execute Cursorless wrap/rewrap with paired delimiter action""" - if action_name == "rewrap": - action_name = "rewrapWithPairedDelimiter" - - actions.user.private_cursorless_command_and_wait( - { - "name": action_name, - "left": paired_delimiter[0], - "right": paired_delimiter[1], - "target": target, - } - ) - - def private_cursorless_wrap_with_snippet( - action_name: str, # pyright: ignore [reportGeneralTypeIssues] - target: CursorlessTarget, - snippet_location: str, - ): - """Execute Cursorless wrap with snippet action""" - if action_name == "wrapWithPairedDelimiter": - action_name = "wrapWithSnippet" - elif action_name == "rewrap": - raise Exception("Rewrapping with snippet not supported") - - snippet_name, variable_name = parse_snippet_location(snippet_location) - - actions.user.private_cursorless_command_and_wait( - { - "name": action_name, - "snippetDescription": { - "type": "named", - "name": snippet_name, - "variableName": variable_name, - }, - "target": target, - } - ) - - -def parse_snippet_location(snippet_location: str) -> tuple[str, str]: - [snippet_name, variable_name] = snippet_location.split(".") - if snippet_name is None or variable_name is None: - raise Exception("Snippet location missing '.'") - return (snippet_name, variable_name) diff --git a/cursorless-talon/src/apps/cursorless_vscode.py b/cursorless-talon/src/apps/cursorless_vscode.py deleted file mode 100644 index 63c20daa94..0000000000 --- a/cursorless-talon/src/apps/cursorless_vscode.py +++ /dev/null @@ -1,26 +0,0 @@ -from talon import Context, actions - -ctx = Context() - -ctx.matches = r""" -app: vscode -""" - -ctx.tags = ["user.cursorless"] - - -@ctx.action_class("user") -class Actions: - def private_cursorless_show_settings_in_ide(): - """Show Cursorless-specific settings in ide""" - actions.user.private_cursorless_run_rpc_command_no_wait( - "workbench.action.openSettings", "@ext:pokey.cursorless " - ) - actions.sleep("250ms") - actions.key("right") - - def private_cursorless_show_sidebar(): - """Show Cursorless sidebar""" - actions.user.private_cursorless_run_rpc_command_and_wait( - "workbench.view.extension.cursorless" - ) diff --git a/cursorless-talon/src/apps/vscode_settings.py b/cursorless-talon/src/apps/vscode_settings.py deleted file mode 100644 index f1ec80a631..0000000000 --- a/cursorless-talon/src/apps/vscode_settings.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -import traceback -from pathlib import Path -from typing import Any - -from talon import Context, Module, actions - -from ..vendor.jstyleson import loads - -mod = Module() - -windows_ctx = Context() -mac_ctx = Context() -linux_ctx = Context() - -windows_ctx.matches = r""" -os: windows -""" -mac_ctx.matches = r""" -os: mac -""" -linux_ctx.matches = r""" -os: linux -""" - - -@mod.action_class -class Actions: - def vscode_settings_path() -> Path: - """Get path of vscode settings json file""" - ... - - def vscode_get_setting(key: str, default_value: Any = None): # pyright: ignore [reportGeneralTypeIssues] - """Get the value of vscode setting at the given key""" - path: Path = actions.user.vscode_settings_path() - settings: dict = loads(path.read_text()) - - if default_value is not None: - return settings.get(key, default_value) - else: - return settings[key] - - def vscode_get_setting_with_fallback( - key: str, # pyright: ignore [reportGeneralTypeIssues] - default_value: Any, - fallback_value: Any, - fallback_message: str, - ) -> tuple[Any, bool]: - """Returns a vscode setting with a fallback in case there's an error - - Args: - key (str): The key of the setting to look up - default_value (Any): The default value to return if the setting is not defined - fallback_value (Any): The value to return if there is an error looking up the setting - fallback_message (str): The message to show to the user if we end up having to use the fallback - - Returns: - tuple[Any, bool]: The value of the setting or the default or fall back, along with boolean which is true if there was an error - """ - try: - return actions.user.vscode_get_setting(key, default_value), False - except Exception: - print(fallback_message) - traceback.print_exc() - return fallback_value, True - - -def pick_path(paths: list[Path]) -> Path: - existing_paths = [path for path in paths if path.exists()] - return max(existing_paths, key=lambda path: path.stat().st_mtime) - - -@mac_ctx.action_class("user") -class MacUserActions: - def vscode_settings_path() -> Path: - application_support = Path.home() / "Library/Application Support" - return pick_path( - [ - application_support / "Code/User/settings.json", - application_support / "VSCodium/User/settings.json", - ] - ) - - -@linux_ctx.action_class("user") -class LinuxUserActions: - def vscode_settings_path() -> Path: - xdg_config_home = Path( - os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config") - ) - flatpak_apps = Path.home() / ".var/app" - return pick_path( - [ - xdg_config_home / "Code/User/settings.json", - xdg_config_home / "VSCodium/User/settings.json", - xdg_config_home / "Code - OSS/User/settings.json", - flatpak_apps / "com.visualstudio.code/config/Code/User/settings.json", - flatpak_apps / "com.vscodium.codium/config/VSCodium/User/settings.json", - flatpak_apps - / "com.visualstudio.code-oss/config/Code - OSS/User/settings.json", - ] - ) - - -@windows_ctx.action_class("user") -class WindowsUserActions: - def vscode_settings_path() -> Path: - appdata = Path(os.environ["APPDATA"]) - return pick_path( - [ - appdata / "Code/User/settings.json", - appdata / "VSCodium/User/settings.json", - ] - ) diff --git a/cursorless-talon/src/cheatsheet/cheat_sheet.py b/cursorless-talon/src/cheatsheet/cheat_sheet.py deleted file mode 100644 index 5d4b938576..0000000000 --- a/cursorless-talon/src/cheatsheet/cheat_sheet.py +++ /dev/null @@ -1,154 +0,0 @@ -import webbrowser -from pathlib import Path - -from talon import Context, Module, actions, app - -from .get_list import get_list, get_lists -from .sections.actions import get_actions -from .sections.compound_targets import get_compound_targets -from .sections.destinations import get_destinations -from .sections.get_scope_visualizer import get_scope_visualizer -from .sections.modifiers import get_modifiers -from .sections.scopes import get_scopes -from .sections.special_marks import get_special_marks - -mod = Module() -ctx = Context() -ctx.matches = r""" -tag: user.cursorless -""" - -instructions_url = "https://www.cursorless.org/docs/" - - -@mod.action_class -class Actions: - def private_cursorless_cheat_sheet_show_html(): - """Show new cursorless html cheat sheet""" - app.notify( - 'Please first focus an app that supports cursorless, eg say "focus code"' - ) - - def private_cursorless_cheat_sheet_update_json(): - """Update default cursorless cheatsheet json (for developer use only)""" - app.notify( - 'Please first focus an app that supports cursorless, eg say "focus code"' - ) - - def private_cursorless_open_instructions(): - """Open web page with cursorless instructions""" - webbrowser.open(instructions_url) - - -@ctx.action_class("user") -class CursorlessActions: - def private_cursorless_cheat_sheet_show_html(): - """Show cursorless html cheat sheet""" - # On Linux browsers installed using snap can't open files in a hidden directory - if app.platform == "linux": - cheatsheet_out_dir = cheatsheet_dir_linux() - cheatsheet_filename = "cursorless-cheatsheet.html" - else: - cheatsheet_out_dir = Path.home() / ".cursorless" - cheatsheet_filename = "cheatsheet.html" - - cheatsheet_out_dir.mkdir(parents=True, exist_ok=True) - cheatsheet_out_path = cheatsheet_out_dir / cheatsheet_filename - actions.user.private_cursorless_run_rpc_command_and_wait( - "cursorless.showCheatsheet", - { - "version": 0, - "spokenFormInfo": cursorless_cheat_sheet_get_json(), - "outputPath": str(cheatsheet_out_path), - }, - ) - webbrowser.open(cheatsheet_out_path.as_uri()) - - def private_cursorless_cheat_sheet_update_json(): - """Update default cursorless cheatsheet json (for developer use only)""" - actions.user.private_cursorless_run_rpc_command_and_wait( - "cursorless.internal.updateCheatsheetDefaults", - cursorless_cheat_sheet_get_json(), - ) - - -def cheatsheet_dir_linux() -> Path: - """Get cheatsheet directory for Linux""" - try: - # 1. Get users actual document directory - import platformdirs # pyright: ignore [reportMissingImports] - - return Path(platformdirs.user_documents_dir()) - except Exception: - # 2. Look for a documents directory in user home - user_documents_dir = Path.home() / "Documents" - if user_documents_dir.is_dir(): - return user_documents_dir - - # 3. Fall back to user home - return Path.home() - - -def cursorless_cheat_sheet_get_json(): - """Get cursorless cheat sheet json""" - return { - "sections": [ - { - "name": "Actions", - "id": "actions", - "items": get_actions(), - }, - { - "name": "Destinations", - "id": "destinations", - "items": get_destinations(), - }, - { - "name": "Scopes", - "id": "scopes", - "items": get_scopes(), - }, - { - "name": "Scope visualizer", - "id": "scopeVisualizer", - "items": get_scope_visualizer(), - }, - { - "name": "Modifiers", - "id": "modifiers", - "items": get_modifiers(), - }, - { - "name": "Paired delimiters", - "id": "pairedDelimiters", - "items": get_lists( - [ - "wrapper_only_paired_delimiter", - "wrapper_selectable_paired_delimiter", - "selectable_only_paired_delimiter", - ], - "pairedDelimiter", - ), - }, - { - "name": "Special marks", - "id": "specialMarks", - "items": get_special_marks(), - }, - { - "name": "Compound targets", - "id": "compoundTargets", - "items": get_compound_targets(), - }, - { - "name": "Colors", - "id": "colors", - "items": get_list("hat_color", "hatColor"), - }, - { - "name": "Shapes", - "id": "shapes", - "items": get_list("hat_shape", "hatShape"), - }, - ] - } diff --git a/cursorless-talon/src/cheatsheet/get_list.py b/cursorless-talon/src/cheatsheet/get_list.py deleted file mode 100644 index 341873fd6a..0000000000 --- a/cursorless-talon/src/cheatsheet/get_list.py +++ /dev/null @@ -1,92 +0,0 @@ -import re -import typing -from collections.abc import Mapping, Sequence -from typing import Optional, TypedDict - -from talon import registry - -from ..conventions import get_cursorless_list_name - - -class Variation(TypedDict): - spokenForm: str - description: str - - -class ListItemDescriptor(TypedDict): - id: str - type: str - variations: list[Variation] - - -def get_list( - name: str, type: str, descriptions: Optional[Mapping[str, str]] = None -) -> list[ListItemDescriptor]: - if descriptions is None: - descriptions = {} - - items = get_raw_list(name) - - return make_dict_readable(type, items, descriptions) - - -def get_lists( - names: Sequence[str], type: str, descriptions: Optional[Mapping[str, str]] = None -) -> list[ListItemDescriptor]: - return [item for name in names for item in get_list(name, type, descriptions)] - - -def get_raw_list(name: str) -> Mapping[str, str]: - cursorless_list_name = get_cursorless_list_name(name) - return typing.cast(dict[str, str], registry.lists[cursorless_list_name][0]).copy() - - -def get_spoken_form_from_list(list_name: str, value: str) -> str: - """Get the spoken form of a value from a list. - - Args: - list_name (str): The name of the list. - value (str): The value to look up. - - Returns: - str: The spoken form of the value. - """ - return next( - spoken_form for spoken_form, v in get_raw_list(list_name).items() if v == value - ) - - -def make_dict_readable( - type: str, dict: Mapping[str, str], descriptions: Mapping[str, str] -) -> list[ListItemDescriptor]: - return [ - { - "id": value, - "type": type, - "variations": [ - { - "spokenForm": key, - "description": descriptions.get(value, make_readable(value)), - } - ], - } - for key, value in dict.items() - ] - - -def make_readable(text: str) -> str: - text, is_private = ( - (text[8:], True) if text.startswith("private.") else (text, False) - ) - text = text.replace(".", " ") - text = de_camel(text).lower().capitalize() - return f"{text} (PRIVATE)" if is_private else text - - -def de_camel(text: str) -> str: - """Replacing camelCase boundaries with blank space""" - return re.sub( - r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9])|(?<=[0-9])(?=[a-zA-Z])", - " ", - text, - ) diff --git a/cursorless-talon/src/cheatsheet/sections/actions.py b/cursorless-talon/src/cheatsheet/sections/actions.py deleted file mode 100644 index b83c3e5ec8..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/actions.py +++ /dev/null @@ -1,138 +0,0 @@ -from ...actions.actions import ACTION_LIST_NAMES -from ..get_list import get_raw_list, make_dict_readable - - -def get_actions(): - all_actions = {} - for name in ACTION_LIST_NAMES: - all_actions.update(get_raw_list(name)) - - multiple_target_action_names = [ - "replaceWithTarget", - "moveToTarget", - "swapTargets", - "applyFormatter", - "callAsFunction", - "wrapWithPairedDelimiter", - "rewrap", - "pasteFromClipboard", - ] - simple_actions = { - f"{key} ": value - for key, value in all_actions.items() - if value not in multiple_target_action_names - } - complex_actions = { - value: key - for key, value in all_actions.items() - if value in multiple_target_action_names - } - - swap_connective = list(get_raw_list("swap_connective").keys())[0] - - return [ - *make_dict_readable( - "action", - simple_actions, - { - "editNewLineAfter": "Edit new line/scope after", - "editNewLineBefore": "Edit new line/scope before", - }, - ), - { - "id": "replaceWithTarget", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['replaceWithTarget']} ", - "description": "Copy to ", - }, - { - "spokenForm": f"{complex_actions['replaceWithTarget']} ", - "description": "Insert copy of at cursor", - }, - ], - }, - { - "id": "pasteFromClipboard", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['pasteFromClipboard']} ", - "description": "Paste from clipboard at ", - } - ], - }, - { - "id": "moveToTarget", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['moveToTarget']} ", - "description": "Move to ", - }, - { - "spokenForm": f"{complex_actions['moveToTarget']} ", - "description": "Move to cursor position", - }, - ], - }, - { - "id": "swapTargets", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['swapTargets']} {swap_connective} ", - "description": "Swap with ", - }, - { - "spokenForm": f"{complex_actions['swapTargets']} {swap_connective} ", - "description": "Swap selection with ", - }, - ], - }, - { - "id": "applyFormatter", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['applyFormatter']} at ", - "description": "Reformat as ", - } - ], - }, - { - "id": "callAsFunction", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['callAsFunction']} ", - "description": "Call on selection", - }, - { - "spokenForm": f"{complex_actions['callAsFunction']} on ", - "description": "Call on ", - }, - ], - }, - { - "id": "wrapWithPairedDelimiter", - "type": "action", - "variations": [ - { - "spokenForm": f" {complex_actions['wrapWithPairedDelimiter']} ", - "description": "Wrap with ", - } - ], - }, - { - "id": "rewrap", - "type": "action", - "variations": [ - { - "spokenForm": f" {complex_actions['rewrap']} ", - "description": "Rewrap with ", - } - ], - }, - ] diff --git a/cursorless-talon/src/cheatsheet/sections/compound_targets.py b/cursorless-talon/src/cheatsheet/sections/compound_targets.py deleted file mode 100644 index 9f1b85b439..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/compound_targets.py +++ /dev/null @@ -1,53 +0,0 @@ -from ..get_list import get_raw_list, get_spoken_form_from_list - -FORMATTERS = { - "rangeExclusive": lambda start, end: f"between {start} and {end}", - "rangeInclusive": lambda start, end: f"{start} through {end}", - "rangeExcludingStart": lambda start, end: f"end of {start} through {end}", - "rangeExcludingEnd": lambda start, end: f"{start} until start of {end}", - "verticalRange": lambda start, end: f"{start} vertically through {end}", -} - - -def get_compound_targets(): - list_connective_term = get_spoken_form_from_list( - "list_connective", "listConnective" - ) - vertical_range_term = get_spoken_form_from_list("range_type", "verticalRange") - - return [ - { - "id": "listConnective", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": f" {list_connective_term} ", - "description": " and ", - }, - ], - }, - *[ - get_entry(spoken_form, id) - for spoken_form, id in get_raw_list("range_connective").items() - ], - get_entry(vertical_range_term, "verticalRange"), - ] - - -def get_entry(spoken_form, id): - formatter = FORMATTERS[id] - - return { - "id": id, - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": f" {spoken_form} ", - "description": formatter("", ""), - }, - { - "spokenForm": f"{spoken_form} ", - "description": formatter("selection", ""), - }, - ], - } diff --git a/cursorless-talon/src/cheatsheet/sections/destinations.py b/cursorless-talon/src/cheatsheet/sections/destinations.py deleted file mode 100644 index d269fb2e53..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/destinations.py +++ /dev/null @@ -1,28 +0,0 @@ -from ..get_list import get_raw_list - - -def get_destinations(): - insertion_modes = { - **{p: "to" for p in get_raw_list("insertion_mode_to")}, - **get_raw_list("insertion_mode_before_after"), - } - - descriptions = { - "to": "Replace ", - "before": "Insert before ", - "after": "Insert after ", - } - - return [ - { - "id": f"destination_{id}", - "type": "destination", - "variations": [ - { - "spokenForm": f"{spoken_form} ", - "description": descriptions[id], - } - ], - } - for spoken_form, id in insertion_modes.items() - ] diff --git a/cursorless-talon/src/cheatsheet/sections/get_scope_visualizer.py b/cursorless-talon/src/cheatsheet/sections/get_scope_visualizer.py deleted file mode 100644 index 69115fbe1e..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/get_scope_visualizer.py +++ /dev/null @@ -1,37 +0,0 @@ -from ..get_list import get_list, get_raw_list, make_readable - - -def get_scope_visualizer(): - show_scope_visualizer = list(get_raw_list("show_scope_visualizer").keys())[0] - visualization_types = get_raw_list("visualization_type") - - return [ - *get_list("hide_scope_visualizer", "command"), - { - "id": "show_scope_visualizer", - "type": "command", - "variations": [ - { - "spokenForm": f"{show_scope_visualizer} ", - "description": "Visualize ", - }, - *[ - { - "spokenForm": f"{show_scope_visualizer} {spoken_form}", - "description": f"Visualize {make_readable(id).lower()} range", - } - for spoken_form, id in visualization_types.items() - ], - ], - }, - { - "id": "show_scope_sidebar", - "type": "command", - "variations": [ - { - "spokenForm": "bar cursorless", - "description": "Show cursorless sidebar", - }, - ], - }, - ] diff --git a/cursorless-talon/src/cheatsheet/sections/modifiers.py b/cursorless-talon/src/cheatsheet/sections/modifiers.py deleted file mode 100644 index af8164d312..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/modifiers.py +++ /dev/null @@ -1,216 +0,0 @@ -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", - "every_scope_modifier", - "ancestor_scope_modifier", - "first_modifier", - "last_modifier", - "previous_next_modifier", - "forward_backward_modifier", - "position", -] - - -def get_modifiers(): - all_modifiers = {} - for name in MODIFIER_LIST_NAMES: - all_modifiers.update(get_raw_list(name)) - - complex_modifier_ids = [ - "extendThroughStartOf", - "extendThroughEndOf", - "every", - "ancestor", - "first", - "last", - "previous", - "next", - "backward", - "forward", - ] - simple_modifiers = { - key: value - for key, value in all_modifiers.items() - if value not in complex_modifier_ids - } - complex_modifiers = { - value: key - for key, value in all_modifiers.items() - if value in complex_modifier_ids - } - - return [ - *make_dict_readable( - "modifier", - simple_modifiers, - { - "excludeInterior": "Bounding paired delimiters", - "toRawSelection": "No inference", - "leading": "Leading delimiter range", - "trailing": "Trailing delimiter range", - "start": "Empty position at start of target", - "end": "Empty position at end of target", - }, - ), - { - "id": "extendThroughStartOf", - "type": "modifier", - "variations": [ - { - "spokenForm": complex_modifiers["extendThroughStartOf"], - "description": "Extend through start of line", - }, - { - "spokenForm": f"{complex_modifiers['extendThroughStartOf']} ", - "description": "Extend through start of ", - }, - ], - }, - { - "id": "extendThroughEndOf", - "type": "modifier", - "variations": [ - { - "spokenForm": complex_modifiers["extendThroughEndOf"], - "description": "Extend through end of line", - }, - { - "spokenForm": f"{complex_modifiers['extendThroughEndOf']} ", - "description": "Extend through end of ", - }, - ], - }, - { - "id": "containingScope", - "type": "modifier", - "variations": [ - { - "spokenForm": "", - "description": "Containing instance of ", - }, - ], - }, - { - "id": "every", - "type": "modifier", - "variations": [ - { - "spokenForm": f"{complex_modifiers['every']} ", - "description": "Every instance of ", - }, - ], - }, - { - "id": "ancestor", - "type": "modifier", - "variations": [ - { - "spokenForm": f"{complex_modifiers['ancestor']} ", - "description": "Grandparent containing instance of ", - }, - ], - }, - { - "id": "relativeScope", - "type": "modifier", - "variations": [ - { - "spokenForm": f"{complex_modifiers['previous']} ", - "description": "Previous instance of ", - }, - { - "spokenForm": f"{complex_modifiers['next']} ", - "description": "Next instance of ", - }, - { - "spokenForm": f" {complex_modifiers['previous']} ", - "description": " instance of before target", - }, - { - "spokenForm": f" {complex_modifiers['next']} ", - "description": " instance of after target", - }, - { - "spokenForm": f" {complex_modifiers['backward']}", - "description": "single instance of including target, going backwards", - }, - { - "spokenForm": f" {complex_modifiers['forward']}", - "description": "single instance of including target, going forwards", - }, - *generateOptionalEvery( - complex_modifiers["every"], - { - "spokenForm": f" s {complex_modifiers['backward']}", - "description": " instances of including target, going backwards", - }, - { - "spokenForm": " s", - "description": " instances of including target, going forwards", - }, - { - "spokenForm": f"{complex_modifiers['previous']} s", - "description": "previous instances of ", - }, - { - "spokenForm": f"{complex_modifiers['next']} s", - "description": "next instances of ", - }, - ), - ], - }, - { - "id": "ordinalScope", - "type": "modifier", - "variations": [ - { - "spokenForm": " ", - "description": " instance of in iteration scope", - }, - { - "spokenForm": f" {complex_modifiers['last']} ", - "description": "-to-last instance of in iteration scope", - }, - *generateOptionalEvery( - complex_modifiers["every"], - { - "spokenForm": f"{complex_modifiers['first']} s", - "description": "first instances of in iteration scope", - }, - { - "spokenForm": f"{complex_modifiers['last']} s", - "description": "last instances of in iteration scope", - }, - ), - ], - }, - ] - - -class Entry(TypedDict): - spokenForm: str - description: str - - -def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]: - return list( - chain.from_iterable( - [ - { - "spokenForm": entry["spokenForm"], - "description": f"{entry['description']}, as contiguous range", - }, - { - "spokenForm": f"{every} {entry['spokenForm']}", - "description": f"{entry['description']}, as individual targets", - }, - ] - for entry in entries - ) - ) diff --git a/cursorless-talon/src/cheatsheet/sections/scopes.py b/cursorless-talon/src/cheatsheet/sections/scopes.py deleted file mode 100644 index 462074beae..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/scopes.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..get_list import get_lists, get_spoken_form_from_list - - -def get_scopes(): - glyph_spoken_form = get_spoken_form_from_list("glyph_scope_type", "glyph") - return [ - *get_lists( - ["scope_type"], - "scopeType", - { - "argumentOrParameter": "Argument", - "boundedNonWhitespaceSequence": "Non whitespace sequence stopped by surrounding pair delimeters", - }, - ), - { - "id": "glyph", - "type": "scopeType", - "variations": [ - { - "spokenForm": f"{glyph_spoken_form} ", - "description": "Instance of single character ", - }, - ], - }, - ] diff --git a/cursorless-talon/src/cheatsheet/sections/special_marks.py b/cursorless-talon/src/cheatsheet/sections/special_marks.py deleted file mode 100644 index 41f2572c38..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/special_marks.py +++ /dev/null @@ -1,20 +0,0 @@ -from ..get_list import get_lists, get_raw_list, make_dict_readable - - -def get_special_marks(): - line_direction_marks = make_dict_readable( - "mark", - { - f"{key} ": value - for key, value in get_raw_list("line_direction").items() - }, - { - "lineNumberRelativeUp": "Line number up from cursor", - "lineNumberRelativeDown": "Line number down from cursor", - }, - ) - - return [ - *get_lists(["simple_mark", "unknown_symbol"], "mark"), - *line_direction_marks, - ] diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py deleted file mode 100644 index 5d7c94594d..0000000000 --- a/cursorless-talon/src/command.py +++ /dev/null @@ -1,103 +0,0 @@ -import dataclasses -from typing import Any - -from talon import Module, actions, speech_system - -from .fallback import perform_fallback -from .versions import COMMAND_VERSION - - -@dataclasses.dataclass -class CursorlessCommand: - version = COMMAND_VERSION - spokenForm: str - usePrePhraseSnapshot: bool - action: dict - - -CURSORLESS_COMMAND_ID = "cursorless.command" -last_phrase: dict = {} - -mod = Module() - - -def on_phrase(d): - global last_phrase - last_phrase = d - - -speech_system.register("pre:phrase", on_phrase) - - -@mod.action_class -class Actions: - def private_cursorless_command_and_wait(action: dict): # pyright: ignore [reportGeneralTypeIssues] - """Execute cursorless command and wait for it to finish""" - 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): # pyright: ignore [reportGeneralTypeIssues] - """Execute cursorless command without waiting""" - actions.user.private_cursorless_run_rpc_command_no_wait( - CURSORLESS_COMMAND_ID, - construct_cursorless_command(action), - ) - - def private_cursorless_command_get(action: dict): # pyright: ignore [reportGeneralTypeIssues] - """Execute cursorless command and return result""" - 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: - try: - use_pre_phrase_snapshot = actions.user.did_emit_pre_phrase_signal() - except KeyError: - use_pre_phrase_snapshot = False - - spoken_form = " ".join(last_phrase["phrase"]) - - return make_serializable( - CursorlessCommand( - spoken_form, - use_pre_phrase_snapshot, - action, - ) - ) - - -def make_serializable(value: Any) -> Any: - """ - Converts a dataclass into a serializable dict - - Note that we don't use the built-in asdict() function because it will - ignore the static `type` field. - - Args: - value (any): The value to convert - - Returns: - _type_: The converted value, ready for serialization - """ - if isinstance(value, dict): - return {k: make_serializable(v) for k, v in value.items()} - if isinstance(value, list): - return [make_serializable(v) for v in value] - if dataclasses.is_dataclass(value): - items = { - **{k: v for k, v in value.__class__.__dict__.items() if k[0] != "_"}, - **value.__dict__, - } - return {k: make_serializable(v) for k, v in items.items() if v is not None} - return value diff --git a/cursorless-talon/src/conventions.py b/cursorless-talon/src/conventions.py deleted file mode 100644 index a3495f1f2e..0000000000 --- a/cursorless-talon/src/conventions.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_cursorless_list_name(name: str): - return f"user.cursorless_{name}" diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py deleted file mode 100644 index f71b0d9097..0000000000 --- a/cursorless-talon/src/csv_overrides.py +++ /dev/null @@ -1,471 +0,0 @@ -import csv -import typing -from collections import defaultdict -from collections.abc import Container -from dataclasses import dataclass -from datetime import datetime -from pathlib import Path -from typing import Callable, Iterable, Optional, TypedDict - -from talon import Context, Module, actions, app, fs, settings - -from .conventions import get_cursorless_list_name -from .vendor.inflection import pluralize - -SPOKEN_FORM_HEADER = "Spoken form" -CURSORLESS_IDENTIFIER_HEADER = "Cursorless identifier" - - -mod = Module() -mod.tag( - "cursorless_default_vocabulary", - desc="Use default cursorless vocabulary instead of user custom", -) -mod.setting( - "cursorless_settings_directory", - type=str, - default="cursorless-settings", - desc="The directory to use for cursorless settings csvs relative to talon user directory", -) - -# The global context we use for our lists -ctx = Context() - -# A context that contains default vocabulary, for use in testing -normalized_ctx = Context() -normalized_ctx.matches = r""" -tag: user.cursorless_default_vocabulary -""" - - -# Maps from Talon list name to a map from spoken form to value -ListToSpokenForms = dict[str, dict[str, str]] - - -@dataclass -class SpokenFormEntry: - list_name: str - id: str - spoken_forms: list[str] - - -def init_csv_and_watch_changes( - filename: str, - default_values: ListToSpokenForms, - handle_new_values: Optional[Callable[[list[SpokenFormEntry]], None]] = None, - *, - extra_ignored_values: Optional[list[str]] = None, - extra_allowed_values: Optional[list[str]] = None, - allow_unknown_values: bool = False, - default_list_name: Optional[str] = None, - headers: list[str] = [SPOKEN_FORM_HEADER, CURSORLESS_IDENTIFIER_HEADER], - no_update_file: bool = False, - pluralize_lists: Optional[list[str]] = None, -): - """ - Initialize a cursorless settings csv, creating it if necessary, and watch - for changes to the csv. Talon lists will be generated based on the keys of - `default_values`. For example, if there is a key `foo`, there will be a - list created called `user.cursorless_foo` that will contain entries from the - original dict at the key `foo`, updated according to customization in the - csv at - - ``` - actions.path.talon_user() / "cursorless-settings" / filename - ``` - - Note that the settings directory location can be customized using the - `user.cursorless_settings_directory` setting. - - Args: - filename (str): The name of the csv file to be placed in - `cursorles-settings` dir - default_values (ListToSpokenForms): The default values for the lists to - be customized in the given csv - handle_new_values (Optional[Callable[[list[SpokenFormEntry]], None]]): A - callback to be called when the lists are updated - extra_ignored_values (Optional[list[str]]): Don't throw an exception if - any of these appear as values; just ignore them and don't add them - to any list - allow_unknown_values (bool): If unknown values appear, just put them in - the list - default_list_name (Optional[str]): If unknown values are - allowed, put any unknown values in this list - headers (list[str]): The headers to use for the csv - no_update_file (bool): Set this to `True` to indicate that we should not - update the csv. This is used generally in case there was an issue - coming up with the default set of values so we don't want to persist - those to disk - pluralize_lists (list[str]): Create plural version of given lists - """ - # Don't allow both `extra_allowed_values` and `allow_unknown_values` - assert not (extra_allowed_values and allow_unknown_values) - - # If `extra_allowed_values` or `allow_unknown_values` is given, we need a - # `default_list_name` to put unknown values in - assert not ( - (extra_allowed_values or allow_unknown_values) and not default_list_name - ) - - if extra_ignored_values is None: - extra_ignored_values = [] - if extra_allowed_values is None: - extra_allowed_values = [] - if pluralize_lists is None: - pluralize_lists = [] - - file_path = get_full_path(filename) - super_default_values = get_super_values(default_values) - - file_path.parent.mkdir(parents=True, exist_ok=True) - - check_for_duplicates(filename, default_values) - create_default_vocabulary_dicts(default_values, pluralize_lists) - - def on_watch(path, flags): - if file_path.match(path): - current_values, has_errors = read_file( - path=file_path, - headers=headers, - default_identifiers=super_default_values.values(), - extra_ignored_values=extra_ignored_values, - extra_allowed_values=extra_allowed_values, - allow_unknown_values=allow_unknown_values, - ) - update_dicts( - default_values=default_values, - current_values=current_values, - extra_ignored_values=extra_ignored_values, - extra_allowed_values=extra_allowed_values, - allow_unknown_values=allow_unknown_values, - default_list_name=default_list_name, - pluralize_lists=pluralize_lists, - handle_new_values=handle_new_values, - ) - - fs.watch(str(file_path.parent), on_watch) - - if file_path.is_file(): - current_values = update_file( - path=file_path, - headers=headers, - default_values=super_default_values, - extra_ignored_values=extra_ignored_values, - extra_allowed_values=extra_allowed_values, - allow_unknown_values=allow_unknown_values, - no_update_file=no_update_file, - ) - update_dicts( - default_values=default_values, - current_values=current_values, - extra_ignored_values=extra_ignored_values, - extra_allowed_values=extra_allowed_values, - allow_unknown_values=allow_unknown_values, - default_list_name=default_list_name, - pluralize_lists=pluralize_lists, - handle_new_values=handle_new_values, - ) - else: - if not no_update_file: - create_file(file_path, headers, super_default_values) - update_dicts( - default_values=default_values, - current_values=super_default_values, - extra_ignored_values=extra_ignored_values, - extra_allowed_values=extra_allowed_values, - allow_unknown_values=allow_unknown_values, - default_list_name=default_list_name, - pluralize_lists=pluralize_lists, - handle_new_values=handle_new_values, - ) - - def unsubscribe(): - fs.unwatch(str(file_path.parent), on_watch) - - return unsubscribe - - -def check_for_duplicates(filename, default_values): - results_map = {} - for list_name, dict in default_values.items(): - for key, value in dict.items(): - if value in results_map: - existing_list_name = results_map[value]["list"] - warning = f"WARNING ({filename}): Value `{value}` duplicated between lists '{existing_list_name}' and '{list_name}'" - print(warning) - app.notify(warning) - - -def is_removed(value: str): - return value.startswith("-") - - -def create_default_vocabulary_dicts( - default_values: dict[str, dict], pluralize_lists: list[str] -): - default_values_updated = {} - for key, value in default_values.items(): - updated_dict = {} - for key2, value2 in value.items(): - # Enable deactivated(prefixed with a `-`) items - active_key = key2[1:] if key2.startswith("-") else key2 - if active_key: - updated_dict[active_key] = value2 - default_values_updated[key] = updated_dict - assign_lists_to_context(normalized_ctx, default_values_updated, pluralize_lists) - - -def update_dicts( - default_values: ListToSpokenForms, - current_values: dict[str, str], - extra_ignored_values: list[str], - extra_allowed_values: list[str], - allow_unknown_values: bool, - default_list_name: Optional[str], - pluralize_lists: list[str], - handle_new_values: Optional[Callable[[list[SpokenFormEntry]], None]], -): - # Create map with all default values - results_map: dict[str, ResultsListEntry] = {} - for list_name, obj in default_values.items(): - for spoken, id in obj.items(): - results_map[id] = {"spoken": spoken, "id": id, "list": list_name} - - # Update result with current values - for spoken, id in current_values.items(): - try: - results_map[id]["spoken"] = spoken - except KeyError: - if id in extra_ignored_values: - pass - elif allow_unknown_values or id in extra_allowed_values: - assert default_list_name is not None - results_map[id] = { - "spoken": spoken, - "id": id, - "list": default_list_name, - } - else: - raise - - spoken_form_entries = list(generate_spoken_forms(results_map.values())) - - # Assign result to talon context list - lists: ListToSpokenForms = defaultdict(dict) - for entry in spoken_form_entries: - for spoken_form in entry.spoken_forms: - lists[entry.list_name][spoken_form] = entry.id - assign_lists_to_context(ctx, lists, pluralize_lists) - - if handle_new_values is not None: - handle_new_values(spoken_form_entries) - - -class ResultsListEntry(TypedDict): - spoken: str - id: str - list: str - - -def generate_spoken_forms(results_list: Iterable[ResultsListEntry]): - for obj in results_list: - id = obj["id"] - spoken = obj["spoken"] - - spoken_forms = [] - if not is_removed(spoken): - for k in spoken.split("|"): - if id == "pasteFromClipboard" and k.endswith(" to"): - # FIXME: This is a hack to work around the fact that the - # spoken form of the `pasteFromClipboard` action used to be - # "paste to", but now the spoken form is just "paste" and - # the "to" is part of the positional target. Users who had - # cursorless before this change would have "paste to" as - # their spoken form and so would need to say "paste to to". - k = k[:-3] - spoken_forms.append(k.strip()) - - yield SpokenFormEntry( - list_name=obj["list"], - id=id, - spoken_forms=spoken_forms, - ) - - -def assign_lists_to_context( - ctx: Context, - lists: ListToSpokenForms, - pluralize_lists: list[str], -): - for list_name, dict in lists.items(): - list_singular_name = get_cursorless_list_name(list_name) - ctx.lists[list_singular_name] = dict - if list_name in pluralize_lists: - list_plural_name = f"{list_singular_name}_plural" - ctx.lists[list_plural_name] = {pluralize(k): v for k, v in dict.items()} - - -def update_file( - path: Path, - headers: list[str], - default_values: dict[str, str], - extra_ignored_values: list[str], - extra_allowed_values: list[str], - allow_unknown_values: bool, - no_update_file: bool, -): - current_values, has_errors = read_file( - path=path, - headers=headers, - default_identifiers=default_values.values(), - extra_ignored_values=extra_ignored_values, - extra_allowed_values=extra_allowed_values, - allow_unknown_values=allow_unknown_values, - ) - current_identifiers = current_values.values() - - missing = {} - for key, value in default_values.items(): - if value not in current_identifiers: - missing[key] = value - - if missing: - if has_errors or no_update_file: - print( - "NOTICE: New cursorless features detected, but refusing to update " - "csv due to errors. Please fix csv errors above and restart talon" - ) - else: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - lines = [ - f"# {timestamp} - New entries automatically added by cursorless", - *[create_line(key, missing[key]) for key in sorted(missing)], - ] - with open(path, "a") as f: - f.write("\n\n" + "\n".join(lines)) - - print(f"New cursorless features added to {path.name}") - for key in sorted(missing): - print(f"{key}: {missing[key]}") - print( - "See release notes for more info: " - "https://github.com/cursorless-dev/cursorless/blob/main/CHANGELOG.md" - ) - app.notify("🎉🎉 New cursorless features; see log") - - return current_values - - -def create_line(*cells: str): - return ", ".join(cells) - - -def create_file(path: Path, headers: list[str], default_values: dict): - lines = [create_line(key, default_values[key]) for key in sorted(default_values)] - lines.insert(0, create_line(*headers)) - lines.append("") - path.write_text("\n".join(lines)) - - -def csv_error(path: Path, index: int, message: str, value: str): - """Check that an expected condition is true - - Note that we try to continue reading in this case so cursorless doesn't get bricked - - Args: - path (Path): The path of the CSV (for error reporting) - index (int): The index into the file (for error reporting) - text (str): The text of the error message to report if condition is false - """ - print(f"ERROR: {path}:{index+1}: {message} '{value}'") - - -def read_file( - path: Path, - headers: list[str], - default_identifiers: Container[str], - extra_ignored_values: list[str], - extra_allowed_values: list[str], - allow_unknown_values: bool, -): - with open(path) as csv_file: - # Use `skipinitialspace` to allow spaces before quote. `, "a,b"` - csv_reader = csv.reader(csv_file, skipinitialspace=True) - rows = list(csv_reader) - - result = {} - used_identifiers = [] - has_errors = False - seen_headers = False - - for i, row in enumerate(rows): - # Remove trailing whitespaces for each cell - row = [x.rstrip() for x in row] - # Exclude empty or comment rows - if len(row) == 0 or (len(row) == 1 and row[0] == "") or row[0].startswith("#"): - continue - - if not seen_headers: - seen_headers = True - if row != headers: - has_errors = True - csv_error(path, i, "Malformed header", create_line(*row)) - print(f"Expected '{create_line(*headers)}'") - continue - - if len(row) != len(headers): - has_errors = True - csv_error( - path, - i, - f"Malformed csv entry. Expected {len(headers)} columns.", - create_line(*row), - ) - continue - - key, value = row - - if ( - value not in default_identifiers - and value not in extra_ignored_values - and value not in extra_allowed_values - and not allow_unknown_values - ): - has_errors = True - csv_error(path, i, "Unknown identifier", value) - continue - - if value in used_identifiers: - has_errors = True - csv_error(path, i, "Duplicate identifier", value) - continue - - result[key] = value - used_identifiers.append(value) - - if has_errors: - app.notify("Cursorless settings error; see log") - - return result, has_errors - - -def get_full_path(filename: str): - if not filename.endswith(".csv"): - filename = f"{filename}.csv" - - user_dir: Path = actions.path.talon_user() - settings_directory = Path( - typing.cast(str, settings.get("user.cursorless_settings_directory")) - ) - - if not settings_directory.is_absolute(): - settings_directory = user_dir / settings_directory - - return (settings_directory / filename).resolve() - - -def get_super_values(values: ListToSpokenForms): - result: dict[str, str] = {} - for value_dict in values.values(): - result.update(value_dict) - return result diff --git a/cursorless-talon/src/cursorless.py b/cursorless-talon/src/cursorless.py deleted file mode 100644 index 9617f51593..0000000000 --- a/cursorless-talon/src/cursorless.py +++ /dev/null @@ -1,23 +0,0 @@ -from talon import Module, actions - -mod = Module() - -mod.tag( - "cursorless", - "Application supporting cursorless commands", -) - - -@mod.action_class -class Actions: - def private_cursorless_show_settings_in_ide(): - """Show Cursorless-specific settings in ide""" - - def private_cursorless_show_sidebar(): - """Show Cursorless-specific settings in ide""" - - def private_cursorless_show_command_statistics(): - """Show Cursorless command statistics""" - actions.user.private_cursorless_run_rpc_command_no_wait( - "cursorless.analyzeCommandHistory" - ) diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon deleted file mode 100644 index f1ab5aa206..0000000000 --- a/cursorless-talon/src/cursorless.talon +++ /dev/null @@ -1,39 +0,0 @@ -mode: command -mode: user.cursorless_spoken_form_test -tag: user.cursorless -- - - : - user.private_cursorless_action_or_ide_command(cursorless_action_or_ide_command, cursorless_target) - -{user.cursorless_bring_move_action} : - user.private_cursorless_bring_move(cursorless_bring_move_action, cursorless_bring_move_targets) - -{user.cursorless_swap_action} : - user.private_cursorless_swap(cursorless_swap_targets) - -{user.cursorless_paste_action} : - user.private_cursorless_paste(cursorless_destination) - -{user.cursorless_reformat_action} at : - user.private_cursorless_reformat(cursorless_target, formatters) - -{user.cursorless_call_action} on : - user.private_cursorless_call(cursorless_target_1, cursorless_target_2) - - {user.cursorless_wrap_action} : - user.private_cursorless_wrap_with_paired_delimiter(cursorless_wrap_action, cursorless_target, cursorless_wrapper_paired_delimiter) - -{user.cursorless_show_scope_visualizer} [{user.cursorless_visualization_type}]: - user.private_cursorless_show_scope_visualizer(cursorless_scope_type, cursorless_visualization_type or "content") -{user.cursorless_hide_scope_visualizer}: - user.private_cursorless_hide_scope_visualizer() - -{user.cursorless_homophone} settings: - user.private_cursorless_show_settings_in_ide() - -bar {user.cursorless_homophone}: - user.private_cursorless_show_sidebar() - -{user.cursorless_homophone} stats: - user.private_cursorless_show_command_statistics() diff --git a/cursorless-talon/src/cursorless_command_server.py b/cursorless-talon/src/cursorless_command_server.py deleted file mode 100644 index 78d30ad78d..0000000000 --- a/cursorless-talon/src/cursorless_command_server.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any - -from talon import Module, actions - -mod = Module() - - -@mod.action_class -class Actions: - def private_cursorless_run_rpc_command_and_wait( - command_id: str, # pyright: ignore [reportGeneralTypeIssues] - arg1: Any = None, - arg2: Any = None, - ): - """Execute command via rpc and wait for command to finish.""" - try: - actions.user.run_rpc_command_and_wait(command_id, arg1, arg2) - except KeyError: - actions.user.vscode_with_plugin_and_wait(command_id, arg1, arg2) - - def private_cursorless_run_rpc_command_no_wait( - command_id: str, # pyright: ignore [reportGeneralTypeIssues] - arg1: Any = None, - arg2: Any = None, - ): - """Execute command via rpc and DON'T wait.""" - try: - actions.user.run_rpc_command(command_id, arg1, arg2) - except KeyError: - actions.user.vscode_with_plugin(command_id, arg1, arg2) - - def private_cursorless_run_rpc_command_get( - command_id: str, # pyright: ignore [reportGeneralTypeIssues] - arg1: Any = None, - arg2: Any = None, - ) -> Any: - """Execute command via rpc and return command output.""" - try: - return actions.user.run_rpc_command_get(command_id, arg1, arg2) - except KeyError: - return actions.user.vscode_get(command_id, arg1, arg2) diff --git a/cursorless-talon/src/cursorless_global.talon b/cursorless-talon/src/cursorless_global.talon deleted file mode 100644 index 97675e61ea..0000000000 --- a/cursorless-talon/src/cursorless_global.talon +++ /dev/null @@ -1,4 +0,0 @@ -{user.cursorless_homophone} (reference | ref | cheatsheet | cheat sheet): - user.private_cursorless_cheat_sheet_show_html() -{user.cursorless_homophone} (instructions | docks | help) | help {user.cursorless_homophone}: - user.private_cursorless_open_instructions() diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py deleted file mode 100644 index 55d86ab8bd..0000000000 --- a/cursorless-talon/src/fallback.py +++ /dev/null @@ -1,107 +0,0 @@ -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}") diff --git a/cursorless-talon/src/marks/decorated_mark.py b/cursorless-talon/src/marks/decorated_mark.py deleted file mode 100644 index 3592d4f6cc..0000000000 --- a/cursorless-talon/src/marks/decorated_mark.py +++ /dev/null @@ -1,176 +0,0 @@ -from pathlib import Path -from typing import Any - -from talon import Module, actions, cron, fs - -from ..csv_overrides import init_csv_and_watch_changes -from .mark_types import DecoratedSymbol - -mod = Module() - -mod.list("cursorless_hat_color", desc="Supported hat colors for cursorless") -mod.list("cursorless_hat_shape", desc="Supported hat shapes for cursorless") -mod.list( - "cursorless_unknown_symbol", - "This list contains the term that is used to refer to any unknown symbol", -) - - -@mod.capture(rule=" | {user.cursorless_unknown_symbol}") -def cursorless_grapheme(m) -> str: - try: - return m.any_alphanumeric_key - except AttributeError: - # NB: This represents unknown char in Unicode. It will be translated - # to "[unk]" by Cursorless extension. - return "\ufffd" - - -@mod.capture( - rule="[{user.cursorless_hat_color}] [{user.cursorless_hat_shape}] " -) -def cursorless_decorated_symbol(m) -> DecoratedSymbol: - """A decorated symbol""" - hat_color: str = getattr(m, "cursorless_hat_color", "default") - try: - hat_style_name = f"{hat_color}-{m.cursorless_hat_shape}" - except AttributeError: - hat_style_name = hat_color - return { - "type": "decoratedSymbol", - "symbolColor": hat_style_name, - "character": m.cursorless_grapheme, - } - - -DEFAULT_COLOR_ENABLEMENT = { - "blue": True, - "green": True, - "red": True, - "pink": True, - "yellow": True, - "userColor1": False, - "userColor2": False, -} - -DEFAULT_SHAPE_ENABLEMENT = { - "ex": False, - "fox": False, - "wing": False, - "hole": False, - "frame": False, - "curve": False, - "eye": False, - "play": False, - "bolt": False, - "crosshairs": False, -} - -# Fall back to full enablement in case of error reading settings file -# NB: This won't actually enable all the shapes and colors extension-side. -# It'll just make it so that the user can say them whether or not they are enabled -FALLBACK_SHAPE_ENABLEMENT = { - "ex": True, - "fox": True, - "wing": True, - "hole": True, - "frame": True, - "curve": True, - "eye": True, - "play": True, - "bolt": True, - "crosshairs": True, -} -FALLBACK_COLOR_ENABLEMENT = DEFAULT_COLOR_ENABLEMENT - -unsubscribe_hat_styles: Any = None - - -def setup_hat_styles_csv(hat_colors: dict[str, str], hat_shapes: dict[str, str]): - global unsubscribe_hat_styles - - ( - color_enablement_settings, - is_color_error, - ) = actions.user.vscode_get_setting_with_fallback( - "cursorless.hatEnablement.colors", - default_value={}, - fallback_value=FALLBACK_COLOR_ENABLEMENT, - fallback_message="Error finding color enablement; falling back to full enablement", - ) - - ( - shape_enablement_settings, - is_shape_error, - ) = actions.user.vscode_get_setting_with_fallback( - "cursorless.hatEnablement.shapes", - default_value={}, - fallback_value=FALLBACK_SHAPE_ENABLEMENT, - fallback_message="Error finding shape enablement; falling back to full enablement", - ) - - color_enablement = { - **DEFAULT_COLOR_ENABLEMENT, - **color_enablement_settings, - } - shape_enablement = { - **DEFAULT_SHAPE_ENABLEMENT, - **shape_enablement_settings, - } - - active_hat_colors = { - spoken_form: value - for spoken_form, value in hat_colors.items() - if color_enablement[value] - } - active_hat_shapes = { - spoken_form: value - for spoken_form, value in hat_shapes.items() - if shape_enablement[value] - } - - if unsubscribe_hat_styles is not None: - unsubscribe_hat_styles() - - unsubscribe_hat_styles = init_csv_and_watch_changes( - "hat_styles.csv", - { - "hat_color": active_hat_colors, - "hat_shape": active_hat_shapes, - }, - extra_ignored_values=[*hat_colors.values(), *hat_shapes.values()], - no_update_file=is_shape_error or is_color_error, - ) - - if is_shape_error or is_color_error: - actions.app.notify("Error reading vscode settings. Restart talon; see log") - - -fast_reload_job = None -slow_reload_job = None - - -def init_hats(hat_colors: dict[str, str], hat_shapes: dict[str, str]): - setup_hat_styles_csv(hat_colors, hat_shapes) - - vscode_settings_path: Path = actions.user.vscode_settings_path().resolve() - - def on_watch(path, flags): - global fast_reload_job, slow_reload_job - cron.cancel(fast_reload_job) - cron.cancel(slow_reload_job) - fast_reload_job = cron.after( - "500ms", lambda: setup_hat_styles_csv(hat_colors, hat_shapes) - ) - slow_reload_job = cron.after( - "10s", lambda: setup_hat_styles_csv(hat_colors, hat_shapes) - ) - - fs.watch(str(vscode_settings_path), on_watch) - - def unsubscribe(): - fs.unwatch(str(vscode_settings_path), on_watch) - if unsubscribe_hat_styles is not None: - unsubscribe_hat_styles() - - return unsubscribe diff --git a/cursorless-talon/src/marks/lines_number.py b/cursorless-talon/src/marks/lines_number.py deleted file mode 100644 index a1ff66fe62..0000000000 --- a/cursorless-talon/src/marks/lines_number.py +++ /dev/null @@ -1,62 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass - -from talon import Module - -from ..targets.range_target import RangeConnective -from .mark_types import LineNumber, LineNumberMark, LineNumberType - -mod = Module() - -mod.list("cursorless_line_direction", desc="Supported directions for line modifier") - - -@dataclass -class CustomizableTerm: - cursorlessIdentifier: str - type: LineNumberType - formatter: Callable[[int], int] - - -# NOTE: Please do not change these dicts. Use the CSVs for customization. -# See https://www.cursorless.org/docs/user/customization/ -directions = [ - CustomizableTerm("lineNumberModulo100", "modulo100", lambda number: number - 1), - CustomizableTerm("lineNumberRelativeUp", "relative", lambda number: -number), - CustomizableTerm("lineNumberRelativeDown", "relative", lambda number: number), -] - -directions_map = {d.cursorlessIdentifier: d for d in directions} - - -@mod.capture( - rule=( - "{user.cursorless_line_direction} " - "[ ]" - ) -) -def cursorless_line_number(m) -> LineNumber: - direction = directions_map[m.cursorless_line_direction] - numbers: list[int] = m.private_cursorless_number_small_list - anchor = create_line_number_mark(direction.type, direction.formatter(numbers[0])) - if len(numbers) > 1: - active = create_line_number_mark( - direction.type, direction.formatter(numbers[1]) - ) - range_connective: RangeConnective = m.cursorless_range_connective - return { - "type": "range", - "anchor": anchor, - "active": active, - "excludeAnchor": range_connective.excludeAnchor, - "excludeActive": range_connective.excludeActive, - } - return anchor - - -def create_line_number_mark(type: LineNumberType, line_number: int) -> LineNumberMark: - return { - "type": "lineNumber", - "lineNumberType": type, - "lineNumber": line_number, - } diff --git a/cursorless-talon/src/marks/mark.py b/cursorless-talon/src/marks/mark.py deleted file mode 100644 index 18e21abb75..0000000000 --- a/cursorless-talon/src/marks/mark.py +++ /dev/null @@ -1,16 +0,0 @@ -from talon import Module - -from .mark_types import Mark - -mod = Module() - - -@mod.capture( - rule=( - " | " - " |" - "" # row (ie absolute mod 100), up, down - ) -) -def cursorless_mark(m) -> Mark: - return m[0] diff --git a/cursorless-talon/src/marks/mark_types.py b/cursorless-talon/src/marks/mark_types.py deleted file mode 100644 index 3985b7d4f5..0000000000 --- a/cursorless-talon/src/marks/mark_types.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Literal, TypedDict, Union - - -class DecoratedSymbol(TypedDict): - type: Literal["decoratedSymbol"] - symbolColor: str - character: str - - -SimpleMark = dict[Literal["type"], str] - -LineNumberType = Literal["modulo100", "relative"] - - -class LineNumberMark(TypedDict): - type: Literal["lineNumber"] - lineNumberType: LineNumberType - lineNumber: int - - -class LineNumberRange(TypedDict): - type: Literal["range"] - anchor: LineNumberMark - active: LineNumberMark - excludeAnchor: bool - excludeActive: bool - - -LineNumber = Union[LineNumberMark, LineNumberRange] - -Mark = Union[DecoratedSymbol, SimpleMark, LineNumber] diff --git a/cursorless-talon/src/marks/simple_mark.py b/cursorless-talon/src/marks/simple_mark.py deleted file mode 100644 index a98d3d84c9..0000000000 --- a/cursorless-talon/src/marks/simple_mark.py +++ /dev/null @@ -1,23 +0,0 @@ -from talon import Module - -from .mark_types import SimpleMark - -mod = Module() - -mod.list("cursorless_simple_mark", desc="Cursorless simple marks") - -# Maps from the id we use in the spoken form csv to the modifier type -# expected by Cursorless extension -simple_marks = { - "currentSelection": "cursor", - "previousTarget": "that", - "previousSource": "source", - "nothing": "nothing", -} - - -@mod.capture(rule="{user.cursorless_simple_mark}") -def cursorless_simple_mark(m) -> SimpleMark: - return { - "type": simple_marks[m.cursorless_simple_mark], - } diff --git a/cursorless-talon/src/modifiers/glyph_scope.py b/cursorless-talon/src/modifiers/glyph_scope.py deleted file mode 100644 index beff4c35ed..0000000000 --- a/cursorless-talon/src/modifiers/glyph_scope.py +++ /dev/null @@ -1,30 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list( - "cursorless_glyph_scope_type", - desc="Cursorless glyph scope type", -) -mod.list( - "cursorless_glyph_scope_type_plural", - desc="Plural version of Cursorless glyph scope type", -) - - -@mod.capture(rule="{user.cursorless_glyph_scope_type} ") -def cursorless_glyph_scope_type(m) -> dict[str, str]: - return { - "type": "glyph", - "character": m.any_alphanumeric_key, - } - - -@mod.capture( - rule="{user.cursorless_glyph_scope_type_plural} " -) -def cursorless_glyph_scope_type_plural(m) -> dict[str, str]: - return { - "type": "glyph", - "character": m.any_alphanumeric_key, - } diff --git a/cursorless-talon/src/modifiers/head_tail.py b/cursorless-talon/src/modifiers/head_tail.py deleted file mode 100644 index c6aa5f061e..0000000000 --- a/cursorless-talon/src/modifiers/head_tail.py +++ /dev/null @@ -1,39 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list( - "cursorless_head_tail_modifier", - desc="Cursorless head and tail modifiers", -) - - -@mod.capture( - rule=( - "{user.cursorless_head_tail_modifier} " - "[] " - "[]" - ) -) -def cursorless_head_tail_modifier(m) -> dict[str, str]: - """Cursorless head and tail modifier""" - modifiers = [] - - try: - modifiers.append(m.cursorless_interior_modifier) - except AttributeError: - pass - - try: - modifiers.append(m.cursorless_head_tail_swallowed_modifier) - except AttributeError: - pass - - result = { - "type": m.cursorless_head_tail_modifier, - } - - if modifiers: - result["modifiers"] = modifiers - - return result diff --git a/cursorless-talon/src/modifiers/interior.py b/cursorless-talon/src/modifiers/interior.py deleted file mode 100644 index 0ee0845cbe..0000000000 --- a/cursorless-talon/src/modifiers/interior.py +++ /dev/null @@ -1,16 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list( - "cursorless_interior_modifier", - desc="Cursorless interior modifier", -) - - -@mod.capture(rule="{user.cursorless_interior_modifier}") -def cursorless_interior_modifier(m) -> dict[str, str]: - """Cursorless interior modifier""" - return { - "type": m.cursorless_interior_modifier, - } diff --git a/cursorless-talon/src/modifiers/matching_pair_symbol.py b/cursorless-talon/src/modifiers/matching_pair_symbol.py deleted file mode 100644 index 6760e5663d..0000000000 --- a/cursorless-talon/src/modifiers/matching_pair_symbol.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Any - -from talon import Module - -mod = Module() - - -@mod.capture(rule="matching") -def cursorless_matching_paired_delimiter(m) -> dict[str, Any]: - return {"modifier": {"type": "matchingPairedDelimiter"}} diff --git a/cursorless-talon/src/modifiers/modifiers.py b/cursorless-talon/src/modifiers/modifiers.py deleted file mode 100644 index d63351ab37..0000000000 --- a/cursorless-talon/src/modifiers/modifiers.py +++ /dev/null @@ -1,50 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list( - "cursorless_simple_modifier", - desc="Simple cursorless modifiers that only need to specify their type", -) - - -@mod.capture(rule="{user.cursorless_simple_modifier}") -def cursorless_simple_modifier(m) -> dict[str, str]: - """Simple cursorless modifiers that only need to specify their type""" - return { - "type": m.cursorless_simple_modifier, - } - - -# These are the modifiers that will be "swallowed" by the head/tail modifier. -# For example, saying "head funk" will result in a "head" modifier that will -# select past the start of the function. -# Note that we don't include "inside" here, because that requires slightly -# special treatment to ensure that "head inside round" swallows "inside round" -# rather than just "inside". -head_tail_swallowed_modifiers = [ - "", # bounds, just, leading, trailing - "", # funk, state, class, every funk - "", # first past second word - "", # next funk, 3 funks - "", # matching/pair [curly, round] -] - -modifiers = [ - "", # inside - "", # head, tail - "", # start of, end of - *head_tail_swallowed_modifiers, -] - - -@mod.capture(rule="|".join(modifiers)) -def cursorless_modifier(m) -> str: - """Cursorless modifier""" - return m[0] - - -@mod.capture(rule="|".join(head_tail_swallowed_modifiers)) -def cursorless_head_tail_swallowed_modifier(m) -> str: - """Cursorless modifier that is swallowed by the head/tail modifier, excluding interior, which requires special treatment""" - return m[0] diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py deleted file mode 100644 index 0ff0ac3982..0000000000 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import Any - -from talon import Module - -from ..targets.range_target import RangeConnective - -mod = Module() - -mod.list("cursorless_first_modifier", desc="Cursorless first modifiers") -mod.list("cursorless_last_modifier", desc="Cursorless last modifiers") - - -@mod.capture( - rule=" | [] {user.cursorless_last_modifier}" -) -def ordinal_or_last(m) -> int: - """An ordinal or the word 'last'""" - if m[-1] == "last": - return -getattr(m, "ordinals_small", 1) - return m.ordinals_small - 1 - - -@mod.capture( - rule=" [ ] " -) -def cursorless_ordinal_range(m) -> dict[str, Any]: - """Ordinal range""" - anchor = create_ordinal_scope_modifier( - m.cursorless_scope_type, m.ordinal_or_last_list[0] - ) - if len(m.ordinal_or_last_list) > 1: - active = create_ordinal_scope_modifier( - m.cursorless_scope_type, m.ordinal_or_last_list[1] - ) - range_connective: RangeConnective = m.cursorless_range_connective - return { - "type": "range", - "anchor": anchor, - "active": active, - "excludeAnchor": range_connective.excludeAnchor, - "excludeActive": range_connective.excludeActive, - } - return anchor - - -@mod.capture( - rule=( - "[{user.cursorless_every_scope_modifier}] " - "({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " - " " - ), -) -def cursorless_first_last(m) -> dict[str, Any]: - """First/last `n` scopes; eg "first three funks""" - 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, - is_every, - ) - return create_ordinal_scope_modifier( - m.cursorless_scope_type_plural, - -m.private_cursorless_number_small, - m.private_cursorless_number_small, - is_every, - ) - - -@mod.capture(rule=" | ") -def cursorless_ordinal_scope(m) -> dict[str, Any]: - """Ordinal ranges such as subwords or characters""" - return m[0] - - -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 diff --git a/cursorless-talon/src/modifiers/position.py b/cursorless-talon/src/modifiers/position.py deleted file mode 100644 index 800ca1f25b..0000000000 --- a/cursorless-talon/src/modifiers/position.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any - -from talon import Module - -mod = Module() - -mod.list("cursorless_position", desc='Positions such as "before", "after" etc') - - -@mod.capture(rule="{user.cursorless_position}") -def cursorless_position_modifier(m) -> dict[str, Any]: - return {"type": "startOf" if m.cursorless_position == "start" else "endOf"} diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py deleted file mode 100644 index 1fd60ac693..0000000000 --- a/cursorless-talon/src/modifiers/relative_scope.py +++ /dev/null @@ -1,104 +0,0 @@ -from typing import Any - -from talon import Module - -mod = Module() - -mod.list("cursorless_previous_next_modifier", desc="Cursorless previous/next modifiers") -mod.list( - "cursorless_forward_backward_modifier", desc="Cursorless forward/backward modifiers" -) - - -@mod.capture(rule="{user.cursorless_previous_next_modifier}") -def cursorless_relative_direction(m) -> str: - """Previous/next""" - return "backward" if m[0] == "previous" else "forward" - - -@mod.capture( - rule="[] " -) -def cursorless_relative_scope_singular(m) -> dict[str, Any]: - """Relative previous/next singular scope, eg `"next funk"` or `"third next funk"`.""" - return create_relative_scope_modifier( - m.cursorless_scope_type, - getattr(m, "ordinals_small", 1), - 1, - m.cursorless_relative_direction, - False, - ) - - -@mod.capture( - rule="[{user.cursorless_every_scope_modifier}] " -) -def cursorless_relative_scope_plural(m) -> dict[str, Any]: - """Relative previous/next plural scope. `next three funks`""" - return create_relative_scope_modifier( - m.cursorless_scope_type_plural, - 1, - m.private_cursorless_number_small, - m.cursorless_relative_direction, - hasattr(m, "cursorless_every_scope_modifier"), - ) - - -@mod.capture( - rule="[{user.cursorless_every_scope_modifier}] [{user.cursorless_forward_backward_modifier}]" -) -def cursorless_relative_scope_count(m) -> dict[str, Any]: - """Relative count scope. `three funks`""" - return create_relative_scope_modifier( - m.cursorless_scope_type_plural, - 0, - m.private_cursorless_number_small, - getattr(m, "cursorless_forward_backward_modifier", "forward"), - hasattr(m, "cursorless_every_scope_modifier"), - ) - - -@mod.capture( - rule=" {user.cursorless_forward_backward_modifier}" -) -def cursorless_relative_scope_one_backward(m) -> dict[str, Any]: - """Take scope backward, eg `funk backward`""" - return create_relative_scope_modifier( - m.cursorless_scope_type, - 0, - 1, - m.cursorless_forward_backward_modifier, - False, - ) - - -@mod.capture( - rule=( - " | " - " | " - " | " - "" - ) -) -def cursorless_relative_scope(m) -> dict[str, Any]: - """Previous/next scope""" - return m[0] - - -def create_relative_scope_modifier( - scope_type: dict, - offset: int, - length: int, - direction: str, - is_every: bool, -) -> dict[str, Any]: - res = { - "type": "relativeScope", - "scopeType": scope_type, - "offset": offset, - "length": length, - "direction": direction, - } - if is_every: - res["isEvery"] = True - return res diff --git a/cursorless-talon/src/modifiers/scopes.py b/cursorless-talon/src/modifiers/scopes.py deleted file mode 100644 index ee1efb9b2c..0000000000 --- a/cursorless-talon/src/modifiers/scopes.py +++ /dev/null @@ -1,53 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list("cursorless_scope_type", desc="Supported scope types") -mod.list("cursorless_scope_type_plural", desc="Supported plural scope types") -mod.list( - "cursorless_custom_regex_scope_type", - desc="Supported custom regular expression scope types", -) -mod.list( - "cursorless_custom_regex_scope_type_plural", - desc="Supported plural custom regular expression scope types", -) - - -@mod.capture( - rule="{user.cursorless_scope_type} | | {user.cursorless_custom_regex_scope_type}" -) -def cursorless_scope_type(m) -> dict[str, str]: - """Cursorless scope type singular""" - try: - return {"type": m.cursorless_scope_type} - except AttributeError: - pass - - try: - return m.cursorless_glyph_scope_type - except AttributeError: - pass - - return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type} - - -@mod.capture( - rule="{user.cursorless_scope_type_plural} | | {user.cursorless_custom_regex_scope_type_plural}" -) -def cursorless_scope_type_plural(m) -> dict[str, str]: - """Cursorless scope type plural""" - try: - return {"type": m.cursorless_scope_type_plural} - except AttributeError: - pass - - try: - return m.cursorless_glyph_scope_type_plural - except AttributeError: - pass - - return { - "type": "customRegex", - "regex": m.cursorless_custom_regex_scope_type_plural, - } diff --git a/cursorless-talon/src/modifiers/simple_scope_modifier.py b/cursorless-talon/src/modifiers/simple_scope_modifier.py deleted file mode 100644 index cb0d2d4868..0000000000 --- a/cursorless-talon/src/modifiers/simple_scope_modifier.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any - -from talon import Module - -mod = Module() - -mod.list( - "cursorless_every_scope_modifier", - desc="Cursorless every scope modifiers", -) -mod.list( - "cursorless_ancestor_scope_modifier", - desc="Cursorless ancestor scope modifiers", -) - - -@mod.capture( - rule=( - "[{user.cursorless_every_scope_modifier} | {user.cursorless_ancestor_scope_modifier}] " - "" - ), -) -def cursorless_simple_scope_modifier(m) -> dict[str, Any]: - """Containing scope, every scope, etc""" - if hasattr(m, "cursorless_every_scope_modifier"): - return { - "type": "everyScope", - "scopeType": m.cursorless_scope_type, - } - - if hasattr(m, "cursorless_ancestor_scope_modifier"): - return { - "type": "containingScope", - "scopeType": m.cursorless_scope_type, - "ancestorIndex": 1, - } - - return { - "type": "containingScope", - "scopeType": m.cursorless_scope_type, - } diff --git a/cursorless-talon/src/modifiers/surrounding_pair.py b/cursorless-talon/src/modifiers/surrounding_pair.py deleted file mode 100644 index 72e1cbddf0..0000000000 --- a/cursorless-talon/src/modifiers/surrounding_pair.py +++ /dev/null @@ -1,62 +0,0 @@ -from contextlib import suppress -from typing import Any - -from talon import Context, Module - -mod = Module() -ctx = Context() - - -mod.list( - "cursorless_delimiter_force_direction", - desc="Can be used to force an ambiguous delimiter to extend in one direction", -) -# FIXME: Remove type ignore once Talon supports list types -# See https://github.com/talonvoice/talon/issues/654 -ctx.lists["user.cursorless_delimiter_force_direction"] = [ # pyright: ignore [reportArgumentType] - "left", - "right", -] - -mod.list( - "cursorless_surrounding_pair_scope_type", - desc="Scope types that can function as surrounding pairs", -) - - -@mod.capture( - rule=( - " |" - "{user.cursorless_surrounding_pair_scope_type}" - ) -) -def cursorless_surrounding_pair_scope_type(m) -> str: - """Surrounding pair scope type""" - try: - return m.cursorless_surrounding_pair_scope_type - except AttributeError: - return m.cursorless_selectable_paired_delimiter - - -@mod.capture( - rule="[{user.cursorless_delimiter_force_direction}] " -) -def cursorless_surrounding_pair(m) -> dict[str, Any]: - """Expand to containing surrounding pair""" - try: - surrounding_pair_scope_type = m.cursorless_surrounding_pair_scope_type - except AttributeError: - surrounding_pair_scope_type = "any" - - scope_type = { - "type": "surroundingPair", - "delimiter": surrounding_pair_scope_type, - } - - with suppress(AttributeError): - scope_type["forceDirection"] = m.cursorless_delimiter_force_direction - - return { - "type": "containingScope", - "scopeType": scope_type, - } diff --git a/cursorless-talon/src/number_small.py b/cursorless-talon/src/number_small.py deleted file mode 100644 index fd800ae232..0000000000 --- a/cursorless-talon/src/number_small.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -This file allows us to use a custom `number_small` capture. See #1021 for more -info. -""" - -from talon import Context, Module - -mod = Module() -mod.tag( - "cursorless_custom_number_small", - "This tag causes Cursorless to use the global capture", -) - -ctx = Context() -ctx.matches = """ -not tag: user.cursorless_custom_number_small -""" - - -@mod.capture(rule="") -def private_cursorless_number_small(m) -> int: - return m.number_small - - -digit_list = "zero one two three four five six seven eight nine".split() -teens = "ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen".split() -tens = "twenty thirty forty fifty sixty seventy eighty ninety".split() - -number_small_list = [*digit_list, *teens] -for ten in tens: - number_small_list.append(ten) - number_small_list.extend(f"{ten} {digit}" for digit in digit_list[1:]) -number_small_map = {n: i for i, n in enumerate(number_small_list)} - -mod.list("private_cursorless_number_small", desc="List of small numbers") -# FIXME: Remove type ignore once Talon supports list types -# See https://github.com/talonvoice/talon/issues/654 -ctx.lists["self.private_cursorless_number_small"] = number_small_map.keys() # pyright: ignore [reportArgumentType] - - -@ctx.capture( - "user.private_cursorless_number_small", - rule="{user.private_cursorless_number_small}", -) -def override_private_cursorless_number_small(m) -> int: - return number_small_map[m.private_cursorless_number_small] diff --git a/cursorless-talon/src/paired_delimiter.py b/cursorless-talon/src/paired_delimiter.py deleted file mode 100644 index 40126707f1..0000000000 --- a/cursorless-talon/src/paired_delimiter.py +++ /dev/null @@ -1,61 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list( - "cursorless_wrapper_only_paired_delimiter", - desc="A paired delimiter that can only be used as a wrapper", -) -mod.list( - "cursorless_selectable_only_paired_delimiter", - desc="A paired delimiter that can only be used as a scope type", -) -mod.list( - "cursorless_wrapper_selectable_paired_delimiter", - desc="A paired delimiter that can be used as a scope type and as a wrapper", -) - -# Maps from the id we use in the spoken form csv to the delimiter strings -paired_delimiters = { - "curlyBrackets": ["{", "}"], - "angleBrackets": ["<", ">"], - "escapedDoubleQuotes": ['\\"', '\\"'], - "escapedSingleQuotes": ["\\'", "\\'"], - "escapedParentheses": ["\\(", "\\)"], - "escapedSquareBrackets": ["\\[", "\\]"], - "doubleQuotes": ['"', '"'], - "parentheses": ["(", ")"], - "backtickQuotes": ["`", "`"], - "whitespace": [" ", " "], - "squareBrackets": ["[", "]"], - "singleQuotes": ["'", "'"], - "any": ["", ""], - "dollarSignAndCurlyBrackets": ["${", "}"], -} - - -@mod.capture( - rule=( - "{user.cursorless_wrapper_only_paired_delimiter} |" - "{user.cursorless_wrapper_selectable_paired_delimiter}" - ) -) -def cursorless_wrapper_paired_delimiter(m) -> list[str]: - try: - id = m.cursorless_wrapper_only_paired_delimiter - except AttributeError: - id = m.cursorless_wrapper_selectable_paired_delimiter - return paired_delimiters[id] - - -@mod.capture( - rule=( - "{user.cursorless_selectable_only_paired_delimiter} |" - "{user.cursorless_wrapper_selectable_paired_delimiter}" - ) -) -def cursorless_selectable_paired_delimiter(m) -> str: - try: - return m.cursorless_selectable_only_paired_delimiter - except AttributeError: - return m.cursorless_wrapper_selectable_paired_delimiter diff --git a/cursorless-talon/src/private_api/extract_decorated_marks.py b/cursorless-talon/src/private_api/extract_decorated_marks.py deleted file mode 100644 index 230e5d18c2..0000000000 --- a/cursorless-talon/src/private_api/extract_decorated_marks.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any - -from ..actions.bring_move import BringMoveTargets -from ..actions.swap import SwapTargets -from ..targets.target_types import ( - ImplicitDestination, - ImplicitTarget, - ListDestination, - ListTarget, - PrimitiveDestination, - PrimitiveTarget, - RangeTarget, -) - - -def extract_decorated_marks(capture: Any) -> list[Any]: - match capture: - case PrimitiveTarget(mark=mark): - if mark is None or mark["type"] != "decoratedSymbol": - return [] - return [mark] - case ImplicitTarget(): - return [] - case RangeTarget(anchor=anchor, active=active): - return extract_decorated_marks(anchor) + extract_decorated_marks(active) - case ListTarget(elements=elements): - return [ - mark for target in elements for mark in extract_decorated_marks(target) - ] - case PrimitiveDestination(target=target): - return extract_decorated_marks(target) - case ImplicitDestination(): - return [] - case ListDestination(destinations=destinations): - return [ - mark - for destination in destinations - for mark in extract_decorated_marks(destination) - ] - case BringMoveTargets(source=source, destination=destination): - return extract_decorated_marks(source) + extract_decorated_marks( - destination - ) - case SwapTargets(target1=target1, target2=target2): - return extract_decorated_marks(target1) + extract_decorated_marks(target2) - case _: - raise TypeError(f"Unknown capture type: {type(capture)}") diff --git a/cursorless-talon/src/private_api/private_api.py b/cursorless-talon/src/private_api/private_api.py deleted file mode 100644 index 16665942c2..0000000000 --- a/cursorless-talon/src/private_api/private_api.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Any, Optional, Union - -from talon import Module, actions - -from ..targets.target_types import ( - CursorlessTarget, - ListTarget, - PrimitiveTarget, - RangeTarget, -) -from .extract_decorated_marks import extract_decorated_marks - -mod = Module() - - -@mod.action_class -class MiscActions: - def cursorless_private_extract_decorated_marks(capture: Any) -> list[dict]: - """Cursorless private api: Extract all decorated marks from a Talon capture""" - return extract_decorated_marks(capture) - - -@mod.action_class -class TargetBuilderActions: - """Cursorless private api low-level target builder actions""" - - def cursorless_private_build_primitive_target( - modifiers: list[dict], # pyright: ignore [reportGeneralTypeIssues] - mark: Optional[dict], - ) -> PrimitiveTarget: - """Cursorless private api low-level target builder: Create a primitive target""" - return PrimitiveTarget(mark, modifiers) - - def cursorless_private_build_list_target( - elements: list[Union[PrimitiveTarget, RangeTarget]], # pyright: ignore [reportGeneralTypeIssues] - ) -> Union[PrimitiveTarget, RangeTarget, ListTarget]: - """Cursorless private api low-level target builder: Create a list target""" - if len(elements) == 1: - return elements[0] - - return ListTarget(elements) - - -@mod.action_class -class TargetActions: - def cursorless_private_target_nothing() -> PrimitiveTarget: - """Cursorless private api: Creates the "nothing" target""" - return PrimitiveTarget({"type": "nothing"}, []) - - -@mod.action_class -class ActionActions: - def cursorless_private_action_highlight( - target: CursorlessTarget, # pyright: ignore [reportGeneralTypeIssues] - highlightId: Optional[str] = None, - ) -> None: - """Cursorless private api: Highlights a target""" - payload = { - "name": "highlight", - "target": target, - } - - if highlightId is not None: - payload["highlightId"] = highlightId - - actions.user.private_cursorless_command_and_wait( - payload, - ) diff --git a/cursorless-talon/src/scope_visualizer.py b/cursorless-talon/src/scope_visualizer.py deleted file mode 100644 index 423796bacf..0000000000 --- a/cursorless-talon/src/scope_visualizer.py +++ /dev/null @@ -1,27 +0,0 @@ -from talon import Module, actions - -mod = Module() -mod.list("cursorless_show_scope_visualizer", desc="Show scope visualizer") -mod.list("cursorless_hide_scope_visualizer", desc="Hide scope visualizer") -mod.list( - "cursorless_visualization_type", - desc='Cursorless visualization type, e.g. "removal" or "iteration"', -) - - -@mod.action_class -class Actions: - def private_cursorless_show_scope_visualizer( - scope_type: dict, # pyright: ignore [reportGeneralTypeIssues] - visualization_type: str, - ): - """Shows scope visualizer""" - actions.user.private_cursorless_run_rpc_command_no_wait( - "cursorless.showScopeVisualizer", scope_type, visualization_type - ) - - def private_cursorless_hide_scope_visualizer(): - """Hides scope visualizer""" - actions.user.private_cursorless_run_rpc_command_no_wait( - "cursorless.hideScopeVisualizer" - ) diff --git a/cursorless-talon/src/snippet_cursorless.talon b/cursorless-talon/src/snippet_cursorless.talon deleted file mode 100644 index ffc28e20b4..0000000000 --- a/cursorless-talon/src/snippet_cursorless.talon +++ /dev/null @@ -1,14 +0,0 @@ -mode: command -mode: user.cursorless_spoken_form_test -tag: user.cursorless -and not tag: user.cursorless_use_community_snippets -- - -{user.cursorless_insert_snippet_action} : - user.private_cursorless_insert_snippet(cursorless_insertion_snippet) - -{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} [{user.cursorless_phrase_terminator}]: - user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text) - -{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} : - user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet) diff --git a/cursorless-talon/src/snippets.py b/cursorless-talon/src/snippets.py deleted file mode 100644 index 1a28fb1df8..0000000000 --- a/cursorless-talon/src/snippets.py +++ /dev/null @@ -1,224 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Optional, Union - -from talon import Module, actions - -from .targets.target_types import ( - CursorlessDestination, - CursorlessTarget, - ImplicitDestination, -) - - -@dataclass -class InsertionSnippet: - name: str - destination: CursorlessDestination - - -@dataclass -class CommunityInsertionSnippet: - body: str - scopes: list[str] | None = None - - -@dataclass -class CommunityWrapperSnippet: - body: str - variable_name: str - scope: str | None = None - - -mod = Module() - -mod.list("cursorless_insert_snippet_action", desc="Cursorless insert snippet action") - -# Deprecated tag; we should probably remove this and notify users that they -# should get rid of it, but I don't think it's worth the effort right now -mod.tag( - "cursorless_experimental_snippets", - desc="tag for enabling experimental snippet support", -) - -mod.tag( - "cursorless_use_community_snippets", - "If active use community snippets instead of Cursorless snippets", -) - -mod.list("cursorless_wrapper_snippet", desc="Cursorless wrapper snippet") -mod.list( - "cursorless_insertion_snippet_no_phrase", - desc="Cursorless insertion snippets that don't accept a phrase", -) -mod.list( - "cursorless_insertion_snippet_single_phrase", - desc="Cursorless insertion snippet that can accept a single phrase", -) -mod.list("cursorless_phrase_terminator", "Contains term used to terminate a phrase") - - -@mod.capture( - rule="({user.cursorless_insertion_snippet_no_phrase} | {user.cursorless_insertion_snippet_single_phrase}) []" -) -def cursorless_insertion_snippet(m) -> InsertionSnippet: - try: - name = m.cursorless_insertion_snippet_no_phrase - except AttributeError: - name = m.cursorless_insertion_snippet_single_phrase.split(".")[0] - - try: - destination = m.cursorless_destination - except AttributeError: - destination = ImplicitDestination() - - return InsertionSnippet(name, destination) - - -def wrap_with_snippet(snippet_description: dict, target: CursorlessTarget): - actions.user.private_cursorless_command_and_wait( - { - "name": "wrapWithSnippet", - "snippetDescription": snippet_description, - "target": target, - }, - ) - - -def insert_snippet(snippet_description: dict, destination: CursorlessDestination): - actions.user.private_cursorless_command_and_wait( - { - "name": "insertSnippet", - "snippetDescription": snippet_description, - "destination": destination, - }, - ) - - -def insert_named_snippet( - name: str, - destination: CursorlessDestination, - substitutions: Optional[dict] = None, -): - snippet: dict = { - "type": "named", - "name": name, - } - if substitutions is not None: - snippet["substitutions"] = substitutions - insert_snippet(snippet, destination) - - -def insert_custom_snippet( - body: str, - destination: CursorlessDestination, - scope_types: Optional[list[dict]] = None, -): - snippet: dict = { - "type": "custom", - "body": body, - } - - if scope_types: - snippet["scopeTypes"] = scope_types - - insert_snippet(snippet, destination) - - -@mod.action_class -class Actions: - def private_cursorless_insert_snippet(insertion_snippet: InsertionSnippet): # pyright: ignore [reportGeneralTypeIssues] - """Execute Cursorless insert snippet action""" - insert_named_snippet( - insertion_snippet.name, - insertion_snippet.destination, - ) - - def private_cursorless_insert_snippet_with_phrase( - snippet_description: str, # pyright: ignore [reportGeneralTypeIssues] - text: str, - ): - """Cursorless: Insert snippet with phrase """ - snippet_name, snippet_variable = snippet_description.split(".") - insert_named_snippet( - snippet_name, - ImplicitDestination(), - {snippet_variable: text}, - ) - - def cursorless_insert_snippet_by_name(name: str): # pyright: ignore [reportGeneralTypeIssues] - """Cursorless: Insert named snippet """ - insert_named_snippet( - name, - ImplicitDestination(), - ) - - def cursorless_insert_snippet( - body: str, # pyright: ignore [reportGeneralTypeIssues] - destination: CursorlessDestination = ImplicitDestination(), - scope_type: Optional[Union[str, list[str]]] = None, - ): - """Cursorless: Insert custom snippet """ - if isinstance(scope_type, str): - scope_type = [scope_type] - - if scope_type is not None: - scope_types = [{"type": st} for st in scope_type] - else: - scope_types = None - - insert_custom_snippet(body, destination, scope_types) - - def cursorless_wrap_with_snippet_by_name( - name: str, # pyright: ignore [reportGeneralTypeIssues] - variable_name: str, - target: CursorlessTarget, - ): - """Cursorless: Wrap target with a named snippet """ - wrap_with_snippet( - { - "type": "named", - "name": name, - "variableName": variable_name, - }, - target, - ) - - def cursorless_wrap_with_snippet( - body: str, # pyright: ignore [reportGeneralTypeIssues] - target: CursorlessTarget, - variable_name: Optional[str] = None, - scope: Optional[str] = None, - ): - """Cursorless: Wrap target with custom snippet """ - snippet_arg: dict[str, Any] = { - "type": "custom", - "body": body, - } - if scope is not None: - snippet_arg["scopeType"] = {"type": scope} - if variable_name is not None: - snippet_arg["variableName"] = variable_name - wrap_with_snippet( - snippet_arg, - target, - ) - - def private_cursorless_insert_community_snippet( - name: str, # pyright: ignore [reportGeneralTypeIssues] - destination: CursorlessDestination, - ): - """Cursorless: Insert community snippet """ - snippet: CommunityInsertionSnippet = actions.user.get_insertion_snippet(name) - actions.user.cursorless_insert_snippet( - snippet.body, destination, snippet.scopes - ) - - def private_cursorless_wrap_with_community_snippet( - name: str, # pyright: ignore [reportGeneralTypeIssues] - target: CursorlessTarget, - ): - """Cursorless: Wrap target with community snippet """ - snippet: CommunityWrapperSnippet = actions.user.get_wrapper_snippet(name) - actions.user.cursorless_wrap_with_snippet( - snippet.body, target, snippet.variable_name, snippet.scope - ) diff --git a/cursorless-talon/src/snippets_community.talon b/cursorless-talon/src/snippets_community.talon deleted file mode 100644 index f94e491f83..0000000000 --- a/cursorless-talon/src/snippets_community.talon +++ /dev/null @@ -1,13 +0,0 @@ -mode: command -mode: user.cursorless_spoken_form_test -tag: user.cursorless -and tag: user.cursorless_use_community_snippets -- - -# These snippets are defined in community - -{user.cursorless_insert_snippet_action} {user.snippet} : - user.private_cursorless_insert_community_snippet(snippet, cursorless_destination) - -{user.snippet_wrapper} {user.cursorless_wrap_action} : - user.private_cursorless_wrap_with_community_snippet(snippet_wrapper, cursorless_target) diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json deleted file mode 100644 index 4322a3e6ff..0000000000 --- a/cursorless-talon/src/spoken_forms.json +++ /dev/null @@ -1,277 +0,0 @@ -{ - "NOTE FOR USERS": "Please don't edit this json file; see https://www.cursorless.org/docs/user/customization", - "actions.csv": { - "simple_action": { - "bottom": "scrollToBottom", - "break": "breakLine", - "break point": "toggleLineBreakpoint", - "carve": "cutToClipboard", - "center": "scrollToCenter", - "change": "clearAndSetSelection", - "chuck": "remove", - "clone up": "insertCopyBefore", - "clone": "insertCopyAfter", - "comment": "toggleLineComment", - "copy": "copyToClipboard", - "crown": "scrollToTop", - "decrement": "decrement", - "dedent": "outdentLine", - "define": "revealDefinition", - "drink": "editNewLineBefore", - "drop": "insertEmptyLineBefore", - "extract": "extractVariable", - "float": "insertEmptyLineAfter", - "fold": "foldRegion", - "follow": "followLink", - "give": "deselect", - "highlight": "highlight", - "hover": "showHover", - "increment": "increment", - "indent": "indentLine", - "inspect": "showDebugHover", - "join": "joinLines", - "post": "setSelectionAfter", - "pour": "editNewLineAfter", - "pre": "setSelectionBefore", - "puff": "insertEmptyLinesAround", - "quick fix": "showQuickFix", - "reference": "showReferences", - "rename": "rename", - "reverse": "reverseTargets", - "scout": "findInDocument", - "scout all": "findInWorkspace", - "shuffle": "randomizeTargets", - "snippet make": "generateSnippet", - "sort": "sortTargets", - "take": "setSelection", - "type deaf": "revealTypeDefinition", - "unfold": "unfoldRegion" - }, - "callback_action": { - "phones": "nextHomophone" - }, - "paste_action": { "paste": "pasteFromClipboard" }, - "bring_move_action": { - "bring": "replaceWithTarget", - "move": "moveToTarget" - }, - "swap_action": { "swap": "swapTargets" }, - "wrap_action": { "wrap": "wrapWithPairedDelimiter", "repack": "rewrap" }, - "insert_snippet_action": { "snippet": "insertSnippet" }, - "reformat_action": { "format": "applyFormatter" }, - "call_action": { "call": "callAsFunction" } - }, - "target_connectives.csv": { - "range_connective": { - "between": "rangeExclusive", - "past": "rangeInclusive", - "-": "rangeExcludingStart", - "until": "rangeExcludingEnd" - }, - "list_connective": { "and": "listConnective" }, - "swap_connective": { "with": "swapConnective" }, - "insertion_mode_to": { "to": "sourceDestinationConnective" } - }, - "modifiers.csv": { - "simple_modifier": { - "bounds": "excludeInterior", - "just": "toRawSelection", - "leading": "leading", - "trailing": "trailing", - "content": "keepContentFilter", - "empty": "keepEmptyFilter", - "its": "inferPreviousMark", - "visible": "visible" - }, - "every_scope_modifier": { "every": "every" }, - "ancestor_scope_modifier": { "grand": "ancestor" }, - "interior_modifier": { - "inside": "interiorOnly" - }, - "head_tail_modifier": { - "head": "extendThroughStartOf", - "tail": "extendThroughEndOf" - }, - "range_type": { - "slice": "verticalRange" - }, - "first_modifier": { "first": "first" }, - "last_modifier": { "last": "last" }, - "previous_next_modifier": { "previous": "previous", "next": "next" }, - "forward_backward_modifier": { - "forward": "forward", - "backward": "backward" - } - }, - "positions.csv": { - "position": { - "start of": "start", - "end of": "end" - }, - "insertion_mode_before_after": { - "before": "before", - "after": "after" - } - }, - "modifier_scope_types.csv": { - "scope_type": { - "arg": "argumentOrParameter", - "attribute": "attribute", - "call": "functionCall", - "callee": "functionCallee", - "class name": "className", - "class": "class", - "comment": "comment", - "funk name": "functionName", - "funk": "namedFunction", - "if state": "ifStatement", - "instance": "instance", - "item": "collectionItem", - "key": "collectionKey", - "lambda": "anonymousFunction", - "list": "list", - "map": "map", - "name": "name", - "regex": "regularExpression", - "section": "section", - "-one section": "sectionLevelOne", - "-two section": "sectionLevelTwo", - "-three section": "sectionLevelThree", - "-four section": "sectionLevelFour", - "-five section": "sectionLevelFive", - "-six section": "sectionLevelSix", - "selector": "selector", - "state": "statement", - "branch": "branch", - "type": "type", - "value": "value", - "condition": "condition", - "unit": "unit", - "element": "xmlElement", - "tags": "xmlBothTags", - "start tag": "xmlStartTag", - "end tag": "xmlEndTag", - "part": "part", - "chapter": "chapter", - "subsection": "subSection", - "subsubsection": "subSubSection", - "paragraph": "namedParagraph", - "subparagraph": "subParagraph", - "environment": "environment", - "command": "command", - "char": "character", - "sub": "word", - "token": "token", - "identifier": "identifier", - "line": "line", - "sentence": "sentence", - "block": "paragraph", - "file": "document", - "paint": "nonWhitespaceSequence", - "short paint": "boundedNonWhitespaceSequence", - "link": "url", - "cell": "notebookCell" - }, - "surrounding_pair_scope_type": { - "string": "string" - }, - "glyph_scope_type": { - "glyph": "glyph" - } - }, - "paired_delimiters.csv": { - "selectable_only_paired_delimiter": { "pair": "any" }, - "wrapper_only_paired_delimiter": { "void": "whitespace" }, - "wrapper_selectable_paired_delimiter": { - "curly": "curlyBrackets", - "diamond": "angleBrackets", - "escaped quad": "escapedDoubleQuotes", - "escaped twin": "escapedSingleQuotes", - "escaped round": "escapedParentheses", - "escaped box": "escapedSquareBrackets", - "quad": "doubleQuotes", - "round": "parentheses", - "skis": "backtickQuotes", - "box": "squareBrackets", - "twin": "singleQuotes" - } - }, - "special_marks.csv": { - "simple_mark": { - "this": "currentSelection", - "that": "previousTarget", - "source": "previousSource", - "nothing": "nothing" - }, - "unknown_symbol": { "special": "unknownSymbol" }, - "line_direction": { - "row": "lineNumberModulo100", - "up": "lineNumberRelativeUp", - "down": "lineNumberRelativeDown" - } - }, - "scope_visualizer.csv": { - "show_scope_visualizer": { "visualize": "showScopeVisualizer" }, - "hide_scope_visualizer": { "visualize nothing": "hideScopeVisualizer" }, - "visualization_type": { - "removal": "removal", - "iteration": "iteration" - } - }, - "experimental/experimental_actions.csv": { - "experimental_action": { - "-from": "experimental.setInstanceReference" - } - }, - "experimental/wrapper_snippets.csv": { - "wrapper_snippet": { - "else": "ifElseStatement.alternative", - "funk": "functionDeclaration.body", - "if else": "ifElseStatement.consequence", - "if": "ifStatement.consequence", - "try": "tryCatchStatement.body", - "link": "link.text" - } - }, - "experimental/insertion_snippets.csv": { - "insertion_snippet_no_phrase": { - "if": "ifStatement", - "if else": "ifElseStatement", - "try": "tryCatchStatement" - } - }, - "experimental/insertion_snippets_single_phrase.csv": { - "insertion_snippet_single_phrase": { - "funk": "functionDeclaration.name", - "link": "link.text" - } - }, - "experimental/miscellaneous.csv": { - "phrase_terminator": { "over": "phraseTerminator" } - }, - "experimental/actions_custom.csv": {}, - "experimental/regex_scope_types.csv": {}, - "hat_styles.csv": { - "hat_color": { - "blue": "blue", - "green": "green", - "red": "red", - "pink": "pink", - "yellow": "yellow", - "navy": "userColor1", - "apricot": "userColor2" - }, - "hat_shape": { - "ex": "ex", - "fox": "fox", - "wing": "wing", - "hole": "hole", - "frame": "frame", - "curve": "curve", - "eye": "eye", - "play": "play", - "cross": "crosshairs", - "bolt": "bolt" - } - } -} diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py deleted file mode 100644 index 30ad16498d..0000000000 --- a/cursorless-talon/src/spoken_forms.py +++ /dev/null @@ -1,185 +0,0 @@ -import json -from pathlib import Path -from typing import Callable, Concatenate, ParamSpec, TypeVar - -from talon import app, fs - -from .csv_overrides import ( - SPOKEN_FORM_HEADER, - ListToSpokenForms, - SpokenFormEntry, - init_csv_and_watch_changes, -) -from .marks.decorated_mark import init_hats -from .spoken_forms_output import SpokenFormsOutput - -JSON_FILE = Path(__file__).parent / "spoken_forms.json" -disposables: list[Callable] = [] - - -P = ParamSpec("P") -R = TypeVar("R") - - -def auto_construct_defaults( - spoken_forms: dict[str, ListToSpokenForms], - handle_new_values: Callable[[str, list[SpokenFormEntry]], None], - f: Callable[ - Concatenate[str, ListToSpokenForms, Callable[[list[SpokenFormEntry]], None], P], - R, - ], -): - """ - Decorator that automatically constructs the default values for the - `default_values` parameter of `f` based on the spoken forms in - `spoken_forms`, by extracting the value at the key given by the csv - filename. - - Note that we only ever pass `init_csv_and_watch_changes` as `f`. The - reason we have this decorator is so that we can destructure the kwargs - of `init_csv_and_watch_changes` to remove the `default_values` parameter. - - Args: - spoken_forms (dict[str, ListToSpokenForms]): The spoken forms - handle_new_values (Callable[[ListToSpokenForms], None]): A callback to be called when the lists are updated - f (Callable[Concatenate[str, ListToSpokenForms, P], R]): Will always be `init_csv_and_watch_changes` - """ - - def ret(filename: str, *args: P.args, **kwargs: P.kwargs) -> R: - default_values = spoken_forms[filename] - return f( - filename, - default_values, - lambda new_values: handle_new_values(filename, new_values), - *args, - **kwargs, - ) - - return ret - - -# Maps from Talon list name to the type of the value in that list, e.g. -# `pairedDelimiter` or `simpleScopeTypeType` -# FIXME: This is a hack until we generate spoken_forms.json from Typescript side -# At that point we can just include its type as part of that file -LIST_TO_TYPE_MAP = { - "wrapper_selectable_paired_delimiter": "pairedDelimiter", - "selectable_only_paired_delimiter": "pairedDelimiter", - "wrapper_only_paired_delimiter": "pairedDelimiter", - "surrounding_pair_scope_type": "pairedDelimiter", - "scope_type": "simpleScopeTypeType", - "glyph_scope_type": "complexScopeTypeType", - "custom_regex_scope_type": "customRegex", -} - - -def update(): - global disposables - - for disposable in disposables: - disposable() - - with open(JSON_FILE, encoding="utf-8") as file: - spoken_forms = json.load(file) - - initialized = False - - # Maps from csv name to list of SpokenFormEntry - custom_spoken_forms: dict[str, list[SpokenFormEntry]] = {} - spoken_forms_output = SpokenFormsOutput() - spoken_forms_output.init() - - def update_spoken_forms_output(): - spoken_forms_output.write( - [ - { - "type": LIST_TO_TYPE_MAP[entry.list_name], - "id": entry.id, - "spokenForms": entry.spoken_forms, - } - for spoken_form_list in custom_spoken_forms.values() - for entry in spoken_form_list - if entry.list_name in LIST_TO_TYPE_MAP - ] - ) - - def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): - custom_spoken_forms[csv_name] = values - if initialized: - # On first run, we just do one update at the end, so we suppress - # writing until we get there - update_spoken_forms_output() - - handle_csv = auto_construct_defaults( - spoken_forms, handle_new_values, init_csv_and_watch_changes - ) - - disposables = [ - handle_csv("actions.csv"), - handle_csv("target_connectives.csv"), - handle_csv("modifiers.csv"), - handle_csv("positions.csv"), - handle_csv("paired_delimiters.csv"), - handle_csv("special_marks.csv"), - handle_csv("scope_visualizer.csv"), - handle_csv("experimental/experimental_actions.csv"), - handle_csv("experimental/miscellaneous.csv"), - handle_csv( - "modifier_scope_types.csv", - pluralize_lists=["scope_type", "glyph_scope_type"], - extra_allowed_values=[ - "private.fieldAccess", - "private.switchStatementSubject", - ], - default_list_name="scope_type", - ), - handle_csv( - "experimental/wrapper_snippets.csv", - allow_unknown_values=True, - default_list_name="wrapper_snippet", - ), - handle_csv( - "experimental/insertion_snippets.csv", - allow_unknown_values=True, - default_list_name="insertion_snippet_no_phrase", - ), - handle_csv( - "experimental/insertion_snippets_single_phrase.csv", - allow_unknown_values=True, - default_list_name="insertion_snippet_single_phrase", - ), - handle_csv( - "experimental/actions_custom.csv", - headers=[SPOKEN_FORM_HEADER, "VSCode command"], - allow_unknown_values=True, - default_list_name="custom_action", - ), - handle_csv( - "experimental/regex_scope_types.csv", - headers=[SPOKEN_FORM_HEADER, "Regex"], - allow_unknown_values=True, - default_list_name="custom_regex_scope_type", - pluralize_lists=["custom_regex_scope_type"], - ), - init_hats( - spoken_forms["hat_styles.csv"]["hat_color"], - spoken_forms["hat_styles.csv"]["hat_shape"], - ), - ] - - update_spoken_forms_output() - initialized = True - - -def on_watch(path, flags): - if JSON_FILE.match(path): - update() - - -def on_ready(): - update() - - fs.watch(str(JSON_FILE.parent), on_watch) - - -app.register("ready", on_ready) diff --git a/cursorless-talon/src/spoken_forms_output.py b/cursorless-talon/src/spoken_forms_output.py deleted file mode 100644 index 20f1f8623b..0000000000 --- a/cursorless-talon/src/spoken_forms_output.py +++ /dev/null @@ -1,48 +0,0 @@ -import json -from pathlib import Path -from typing import TypedDict - -from talon import app - -SPOKEN_FORMS_OUTPUT_PATH = Path.home() / ".cursorless" / "state.json" -STATE_JSON_VERSION_NUMBER = 0 - - -class SpokenFormEntry(TypedDict): - type: str - id: str - spokenForms: list[str] - - -class SpokenFormsOutput: - """ - Writes spoken forms to a json file for use by the Cursorless vscode extension - """ - - def init(self): - try: - SPOKEN_FORMS_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) - except Exception: - error_message = ( - f"Error creating spoken form dir {SPOKEN_FORMS_OUTPUT_PATH.parent}" - ) - print(error_message) - app.notify(error_message) - - def write(self, spoken_forms: list[SpokenFormEntry]): - with open(SPOKEN_FORMS_OUTPUT_PATH, "w", encoding="UTF-8") as out: - try: - out.write( - json.dumps( - { - "version": STATE_JSON_VERSION_NUMBER, - "spokenForms": spoken_forms, - } - ) - ) - except Exception: - error_message = ( - f"Error writing spoken form json {SPOKEN_FORMS_OUTPUT_PATH}" - ) - print(error_message) - app.notify(error_message) diff --git a/cursorless-talon/src/targets/destination.py b/cursorless-talon/src/targets/destination.py deleted file mode 100644 index 85ef3a517c..0000000000 --- a/cursorless-talon/src/targets/destination.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Union - -from talon import Context, Module, actions - -from .target_types import ListDestination, PrimitiveDestination - -mod = Module() - -mod.list( - "cursorless_insertion_mode_before_after", - desc="Cursorless insertion mode before/after", -) -mod.list("cursorless_insertion_mode_to", desc="Cursorless insertion mode to") -mod.tag( - "cursorless_disable_legacy_destination", - desc="Disabled the Cursorless legacy destination(to after) support", -) - -ctx = Context() -ctx.matches = r""" -tag: user.cursorless_disable_legacy_destination -""" - - -# DEPRECATED @ 2023-08-01 -@mod.capture( - rule="([{user.cursorless_insertion_mode_to}] {user.cursorless_insertion_mode_before_after}) | {user.cursorless_insertion_mode_to}" -) -def cursorless_insertion_mode(m) -> str: - try: - before_after = m.cursorless_insertion_mode_before_after - if hasattr(m, "cursorless_insertion_mode_to"): - words = m._unmapped - actions.app.notify( - f"'{' '.join(words)}' is deprecated. Please just say '{words[-1]}'" - ) - return before_after - except AttributeError: - return "to" - - -@ctx.capture( - "user.cursorless_insertion_mode", - rule="{user.cursorless_insertion_mode_before_after} | {user.cursorless_insertion_mode_to}", -) -def cursorless_insertion_mode_ctx(m) -> str: - try: - return m.cursorless_insertion_mode_before_after - except AttributeError: - return "to" - - -@mod.capture( - rule=( - " " - "({user.cursorless_list_connective} )*" - ) -) -def cursorless_destination(m) -> Union[ListDestination, PrimitiveDestination]: - destinations = [ - PrimitiveDestination(insertion_mode, target) - for insertion_mode, target in zip( - m.cursorless_insertion_mode_list, m.cursorless_target_list - ) - ] - - if len(destinations) == 1: - return destinations[0] - - return ListDestination(destinations) diff --git a/cursorless-talon/src/targets/primitive_target.py b/cursorless-talon/src/targets/primitive_target.py deleted file mode 100644 index 0051a69e48..0000000000 --- a/cursorless-talon/src/targets/primitive_target.py +++ /dev/null @@ -1,17 +0,0 @@ -from talon import Module - -from .target_types import PrimitiveTarget - -mod = Module() - - -@mod.capture( - rule=( - "+ [] | " - ) -) -def cursorless_primitive_target(m) -> PrimitiveTarget: - return PrimitiveTarget( - getattr(m, "cursorless_mark", None), - getattr(m, "cursorless_modifier_list", None), - ) diff --git a/cursorless-talon/src/targets/range_target.py b/cursorless-talon/src/targets/range_target.py deleted file mode 100644 index 37dbab69dc..0000000000 --- a/cursorless-talon/src/targets/range_target.py +++ /dev/null @@ -1,66 +0,0 @@ -from dataclasses import dataclass -from typing import Optional - -from talon import Module - -from .target_types import ImplicitTarget, PrimitiveTarget, RangeTarget, RangeTargetType - -mod = Module() - -mod.list( - "cursorless_range_connective", - desc="A range joiner that indicates whether to include or exclude anchor and active", -) - - -@dataclass -class RangeConnective: - excludeAnchor: bool - excludeActive: bool - - -@dataclass -class RangeConnectiveWithType: - connective: RangeConnective - type: Optional[RangeTargetType] - - -@mod.capture(rule="{user.cursorless_range_connective}") -def cursorless_range_connective(m) -> RangeConnective: - return RangeConnective( - m.cursorless_range_connective in ["rangeExclusive", "rangeExcludingStart"], - m.cursorless_range_connective in ["rangeExclusive", "rangeExcludingEnd"], - ) - - -@mod.capture( - rule="[] | " -) -def cursorless_range_connective_with_type(m) -> RangeConnectiveWithType: - return RangeConnectiveWithType( - getattr(m, "cursorless_range_connective", RangeConnective(False, False)), - getattr(m, "cursorless_range_type", None), - ) - - -@mod.capture( - rule=( - "[] " - ) -) -def cursorless_range_target(m) -> RangeTarget: - primitive_targets: list[PrimitiveTarget] = m.cursorless_primitive_target_list - range_connective_with_type: RangeConnectiveWithType = ( - m.cursorless_range_connective_with_type - ) - range_connective = range_connective_with_type.connective - - anchor = ImplicitTarget() if len(primitive_targets) == 1 else primitive_targets[0] - - return RangeTarget( - anchor, - primitive_targets[-1], - range_connective.excludeAnchor, - range_connective.excludeActive, - range_connective_with_type.type, - ) diff --git a/cursorless-talon/src/targets/range_type.py b/cursorless-talon/src/targets/range_type.py deleted file mode 100644 index a6f3c21a33..0000000000 --- a/cursorless-talon/src/targets/range_type.py +++ /dev/null @@ -1,20 +0,0 @@ -from talon import Module - -mod = Module() - -mod.list( - "cursorless_range_type", - desc="A range modifier that indicates the specific type of the range", -) - -# Maps from the id we use in the spoken form csv to the modifier type -# expected by Cursorless extension -range_type_map = { - "verticalRange": "vertical", -} - - -@mod.capture(rule="{user.cursorless_range_type}") -def cursorless_range_type(m) -> str: - """Range type modifier""" - return range_type_map[m.cursorless_range_type] diff --git a/cursorless-talon/src/targets/target.py b/cursorless-talon/src/targets/target.py deleted file mode 100644 index 417d8c1c4b..0000000000 --- a/cursorless-talon/src/targets/target.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Union - -from talon import Module - -from .target_types import ListTarget, PrimitiveTarget, RangeTarget - -mod = Module() - - -mod.list( - "cursorless_list_connective", - desc="A list joiner", -) - - -@mod.capture( - rule=(" | ") -) -def cursorless_primitive_or_range_target(m) -> Union[RangeTarget, PrimitiveTarget]: - return m[0] - - -@mod.capture( - rule=( - " " - "({user.cursorless_list_connective} )*" - ) -) -def cursorless_target(m) -> Union[ListTarget, RangeTarget, PrimitiveTarget]: - targets = m.cursorless_primitive_or_range_target_list - - if len(targets) == 1: - return targets[0] - - return ListTarget(targets) diff --git a/cursorless-talon/src/targets/target_types.py b/cursorless-talon/src/targets/target_types.py deleted file mode 100644 index 05800e52d2..0000000000 --- a/cursorless-talon/src/targets/target_types.py +++ /dev/null @@ -1,72 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Literal, Optional, Union - -from ..marks.mark_types import Mark - -RangeTargetType = Literal["vertical"] - - -@dataclass -class PrimitiveTarget: - type = "primitive" - mark: Optional[Mark] - modifiers: Optional[list[dict[str, Any]]] - - -@dataclass -class ImplicitTarget: - type = "implicit" - - -@dataclass -class RangeTarget: - type = "range" - anchor: Union[PrimitiveTarget, ImplicitTarget] - active: PrimitiveTarget - excludeAnchor: bool - excludeActive: bool - rangeType: Optional[RangeTargetType] - - -@dataclass -class ListTarget: - type = "list" - elements: list[Union[PrimitiveTarget, RangeTarget]] - - -CursorlessTarget = Union[ - ListTarget, - RangeTarget, - PrimitiveTarget, - ImplicitTarget, -] -CursorlessExplicitTarget = Union[ - ListTarget, - RangeTarget, - PrimitiveTarget, -] - - -@dataclass -class PrimitiveDestination: - type = "primitive" - insertionMode: Literal["to", "before", "after"] - target: Union[ListTarget, RangeTarget, PrimitiveTarget] - - -@dataclass -class ImplicitDestination: - type = "implicit" - - -@dataclass -class ListDestination: - type = "list" - destinations: list[PrimitiveDestination] - - -CursorlessDestination = Union[ - ListDestination, - PrimitiveDestination, - ImplicitDestination, -] diff --git a/cursorless-talon/src/terms.py b/cursorless-talon/src/terms.py deleted file mode 100644 index d41727af64..0000000000 --- a/cursorless-talon/src/terms.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Stores terms that are used in many different places -""" - -from talon import Context, Module - -mod = Module() -ctx = Context() - -mod.list( - "cursorless_homophone", - "Various alternative pronunciations of 'cursorless' to improve accuracy", -) - -# FIXME: Remove type ignore once Talon supports list types -# See https://github.com/talonvoice/talon/issues/654 -ctx.lists["user.cursorless_homophone"] = [ # pyright: ignore [reportArgumentType] - "cursorless", - "cursor less", - "cursor list", -] diff --git a/cursorless-talon/src/vendor/__init__.py b/cursorless-talon/src/vendor/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cursorless-talon/src/vendor/inflection.py b/cursorless-talon/src/vendor/inflection.py deleted file mode 100644 index 19310db360..0000000000 --- a/cursorless-talon/src/vendor/inflection.py +++ /dev/null @@ -1,175 +0,0 @@ -# From https://github.com/jpvanhal/inflection/blob/b00d4d348b32ef5823221b20ee4cbd1d2d924462/inflection/__init__.py -# License https://github.com/jpvanhal/inflection/blob/b00d4d348b32ef5823221b20ee4cbd1d2d924462/LICENSE -import re - -PLURALS = [ - (r"(?i)(quiz)$", r"\1zes"), - (r"(?i)^(oxen)$", r"\1"), - (r"(?i)^(ox)$", r"\1en"), - (r"(?i)(m|l)ice$", r"\1ice"), - (r"(?i)(m|l)ouse$", r"\1ice"), - (r"(?i)(passer)s?by$", r"\1sby"), - (r"(?i)(matr|vert|ind)(?:ix|ex)$", r"\1ices"), - (r"(?i)(x|ch|ss|sh)$", r"\1es"), - (r"(?i)([^aeiouy]|qu)y$", r"\1ies"), - (r"(?i)(hive)$", r"\1s"), - (r"(?i)([lr])f$", r"\1ves"), - (r"(?i)([^f])fe$", r"\1ves"), - (r"(?i)sis$", "ses"), - (r"(?i)([ti])a$", r"\1a"), - (r"(?i)([ti])um$", r"\1a"), - (r"(?i)(buffal|potat|tomat)o$", r"\1oes"), - (r"(?i)(bu)s$", r"\1ses"), - (r"(?i)(alias|status)$", r"\1es"), - (r"(?i)(octop|vir)i$", r"\1i"), - (r"(?i)(octop|vir)us$", r"\1i"), - (r"(?i)^(ax|test)is$", r"\1es"), - (r"(?i)s$", "s"), - (r"$", "s"), -] - - -SINGULARS = [ - (r"(?i)(database)s$", r"\1"), - (r"(?i)(quiz)zes$", r"\1"), - (r"(?i)(matr)ices$", r"\1ix"), - (r"(?i)(vert|ind)ices$", r"\1ex"), - (r"(?i)(passer)sby$", r"\1by"), - (r"(?i)^(ox)en", r"\1"), - (r"(?i)(alias|status)(es)?$", r"\1"), - (r"(?i)(octop|vir)(us|i)$", r"\1us"), - (r"(?i)^(a)x[ie]s$", r"\1xis"), - (r"(?i)(cris|test)(is|es)$", r"\1is"), - (r"(?i)(shoe)s$", r"\1"), - (r"(?i)(o)es$", r"\1"), - (r"(?i)(bus)(es)?$", r"\1"), - (r"(?i)(m|l)ice$", r"\1ouse"), - (r"(?i)(x|ch|ss|sh)es$", r"\1"), - (r"(?i)(m)ovies$", r"\1ovie"), - (r"(?i)(s)eries$", r"\1eries"), - (r"(?i)([^aeiouy]|qu)ies$", r"\1y"), - (r"(?i)([lr])ves$", r"\1f"), - (r"(?i)(tive)s$", r"\1"), - (r"(?i)(hive)s$", r"\1"), - (r"(?i)([^f])ves$", r"\1fe"), - (r"(?i)(t)he(sis|ses)$", r"\1hesis"), - (r"(?i)(s)ynop(sis|ses)$", r"\1ynopsis"), - (r"(?i)(p)rogno(sis|ses)$", r"\1rognosis"), - (r"(?i)(p)arenthe(sis|ses)$", r"\1arenthesis"), - (r"(?i)(d)iagno(sis|ses)$", r"\1iagnosis"), - (r"(?i)(b)a(sis|ses)$", r"\1asis"), - (r"(?i)(a)naly(sis|ses)$", r"\1nalysis"), - (r"(?i)([ti])a$", r"\1um"), - (r"(?i)(n)ews$", r"\1ews"), - (r"(?i)(ss)$", r"\1"), - (r"(?i)s$", ""), -] - - -UNCOUNTABLES = { - "equipment", - "fish", - "information", - "jeans", - "money", - "rice", - "series", - "sheep", - "species", -} - - -def _irregular(singular: str, plural: str) -> None: - """ - A convenience function to add appropriate rules to plurals and singular - for irregular words. - - :param singular: irregular word in singular form - :param plural: irregular word in plural form - """ - - def caseinsensitive(string: str) -> str: - return "".join("[" + char + char.upper() + "]" for char in string) - - if singular[0].upper() == plural[0].upper(): - PLURALS.insert(0, (rf"(?i)({singular[0]}){singular[1:]}$", r"\1" + plural[1:])) - PLURALS.insert(0, (rf"(?i)({plural[0]}){plural[1:]}$", r"\1" + plural[1:])) - SINGULARS.insert(0, (rf"(?i)({plural[0]}){plural[1:]}$", r"\1" + singular[1:])) - else: - PLURALS.insert( - 0, - ( - rf"{singular[0].upper()}{caseinsensitive(singular[1:])}$", - plural[0].upper() + plural[1:], - ), - ) - PLURALS.insert( - 0, - ( - rf"{singular[0].lower()}{caseinsensitive(singular[1:])}$", - plural[0].lower() + plural[1:], - ), - ) - PLURALS.insert( - 0, - ( - rf"{plural[0].upper()}{caseinsensitive(plural[1:])}$", - plural[0].upper() + plural[1:], - ), - ) - PLURALS.insert( - 0, - ( - rf"{plural[0].lower()}{caseinsensitive(plural[1:])}$", - plural[0].lower() + plural[1:], - ), - ) - SINGULARS.insert( - 0, - ( - rf"{plural[0].upper()}{caseinsensitive(plural[1:])}$", - singular[0].upper() + singular[1:], - ), - ) - SINGULARS.insert( - 0, - ( - rf"{plural[0].lower()}{caseinsensitive(plural[1:])}$", - singular[0].lower() + singular[1:], - ), - ) - - -def pluralize(word: str) -> str: - """ - Return the plural form of a word. - - Examples:: - - >>> pluralize("posts") - 'posts' - >>> pluralize("octopus") - 'octopi' - >>> pluralize("sheep") - 'sheep' - >>> pluralize("CamelOctopus") - 'CamelOctopi' - - """ - if not word or word.lower() in UNCOUNTABLES: - return word - else: - for rule, replacement in PLURALS: - if re.search(rule, word): - return re.sub(rule, replacement, word) - return word - - -_irregular("person", "people") -_irregular("man", "men") -_irregular("human", "humans") -_irregular("child", "children") -_irregular("sex", "sexes") -_irregular("move", "moves") -_irregular("cow", "kine") -_irregular("zombie", "zombies") diff --git a/cursorless-talon/src/vendor/jstyleson.py b/cursorless-talon/src/vendor/jstyleson.py deleted file mode 100644 index 607ef6b9a0..0000000000 --- a/cursorless-talon/src/vendor/jstyleson.py +++ /dev/null @@ -1,137 +0,0 @@ -# From https://github.com/linjackson78/jstyleson/blob/8c47cc9e665b3b1744cccfaa7a650de5f3c575dd/jstyleson.py -# License https://github.com/linjackson78/jstyleson/blob/8c47cc9e665b3b1744cccfaa7a650de5f3c575dd/LICENSE -import json - - -def dispose(json_str): - """Clear all comments in json_str. - - Clear JS-style comments like // and /**/ in json_str. - Accept a str or unicode as input. - - Args: - json_str: A json string of str or unicode to clean up comment - - Returns: - str: The str without comments (or unicode if you pass in unicode) - """ - result_str = list(json_str) - escaped = False - normal = True - sl_comment = False - ml_comment = False - quoted = False - - a_step_from_comment = False - a_step_from_comment_away = False - - former_index = None - - for index, char in enumerate(json_str): - if escaped: # We have just met a '\' - escaped = False - continue - - if a_step_from_comment: # We have just met a '/' - if char != "/" and char != "*": - a_step_from_comment = False - normal = True - continue - - if a_step_from_comment_away: # We have just met a '*' - if char != "/": - a_step_from_comment_away = False - - if char == '"': - if normal and not escaped: - # We are now in a string - quoted = True - normal = False - elif quoted and not escaped: - # We are now out of a string - quoted = False - normal = True - - elif char == "\\": - # '\' should not take effect in comment - if normal or quoted: - escaped = True - - elif char == "/": - if a_step_from_comment: - # Now we are in single line comment - a_step_from_comment = False - sl_comment = True - normal = False - former_index = index - 1 - elif a_step_from_comment_away: - # Now we are out of comment - a_step_from_comment_away = False - normal = True - ml_comment = False - for i in range(former_index, index + 1): - result_str[i] = "" - - elif normal: - # Now we are just one step away from comment - a_step_from_comment = True - normal = False - - elif char == "*": - if a_step_from_comment: - # We are now in multi-line comment - a_step_from_comment = False - ml_comment = True - normal = False - former_index = index - 1 - elif ml_comment: - a_step_from_comment_away = True - elif char == "\n": - if sl_comment: - sl_comment = False - normal = True - for i in range(former_index, index + 1): - result_str[i] = "" - elif char == "]" or char == "}": - if normal: - _remove_last_comma(result_str, index) - - # To remove single line comment which is the last line of json - if sl_comment: - sl_comment = False - normal = True - for i in range(former_index, len(json_str)): - result_str[i] = "" - - # Show respect to original input if we are in python2 - return ("" if isinstance(json_str, str) else "").join(result_str) - - -# There may be performance suffer backtracking the last comma -def _remove_last_comma(str_list, before_index): - i = before_index - 1 - while str_list[i].isspace() or not str_list[i]: - i -= 1 - - # This is the first none space char before before_index - if str_list[i] == ",": - str_list[i] = "" - - -# Below are just some wrapper function around the standard json module. - - -def loads(text, **kwargs): - return json.loads(dispose(text), **kwargs) - - -def load(fp, **kwargs): - return loads(fp.read(), **kwargs) - - -def dumps(obj, **kwargs): - return json.dumps(obj, **kwargs) - - -def dump(obj, fp, **kwargs): - json.dump(obj, fp, **kwargs) diff --git a/cursorless-talon/src/versions.py b/cursorless-talon/src/versions.py deleted file mode 100644 index 056299a93a..0000000000 --- a/cursorless-talon/src/versions.py +++ /dev/null @@ -1 +0,0 @@ -COMMAND_VERSION = 7