set up vscode and some related talon
This commit is contained in:
parent
9008367ef1
commit
c8ead81d0f
84 changed files with 5249 additions and 0 deletions
|
@ -161,6 +161,7 @@
|
|||
./nixos/configs/flatpak.nix
|
||||
./nixos/configs/fonts-linux.nix
|
||||
./nixos/configs/music-server.nix
|
||||
./nixos/configs/dotfiles/vscode.nix
|
||||
|
||||
#./nixos/overlays/fixup-signal
|
||||
./nixos/overlays/fixup-imhex.nix
|
||||
|
|
68
nixos/configs/dotfiles/vscode.hm.nix
Normal file
68
nixos/configs/dotfiles/vscode.hm.nix
Normal file
|
@ -0,0 +1,68 @@
|
|||
#
|
||||
# Visual Studio Code + Dance (kakoune mode) Experiments
|
||||
#
|
||||
{ pkgs, deprekages, lib, ... }: {
|
||||
|
||||
programs.vscode = {
|
||||
enable = true;
|
||||
|
||||
#
|
||||
# Core config.
|
||||
#
|
||||
|
||||
# Extensions to include with vscode.
|
||||
extensions = with (pkgs.vscode-extensions // deprekages.vscode-extensions); [
|
||||
# Requires a qualified name due to the leading '1'.
|
||||
pkgs.vscode-extensions."1Password".op-vscode
|
||||
|
||||
# appearance
|
||||
brandonkirbyson.solarized-palenight
|
||||
|
||||
# behavior
|
||||
gregoire.dance
|
||||
editorconfig.editorconfig
|
||||
|
||||
# general add-ons
|
||||
ms-vscode.hexeditor
|
||||
ms-vscode.cmake-tools
|
||||
ms-vscode.makefile-tools
|
||||
|
||||
# rust
|
||||
serayuzgur.crates
|
||||
rust-lang.rust-analyzer
|
||||
njpwerner.autodocstring
|
||||
|
||||
# c/c++
|
||||
ms-vscode.cpptools
|
||||
|
||||
# misc languages
|
||||
golang.go
|
||||
graphql.vscode-graphql
|
||||
graphql.vscode-graphql-syntax
|
||||
|
||||
# accessibility / talon, for when our neuro condition is wonk
|
||||
pokey.talon
|
||||
pokey.cursorless
|
||||
|
||||
];
|
||||
|
||||
# VSCode settings; runtime settings are immutable.
|
||||
userSettings = {
|
||||
|
||||
# Theming.
|
||||
"workbench.colorTheme" = lib.mkForce "Solarized-Palenight";
|
||||
|
||||
|
||||
};
|
||||
|
||||
#
|
||||
# For now, let Nix manage everything.
|
||||
#
|
||||
enableUpdateCheck = false;
|
||||
enableExtensionUpdateCheck = false;
|
||||
|
||||
mutableExtensionsDir = false;
|
||||
|
||||
};
|
||||
|
||||
}
|
9
nixos/configs/dotfiles/vscode.nix
Normal file
9
nixos/configs/dotfiles/vscode.nix
Normal file
|
@ -0,0 +1,9 @@
|
|||
#
|
||||
# Entry-point for vscode systems.
|
||||
#
|
||||
{ normalizeModule, ... }:
|
||||
{
|
||||
imports = [
|
||||
(normalizeModule ./vscode.hm.nix)
|
||||
];
|
||||
}
|
|
@ -99,6 +99,9 @@ flake-utils.lib.eachDefaultSystem (
|
|||
# kakoune
|
||||
kak-tree-sitter = callPackage ./packages/kak-tree-sitter { };
|
||||
|
||||
# vscode
|
||||
vscode-extensions = callPackage ./packages/vscode-extensions.nix { };
|
||||
|
||||
# xonsh and xontribs
|
||||
xonsh-with-xontribs = pkgs.xonsh.override {
|
||||
extraPackages = pythonPackages: [
|
||||
|
|
36
packages/vscode-extensions.nix
Normal file
36
packages/vscode-extensions.nix
Normal file
|
@ -0,0 +1,36 @@
|
|||
#
|
||||
# VSCode extensions we use.
|
||||
#
|
||||
|
||||
{ vscode-utils, ... }:
|
||||
let
|
||||
# Helper for creating simple marketplace extesnsions.
|
||||
quickMarketplaceExtension = args: vscode-utils.buildVscodeMarketplaceExtension { mktplcRef = args; };
|
||||
in
|
||||
{
|
||||
#
|
||||
# Various simple extension definitions.
|
||||
#
|
||||
|
||||
gregoire.dance = quickMarketplaceExtension {
|
||||
name = "dance";
|
||||
publisher = "gregoire";
|
||||
version = "0.5.15001";
|
||||
hash = "sha256-gGTpeOQeIQj2ObyC6504+lzLFUS35RNw5z2/isPRpyM=";
|
||||
};
|
||||
|
||||
pokey.talon = quickMarketplaceExtension {
|
||||
name = "talon";
|
||||
publisher = "pokey";
|
||||
version = "0.2.0";
|
||||
hash = "sha256-BPc0jGGoKctANP4m305hoG9dgrhjxZtFdCdkTeWh/Xk=";
|
||||
};
|
||||
|
||||
pokey.cursorless = quickMarketplaceExtension {
|
||||
name = "cursorless";
|
||||
publisher = "pokey";
|
||||
version = "0.29.1295";
|
||||
hash = "sha256-QIfAu76QhIII8Xnt5lCCVsZAaa57OHszC4ZQuq67MZs=";
|
||||
};
|
||||
|
||||
}
|
1
talon/cursorless-talon/.github/CODEOWNERS
vendored
Normal file
1
talon/cursorless-talon/.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @pokey
|
5
talon/cursorless-talon/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
talon/cursorless-talon/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
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
|
4
talon/cursorless-talon/.gitignore
vendored
Normal file
4
talon/cursorless-talon/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*.flac
|
||||
data/
|
||||
.vscode/settings.json
|
||||
.DS_Store
|
21
talon/cursorless-talon/LICENSE
Normal file
21
talon/cursorless-talon/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
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.
|
32
talon/cursorless-talon/README.md
Normal file
32
talon/cursorless-talon/README.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
<h1 align="center">Welcome to Cursorless!</h1>
|
||||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=pokey.cursorless&ssr=false#review-details" target="_blank">
|
||||
<img alt="Rating" src="https://img.shields.io/visual-studio-marketplace/stars/pokey.cursorless?logo=visualstudiocode" />
|
||||
</a>
|
||||
<a href="https://www.cursorless.org/docs/" target="_blank">
|
||||
<img alt="Documentation" src="https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=data:image/svg%2bxml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJib29rIiBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYm9vayBmYS13LTE0IiByb2xlPSJpbWciCiAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj4KICAgIDxwYXRoIGZpbGw9IndoaXRlIiBkPSJNNDQ4IDM2MFYyNGMwLTEzLjMtMTAuNy0yNC0yNC0yNEg5NkM0MyAwIDAgNDMgMCA5NnYzMjBjMCA1MyA0MyA5NiA5NiA5NmgzMjhjMTMuMyAwIDI0LTEwLjcgMjQtMjR2LTE2YzAtNy41LTMuNS0xNC4zLTguOS0xOC43LTQuMi0xNS40LTQuMi01OS4zIDAtNzQuNyA1LjQtNC4zIDguOS0xMS4xIDguOS0xOC42ek0xMjggMTM0YzAtMy4zIDIuNy02IDYtNmgyMTJjMy4zIDAgNiAyLjcgNiA2djIwYzAgMy4zLTIuNyA2LTYgNkgxMzRjLTMuMyAwLTYtMi43LTYtNnYtMjB6bTAgNjRjMC0zLjMgMi43LTYgNi02aDIxMmMzLjMgMCA2IDIuNyA2IDZ2MjBjMCAzLjMtMi43IDYtNiA2SDEzNGMtMy4zIDAtNi0yLjctNi02di0yMHptMjUzLjQgMjUwSDk2Yy0xNy43IDAtMzItMTQuMy0zMi0zMiAwLTE3LjYgMTQuNC0zMiAzMi0zMmgyODUuNGMtMS45IDE3LjEtMS45IDQ2LjkgMCA2NHoiPjwvcGF0aD4KPC9zdmc+" />
|
||||
</a>
|
||||
<a href="https://github.com/cursorless-dev/cursorless/actions/workflows/test.yml?query=branch%3Amain" target="_blank">
|
||||
<img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/cursorless-dev/cursorless-vscode/test.yml?branch=main&logo=github&label=tests" />
|
||||
</a>
|
||||
<a href="https://github.com/cursorless-dev/cursorless/graphs/contributors" target="_blank">
|
||||
<img alt="Maintenance" src="https://img.shields.io/maintenance/yes/2024.svg?logo=data:image/svg%2bxml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJ3cmVuY2giIGNsYXNzPSJzdmctaW5saW5lLS1mYSBmYS13cmVuY2ggZmEtdy0xNiIgcm9sZT0iaW1nIgogICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+CiAgICA8cGF0aCBmaWxsPSJ3aGl0ZSIgZD0iTTUwNy43MyAxMDkuMWMtMi4yNC05LjAzLTEzLjU0LTEyLjA5LTIwLjEyLTUuNTFsLTc0LjM2IDc0LjM2LTY3Ljg4LTExLjMxLTExLjMxLTY3Ljg4IDc0LjM2LTc0LjM2YzYuNjItNi42MiAzLjQzLTE3LjktNS42Ni0yMC4xNi00Ny4zOC0xMS43NC05OS41NS45MS0xMzYuNTggMzcuOTMtMzkuNjQgMzkuNjQtNTAuNTUgOTcuMS0zNC4wNSAxNDcuMkwxOC43NCA0MDIuNzZjLTI0Ljk5IDI0Ljk5LTI0Ljk5IDY1LjUxIDAgOTAuNSAyNC45OSAyNC45OSA2NS41MSAyNC45OSA5MC41IDBsMjEzLjIxLTIxMy4yMWM1MC4xMiAxNi43MSAxMDcuNDcgNS42OCAxNDcuMzctMzQuMjIgMzcuMDctMzcuMDcgNDkuNy04OS4zMiAzNy45MS0xMzYuNzN6TTY0IDQ3MmMtMTMuMjUgMC0yNC0xMC43NS0yNC0yNCAwLTEzLjI2IDEwLjc1LTI0IDI0LTI0czI0IDEwLjc0IDI0IDI0YzAgMTMuMjUtMTAuNzUgMjQtMjQgMjR6Ij48L3BhdGg+Cjwvc3ZnPg==" />
|
||||
</a>
|
||||
<a href="https://github.com/cursorless-dev/cursorless/blob/main/LICENSE" target="_blank">
|
||||
<img alt="License: MIT" src="https://img.shields.io/github/license/cursorless-dev/cursorless-vscode?color=success&logo=data:image/svg%2bxml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJiYWxhbmNlLXNjYWxlIiBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYmFsYW5jZS1zY2FsZSBmYS13LTIwIiByb2xlPSJpbWciCiAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNTEyIj4KICAgIDxwYXRoIGZpbGw9IndoaXRlIiBkPSJNMjU2IDMzNmgtLjAyYzAtMTYuMTggMS4zNC04LjczLTg1LjA1LTE4MS41MS0xNy42NS0zNS4yOS02OC4xOS0zNS4zNi04NS44NyAwQy0yLjA2IDMyOC43NS4wMiAzMjAuMzMuMDIgMzM2SDBjMCA0NC4xOCA1Ny4zMSA4MCAxMjggODBzMTI4LTM1LjgyIDEyOC04MHpNMTI4IDE3Nmw3MiAxNDRINTZsNzItMTQ0em01MTEuOTggMTYwYzAtMTYuMTggMS4zNC04LjczLTg1LjA1LTE4MS41MS0xNy42NS0zNS4yOS02OC4xOS0zNS4zNi04NS44NyAwLTg3LjEyIDE3NC4yNi04NS4wNCAxNjUuODQtODUuMDQgMTgxLjUxSDM4NGMwIDQ0LjE4IDU3LjMxIDgwIDEyOCA4MHMxMjgtMzUuODIgMTI4LTgwaC0uMDJ6TTQ0MCAzMjBsNzItMTQ0IDcyIDE0NEg0NDB6bTg4IDEyOEgzNTJWMTUzLjI1YzIzLjUxLTEwLjI5IDQxLjE2LTMxLjQ4IDQ2LjM5LTU3LjI1SDUyOGM4Ljg0IDAgMTYtNy4xNiAxNi0xNlY0OGMwLTguODQtNy4xNi0xNi0xNi0xNkgzODMuNjRDMzY5LjA0IDEyLjY4IDM0Ni4wOSAwIDMyMCAwcy00OS4wNCAxMi42OC02My42NCAzMkgxMTJjLTguODQgMC0xNiA3LjE2LTE2IDE2djMyYzAgOC44NCA3LjE2IDE2IDE2IDE2aDEyOS42MWM1LjIzIDI1Ljc2IDIyLjg3IDQ2Ljk2IDQ2LjM5IDU3LjI1VjQ0OEgxMTJjLTguODQgMC0xNiA3LjE2LTE2IDE2djMyYzAgOC44NCA3LjE2IDE2IDE2IDE2aDQxNmM4Ljg0IDAgMTYtNy4xNiAxNi0xNnYtMzJjMC04Ljg0LTcuMTYtMTYtMTYtMTZ6Ij48L3BhdGg+Cjwvc3ZnPg==" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
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).
|
1
talon/cursorless-talon/docs/README.md
Normal file
1
talon/cursorless-talon/docs/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Cursorless is now monorepo 🙌. The docs now live at https://www.cursorless.org/docs/.
|
1
talon/cursorless-talon/docs/customization.md
Normal file
1
talon/cursorless-talon/docs/customization.md
Normal file
|
@ -0,0 +1 @@
|
|||
Cursorless is now monorepo 🙌. This document now lives at https://www.cursorless.org/docs/user/customization/.
|
1
talon/cursorless-talon/docs/experimental.md
Normal file
1
talon/cursorless-talon/docs/experimental.md
Normal file
|
@ -0,0 +1 @@
|
|||
Cursorless is now monorepo 🙌. This document now lives at https://www.cursorless.org/docs/user/experimental/
|
142
talon/cursorless-talon/src/actions/actions.py
Normal file
142
talon/cursorless-talon/src/actions/actions.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
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)
|
46
talon/cursorless-talon/src/actions/bring_move.py
Normal file
46
talon/cursorless-talon/src/actions/bring_move.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
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="<user.cursorless_target> [<user.cursorless_destination>]")
|
||||
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,
|
||||
}
|
||||
)
|
22
talon/cursorless-talon/src/actions/call.py
Normal file
22
talon/cursorless-talon/src/actions/call.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
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,
|
||||
}
|
||||
)
|
17
talon/cursorless-talon/src/actions/execute_command.py
Normal file
17
talon/cursorless-talon/src/actions/execute_command.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
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,
|
||||
}
|
||||
)
|
56
talon/cursorless-talon/src/actions/get_text.py
Normal file
56
talon/cursorless-talon/src/actions/get_text.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
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,
|
||||
}
|
||||
)
|
39
talon/cursorless-talon/src/actions/homophones.py
Normal file
39
talon/cursorless-talon/src/actions/homophones.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
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
|
21
talon/cursorless-talon/src/actions/paste.py
Normal file
21
talon/cursorless-talon/src/actions/paste.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
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,
|
||||
}
|
||||
)
|
25
talon/cursorless-talon/src/actions/reformat.py
Normal file
25
talon/cursorless-talon/src/actions/reformat.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
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 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)
|
16
talon/cursorless-talon/src/actions/replace.py
Normal file
16
talon/cursorless-talon/src/actions/replace.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
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,
|
||||
}
|
||||
)
|
49
talon/cursorless-talon/src/actions/swap.py
Normal file
49
talon/cursorless-talon/src/actions/swap.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
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_target>] {user.cursorless_swap_connective} <user.cursorless_target>"
|
||||
)
|
||||
)
|
||||
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,
|
||||
}
|
||||
)
|
60
talon/cursorless-talon/src/actions/wrap.py
Normal file
60
talon/cursorless-talon/src/actions/wrap.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
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)
|
26
talon/cursorless-talon/src/apps/cursorless_vscode.py
Normal file
26
talon/cursorless-talon/src/apps/cursorless_vscode.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
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"
|
||||
)
|
120
talon/cursorless-talon/src/apps/vscode_settings.py
Normal file
120
talon/cursorless-talon/src/apps/vscode_settings.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
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()]
|
||||
if not existing_paths:
|
||||
paths_str = ", ".join(str(path) for path in paths)
|
||||
raise FileNotFoundError(
|
||||
f"Couldn't find VSCode's settings JSON. Tried these paths: {paths_str}"
|
||||
)
|
||||
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",
|
||||
xdg_config_home / "Cursor/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",
|
||||
]
|
||||
)
|
161
talon/cursorless-talon/src/cheatsheet/cheat_sheet.py
Normal file
161
talon/cursorless-talon/src/cheatsheet/cheat_sheet.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
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
|
||||
from .sections.tutorial import get_tutorial_entries
|
||||
|
||||
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"""
|
||||
actions.user.private_cursorless_notify_docs_opened()
|
||||
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"),
|
||||
},
|
||||
{
|
||||
"name": "Tutorial",
|
||||
"id": "tutorial",
|
||||
"items": get_tutorial_entries(),
|
||||
},
|
||||
]
|
||||
}
|
92
talon/cursorless-talon/src/cheatsheet/get_list.py
Normal file
92
talon/cursorless-talon/src/cheatsheet/get_list.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
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,
|
||||
)
|
138
talon/cursorless-talon/src/cheatsheet/sections/actions.py
Normal file
138
talon/cursorless-talon/src/cheatsheet/sections/actions.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
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} <target>": 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']} <target> <destination>",
|
||||
"description": "Copy <target> to <destination>",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_actions['replaceWithTarget']} <target>",
|
||||
"description": "Insert copy of <target> at cursor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "pasteFromClipboard",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_actions['pasteFromClipboard']} <destination>",
|
||||
"description": "Paste from clipboard at <destination>",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "moveToTarget",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_actions['moveToTarget']} <target> <destination>",
|
||||
"description": "Move <target> to <destination>",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_actions['moveToTarget']} <target>",
|
||||
"description": "Move <target> to cursor position",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "swapTargets",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_actions['swapTargets']} <target 1> {swap_connective} <target 2>",
|
||||
"description": "Swap <target 1> with <target 2>",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_actions['swapTargets']} {swap_connective} <target>",
|
||||
"description": "Swap selection with <target>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "applyFormatter",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_actions['applyFormatter']} <formatter> at <target>",
|
||||
"description": "Reformat <target> as <formatter>",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "callAsFunction",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_actions['callAsFunction']} <target>",
|
||||
"description": "Call <target> on selection",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_actions['callAsFunction']} <target 1> on <target 2>",
|
||||
"description": "Call <target 1> on <target 2>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "wrapWithPairedDelimiter",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"<pair> {complex_actions['wrapWithPairedDelimiter']} <target>",
|
||||
"description": "Wrap <target> with <pair>",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "rewrap",
|
||||
"type": "action",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"<pair> {complex_actions['rewrap']} <target>",
|
||||
"description": "Rewrap <target> with <pair>",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
|
@ -0,0 +1,53 @@
|
|||
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"<target 1> {list_connective_term} <target 2>",
|
||||
"description": "<target 1> and <target 2>",
|
||||
},
|
||||
],
|
||||
},
|
||||
*[
|
||||
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"<target 1> {spoken_form} <target 2>",
|
||||
"description": formatter("<target 1>", "<target 2>"),
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{spoken_form} <target>",
|
||||
"description": formatter("selection", "<target>"),
|
||||
},
|
||||
],
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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 <target>",
|
||||
"before": "Insert before <target>",
|
||||
"after": "Insert after <target>",
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
"id": f"destination_{id}",
|
||||
"type": "destination",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{spoken_form} <target>",
|
||||
"description": descriptions[id],
|
||||
}
|
||||
],
|
||||
}
|
||||
for spoken_form, id in insertion_modes.items()
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
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} <scope>",
|
||||
"description": "Visualize <scope>",
|
||||
},
|
||||
*[
|
||||
{
|
||||
"spokenForm": f"{show_scope_visualizer} <scope> {spoken_form}",
|
||||
"description": f"Visualize <scope> {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",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
216
talon/cursorless-talon/src/cheatsheet/sections/modifiers.py
Normal file
216
talon/cursorless-talon/src/cheatsheet/sections/modifiers.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
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']} <modifier>",
|
||||
"description": "Extend through start of <modifier>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "extendThroughEndOf",
|
||||
"type": "modifier",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": complex_modifiers["extendThroughEndOf"],
|
||||
"description": "Extend through end of line",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['extendThroughEndOf']} <modifier>",
|
||||
"description": "Extend through end of <modifier>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "containingScope",
|
||||
"type": "modifier",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "<scope>",
|
||||
"description": "Containing instance of <scope>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "every",
|
||||
"type": "modifier",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['every']} <scope>",
|
||||
"description": "Every instance of <scope>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "ancestor",
|
||||
"type": "modifier",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['ancestor']} <scope>",
|
||||
"description": "Grandparent containing instance of <scope>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "relativeScope",
|
||||
"type": "modifier",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['previous']} <scope>",
|
||||
"description": "Previous instance of <scope>",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['next']} <scope>",
|
||||
"description": "Next instance of <scope>",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"<ordinal> {complex_modifiers['previous']} <scope>",
|
||||
"description": "<ordinal> instance of <scope> before target",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"<ordinal> {complex_modifiers['next']} <scope>",
|
||||
"description": "<ordinal> instance of <scope> after target",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"<scope> {complex_modifiers['backward']}",
|
||||
"description": "single instance of <scope> including target, going backwards",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"<scope> {complex_modifiers['forward']}",
|
||||
"description": "single instance of <scope> including target, going forwards",
|
||||
},
|
||||
*generateOptionalEvery(
|
||||
complex_modifiers["every"],
|
||||
{
|
||||
"spokenForm": f"<number> <scope>s {complex_modifiers['backward']}",
|
||||
"description": "<number> instances of <scope> including target, going backwards",
|
||||
},
|
||||
{
|
||||
"spokenForm": "<number> <scope>s",
|
||||
"description": "<number> instances of <scope> including target, going forwards",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['previous']} <number> <scope>s",
|
||||
"description": "previous <number> instances of <scope>",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['next']} <number> <scope>s",
|
||||
"description": "next <number> instances of <scope>",
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "ordinalScope",
|
||||
"type": "modifier",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "<ordinal> <scope>",
|
||||
"description": "<ordinal> instance of <scope> in iteration scope",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"<ordinal> {complex_modifiers['last']} <scope>",
|
||||
"description": "<ordinal>-to-last instance of <scope> in iteration scope",
|
||||
},
|
||||
*generateOptionalEvery(
|
||||
complex_modifiers["every"],
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['first']} <number> <scope>s",
|
||||
"description": "first <number> instances of <scope> in iteration scope",
|
||||
},
|
||||
{
|
||||
"spokenForm": f"{complex_modifiers['last']} <number> <scope>s",
|
||||
"description": "last <number> instances of <scope> in iteration scope",
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class Entry(TypedDict):
|
||||
spokenForm: str
|
||||
description: str
|
||||
|
||||
|
||||
def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]:
|
||||
return list(
|
||||
chain.from_iterable(
|
||||
[
|
||||
{
|
||||
"spokenForm": 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
|
||||
)
|
||||
)
|
26
talon/cursorless-talon/src/cheatsheet/sections/scopes.py
Normal file
26
talon/cursorless-talon/src/cheatsheet/sections/scopes.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
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 bounded by surrounding pair delimeters",
|
||||
"boundedParagraph": "Paragraph bounded by surrounding pair delimeters",
|
||||
},
|
||||
),
|
||||
{
|
||||
"id": "glyph",
|
||||
"type": "scopeType",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": f"{glyph_spoken_form} <character>",
|
||||
"description": "Instance of single character <character>",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
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} <number>": 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,
|
||||
]
|
83
talon/cursorless-talon/src/cheatsheet/sections/tutorial.py
Normal file
83
talon/cursorless-talon/src/cheatsheet/sections/tutorial.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
def get_tutorial_entries():
|
||||
return [
|
||||
{
|
||||
"id": "start_tutorial",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "cursorless tutorial",
|
||||
"description": "Start the introductory Cursorless tutorial",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_next",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial next",
|
||||
"description": "Advance to next step in tutorial",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_previous",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial previous",
|
||||
"description": "Go back to previous step in tutorial",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_restart",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial restart",
|
||||
"description": "Restart the tutorial",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_resume",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial resume",
|
||||
"description": "Resume the tutorial",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_list",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial list",
|
||||
"description": "List all available tutorials",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_close",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial close",
|
||||
"description": "Close the tutorial",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tutorial_start_by_number",
|
||||
"type": "command",
|
||||
"variations": [
|
||||
{
|
||||
"spokenForm": "tutorial <number>",
|
||||
"description": "Start a specific tutorial by number",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
107
talon/cursorless-talon/src/command.py
Normal file
107
talon/cursorless-talon/src/command.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
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 vars(type(value)).items()
|
||||
if not k.startswith("_") and not isinstance(v, property)
|
||||
},
|
||||
**value.__dict__,
|
||||
}
|
||||
return {k: make_serializable(v) for k, v in items.items() if v is not None}
|
||||
return value
|
2
talon/cursorless-talon/src/conventions.py
Normal file
2
talon/cursorless-talon/src/conventions.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def get_cursorless_list_name(name: str):
|
||||
return f"user.cursorless_{name}"
|
479
talon/cursorless-talon/src/csv_overrides.py
Normal file
479
talon/cursorless-talon/src/csv_overrides.py
Normal file
|
@ -0,0 +1,479 @@
|
|||
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 csv_get_ctx():
|
||||
return ctx
|
||||
|
||||
|
||||
def csv_get_normalized_ctx():
|
||||
return normalized_ctx
|
||||
|
||||
|
||||
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
|
82
talon/cursorless-talon/src/cursorless.py
Normal file
82
talon/cursorless-talon/src/cursorless.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
from talon import Context, Module, actions
|
||||
|
||||
mod = Module()
|
||||
|
||||
mod.tag(
|
||||
"cursorless",
|
||||
"Application supporting cursorless commands",
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
ctx.matches = r"""
|
||||
tag: user.cursorless
|
||||
"""
|
||||
|
||||
|
||||
@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_notify_docs_opened():
|
||||
"""Notify the ide that the docs were opened in case the tutorial is waiting for that event"""
|
||||
actions.skip()
|
||||
|
||||
def private_cursorless_show_command_statistics():
|
||||
"""Show Cursorless command statistics"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.analyzeCommandHistory"
|
||||
)
|
||||
|
||||
def private_cursorless_start_tutorial():
|
||||
"""Start the introductory Cursorless tutorial"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.start", "tutorial-1-basics"
|
||||
)
|
||||
|
||||
def private_cursorless_tutorial_next():
|
||||
"""Cursorless tutorial: next"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.next"
|
||||
)
|
||||
|
||||
def private_cursorless_tutorial_previous():
|
||||
"""Cursorless tutorial: previous"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.previous"
|
||||
)
|
||||
|
||||
def private_cursorless_tutorial_restart():
|
||||
"""Cursorless tutorial: restart"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.restart"
|
||||
)
|
||||
|
||||
def private_cursorless_tutorial_resume():
|
||||
"""Cursorless tutorial: resume"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.resume"
|
||||
)
|
||||
|
||||
def private_cursorless_tutorial_list():
|
||||
"""Cursorless tutorial: list all available tutorials"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.list"
|
||||
)
|
||||
|
||||
def private_cursorless_tutorial_start_by_number(number: int): # pyright: ignore [reportGeneralTypeIssues]
|
||||
"""Start Cursorless tutorial by number"""
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.tutorial.start", number - 1
|
||||
)
|
||||
|
||||
|
||||
@ctx.action_class("user")
|
||||
class CursorlessActions:
|
||||
def private_cursorless_notify_docs_opened():
|
||||
actions.user.private_cursorless_run_rpc_command_no_wait(
|
||||
"cursorless.documentationOpened"
|
||||
)
|
49
talon/cursorless-talon/src/cursorless.talon
Normal file
49
talon/cursorless-talon/src/cursorless.talon
Normal file
|
@ -0,0 +1,49 @@
|
|||
mode: command
|
||||
mode: user.cursorless_spoken_form_test
|
||||
tag: user.cursorless
|
||||
-
|
||||
|
||||
<user.cursorless_action_or_ide_command> <user.cursorless_target>:
|
||||
user.private_cursorless_action_or_ide_command(cursorless_action_or_ide_command, cursorless_target)
|
||||
|
||||
{user.cursorless_bring_move_action} <user.cursorless_bring_move_targets>:
|
||||
user.private_cursorless_bring_move(cursorless_bring_move_action, cursorless_bring_move_targets)
|
||||
|
||||
{user.cursorless_swap_action} <user.cursorless_swap_targets>:
|
||||
user.private_cursorless_swap(cursorless_swap_targets)
|
||||
|
||||
{user.cursorless_paste_action} <user.cursorless_destination>:
|
||||
user.private_cursorless_paste(cursorless_destination)
|
||||
|
||||
{user.cursorless_reformat_action} <user.formatters> at <user.cursorless_target>:
|
||||
user.cursorless_reformat(cursorless_target, formatters)
|
||||
|
||||
{user.cursorless_call_action} <user.cursorless_target> on <user.cursorless_target>:
|
||||
user.private_cursorless_call(cursorless_target_1, cursorless_target_2)
|
||||
|
||||
<user.cursorless_wrapper_paired_delimiter> {user.cursorless_wrap_action} <user.cursorless_target>:
|
||||
user.private_cursorless_wrap_with_paired_delimiter(cursorless_wrap_action, cursorless_target, cursorless_wrapper_paired_delimiter)
|
||||
|
||||
{user.cursorless_show_scope_visualizer} <user.cursorless_scope_type> [{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()
|
||||
|
||||
{user.cursorless_homophone} tutorial:
|
||||
user.private_cursorless_start_tutorial()
|
||||
tutorial next: user.private_cursorless_tutorial_next()
|
||||
tutorial (previous | last): user.private_cursorless_tutorial_previous()
|
||||
tutorial restart: user.private_cursorless_tutorial_restart()
|
||||
tutorial resume: user.private_cursorless_tutorial_resume()
|
||||
tutorial (list | close): user.private_cursorless_tutorial_list()
|
||||
tutorial <user.private_cursorless_number_small>:
|
||||
user.private_cursorless_tutorial_start_by_number(private_cursorless_number_small)
|
41
talon/cursorless-talon/src/cursorless_command_server.py
Normal file
41
talon/cursorless-talon/src/cursorless_command_server.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
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)
|
4
talon/cursorless-talon/src/cursorless_global.talon
Normal file
4
talon/cursorless-talon/src/cursorless_global.talon
Normal file
|
@ -0,0 +1,4 @@
|
|||
{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()
|
111
talon/cursorless-talon/src/fallback.py
Normal file
111
talon/cursorless-talon/src/fallback.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
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))
|
||||
raise 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 "preferredScope":
|
||||
scope_type_type = modifier["scopeType"]["type"]
|
||||
return get_simple_modifier_callback(f"containingScope.{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}")
|
105
talon/cursorless-talon/src/get_grapheme_spoken_form_entries.py
Normal file
105
talon/cursorless-talon/src/get_grapheme_spoken_form_entries.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
import re
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Iterator, Mapping
|
||||
from uu import Error
|
||||
|
||||
from talon import app, registry, scope
|
||||
|
||||
from .spoken_forms_output import SpokenFormOutputEntry
|
||||
|
||||
grapheme_capture_name = "user.any_alphanumeric_key"
|
||||
|
||||
|
||||
def get_grapheme_spoken_form_entries(
|
||||
grapheme_talon_list: dict[str, str],
|
||||
) -> list[SpokenFormOutputEntry]:
|
||||
return [
|
||||
{
|
||||
"type": "grapheme",
|
||||
"id": id,
|
||||
"spokenForms": spoken_forms,
|
||||
}
|
||||
for id, spoken_forms in talon_list_to_spoken_form_map(
|
||||
grapheme_talon_list
|
||||
).items()
|
||||
]
|
||||
|
||||
|
||||
def get_graphemes_talon_list() -> dict[str, str]:
|
||||
if grapheme_capture_name not in registry.captures:
|
||||
# We require this capture, and expect it to be defined. We want to show a user friendly error if it isn't present (usually indicating a problem with their community.git setup) and we think the user is going to use Cursorless.
|
||||
# However, sometimes users use different dictation engines (Vosk, Webspeech) with entirely different/smaller grammars that don't have the capture, and this code will run then, and falsely error. We don't want to show an error in that case because they don't plan to actually use Cursorless.
|
||||
if "en" in scope.get("language", {}):
|
||||
app.notify(f"Capture <{grapheme_capture_name}> isn't defined")
|
||||
print(
|
||||
f"Capture <{grapheme_capture_name}> isn't defined, which is required by Cursorless. Please check your community setup"
|
||||
)
|
||||
return {}
|
||||
|
||||
return {
|
||||
spoken_form: id
|
||||
for symbol_list in generate_lists_from_capture(grapheme_capture_name)
|
||||
for spoken_form, id in get_id_to_talon_list(symbol_list).items()
|
||||
}
|
||||
|
||||
|
||||
def generate_lists_from_capture(capture_name) -> Iterator[str]:
|
||||
"""
|
||||
Given the name of a capture, yield the names of each list that the capture
|
||||
expands to. Note that we are somewhat strict about the format of the
|
||||
capture rule, and will not handle all possible cases.
|
||||
"""
|
||||
if capture_name.startswith("self."):
|
||||
capture_name = "user." + capture_name[5:]
|
||||
try:
|
||||
# NB: [-1] because the last capture is the active one
|
||||
rule = registry.captures[capture_name][-1].rule.rule
|
||||
except Error:
|
||||
app.notify("Error constructing spoken forms for graphemes")
|
||||
print(f"Error getting rule for capture {capture_name}")
|
||||
return
|
||||
rule = rule.strip()
|
||||
if rule.startswith("(") and rule.endswith(")"):
|
||||
rule = rule[1:-1]
|
||||
rule = rule.strip()
|
||||
components = re.split(r"\s*\|\s*", rule)
|
||||
for component in components:
|
||||
if component.startswith("<") and component.endswith(">"):
|
||||
yield from generate_lists_from_capture(component[1:-1])
|
||||
elif component.startswith("{") and component.endswith("}"):
|
||||
component = component[1:-1]
|
||||
if component.startswith("self."):
|
||||
component = "user." + component[5:]
|
||||
yield component
|
||||
else:
|
||||
app.notify("Error constructing spoken forms for graphemes")
|
||||
print(
|
||||
f"Unexpected component {component} while processing rule {rule} for capture {capture_name}"
|
||||
)
|
||||
|
||||
|
||||
def get_id_to_talon_list(list_name: str) -> dict[str, str]:
|
||||
"""
|
||||
Given the name of a Talon list, return that list
|
||||
"""
|
||||
try:
|
||||
# NB: [-1] because the last list is the active one
|
||||
return typing.cast(dict[str, str], registry.lists[list_name][-1]).copy()
|
||||
except Error:
|
||||
app.notify(f"Error getting list {list_name}")
|
||||
return {}
|
||||
|
||||
|
||||
def talon_list_to_spoken_form_map(
|
||||
talon_list: dict[str, str],
|
||||
) -> Mapping[str, list[str]]:
|
||||
"""
|
||||
Given a Talon list, return a mapping from the values in that
|
||||
list to the list of spoken forms that map to the given value.
|
||||
"""
|
||||
inverted_list: defaultdict[str, list[str]] = defaultdict(list)
|
||||
for key, value in talon_list.items():
|
||||
inverted_list[value].append(key)
|
||||
|
||||
return inverted_list
|
176
talon/cursorless-talon/src/marks/decorated_mark.py
Normal file
176
talon/cursorless-talon/src/marks/decorated_mark.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
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.any_alphanumeric_key> | {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}] <user.cursorless_grapheme>"
|
||||
)
|
||||
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
|
62
talon/cursorless-talon/src/marks/lines_number.py
Normal file
62
talon/cursorless-talon/src/marks/lines_number.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
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} <user.private_cursorless_number_small> "
|
||||
"[<user.cursorless_range_connective> <user.private_cursorless_number_small>]"
|
||||
)
|
||||
)
|
||||
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,
|
||||
}
|
50
talon/cursorless-talon/src/marks/literal_mark.py
Normal file
50
talon/cursorless-talon/src/marks/literal_mark.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from talon import Context, Module
|
||||
|
||||
from .mark_types import LiteralMark
|
||||
|
||||
mod = Module()
|
||||
|
||||
mod.list("private_cursorless_literal_mark", desc="Cursorless literal mark")
|
||||
|
||||
# This is a private tag and should not be used by non Cursorless developers
|
||||
mod.tag(
|
||||
"private_cursorless_literal_mark_no_prefix",
|
||||
desc="Tag for enabling literal mark without prefix",
|
||||
)
|
||||
|
||||
ctx_no_prefix = Context()
|
||||
ctx_no_prefix.matches = r"""
|
||||
tag: user.private_cursorless_literal_mark_no_prefix
|
||||
"""
|
||||
|
||||
|
||||
# NB: <phrase> is used over <user.text> for DFA performance reasons
|
||||
# (we intend to replace this with a dynamic list of document contents eventually)
|
||||
@mod.capture(rule="{user.private_cursorless_literal_mark} <phrase>")
|
||||
def cursorless_literal_mark(m) -> LiteralMark:
|
||||
return construct_mark(str(m.phrase))
|
||||
|
||||
|
||||
@ctx_no_prefix.capture("user.cursorless_literal_mark", rule="<phrase>")
|
||||
def cursorless_literal_mark_no_prefix(m) -> LiteralMark:
|
||||
return construct_mark(str(m.phrase))
|
||||
|
||||
|
||||
def construct_mark(text: str) -> LiteralMark:
|
||||
return {
|
||||
"type": "literal",
|
||||
"modifier": {
|
||||
"type": "preferredScope",
|
||||
"scopeType": {
|
||||
"type": "customRegex",
|
||||
"regex": construct_fuzzy_regex(text),
|
||||
"flags": "gui",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def construct_fuzzy_regex(text: str) -> str:
|
||||
parts = text.split(" ")
|
||||
# Between each word there can be any number of non-alpha symbols (including escape characters: \t\r\n). No separator at all is also valid -- for example, when searching for a camelCase identifier.
|
||||
return r"([^a-zA-Z]|\\[trn])*".join(parts)
|
17
talon/cursorless-talon/src/marks/mark.py
Normal file
17
talon/cursorless-talon/src/marks/mark.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from talon import Module
|
||||
|
||||
from .mark_types import Mark
|
||||
|
||||
mod = Module()
|
||||
|
||||
|
||||
@mod.capture(
|
||||
rule=(
|
||||
"<user.cursorless_decorated_symbol> | "
|
||||
"<user.cursorless_literal_mark> | "
|
||||
"<user.cursorless_simple_mark> |"
|
||||
"<user.cursorless_line_number>" # row (ie absolute mod 100), up, down
|
||||
)
|
||||
)
|
||||
def cursorless_mark(m) -> Mark:
|
||||
return m[0]
|
36
talon/cursorless-talon/src/marks/mark_types.py
Normal file
36
talon/cursorless-talon/src/marks/mark_types.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from typing import Literal, TypedDict, Union
|
||||
|
||||
|
||||
class DecoratedSymbol(TypedDict):
|
||||
type: Literal["decoratedSymbol"]
|
||||
symbolColor: str
|
||||
character: str
|
||||
|
||||
|
||||
class LiteralMark(TypedDict):
|
||||
type: Literal["literal"]
|
||||
modifier: dict
|
||||
|
||||
|
||||
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, LiteralMark, SimpleMark, LineNumber]
|
23
talon/cursorless-talon/src/marks/simple_mark.py
Normal file
23
talon/cursorless-talon/src/marks/simple_mark.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
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],
|
||||
}
|
39
talon/cursorless-talon/src/modifiers/head_tail.py
Normal file
39
talon/cursorless-talon/src/modifiers/head_tail.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
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} "
|
||||
"[<user.cursorless_interior_modifier>] "
|
||||
"[<user.cursorless_head_tail_swallowed_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
|
16
talon/cursorless-talon/src/modifiers/interior.py
Normal file
16
talon/cursorless-talon/src/modifiers/interior.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
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,
|
||||
}
|
10
talon/cursorless-talon/src/modifiers/matching_pair_symbol.py
Normal file
10
talon/cursorless-talon/src/modifiers/matching_pair_symbol.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
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"}}
|
49
talon/cursorless-talon/src/modifiers/modifiers.py
Normal file
49
talon/cursorless-talon/src/modifiers/modifiers.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
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 = [
|
||||
"<user.cursorless_simple_modifier>", # bounds, just, leading, trailing
|
||||
"<user.cursorless_simple_scope_modifier>", # funk, state, class, every funk
|
||||
"<user.cursorless_ordinal_scope>", # first past second word
|
||||
"<user.cursorless_relative_scope>", # next funk, 3 funks
|
||||
]
|
||||
|
||||
modifiers = [
|
||||
"<user.cursorless_interior_modifier>", # inside
|
||||
"<user.cursorless_head_tail_modifier>", # head, tail
|
||||
"<user.cursorless_position_modifier>", # 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]
|
91
talon/cursorless-talon/src/modifiers/ordinal_scope.py
Normal file
91
talon/cursorless-talon/src/modifiers/ordinal_scope.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
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.ordinals_small> | [<user.ordinals_small>] {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="<user.ordinal_or_last> [<user.cursorless_range_connective> <user.ordinal_or_last>] <user.cursorless_scope_type>"
|
||||
)
|
||||
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}) "
|
||||
"<user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
|
||||
),
|
||||
)
|
||||
def cursorless_first_last(m) -> dict[str, Any]:
|
||||
"""First/last `n` scopes; eg "first three funks"""
|
||||
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="<user.cursorless_ordinal_range> | <user.cursorless_first_last>")
|
||||
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
|
12
talon/cursorless-talon/src/modifiers/position.py
Normal file
12
talon/cursorless-talon/src/modifiers/position.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
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"}
|
104
talon/cursorless-talon/src/modifiers/relative_scope.py
Normal file
104
talon/cursorless-talon/src/modifiers/relative_scope.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
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="[<user.ordinals_small>] <user.cursorless_relative_direction> <user.cursorless_scope_type>"
|
||||
)
|
||||
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}] <user.cursorless_relative_direction> <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
|
||||
)
|
||||
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.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{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_scope_type> {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=(
|
||||
"<user.cursorless_relative_scope_singular> | "
|
||||
"<user.cursorless_relative_scope_plural> | "
|
||||
"<user.cursorless_relative_scope_count> | "
|
||||
"<user.cursorless_relative_scope_one_backward>"
|
||||
)
|
||||
)
|
||||
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
|
80
talon/cursorless-talon/src/modifiers/scopes.py
Normal file
80
talon/cursorless-talon/src/modifiers/scopes.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
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_glyph_scope_type",
|
||||
desc="Cursorless glyph scope type",
|
||||
)
|
||||
mod.list(
|
||||
"cursorless_glyph_scope_type_plural",
|
||||
desc="Plural version of Cursorless glyph scope type",
|
||||
)
|
||||
|
||||
mod.list(
|
||||
"cursorless_surrounding_pair_scope_type",
|
||||
desc="Scope types that can function as surrounding pairs",
|
||||
)
|
||||
mod.list(
|
||||
"cursorless_surrounding_pair_scope_type_plural",
|
||||
desc="Plural form of scope types that can function as surrounding pairs",
|
||||
)
|
||||
|
||||
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.list(
|
||||
"cursorless_scope_type_flattened",
|
||||
desc="All supported scope types flattened",
|
||||
)
|
||||
mod.list(
|
||||
"cursorless_scope_type_flattened_plural",
|
||||
desc="All supported plural scope types flattened",
|
||||
)
|
||||
|
||||
|
||||
@mod.capture(rule="{user.cursorless_scope_type_flattened}")
|
||||
def cursorless_scope_type(m) -> dict[str, str]:
|
||||
"""Cursorless scope type singular"""
|
||||
return creates_scope_type(m.cursorless_scope_type_flattened)
|
||||
|
||||
|
||||
@mod.capture(rule="{user.cursorless_scope_type_flattened_plural}")
|
||||
def cursorless_scope_type_plural(m) -> dict[str, str]:
|
||||
"""Cursorless scope type plural"""
|
||||
return creates_scope_type(m.cursorless_scope_type_flattened_plural)
|
||||
|
||||
|
||||
def creates_scope_type(id: str) -> dict[str, str]:
|
||||
grouping, value = id.split(".", 1)
|
||||
match grouping:
|
||||
case "simple":
|
||||
return {
|
||||
"type": value,
|
||||
}
|
||||
case "surroundingPair":
|
||||
return {
|
||||
"type": "surroundingPair",
|
||||
"delimiter": value,
|
||||
}
|
||||
case "customRegex":
|
||||
return {
|
||||
"type": "customRegex",
|
||||
"regex": value,
|
||||
}
|
||||
case "glyph":
|
||||
return {
|
||||
"type": "glyph",
|
||||
"character": value,
|
||||
}
|
||||
case _:
|
||||
raise ValueError(f"Unsupported scope type grouping: {grouping}")
|
|
@ -0,0 +1,54 @@
|
|||
from typing import Any
|
||||
|
||||
from talon import Module, settings
|
||||
|
||||
mod = Module()
|
||||
|
||||
mod.list(
|
||||
"cursorless_every_scope_modifier",
|
||||
desc="Cursorless every scope modifiers",
|
||||
)
|
||||
mod.list(
|
||||
"cursorless_ancestor_scope_modifier",
|
||||
desc="Cursorless ancestor scope modifiers",
|
||||
)
|
||||
|
||||
# This is a private setting and should not be used by non Cursorless developers
|
||||
mod.setting(
|
||||
"private_cursorless_use_preferred_scope",
|
||||
bool,
|
||||
desc="Use preferred scope instead of containing scope for all scopes by default (EXPERIMENTAL)",
|
||||
)
|
||||
|
||||
|
||||
@mod.capture(
|
||||
rule=(
|
||||
"[{user.cursorless_every_scope_modifier} | {user.cursorless_ancestor_scope_modifier}] "
|
||||
"<user.cursorless_scope_type>"
|
||||
),
|
||||
)
|
||||
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,
|
||||
}
|
||||
|
||||
if settings.get("user.private_cursorless_use_preferred_scope", False):
|
||||
return {
|
||||
"type": "preferredScope",
|
||||
"scopeType": m.cursorless_scope_type,
|
||||
}
|
||||
|
||||
return {
|
||||
"type": "containingScope",
|
||||
"scopeType": m.cursorless_scope_type,
|
||||
}
|
46
talon/cursorless-talon/src/number_small.py
Normal file
46
talon/cursorless-talon/src/number_small.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
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 <number_small> capture",
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
ctx.matches = """
|
||||
not tag: user.cursorless_custom_number_small
|
||||
"""
|
||||
|
||||
|
||||
@mod.capture(rule="<number_small>")
|
||||
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]
|
56
talon/cursorless-talon/src/paired_delimiter.py
Normal file
56
talon/cursorless-talon/src/paired_delimiter.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
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",
|
||||
)
|
||||
|
||||
mod.list(
|
||||
"cursorless_selectable_only_paired_delimiter_plural",
|
||||
desc="Plural form of a paired delimiter that can only be used as a scope type",
|
||||
)
|
||||
mod.list(
|
||||
"cursorless_wrapper_selectable_paired_delimiter_plural",
|
||||
desc="Plural form of 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": ["", ""],
|
||||
}
|
||||
|
||||
|
||||
@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]
|
|
@ -0,0 +1,47 @@
|
|||
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)}")
|
68
talon/cursorless-talon/src/private_api/private_api.py
Normal file
68
talon/cursorless-talon/src/private_api/private_api.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
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,
|
||||
)
|
42
talon/cursorless-talon/src/public_api.py
Normal file
42
talon/cursorless-talon/src/public_api.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
from talon import Module, actions
|
||||
|
||||
from .targets.target_types import (
|
||||
CursorlessDestination,
|
||||
InsertionMode,
|
||||
ListTarget,
|
||||
PrimitiveDestination,
|
||||
PrimitiveTarget,
|
||||
RangeTarget,
|
||||
)
|
||||
|
||||
mod = Module()
|
||||
|
||||
|
||||
@mod.action_class
|
||||
class Actions:
|
||||
def cursorless_create_destination(
|
||||
target: ListTarget | RangeTarget | PrimitiveTarget, # pyright: ignore [reportGeneralTypeIssues]
|
||||
insertion_mode: InsertionMode = "to",
|
||||
) -> CursorlessDestination:
|
||||
"""Cursorless: Create destination from target"""
|
||||
return PrimitiveDestination(insertion_mode, target)
|
||||
|
||||
|
||||
@mod.action_class
|
||||
class CommandActions:
|
||||
def cursorless_custom_command(
|
||||
content: str, # pyright: ignore [reportGeneralTypeIssues]
|
||||
arg1: Optional[Any] = None,
|
||||
arg2: Optional[Any] = None,
|
||||
arg3: Optional[Any] = None,
|
||||
):
|
||||
"""Cursorless: Run custom parsed command"""
|
||||
actions.user.private_cursorless_command_and_wait(
|
||||
{
|
||||
"name": "parsed",
|
||||
"content": content,
|
||||
"arguments": [arg for arg in [arg1, arg2, arg3] if arg is not None],
|
||||
}
|
||||
)
|
27
talon/cursorless-talon/src/scope_visualizer.py
Normal file
27
talon/cursorless-talon/src/scope_visualizer.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
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"
|
||||
)
|
14
talon/cursorless-talon/src/snippet_cursorless.talon
Normal file
14
talon/cursorless-talon/src/snippet_cursorless.talon
Normal file
|
@ -0,0 +1,14 @@
|
|||
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.cursorless_insertion_snippet>:
|
||||
user.private_cursorless_insert_snippet(cursorless_insertion_snippet)
|
||||
|
||||
{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{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.cursorless_target>:
|
||||
user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)
|
224
talon/cursorless-talon/src/snippets.py
Normal file
224
talon/cursorless-talon/src/snippets.py
Normal file
|
@ -0,0 +1,224 @@
|
|||
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}) [<user.cursorless_destination>]"
|
||||
)
|
||||
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 <snippet_description> with phrase <text>"""
|
||||
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 <name>"""
|
||||
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 <body>"""
|
||||
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 <name>"""
|
||||
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 <body>"""
|
||||
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 <name>"""
|
||||
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 <name>"""
|
||||
snippet: CommunityWrapperSnippet = actions.user.get_wrapper_snippet(name)
|
||||
actions.user.cursorless_wrap_with_snippet(
|
||||
snippet.body, target, snippet.variable_name, snippet.scope
|
||||
)
|
13
talon/cursorless-talon/src/snippets_community.talon
Normal file
13
talon/cursorless-talon/src/snippets_community.talon
Normal file
|
@ -0,0 +1,13 @@
|
|||
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.cursorless_destination>:
|
||||
user.private_cursorless_insert_community_snippet(snippet, cursorless_destination)
|
||||
|
||||
{user.snippet_wrapper} {user.cursorless_wrap_action} <user.cursorless_target>:
|
||||
user.private_cursorless_wrap_with_community_snippet(snippet_wrapper, cursorless_target)
|
279
talon/cursorless-talon/src/spoken_forms.json
Normal file
279
talon/cursorless-talon/src/spoken_forms.json
Normal file
|
@ -0,0 +1,279 @@
|
|||
{
|
||||
"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",
|
||||
"follow split": "followLinkAside",
|
||||
"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",
|
||||
"short block": "boundedParagraph",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
237
talon/cursorless-talon/src/spoken_forms.py
Normal file
237
talon/cursorless-talon/src/spoken_forms.py
Normal file
|
@ -0,0 +1,237 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from typing import Callable, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
from talon import app, cron, fs, registry
|
||||
|
||||
from .actions.actions import ACTION_LIST_NAMES
|
||||
from .csv_overrides import (
|
||||
SPOKEN_FORM_HEADER,
|
||||
ListToSpokenForms,
|
||||
SpokenFormEntry,
|
||||
init_csv_and_watch_changes,
|
||||
)
|
||||
from .get_grapheme_spoken_form_entries import (
|
||||
get_grapheme_spoken_form_entries,
|
||||
get_graphemes_talon_list,
|
||||
grapheme_capture_name,
|
||||
)
|
||||
from .marks.decorated_mark import init_hats
|
||||
from .spoken_forms_output import SpokenFormsOutput
|
||||
from .spoken_scope_forms import init_scope_spoken_forms
|
||||
|
||||
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",
|
||||
**{
|
||||
action_list_name: "action"
|
||||
for action_list_name in ACTION_LIST_NAMES
|
||||
if action_list_name != "custom_action"
|
||||
},
|
||||
"custom_action": "customAction",
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
graphemes_talon_list = get_graphemes_talon_list()
|
||||
|
||||
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
|
||||
],
|
||||
*get_grapheme_spoken_form_entries(graphemes_talon_list),
|
||||
]
|
||||
)
|
||||
|
||||
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
|
||||
init_scope_spoken_forms(graphemes_talon_list)
|
||||
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",
|
||||
pluralize_lists=[
|
||||
"selectable_only_paired_delimiter",
|
||||
"wrapper_selectable_paired_delimiter",
|
||||
],
|
||||
),
|
||||
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",
|
||||
"surrounding_pair_scope_type",
|
||||
],
|
||||
extra_allowed_values=[
|
||||
"private.fieldAccess",
|
||||
"private.switchStatementSubject",
|
||||
"textFragment",
|
||||
"disqualifyDelimiter",
|
||||
],
|
||||
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"],
|
||||
),
|
||||
]
|
||||
|
||||
init_scope_spoken_forms(graphemes_talon_list)
|
||||
update_spoken_forms_output()
|
||||
initialized = True
|
||||
|
||||
|
||||
def on_watch(path, flags):
|
||||
if JSON_FILE.match(path):
|
||||
update()
|
||||
|
||||
|
||||
update_captures_cron = None
|
||||
|
||||
|
||||
def update_captures_debounced(updated_captures: set[str]):
|
||||
if grapheme_capture_name not in updated_captures:
|
||||
return
|
||||
|
||||
global update_captures_cron
|
||||
cron.cancel(update_captures_cron)
|
||||
update_captures_cron = cron.after("100ms", update_captures)
|
||||
|
||||
|
||||
def update_captures():
|
||||
global update_captures_cron
|
||||
update_captures_cron = None
|
||||
|
||||
update()
|
||||
|
||||
|
||||
def on_ready():
|
||||
update()
|
||||
|
||||
registry.register("update_captures", update_captures_debounced)
|
||||
|
||||
fs.watch(str(JSON_FILE.parent), on_watch)
|
||||
|
||||
|
||||
app.register("ready", on_ready)
|
48
talon/cursorless-talon/src/spoken_forms_output.py
Normal file
48
talon/cursorless-talon/src/spoken_forms_output.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
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 SpokenFormOutputEntry(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[SpokenFormOutputEntry]):
|
||||
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)
|
49
talon/cursorless-talon/src/spoken_scope_forms.py
Normal file
49
talon/cursorless-talon/src/spoken_scope_forms.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
from talon import Context, scope
|
||||
|
||||
from .csv_overrides import csv_get_ctx, csv_get_normalized_ctx
|
||||
|
||||
|
||||
def init_scope_spoken_forms(graphemes_talon_list: dict[str, str]):
|
||||
create_flattened_talon_list(csv_get_ctx(), graphemes_talon_list)
|
||||
if is_cursorless_test_mode():
|
||||
create_flattened_talon_list(csv_get_normalized_ctx(), graphemes_talon_list)
|
||||
|
||||
|
||||
def create_flattened_talon_list(ctx: Context, graphemes_talon_list: dict[str, str]):
|
||||
lists_to_merge = {
|
||||
"cursorless_scope_type": "simple",
|
||||
"cursorless_selectable_only_paired_delimiter": "surroundingPair",
|
||||
"cursorless_wrapper_selectable_paired_delimiter": "surroundingPair",
|
||||
"cursorless_surrounding_pair_scope_type": "surroundingPair",
|
||||
}
|
||||
# If the user have no custom regex scope type, then that list is missing from the context
|
||||
if "user.cursorless_custom_regex_scope_type" in ctx.lists.keys(): # noqa: SIM118
|
||||
lists_to_merge["cursorless_custom_regex_scope_type"] = "customRegex"
|
||||
|
||||
scope_types_singular: dict[str, str] = {}
|
||||
scope_types_plural: dict[str, str] = {}
|
||||
|
||||
for list_name, prefix in lists_to_merge.items():
|
||||
for key, value in ctx.lists[f"user.{list_name}"].items():
|
||||
scope_types_singular[key] = f"{prefix}.{value}"
|
||||
for key, value in ctx.lists[f"user.{list_name}_plural"].items():
|
||||
scope_types_plural[key] = f"{prefix}.{value}"
|
||||
|
||||
glyph_singular_spoken_forms = ctx.lists["user.cursorless_glyph_scope_type"]
|
||||
glyph_plural_spoken_forms = ctx.lists["user.cursorless_glyph_scope_type_plural"]
|
||||
|
||||
for grapheme_key, grapheme_value in graphemes_talon_list.items():
|
||||
value = f"glyph.{grapheme_value}"
|
||||
for glyph in glyph_singular_spoken_forms:
|
||||
key = f"{glyph} {grapheme_key}"
|
||||
scope_types_singular[key] = value
|
||||
for glyph in glyph_plural_spoken_forms:
|
||||
key = f"{glyph} {grapheme_key}"
|
||||
scope_types_plural[key] = value
|
||||
|
||||
ctx.lists["user.cursorless_scope_type_flattened"] = scope_types_singular
|
||||
ctx.lists["user.cursorless_scope_type_flattened_plural"] = scope_types_plural
|
||||
|
||||
|
||||
def is_cursorless_test_mode():
|
||||
return "user.cursorless_spoken_form_test" in scope.get("mode")
|
70
talon/cursorless-talon/src/targets/destination.py
Normal file
70
talon/cursorless-talon/src/targets/destination.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
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_insertion_mode> <user.cursorless_target> "
|
||||
"({user.cursorless_list_connective} <user.cursorless_insertion_mode> <user.cursorless_target>)*"
|
||||
)
|
||||
)
|
||||
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)
|
25
talon/cursorless-talon/src/targets/primitive_target.py
Normal file
25
talon/cursorless-talon/src/targets/primitive_target.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from talon import Module
|
||||
|
||||
from .target_types import PrimitiveTarget
|
||||
|
||||
mod = Module()
|
||||
|
||||
|
||||
@mod.capture(
|
||||
rule=(
|
||||
"<user.cursorless_modifier>+ [<user.cursorless_mark>] | <user.cursorless_mark>"
|
||||
)
|
||||
)
|
||||
def cursorless_primitive_target(m) -> PrimitiveTarget:
|
||||
mark = getattr(m, "cursorless_mark", None)
|
||||
modifiers = getattr(m, "cursorless_modifier_list", None)
|
||||
|
||||
# for grammar performance reasons, the literal modifier is exposed to Talon as a mark,
|
||||
# but is converted to a modifier in the engine.
|
||||
if mark is not None and mark["type"] == "literal":
|
||||
if modifiers is None:
|
||||
modifiers = []
|
||||
modifiers.append(mark["modifier"])
|
||||
mark = None
|
||||
|
||||
return PrimitiveTarget(mark, modifiers)
|
66
talon/cursorless-talon/src/targets/range_target.py
Normal file
66
talon/cursorless-talon/src/targets/range_target.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
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="[<user.cursorless_range_type>] <user.cursorless_range_connective> | <user.cursorless_range_type>"
|
||||
)
|
||||
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=(
|
||||
"[<user.cursorless_primitive_target>] <user.cursorless_range_connective_with_type> <user.cursorless_primitive_target>"
|
||||
)
|
||||
)
|
||||
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,
|
||||
)
|
20
talon/cursorless-talon/src/targets/range_type.py
Normal file
20
talon/cursorless-talon/src/targets/range_type.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
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]
|
35
talon/cursorless-talon/src/targets/target.py
Normal file
35
talon/cursorless-talon/src/targets/target.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
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=("<user.cursorless_range_target> | <user.cursorless_primitive_target>")
|
||||
)
|
||||
def cursorless_primitive_or_range_target(m) -> Union[RangeTarget, PrimitiveTarget]:
|
||||
return m[0]
|
||||
|
||||
|
||||
@mod.capture(
|
||||
rule=(
|
||||
"<user.cursorless_primitive_or_range_target> "
|
||||
"({user.cursorless_list_connective} <user.cursorless_primitive_or_range_target>)*"
|
||||
)
|
||||
)
|
||||
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)
|
74
talon/cursorless-talon/src/targets/target_types.py
Normal file
74
talon/cursorless-talon/src/targets/target_types.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
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,
|
||||
]
|
||||
|
||||
InsertionMode = Literal["to", "before", "after"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimitiveDestination:
|
||||
type = "primitive"
|
||||
insertionMode: InsertionMode
|
||||
target: Union[ListTarget, RangeTarget, PrimitiveTarget]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImplicitDestination:
|
||||
type = "implicit"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListDestination:
|
||||
type = "list"
|
||||
destinations: list[PrimitiveDestination]
|
||||
|
||||
|
||||
CursorlessDestination = Union[
|
||||
ListDestination,
|
||||
PrimitiveDestination,
|
||||
ImplicitDestination,
|
||||
]
|
21
talon/cursorless-talon/src/terms.py
Normal file
21
talon/cursorless-talon/src/terms.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
"""
|
||||
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",
|
||||
]
|
0
talon/cursorless-talon/src/vendor/__init__.py
vendored
Normal file
0
talon/cursorless-talon/src/vendor/__init__.py
vendored
Normal file
175
talon/cursorless-talon/src/vendor/inflection.py
vendored
Normal file
175
talon/cursorless-talon/src/vendor/inflection.py
vendored
Normal file
|
@ -0,0 +1,175 @@
|
|||
# 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")
|
137
talon/cursorless-talon/src/vendor/jstyleson.py
vendored
Normal file
137
talon/cursorless-talon/src/vendor/jstyleson.py
vendored
Normal file
|
@ -0,0 +1,137 @@
|
|||
# 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)
|
1
talon/cursorless-talon/src/versions.py
Normal file
1
talon/cursorless-talon/src/versions.py
Normal file
|
@ -0,0 +1 @@
|
|||
COMMAND_VERSION = 7
|
Loading…
Add table
Reference in a new issue