Skip to content

Commit 1b9a97b

Browse files
committed
Cursorless tutorial
1 parent 8a42e8e commit 1b9a97b

33 files changed

+1694
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from typing import Callable
2+
3+
from talon import registry
4+
5+
from .actions.actions import ACTION_LIST_NAMES
6+
from .conventions import get_cursorless_list_name
7+
from .modifiers.containing_scope import SCOPE_LIST_NAMES
8+
9+
10+
def make_cursorless_list_reverse_look_up(*raw_list_names: str):
11+
return make_list_reverse_look_up(
12+
*[get_cursorless_list_name(raw_list_name) for raw_list_name in raw_list_names]
13+
)
14+
15+
16+
def make_list_reverse_look_up(*list_names: str):
17+
"""
18+
Given a list of talon list names, returns a function that does a reverse
19+
look-up in all lists to find the spoken form for its input.
20+
"""
21+
22+
def return_func(argument: str):
23+
for list_name in list_names:
24+
for spoken_form, value in registry.lists[list_name][-1].items():
25+
if value == argument:
26+
return list_name, spoken_form
27+
28+
raise LookupError(f"Unknown identifier `{argument}`")
29+
30+
return return_func
31+
32+
33+
lookup_action = make_cursorless_list_reverse_look_up(*ACTION_LIST_NAMES)
34+
lookup_scope_type = make_cursorless_list_reverse_look_up(*SCOPE_LIST_NAMES)
35+
36+
37+
def cursorless_command_to_spoken_form(command: dict):
38+
action_list_name, action_spoken_form = lookup_action(command["action"])
39+
targets_spoken_form = targets_processor_map[action_list_name](command["targets"])
40+
return f"{action_spoken_form} {targets_spoken_form}"
41+
42+
43+
def process_simple_action_targets(targets: list[dict]):
44+
return process_target(targets[0])
45+
46+
47+
raw_targets_processor_map: dict[str, Callable[[list[dict]], str]] = {
48+
"simple_action": process_simple_action_targets,
49+
"positional_action": process_simple_action_targets,
50+
"callback_action": process_simple_action_targets,
51+
"makeshift_action": process_simple_action_targets,
52+
"custom_action": process_simple_action_targets,
53+
"swap_action": {"swap": "swapTargets"},
54+
"move_bring_action": {"bring": "replaceWithTarget", "move": "moveToTarget"},
55+
"wrap_action": {"wrap": "wrapWithPairedDelimiter", "repack": "rewrap"},
56+
"insert_snippet_action": {"snippet": "insertSnippet"},
57+
"reformat_action": {"format": "applyFormatter"},
58+
}
59+
60+
targets_processor_map = {
61+
get_cursorless_list_name(key): value
62+
for key, value in raw_targets_processor_map.items()
63+
}
64+
65+
66+
def process_target(target: dict):
67+
if target["type"] == "primitive":
68+
return process_primitive_target(target)
69+
elif target["type"] == "range":
70+
return process_range_target(target)
71+
elif target["type"] == "list":
72+
return process_list_target(target)
73+
else:
74+
raise Exception(f"Unknown target type {target['type']}")
75+
76+
77+
class MarkProcessor:
78+
field_name = "mark"
79+
80+
def __init__(self):
81+
self.process_character = make_list_reverse_look_up(
82+
"user.letter",
83+
"user.number_key",
84+
"user.symbol_key",
85+
)
86+
87+
def process_value(self, field_value: dict):
88+
mark_type = field_value["type"]
89+
if mark_type == "decoratedSymbol":
90+
return self.process_decorated_symbol(field_value)
91+
elif mark_type == "that":
92+
return self.process_that_mark(field_value)
93+
elif mark_type == "source":
94+
return self.process_source_mark(field_value)
95+
elif mark_type == "cursor":
96+
return self.process_cursor_mark(field_value)
97+
98+
def process_decorated_symbol(self, field_value: dict):
99+
# TODO: Handle `symbolColor`
100+
return self.process_character(field_value["character"])[1]
101+
102+
def process_that_mark(self, field_value: dict):
103+
# TODO: Handle this case properly using users custom term
104+
return "that"
105+
106+
def process_source_mark(self, field_value: dict):
107+
# TODO: Handle this case properly using users custom term
108+
return "source"
109+
110+
def process_cursor_mark(self, field_value: dict):
111+
# TODO: Handle this case properly using users custom term
112+
return "this"
113+
114+
115+
field_processors = [MarkProcessor()]
116+
117+
118+
def process_primitive_target(target: dict):
119+
field_spoken_forms = [
120+
field_processor.process_value(target.get(field_processor.field_name, None))
121+
for field_processor in field_processors
122+
]
123+
124+
return " ".join(
125+
[spoken_form for spoken_form in field_spoken_forms if spoken_form is not None]
126+
)

