Skip to content

Commit 6831391

Browse files
AndreasArvidssonpokeypre-commit-ci-lite[bot]
authored
Talon side spoken form tests (#1637)
- Fixes #449 - Fixes #1675 ## Potential manual tests to add For these, just bake payload and spoken form into `const`s in the test itself - [x] "slice past" - [x] "two tokens forward" - [x] "one token" - [x] "take inside pair" ## Checklist - [x] Handle if Talon was in sleep mode initially - [x] Add deprecation warning if user says "to after"? I believe we support this today, and some users are still saying that, as indicated by slack questions from the past month. We could probably have this as an alternate spoken form for "after" and show a warning if they use it - [x] Stop using `generateSpokenForms` to check whether to run a test; instead read `spokenFormError` field on recorded test - [x] How do we actually want to launch this? Launch config in vscode and then? - [x] Add notifications when test start and stop - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) - [x] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 868ff28 commit 6831391

File tree

36 files changed

+858
-272
lines changed

36 files changed

+858
-272
lines changed

.vscode/launch.json

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
]
2525
},
2626
{
27-
"name": "Extension Tests",
27+
"name": "Extension tests",
2828
"type": "extensionHost",
2929
"request": "launch",
3030
"env": {
@@ -34,7 +34,7 @@
3434
"args": [
3535
"--profile=cursorlessDevelopment",
3636
"--extensionDevelopmentPath=${workspaceFolder}/packages/cursorless-vscode/dist",
37-
"--extensionTestsPath=${workspaceFolder}/packages/test-harness/out/runners/all"
37+
"--extensionTestsPath=${workspaceFolder}/packages/test-harness/out/runners/extensionTests"
3838
],
3939
"outFiles": ["${workspaceFolder}/**/out/**/*.js"],
4040
"preLaunchTask": "${defaultBuildTask}",
@@ -43,6 +43,22 @@
4343
"!**/node_modules/**"
4444
]
4545
},
46+
{
47+
"type": "node",
48+
"request": "launch",
49+
"name": "Talon tests",
50+
"program": "${workspaceFolder}/packages/test-harness/out/scripts/runTalonTests",
51+
"env": {
52+
"CURSORLESS_TEST": "true",
53+
"CURSORLESS_REPO_ROOT": "${workspaceFolder}"
54+
},
55+
"outFiles": ["${workspaceFolder}/**/out/**/*.js"],
56+
"preLaunchTask": "${defaultBuildTask}",
57+
"resolveSourceMapLocations": [
58+
"${workspaceFolder}/**",
59+
"!**/node_modules/**"
60+
]
61+
},
4662
{
4763
"type": "node",
4864
"request": "launch",

cursorless-talon-dev/src/cursorless_dev.talon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
tag: user.cursorless
22
-
33

4+
# Activate this if you want the default Cursorless vocabulary
5+
# tag(): user.cursorless_default_vocabulary
6+
47
{user.cursorless_homophone} record:
58
user.run_rpc_command("cursorless.recordTestCase")
69
{user.cursorless_homophone} record one:
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from talon import Context
2+
3+
ctx = Context()
4+
ctx.matches = r"""
5+
tag: user.cursorless_default_vocabulary
6+
"""
7+
8+
# https://github.com/talonhub/community/blob/9acb6c9659bb0c9b794a7b7126d025603b4ed726/core/keys/keys.py#L10
9+
initial_default_alphabet = "air bat cap drum each fine gust harp sit jury crunch look made near odd pit quench red sun trap urge vest whale plex yank zip".split()
10+
11+
# https://github.com/talonhub/community/blob/9acb6c9659bb0c9b794a7b7126d025603b4ed726/core/keys/keys.py#L24
12+
digits = "zero one two three four five six seven eight nine".split()
13+
14+
# https://github.com/talonhub/community/blob/9acb6c9659bb0c9b794a7b7126d025603b4ed726/core/keys/keys.py#L139C1-L171C2
15+
punctuation_words = {
16+
# TODO: I'm not sure why we need these, I think it has something to do with
17+
# Dragon. Possibly it has been fixed by later improvements to talon? -rntz
18+
# "`": "`",
19+
# ",": ",", # <== these things
20+
"back tick": "`",
21+
"comma": ",",
22+
# Workaround for issue with conformer b-series; see #946
23+
"coma": ",",
24+
"period": ".",
25+
"full stop": ".",
26+
"semicolon": ";",
27+
"colon": ":",
28+
"forward slash": "/",
29+
"question mark": "?",
30+
"exclamation mark": "!",
31+
"exclamation point": "!",
32+
"asterisk": "*",
33+
"hash sign": "#",
34+
"number sign": "#",
35+
"percent sign": "%",
36+
"at sign": "@",
37+
"and sign": "&",
38+
"ampersand": "&",
39+
# Currencies
40+
"dollar sign": "$",
41+
"pound sign": "£",
42+
"hyphen": "-",
43+
"L paren": "(",
44+
"left paren": "(",
45+
"R paren": ")",
46+
"right paren": ")",
47+
}
48+
49+
# https://github.com/talonhub/community/blob/9acb6c9659bb0c9b794a7b7126d025603b4ed726/core/keys/keys.py#L172
50+
symbol_key_words = {
51+
"dot": ".",
52+
"point": ".",
53+
"quote": "'",
54+
"question": "?",
55+
"apostrophe": "'",
56+
"L square": "[",
57+
"left square": "[",
58+
"square": "[",
59+
"R square": "]",
60+
"right square": "]",
61+
"slash": "/",
62+
"backslash": "\\",
63+
"minus": "-",
64+
"dash": "-",
65+
"equals": "=",
66+
"plus": "+",
67+
"grave": "`",
68+
"tilde": "~",
69+
"bang": "!",
70+
"down score": "_",
71+
"underscore": "_",
72+
"paren": "(",
73+
"brace": "{",
74+
"left brace": "{",
75+
"brack": "{",
76+
"bracket": "{",
77+
"left bracket": "{",
78+
"r brace": "}",
79+
"right brace": "}",
80+
"r brack": "}",
81+
"r bracket": "}",
82+
"right bracket": "}",
83+
"angle": "<",
84+
"left angle": "<",
85+
"less than": "<",
86+
"rangle": ">",
87+
"R angle": ">",
88+
"right angle": ">",
89+
"greater than": ">",
90+
"star": "*",
91+
"hash": "#",
92+
"percent": "%",
93+
"caret": "^",
94+
"amper": "&",
95+
"pipe": "|",
96+
"dub quote": '"',
97+
"double quote": '"',
98+
# Currencies
99+
"dollar": "$",
100+
"pound": "£",
101+
}
102+
103+
any_alphanumeric_keys = {
104+
**{w: chr(ord("a") + i) for i, w in enumerate(initial_default_alphabet)},
105+
**{digits[i]: str(i) for i in range(10)},
106+
**punctuation_words,
107+
**symbol_key_words,
108+
}
109+
110+
111+
@ctx.capture("user.any_alphanumeric_key", rule="|".join(any_alphanumeric_keys.keys()))
112+
def any_alphanumeric_key(m) -> str:
113+
return any_alphanumeric_keys[str(m)]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
from typing import Any
3+
4+
from talon import Context, Module, actions, scope
5+
6+
mod = Module()
7+
8+
mod.mode(
9+
"cursorless_spoken_form_test",
10+
"Used to run tests on the Cursorless spoken forms/grammar",
11+
)
12+
13+
ctx = Context()
14+
15+
ctx.matches = r"""
16+
mode: user.cursorless_spoken_form_test
17+
"""
18+
19+
ctx.tags = [
20+
"user.cursorless",
21+
"user.cursorless_default_vocabulary",
22+
]
23+
24+
# Keeps track of the microphone that was active before the spoken form test mode
25+
saved_microphone = "None"
26+
27+
# Keeps a list of modes that were active before the spoken form test mode was
28+
# enabled
29+
saved_modes = []
30+
31+
# Keeps a list of commands run over the course of a spoken form test
32+
commands_run = []
33+
34+
35+
@ctx.action_class("user")
36+
class UserActions:
37+
def did_emit_pre_phrase_signal():
38+
return True
39+
40+
def private_cursorless_run_rpc_command_and_wait(
41+
command_id: str, arg1: Any, arg2: Any = None
42+
):
43+
commands_run.append(arg1)
44+
45+
def private_cursorless_run_rpc_command_no_wait(
46+
command_id: str, arg1: Any, arg2: Any = None
47+
):
48+
commands_run.append(arg1)
49+
50+
def private_cursorless_run_rpc_command_get(
51+
command_id: str, arg1: Any, arg2: Any = None
52+
) -> Any:
53+
commands_run.append(arg1)
54+
55+
56+
@mod.action_class
57+
class Actions:
58+
def private_cursorless_spoken_form_test_mode(enable: bool):
59+
"""Enable/disable Cursorless spoken form test mode"""
60+
global saved_modes, saved_microphone
61+
62+
if enable:
63+
saved_modes = scope.get("mode")
64+
saved_microphone = actions.sound.active_microphone()
65+
66+
disable_modes()
67+
actions.mode.enable("user.cursorless_spoken_form_test")
68+
actions.sound.set_microphone("None")
69+
70+
actions.app.notify(
71+
"Cursorless spoken form tests are running. Talon microphone is disabled."
72+
)
73+
else:
74+
actions.mode.disable("user.cursorless_spoken_form_test")
75+
enable_modes()
76+
actions.sound.set_microphone(saved_microphone)
77+
78+
actions.app.notify(
79+
"Cursorless spoken form tests are done. Talon microphone is re-enabled."
80+
)
81+
82+
def private_cursorless_spoken_form_test(phrase: str):
83+
"""Run Cursorless spoken form test"""
84+
global commands_run
85+
commands_run = []
86+
87+
try:
88+
actions.mimic(phrase)
89+
print(json.dumps(commands_run))
90+
except Exception as e:
91+
print(f"{e.__class__.__name__}: {e}")
92+
93+
94+
def enable_modes():
95+
for mode in saved_modes:
96+
try:
97+
actions.mode.enable(mode)
98+
except Exception:
99+
pass
100+
101+
102+
def disable_modes():
103+
for mode in saved_modes:
104+
try:
105+
actions.mode.disable(mode)
106+
except Exception:
107+
pass

cursorless-talon/src/actions/swap.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from talon import Module
22

3-
from ..primitive_target import create_base_target
3+
from ..primitive_target import create_implicit_target
44

55
mod = Module()
66

@@ -20,6 +20,6 @@ def cursorless_swap_targets(m) -> list[dict]:
2020
target_list = m.cursorless_target_list
2121

2222
if len(target_list) == 1:
23-
target_list = [create_base_target()] + target_list
23+
target_list = [create_implicit_target()] + target_list
2424

2525
return target_list

cursorless-talon/src/apps/cursorless_vscode.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from talon import Context, actions, app
22

33
from ..actions.get_text import get_text
4-
from ..cursorless_command_server import run_rpc_command_no_wait
54

65
ctx = Context()
76

@@ -21,12 +20,14 @@ def cursorless_private_run_find_action(target: dict):
2120
if len(search_text) > 200:
2221
search_text = search_text[:200]
2322
app.notify("Search text is longer than 200 characters; truncating")
24-
run_rpc_command_no_wait("actions.find")
23+
actions.user.private_cursorless_run_rpc_command_no_wait("actions.find")
2524
actions.sleep("50ms")
2625
actions.insert(search_text)
2726

2827
def cursorless_show_settings_in_ide():
2928
"""Show Cursorless-specific settings in ide"""
30-
run_rpc_command_no_wait("workbench.action.openGlobalSettings")
29+
actions.user.private_cursorless_run_rpc_command_no_wait(
30+
"workbench.action.openGlobalSettings"
31+
)
3132
actions.sleep("250ms")
3233
actions.insert("cursorless")

cursorless-talon/src/cheatsheet/cheat_sheet.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import webbrowser
22
from pathlib import Path
33

4-
from talon import Context, Module, app
4+
from talon import Context, Module, actions, app
55

6-
from ..cursorless_command_server import run_rpc_command_and_wait
76
from .get_list import get_list, get_lists
87
from .sections.actions import get_actions
98
from .sections.compound_targets import get_compound_targets
@@ -54,7 +53,7 @@ def cursorless_cheat_sheet_show_html():
5453

5554
cheatsheet_out_dir.mkdir(parents=True, exist_ok=True)
5655
cheatsheet_out_path = cheatsheet_out_dir / cheatsheet_filename
57-
run_rpc_command_and_wait(
56+
actions.user.private_cursorless_run_rpc_command_and_wait(
5857
"cursorless.showCheatsheet",
5958
{
6059
"version": 0,
@@ -66,7 +65,7 @@ def cursorless_cheat_sheet_show_html():
6665

6766
def cursorless_cheat_sheet_update_json():
6867
"""Update default cursorless cheatsheet json (for developer use only)"""
69-
run_rpc_command_and_wait(
68+
actions.user.private_cursorless_run_rpc_command_and_wait(
7069
"cursorless.internal.updateCheatsheetDefaults",
7170
cursorless_cheat_sheet_get_json(),
7271
)

cursorless-talon/src/command.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22

33
from talon import Module, actions, speech_system
44

5-
from .cursorless_command_server import (
6-
run_rpc_command_and_wait,
7-
run_rpc_command_get,
8-
run_rpc_command_no_wait,
9-
)
105
from .primitive_target import create_implicit_target
116

127
mod = Module()
@@ -73,7 +68,7 @@ def cursorless_single_target_command_get(
7368
arg3: Any = NotSet,
7469
):
7570
"""Execute single-target cursorless command and return result"""
76-
return run_rpc_command_get(
71+
return actions.user.private_cursorless_run_rpc_command_get(
7772
CURSORLESS_COMMAND_ID,
7873
construct_cursorless_command_argument(
7974
action=action,
@@ -101,7 +96,7 @@ def cursorless_multiple_target_command(
10196
arg3: Any = NotSet,
10297
):
10398
"""Execute multi-target cursorless command"""
104-
run_rpc_command_and_wait(
99+
actions.user.private_cursorless_run_rpc_command_and_wait(
105100
CURSORLESS_COMMAND_ID,
106101
construct_cursorless_command_argument(
107102
action=action,
@@ -118,7 +113,7 @@ def cursorless_multiple_target_command_no_wait(
118113
arg3: Any = NotSet,
119114
):
120115
"""Execute multi-target cursorless command"""
121-
run_rpc_command_no_wait(
116+
actions.user.private_cursorless_run_rpc_command_no_wait(
122117
CURSORLESS_COMMAND_ID,
123118
construct_cursorless_command_argument(
124119
action=action,

0 commit comments

Comments
 (0)