From 11b94f00a56e75b8d6818da02977434aa00eaec1 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 28 Jan 2025 21:10:35 +0100 Subject: [PATCH] Make cheat sheet handle missing spoken forms (#2760) Today the cheat sheet generation crashes if the user has disabled certain spoken forms Fixes #2647 ## Checklist - [/] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --- src/cheatsheet/get_list.py | 11 +- src/cheatsheet/sections/actions.py | 208 +++++------ src/cheatsheet/sections/compound_targets.py | 46 ++- src/cheatsheet/sections/destinations.py | 4 +- .../sections/get_scope_visualizer.py | 54 +-- src/cheatsheet/sections/modifiers.py | 349 +++++++++++------- src/cheatsheet/sections/scopes.py | 50 +-- src/cheatsheet/sections/special_marks.py | 4 +- src/cheatsheet/sections/tutorial.py | 5 +- 9 files changed, 423 insertions(+), 308 deletions(-) diff --git a/src/cheatsheet/get_list.py b/src/cheatsheet/get_list.py index 341873fd6a..20c3c26665 100644 --- a/src/cheatsheet/get_list.py +++ b/src/cheatsheet/get_list.py @@ -41,7 +41,7 @@ def get_raw_list(name: str) -> Mapping[str, str]: 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: +def get_spoken_form_from_list(list_name: str, value: str) -> str | None: """Get the spoken form of a value from a list. Args: @@ -49,10 +49,15 @@ def get_spoken_form_from_list(list_name: str, value: str) -> str: value (str): The value to look up. Returns: - str: The spoken form of the value. + str: The spoken form of the value if found, otherwise None. """ return next( - spoken_form for spoken_form, v in get_raw_list(list_name).items() if v == value + ( + spoken_form + for spoken_form, v in get_raw_list(list_name).items() + if v == value + ), + None, ) diff --git a/src/cheatsheet/sections/actions.py b/src/cheatsheet/sections/actions.py index b83c3e5ec8..9670987715 100644 --- a/src/cheatsheet/sections/actions.py +++ b/src/cheatsheet/sections/actions.py @@ -1,8 +1,10 @@ +from typing import Callable + from ...actions.actions import ACTION_LIST_NAMES -from ..get_list import get_raw_list, make_dict_readable +from ..get_list import ListItemDescriptor, get_raw_list, make_dict_readable -def get_actions(): +def get_actions() -> list[ListItemDescriptor]: all_actions = {} for name in ACTION_LIST_NAMES: all_actions.update(get_raw_list(name)) @@ -28,111 +30,103 @@ def get_actions(): if value in multiple_target_action_names } - swap_connective = list(get_raw_list("swap_connective").keys())[0] + swap_connectives = list(get_raw_list("swap_connective").keys()) + swap_connective = swap_connectives[0] if swap_connectives else None - return [ - *make_dict_readable( - "action", - simple_actions, - { - "editNewLineAfter": "Edit new line/scope after", - "editNewLineBefore": "Edit new line/scope before", - }, - ), - { - "id": "replaceWithTarget", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['replaceWithTarget']} ", - "description": "Copy to ", - }, - { - "spokenForm": f"{complex_actions['replaceWithTarget']} ", - "description": "Insert copy of at cursor", - }, - ], - }, - { - "id": "pasteFromClipboard", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['pasteFromClipboard']} ", - "description": "Paste from clipboard at ", - } - ], - }, - { - "id": "moveToTarget", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['moveToTarget']} ", - "description": "Move to ", - }, - { - "spokenForm": f"{complex_actions['moveToTarget']} ", - "description": "Move to cursor position", - }, - ], - }, - { - "id": "swapTargets", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['swapTargets']} {swap_connective} ", - "description": "Swap with ", - }, - { - "spokenForm": f"{complex_actions['swapTargets']} {swap_connective} ", - "description": "Swap selection with ", - }, - ], - }, - { - "id": "applyFormatter", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['applyFormatter']} at ", - "description": "Reformat as ", - } - ], - }, + items = make_dict_readable( + "action", + simple_actions, { - "id": "callAsFunction", - "type": "action", - "variations": [ - { - "spokenForm": f"{complex_actions['callAsFunction']} ", - "description": "Call on selection", - }, - { - "spokenForm": f"{complex_actions['callAsFunction']} on ", - "description": "Call on ", - }, - ], + "editNewLineAfter": "Edit new line/scope after", + "editNewLineBefore": "Edit new line/scope before", }, - { - "id": "wrapWithPairedDelimiter", - "type": "action", - "variations": [ - { - "spokenForm": f" {complex_actions['wrapWithPairedDelimiter']} ", - "description": "Wrap with ", - } - ], - }, - { - "id": "rewrap", - "type": "action", - "variations": [ - { - "spokenForm": f" {complex_actions['rewrap']} ", - "description": "Rewrap with ", - } - ], - }, - ] + ) + + fixtures: dict[str, list[tuple[Callable, str]]] = { + "replaceWithTarget": [ + ( + lambda value: f"{value} ", + "Copy to ", + ), + ( + lambda value: f"{value} ", + "Insert copy of at cursor", + ), + ], + "pasteFromClipboard": [ + ( + lambda value: f"{value} ", + "Paste from clipboard at ", + ) + ], + "moveToTarget": [ + ( + lambda value: f"{value} ", + "Move to ", + ), + ( + lambda value: f"{value} ", + "Move to cursor position", + ), + ], + "applyFormatter": [ + ( + lambda value: f"{value} at ", + "Reformat as ", + ) + ], + "callAsFunction": [ + ( + lambda value: f"{value} ", + "Call on selection", + ), + ( + lambda value: f"{value} on ", + "Call on ", + ), + ], + "wrapWithPairedDelimiter": [ + ( + lambda value: f" {value} ", + "Wrap with ", + ) + ], + "rewrap": [ + ( + lambda value: f" {value} ", + "Rewrap with ", + ) + ], + } + + if swap_connective: + fixtures["swapTargets"] = [ + ( + lambda value: f"{value} {swap_connective} ", + "Swap with ", + ), + ( + lambda value: f"{value} {swap_connective} ", + "Swap selection with ", + ), + ] + + for action_id, variations in fixtures.items(): + if action_id not in complex_actions: + continue + action = complex_actions[action_id] + items.append( + { + "id": action_id, + "type": "action", + "variations": [ + { + "spokenForm": callback(action), + "description": description, + } + for callback, description in variations + ], + } + ) + + return items diff --git a/src/cheatsheet/sections/compound_targets.py b/src/cheatsheet/sections/compound_targets.py index 9f1b85b439..ce89cfbb61 100644 --- a/src/cheatsheet/sections/compound_targets.py +++ b/src/cheatsheet/sections/compound_targets.py @@ -1,4 +1,4 @@ -from ..get_list import get_raw_list, get_spoken_form_from_list +from ..get_list import ListItemDescriptor, get_raw_list, get_spoken_form_from_list FORMATTERS = { "rangeExclusive": lambda start, end: f"between {start} and {end}", @@ -9,32 +9,42 @@ } -def get_compound_targets(): +def get_compound_targets() -> list[ListItemDescriptor]: list_connective_term = get_spoken_form_from_list( "list_connective", "listConnective" ) vertical_range_term = get_spoken_form_from_list("range_type", "verticalRange") - return [ - { - "id": "listConnective", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": f" {list_connective_term} ", - "description": " and ", - }, - ], - }, - *[ + items: list[ListItemDescriptor] = [] + + if list_connective_term: + items.append( + { + "id": "listConnective", + "type": "compoundTargetConnective", + "variations": [ + { + "spokenForm": f" {list_connective_term} ", + "description": " and ", + }, + ], + } + ) + + items.extend( + [ get_entry(spoken_form, id) for spoken_form, id in get_raw_list("range_connective").items() - ], - get_entry(vertical_range_term, "verticalRange"), - ] + ] + ) + + if vertical_range_term: + items.append(get_entry(vertical_range_term, "verticalRange")) + + return items -def get_entry(spoken_form, id): +def get_entry(spoken_form, id) -> ListItemDescriptor: formatter = FORMATTERS[id] return { diff --git a/src/cheatsheet/sections/destinations.py b/src/cheatsheet/sections/destinations.py index d269fb2e53..6c08e3b41a 100644 --- a/src/cheatsheet/sections/destinations.py +++ b/src/cheatsheet/sections/destinations.py @@ -1,7 +1,7 @@ -from ..get_list import get_raw_list +from ..get_list import ListItemDescriptor, get_raw_list -def get_destinations(): +def get_destinations() -> list[ListItemDescriptor]: insertion_modes = { **{p: "to" for p in get_raw_list("insertion_mode_to")}, **get_raw_list("insertion_mode_before_after"), diff --git a/src/cheatsheet/sections/get_scope_visualizer.py b/src/cheatsheet/sections/get_scope_visualizer.py index 69115fbe1e..8ae2cffc85 100644 --- a/src/cheatsheet/sections/get_scope_visualizer.py +++ b/src/cheatsheet/sections/get_scope_visualizer.py @@ -1,29 +1,37 @@ -from ..get_list import get_list, get_raw_list, make_readable +from ..get_list import ListItemDescriptor, get_list, get_raw_list, make_readable -def get_scope_visualizer(): - show_scope_visualizer = list(get_raw_list("show_scope_visualizer").keys())[0] +def get_scope_visualizer() -> list[ListItemDescriptor]: + show_scope_visualizers = list(get_raw_list("show_scope_visualizer").keys()) + show_scope_visualizer = ( + show_scope_visualizers[0] if show_scope_visualizers else None + ) visualization_types = get_raw_list("visualization_type") - return [ - *get_list("hide_scope_visualizer", "command"), - { - "id": "show_scope_visualizer", - "type": "command", - "variations": [ - { - "spokenForm": f"{show_scope_visualizer} ", - "description": "Visualize ", - }, - *[ + items = get_list("hide_scope_visualizer", "command") + + if show_scope_visualizer: + items.append( + { + "id": "show_scope_visualizer", + "type": "command", + "variations": [ { - "spokenForm": f"{show_scope_visualizer} {spoken_form}", - "description": f"Visualize {make_readable(id).lower()} range", - } - for spoken_form, id in visualization_types.items() + "spokenForm": f"{show_scope_visualizer} ", + "description": "Visualize ", + }, + *[ + { + "spokenForm": f"{show_scope_visualizer} {spoken_form}", + "description": f"Visualize {make_readable(id).lower()} range", + } + for spoken_form, id in visualization_types.items() + ], ], - ], - }, + } + ) + + items.append( { "id": "show_scope_sidebar", "type": "command", @@ -33,5 +41,7 @@ def get_scope_visualizer(): "description": "Show cursorless sidebar", }, ], - }, - ] + } + ) + + return items diff --git a/src/cheatsheet/sections/modifiers.py b/src/cheatsheet/sections/modifiers.py index 406e3acbbc..398f972808 100644 --- a/src/cheatsheet/sections/modifiers.py +++ b/src/cheatsheet/sections/modifiers.py @@ -1,7 +1,7 @@ from itertools import chain -from typing import TypedDict +from typing import Callable, TypedDict -from ..get_list import get_raw_list, make_dict_readable +from ..get_list import ListItemDescriptor, Variation, get_raw_list, make_dict_readable MODIFIER_LIST_NAMES = [ "simple_modifier", @@ -17,7 +17,12 @@ ] -def get_modifiers(): +class Entry(TypedDict): + spokenForm: str + description: str + + +def get_modifiers() -> list[ListItemDescriptor]: all_modifiers = {} for name in MODIFIER_LIST_NAMES: all_modifiers.update(get_raw_list(name)) @@ -45,157 +50,237 @@ def get_modifiers(): 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", - }, - ), + items = make_dict_readable( + "modifier", + simple_modifiers, { - "id": "extendThroughStartOf", - "type": "modifier", - "variations": [ - { - "spokenForm": complex_modifiers["extendThroughStartOf"], - "description": "Extend through start of line/pair", - }, - { - "spokenForm": f"{complex_modifiers['extendThroughStartOf']} ", - "description": "Extend through start of ", - }, - ], + "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": "extendThroughEndOf", - "type": "modifier", - "variations": [ - { - "spokenForm": complex_modifiers["extendThroughEndOf"], - "description": "Extend through end of line/pair", - }, - { - "spokenForm": f"{complex_modifiers['extendThroughEndOf']} ", - "description": "Extend through end of ", - }, - ], - }, - { - "id": "containingScope", - "type": "modifier", - "variations": [ - { - "spokenForm": "", - "description": "Containing instance of ", - }, - ], - }, - { - "id": "every", - "type": "modifier", - "variations": [ - { - "spokenForm": f"{complex_modifiers['every']} ", - "description": "Every instance of ", - }, - ], - }, - { - "id": "ancestor", - "type": "modifier", - "variations": [ - { - "spokenForm": f"{complex_modifiers['ancestor']} ", - "description": "Grandparent containing instance of ", - }, - ], - }, - { - "id": "relativeScope", - "type": "modifier", - "variations": [ - { - "spokenForm": f"{complex_modifiers['previous']} ", - "description": "Previous instance of ", - }, - { - "spokenForm": f"{complex_modifiers['next']} ", - "description": "Next instance of ", - }, - { - "spokenForm": f" {complex_modifiers['previous']} ", - "description": " instance of before target", - }, - { - "spokenForm": f" {complex_modifiers['next']} ", - "description": " instance of after target", - }, - { - "spokenForm": f" {complex_modifiers['backward']}", - "description": "single instance of including target, going backwards", - }, - { - "spokenForm": f" {complex_modifiers['forward']}", - "description": "single instance of including target, going forwards", - }, - *generateOptionalEvery( - complex_modifiers["every"], + ) + + if "extendThroughStartOf" in complex_modifiers: + items.append( + { + "id": "extendThroughStartOf", + "type": "modifier", + "variations": [ { - "spokenForm": f" s {complex_modifiers['backward']}", - "description": " instances of including target, going backwards", + "spokenForm": complex_modifiers["extendThroughStartOf"], + "description": "Extend through start of line/pair", }, { - "spokenForm": " s", - "description": " instances of including target, going forwards", + "spokenForm": f"{complex_modifiers['extendThroughStartOf']} ", + "description": "Extend through start of ", }, + ], + } + ) + + if "extendThroughEndOf" in complex_modifiers: + items.append( + { + "id": "extendThroughEndOf", + "type": "modifier", + "variations": [ { - "spokenForm": f"{complex_modifiers['previous']} s", - "description": "previous instances of ", + "spokenForm": complex_modifiers["extendThroughEndOf"], + "description": "Extend through end of line/pair", }, { - "spokenForm": f"{complex_modifiers['next']} s", - "description": "next instances of ", + "spokenForm": f"{complex_modifiers['extendThroughEndOf']} ", + "description": "Extend through end of ", }, - ), - ], - }, + ], + } + ) + + items.append( { - "id": "ordinalScope", + "id": "containingScope", "type": "modifier", "variations": [ { - "spokenForm": " ", - "description": " instance of in iteration scope", - }, - { - "spokenForm": f" {complex_modifiers['last']} ", - "description": "-to-last instance of in iteration scope", + "spokenForm": "", + "description": "Containing instance of ", }, - *generateOptionalEvery( - complex_modifiers["every"], + ], + } + ) + + if "every" in complex_modifiers: + items.append( + { + "id": "every", + "type": "modifier", + "variations": [ { - "spokenForm": f"{complex_modifiers['first']} s", - "description": "first instances of in iteration scope", + "spokenForm": f"{complex_modifiers['every']} ", + "description": "Every instance of ", }, + ], + } + ) + + if "ancestor" in complex_modifiers: + items.append( + { + "id": "ancestor", + "type": "modifier", + "variations": [ { - "spokenForm": f"{complex_modifiers['last']} s", - "description": "last instances of in iteration scope", + "spokenForm": f"{complex_modifiers['ancestor']} ", + "description": "Grandparent containing instance of ", }, - ), - ], - }, + ], + } + ) + + items.append(get_relative_scope(complex_modifiers)) + items.append(get_ordinal_scope(complex_modifiers)) + + return items + + +def get_relative_scope(complex_modifiers: dict[str, str]) -> ListItemDescriptor: + variations: list[Variation] = [] + + fixtures: dict[str, list[tuple[Callable, str]]] = { + "previous": [ + ( + lambda value: f"{value} ", + "Previous instance of ", + ), + ( + lambda value: f" {value} ", + " instance of before target", + ), + ], + "next": [ + ( + lambda value: f"{value} ", + "Next instance of ", + ), + ( + lambda value: f" {value} ", + " instance of after target", + ), + ], + "backward": [ + ( + lambda value: f" {value}", + "single instance of including target, going backwards", + ) + ], + "forward": [ + ( + lambda value: f" {value}", + "single instance of including target, going forwards", + ) + ], + } + + for mod_id, vars in fixtures.items(): + if mod_id not in complex_modifiers: + continue + mod = complex_modifiers[mod_id] + for callback, description in vars: + variations.append( + { + "spokenForm": callback(mod), + "description": description, + } + ) + + if "every" in complex_modifiers: + entries: list[Entry] = [] + + if "backward" in complex_modifiers: + entries.append( + { + "spokenForm": f" s {complex_modifiers['backward']}", + "description": " instances of including target, going backwards", + } + ) + + entries.append( + { + "spokenForm": " s", + "description": " instances of including target, going forwards", + } + ) + + if "previous" in complex_modifiers: + entries.append( + { + "spokenForm": f"{complex_modifiers['previous']} s", + "description": "previous instances of ", + } + ) + + if "next" in complex_modifiers: + entries.append( + { + "spokenForm": f"{complex_modifiers['next']} s", + "description": "next instances of ", + } + ) + + variations.extend(generateOptionalEvery(complex_modifiers["every"], *entries)) + + return { + "id": "relativeScope", + "type": "modifier", + "variations": variations, + } + + +def get_ordinal_scope(complex_modifiers: dict[str, str]) -> ListItemDescriptor: + variations: list[Variation] = [ + { + "spokenForm": " ", + "description": " instance of in iteration scope", + } ] + if "last" in complex_modifiers: + variations.append( + { + "spokenForm": f" {complex_modifiers['last']} ", + "description": "-to-last instance of in iteration scope", + } + ) -class Entry(TypedDict): - spokenForm: str - description: str + if "every" in complex_modifiers: + entries: list[Entry] = [] + + if "first" in complex_modifiers: + entries.append( + { + "spokenForm": f"{complex_modifiers['first']} s", + "description": "first instances of in iteration scope", + } + ) + + if "last" in complex_modifiers: + entries.append( + { + "spokenForm": f"{complex_modifiers['last']} s", + "description": "last instances of in iteration scope", + } + ) + + variations.extend(generateOptionalEvery(complex_modifiers["every"], *entries)) + + return { + "id": "ordinalScope", + "type": "modifier", + "variations": variations, + } def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]: diff --git a/src/cheatsheet/sections/scopes.py b/src/cheatsheet/sections/scopes.py index 2280d113a6..2fb8e34a47 100644 --- a/src/cheatsheet/sections/scopes.py +++ b/src/cheatsheet/sections/scopes.py @@ -1,28 +1,34 @@ -from ..get_list import get_lists, get_spoken_form_from_list +from ..get_list import ListItemDescriptor, get_lists, get_spoken_form_from_list -def get_scopes(): +def get_scopes() -> list[ListItemDescriptor]: 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", - }, - ), + + items = get_lists( + ["scope_type"], + "scopeType", { - "id": "glyph", - "type": "scopeType", - "variations": [ - { - "spokenForm": f"{glyph_spoken_form} ", - "description": "Instance of single character ", - }, - ], + "argumentOrParameter": "Argument", + "boundedNonWhitespaceSequence": "Non-whitespace sequence bounded by surrounding pair delimeters", + "boundedParagraph": "Paragraph bounded by surrounding pair delimeters", }, + ) + + if glyph_spoken_form: + items.append( + { + "id": "glyph", + "type": "scopeType", + "variations": [ + { + "spokenForm": f"{glyph_spoken_form} ", + "description": "Instance of single character ", + }, + ], + } + ) + + items.append( { "id": "pair", "type": "scopeType", @@ -33,4 +39,6 @@ def get_scopes(): }, ], }, - ] + ) + + return items diff --git a/src/cheatsheet/sections/special_marks.py b/src/cheatsheet/sections/special_marks.py index 41f2572c38..cd388756b7 100644 --- a/src/cheatsheet/sections/special_marks.py +++ b/src/cheatsheet/sections/special_marks.py @@ -1,7 +1,7 @@ -from ..get_list import get_lists, get_raw_list, make_dict_readable +from ..get_list import ListItemDescriptor, get_lists, get_raw_list, make_dict_readable -def get_special_marks(): +def get_special_marks() -> list[ListItemDescriptor]: line_direction_marks = make_dict_readable( "mark", { diff --git a/src/cheatsheet/sections/tutorial.py b/src/cheatsheet/sections/tutorial.py index 38189cfe11..1bc2727d75 100644 --- a/src/cheatsheet/sections/tutorial.py +++ b/src/cheatsheet/sections/tutorial.py @@ -1,4 +1,7 @@ -def get_tutorial_entries(): +from ..get_list import ListItemDescriptor + + +def get_tutorial_entries() -> list[ListItemDescriptor]: return [ { "id": "start_tutorial",