cursorless-talon/src/tutorial.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import json
2+
import re
3+
from pathlib import Path
4+
from typing import Callable
5+
6+
import yaml
7+
from talon import actions, app
8+
9+
from .cursorless_command_to_spoken_form import (
10+
cursorless_command_to_spoken_form,
11+
lookup_action,
12+
lookup_scope_type,
13+
)
14+
15+
regex = re.compile(r"\{(\w+):([^}]+)\}")
16+
tutorial_dir = Path(
17+
"/Users/pokey/src/cursorless-vscode/src/test/suite/fixtures/recorded/tutorial/unit-2-basic-coding"
18+
)
19+
20+
21+
def process_literal_step(argument: str):
22+
return f"<cmd@{argument}/>"
23+
24+
25+
def process_action(argument: str):
26+
_, spoken_form = lookup_action(argument)
27+
return f'<*"{spoken_form}"/>'
28+
29+
30+
def process_scope_type(argument: str):
31+
_, spoken_form = lookup_scope_type(argument)
32+
return f'<*"{spoken_form}"/>'
33+
34+
35+
def process_cursorless_command_step(argument: str):
36+
step_fixture = yaml.safe_load((tutorial_dir / argument).read_text())
37+
return f"<cmd@{cursorless_command_to_spoken_form(step_fixture['command'])}/>"
38+
39+
40+
interpolation_processor_map: dict[str, Callable[[str], str]] = {
41+
"literalStep": process_literal_step,
42+
"action": process_action,
43+
"scopeType": process_scope_type,
44+
"step": process_cursorless_command_step,
45+
}
46+
47+
48+
def process_tutorial_step(raw: str):
49+
print(f"{raw=}")
50+
current_index = 0
51+
content = ""
52+
for match in regex.finditer(raw):
53+
content += raw[current_index : match.start()]
54+
content += interpolation_processor_map[match.group(1)](match.group(2))
55+
current_index = match.end()
56+
content += raw[current_index : len(raw)]
57+
print(f"{content=}")
58+
59+
return {
60+
"content": content,
61+
"restore_callback": print,
62+
"modes": ["command"],
63+
"app": "Code",
64+
"context_hint": "Please open VSCode and enter command mode",
65+
}
66+
67+
68+
def get_basic_coding_walkthrough():
69+
with open(tutorial_dir / "script.json") as f:
70+
script = json.load(f)
71+
72+
return [
73+
actions.user.hud_create_walkthrough_step(**process_tutorial_step(step))
74+
for step in script
75+
]
76+
77+
78+
def on_ready():
79+
actions.user.hud_add_lazy_walkthrough(
80+
"Cursorless basic coding", get_basic_coding_walkthrough
81+
)
82+
83+
84+
app.register("ready", on_ready)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from talon import Context, Module
2+
3+
mod = Module()
4+
ctx = Context()
5+
6+
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
7+
ctx.list["user.cursorless_walkthrough_list"] = {
8+
"spoken form": "whatever",
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
==================================================
2+
========== ==========
3+
========== Welcome to Cursorless! ==========
4+
========== ==========
5+
========== Let's start using marks ==========
6+
========== ==========
7+
========== so we can navigate around ==========
8+
========== ==========
9+
========== without lifting a finger! ==========
10+
========== ==========
11+
==================================================
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def print_color(color, invert=False):
2+
if invert:
3+
print(invert_color(color))
4+
else:
5+
print(color)
6+
7+
8+
def invert_color(color):
9+
if color == "black":
10+
return "white"
11+
12+
13+
print_color("black")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
languageId: python
2+
command:
3+
version: 1
4+
spokenForm: bring block made
5+
action: replaceWithTarget
6+
targets:
7+
- type: primitive
8+
selectionType: paragraph
9+
mark: {type: decoratedSymbol, symbolColor: default, character: m}
10+
- {type: primitive, isImplicit: true}
11+
initialState:
12+
documentContents: |+
13+
from talon import Context, Module
14+
15+
mod = Module()
16+
ctx = Context()
17+
18+
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
19+
ctx.list['user.cursorless_walkthrough_list'] = {
20+
"spoken form": "whatever",
21+
}
22+
23+
selections:
24+
- anchor: {line: 10, character: 0}
25+
active: {line: 10, character: 0}
26+
marks:
27+
default.m:
28+
start: {line: 5, character: 0}
29+
end: {line: 5, character: 3}
30+
finalState:
31+
documentContents: |-
32+
from talon import Context, Module
33+
34+
mod = Module()
35+
ctx = Context()
36+
37+
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
38+
ctx.list['user.cursorless_walkthrough_list'] = {
39+
"spoken form": "whatever",
40+
}
41+
42+
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
43+
ctx.list['user.cursorless_walkthrough_list'] = {
44+
"spoken form": "whatever",
45+
}
46+
selections:
47+
- anchor: {line: 13, character: 1}
48+
active: {line: 13, character: 1}
49+
thatMark:
50+
- anchor: {line: 10, character: 0}
51+
active: {line: 13, character: 1}
52+
sourceMark:
53+
- anchor: {line: 5, character: 0}
54+
active: {line: 8, character: 1}
55+
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: m}, selectionType: paragraph, position: contents, insideOutsideType: null, modifier: {type: identity}}, {type: primitive, mark: {type: cursor}, selectionType: paragraph, position: contents, insideOutsideType: null, modifier: {type: identity}}]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
languageId: python
2+
command:
3+
version: 1
4+
spokenForm: clear core sun
5+
action: clearAndSetSelection
6+
targets:
7+
- type: primitive
8+
modifier: {type: surroundingPair, delimiter: any, delimiterInclusion: interiorOnly}
9+
mark: {type: decoratedSymbol, symbolColor: default, character: s}
10+
initialState:
11+
documentContents: |-
12+
from talon import Context, Module
13+
14+
mod = Module()
15+
ctx = Context()
16+
17+
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
18+
ctx.list['user.cursorless_walkthrough_list'] = {
19+
"spoken form": "whatever",
20+
}
21+
22+
mod.list("emoji", desc="Emojis")
23+
ctx.list['user.emoji'] = {
24+
"spoken form": "whatever",
25+
}
26+
selections:
27+
- anchor: {line: 10, character: 30}
28+
active: {line: 10, character: 30}
29+
marks:
30+
default.s:
31+
start: {line: 12, character: 5}
32+
end: {line: 12, character: 11}
33+
finalState:
34+
documentContents: |-
35+
from talon import Context, Module
36+
37+
mod = Module()
38+
ctx = Context()
39+
40+
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
41+
ctx.list['user.cursorless_walkthrough_list'] = {
42+
"spoken form": "whatever",
43+
}
44+
45+
mod.list("emoji", desc="Emojis")
46+
ctx.list['user.emoji'] = {
47+
"": "whatever",
48+
}
49+
selections:
50+
- anchor: {line: 12, character: 5}
51+
active: {line: 12, character: 5}
52+
thatMark:
53+
- anchor: {line: 12, character: 5}
54+
active: {line: 12, character: 5}
55+
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: s}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any, delimiterInclusion: interiorOnly}}]

0 commit comments

Comments
 (0)