Skip to content

Commit

Permalink
Cursorless tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Jun 11, 2022
1 parent 8a42e8e commit 1b9a97b
Show file tree
Hide file tree
Showing 33 changed files with 1,694 additions and 0 deletions.
126 changes: 126 additions & 0 deletions cursorless-talon/src/cursorless_command_to_spoken_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import Callable

from talon import registry

from .actions.actions import ACTION_LIST_NAMES
from .conventions import get_cursorless_list_name
from .modifiers.containing_scope import SCOPE_LIST_NAMES


def make_cursorless_list_reverse_look_up(*raw_list_names: str):
return make_list_reverse_look_up(
*[get_cursorless_list_name(raw_list_name) for raw_list_name in raw_list_names]
)


def make_list_reverse_look_up(*list_names: str):
"""
Given a list of talon list names, returns a function that does a reverse
look-up in all lists to find the spoken form for its input.
"""

def return_func(argument: str):
for list_name in list_names:
for spoken_form, value in registry.lists[list_name][-1].items():
if value == argument:
return list_name, spoken_form

raise LookupError(f"Unknown identifier `{argument}`")

return return_func


lookup_action = make_cursorless_list_reverse_look_up(*ACTION_LIST_NAMES)
lookup_scope_type = make_cursorless_list_reverse_look_up(*SCOPE_LIST_NAMES)


def cursorless_command_to_spoken_form(command: dict):
action_list_name, action_spoken_form = lookup_action(command["action"])
targets_spoken_form = targets_processor_map[action_list_name](command["targets"])
return f"{action_spoken_form} {targets_spoken_form}"


def process_simple_action_targets(targets: list[dict]):
return process_target(targets[0])


raw_targets_processor_map: dict[str, Callable[[list[dict]], str]] = {
"simple_action": process_simple_action_targets,
"positional_action": process_simple_action_targets,
"callback_action": process_simple_action_targets,
"makeshift_action": process_simple_action_targets,
"custom_action": process_simple_action_targets,
"swap_action": {"swap": "swapTargets"},
"move_bring_action": {"bring": "replaceWithTarget", "move": "moveToTarget"},
"wrap_action": {"wrap": "wrapWithPairedDelimiter", "repack": "rewrap"},
"insert_snippet_action": {"snippet": "insertSnippet"},
"reformat_action": {"format": "applyFormatter"},
}

targets_processor_map = {
get_cursorless_list_name(key): value
for key, value in raw_targets_processor_map.items()
}


def process_target(target: dict):
if target["type"] == "primitive":
return process_primitive_target(target)
elif target["type"] == "range":
return process_range_target(target)
elif target["type"] == "list":
return process_list_target(target)
else:
raise Exception(f"Unknown target type {target['type']}")


class MarkProcessor:
field_name = "mark"

def __init__(self):
self.process_character = make_list_reverse_look_up(
"user.letter",
"user.number_key",
"user.symbol_key",
)

def process_value(self, field_value: dict):
mark_type = field_value["type"]
if mark_type == "decoratedSymbol":
return self.process_decorated_symbol(field_value)
elif mark_type == "that":
return self.process_that_mark(field_value)
elif mark_type == "source":
return self.process_source_mark(field_value)
elif mark_type == "cursor":
return self.process_cursor_mark(field_value)

def process_decorated_symbol(self, field_value: dict):
# TODO: Handle `symbolColor`
return self.process_character(field_value["character"])[1]

def process_that_mark(self, field_value: dict):
# TODO: Handle this case properly using users custom term
return "that"

def process_source_mark(self, field_value: dict):
# TODO: Handle this case properly using users custom term
return "source"

def process_cursor_mark(self, field_value: dict):
# TODO: Handle this case properly using users custom term
return "this"


field_processors = [MarkProcessor()]


def process_primitive_target(target: dict):
field_spoken_forms = [
field_processor.process_value(target.get(field_processor.field_name, None))
for field_processor in field_processors
]

return " ".join(
[spoken_form for spoken_form in field_spoken_forms if spoken_form is not None]
)
84 changes: 84 additions & 0 deletions cursorless-talon/src/tutorial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import json
import re
from pathlib import Path
from typing import Callable

import yaml
from talon import actions, app

from .cursorless_command_to_spoken_form import (
cursorless_command_to_spoken_form,
lookup_action,
lookup_scope_type,
)

regex = re.compile(r"\{(\w+):([^}]+)\}")
tutorial_dir = Path(
"/Users/pokey/src/cursorless-vscode/src/test/suite/fixtures/recorded/tutorial/unit-2-basic-coding"
)


def process_literal_step(argument: str):
return f"<cmd@{argument}/>"


def process_action(argument: str):
_, spoken_form = lookup_action(argument)
return f'<*"{spoken_form}"/>'


def process_scope_type(argument: str):
_, spoken_form = lookup_scope_type(argument)
return f'<*"{spoken_form}"/>'


def process_cursorless_command_step(argument: str):
step_fixture = yaml.safe_load((tutorial_dir / argument).read_text())
return f"<cmd@{cursorless_command_to_spoken_form(step_fixture['command'])}/>"


interpolation_processor_map: dict[str, Callable[[str], str]] = {
"literalStep": process_literal_step,
"action": process_action,
"scopeType": process_scope_type,
"step": process_cursorless_command_step,
}


def process_tutorial_step(raw: str):
print(f"{raw=}")
current_index = 0
content = ""
for match in regex.finditer(raw):
content += raw[current_index : match.start()]
content += interpolation_processor_map[match.group(1)](match.group(2))
current_index = match.end()
content += raw[current_index : len(raw)]
print(f"{content=}")

return {
"content": content,
"restore_callback": print,
"modes": ["command"],
"app": "Code",
"context_hint": "Please open VSCode and enter command mode",
}


def get_basic_coding_walkthrough():
with open(tutorial_dir / "script.json") as f:
script = json.load(f)

return [
actions.user.hud_create_walkthrough_step(**process_tutorial_step(step))
for step in script
]


def on_ready():
actions.user.hud_add_lazy_walkthrough(
"Cursorless basic coding", get_basic_coding_walkthrough
)


app.register("ready", on_ready)
9 changes: 9 additions & 0 deletions data/playground/tutorial/extra-cloning-a-talon-list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from talon import Context, Module

mod = Module()
ctx = Context()

mod.list("cursorless_walkthrough_list", desc="My tutorial list")
ctx.list["user.cursorless_walkthrough_list"] = {
"spoken form": "whatever",
}
11 changes: 11 additions & 0 deletions data/playground/tutorial/unit-1-basics.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
==================================================
========== ==========
========== Welcome to Cursorless! ==========
========== ==========
========== Let's start using marks ==========
========== ==========
========== so we can navigate around ==========
========== ==========
========== without lifting a finger! ==========
========== ==========
==================================================
13 changes: 13 additions & 0 deletions data/playground/tutorial/unit-2-basic-coding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def print_color(color, invert=False):
if invert:
print(invert_color(color))
else:
print(color)


def invert_color(color):
if color == "black":
return "white"


print_color("black")
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
languageId: python
command:
version: 1
spokenForm: bring block made
action: replaceWithTarget
targets:
- type: primitive
selectionType: paragraph
mark: {type: decoratedSymbol, symbolColor: default, character: m}
- {type: primitive, isImplicit: true}
initialState:
documentContents: |+
from talon import Context, Module
mod = Module()
ctx = Context()
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
ctx.list['user.cursorless_walkthrough_list'] = {
"spoken form": "whatever",
}
selections:
- anchor: {line: 10, character: 0}
active: {line: 10, character: 0}
marks:
default.m:
start: {line: 5, character: 0}
end: {line: 5, character: 3}
finalState:
documentContents: |-
from talon import Context, Module
mod = Module()
ctx = Context()
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
ctx.list['user.cursorless_walkthrough_list'] = {
"spoken form": "whatever",
}
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
ctx.list['user.cursorless_walkthrough_list'] = {
"spoken form": "whatever",
}
selections:
- anchor: {line: 13, character: 1}
active: {line: 13, character: 1}
thatMark:
- anchor: {line: 10, character: 0}
active: {line: 13, character: 1}
sourceMark:
- anchor: {line: 5, character: 0}
active: {line: 8, character: 1}
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}}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
languageId: python
command:
version: 1
spokenForm: clear core sun
action: clearAndSetSelection
targets:
- type: primitive
modifier: {type: surroundingPair, delimiter: any, delimiterInclusion: interiorOnly}
mark: {type: decoratedSymbol, symbolColor: default, character: s}
initialState:
documentContents: |-
from talon import Context, Module
mod = Module()
ctx = Context()
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
ctx.list['user.cursorless_walkthrough_list'] = {
"spoken form": "whatever",
}
mod.list("emoji", desc="Emojis")
ctx.list['user.emoji'] = {
"spoken form": "whatever",
}
selections:
- anchor: {line: 10, character: 30}
active: {line: 10, character: 30}
marks:
default.s:
start: {line: 12, character: 5}
end: {line: 12, character: 11}
finalState:
documentContents: |-
from talon import Context, Module
mod = Module()
ctx = Context()
mod.list("cursorless_walkthrough_list", desc="My tutorial list")
ctx.list['user.cursorless_walkthrough_list'] = {
"spoken form": "whatever",
}
mod.list("emoji", desc="Emojis")
ctx.list['user.emoji'] = {
"": "whatever",
}
selections:
- anchor: {line: 12, character: 5}
active: {line: 12, character: 5}
thatMark:
- anchor: {line: 12, character: 5}
active: {line: 12, character: 5}
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: s}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any, delimiterInclusion: interiorOnly}}]
Loading

0 comments on commit 1b9a97b

Please sign in to comment.