diff --git a/cursorless-talon-dev/src/cursorless_dev.talon b/cursorless-talon-dev/src/cursorless_dev.talon index 6f2c56a0d0..9f2430fd44 100644 --- a/cursorless-talon-dev/src/cursorless_dev.talon +++ b/cursorless-talon-dev/src/cursorless_dev.talon @@ -29,9 +29,6 @@ tag: user.cursorless user.private_cursorless_record_that_mark_test() {user.cursorless_homophone} record silent: user.cursorless_record_silent_test() -{user.cursorless_homophone} update cheatsheet: - user.private_cursorless_cheat_sheet_update_json() - test snip make : user.private_cursorless_make_snippet_test(cursorless_target) diff --git a/cursorless-talon/src/actions/actions.py b/cursorless-talon/src/actions/actions.py index 6364b73e20..18893c1f79 100644 --- a/cursorless-talon/src/actions/actions.py +++ b/cursorless-talon/src/actions/actions.py @@ -117,7 +117,7 @@ def cursorless_vscode_command(command_id: str, target: CursorlessTarget): Deprecated: prefer `cursorless_ide_command` """ - # Deprecated 2026-08-31 + # DEPRECATED: 2026-08-31 actions.app.notify( "Deprecated: cursorless_vscode_command is deprecated, prefer cursorless_ide_command" ) diff --git a/cursorless-talon/src/cheatsheet/cheat_sheet.py b/cursorless-talon/src/cheatsheet/cheat_sheet.py index 60eeae2a27..11d6d8e9ed 100644 --- a/cursorless-talon/src/cheatsheet/cheat_sheet.py +++ b/cursorless-talon/src/cheatsheet/cheat_sheet.py @@ -3,16 +3,6 @@ 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""" @@ -30,12 +20,6 @@ def private_cursorless_cheat_sheet_show_html(): '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() @@ -59,20 +43,12 @@ def private_cursorless_cheat_sheet_show_html(): actions.user.private_cursorless_run_rpc_command_and_wait( "cursorless.showCheatsheet", { - "version": 0, - "spokenFormInfo": cursorless_cheat_sheet_get_json(), + "version": 1, "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""" @@ -89,74 +65,3 @@ def cheatsheet_dir_linux() -> Path: # 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", - "surrounding_pair_scope_type", - ], - "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(), - }, - ] - } diff --git a/cursorless-talon/src/cheatsheet/get_list.py b/cursorless-talon/src/cheatsheet/get_list.py deleted file mode 100644 index 20c3c26665..0000000000 --- a/cursorless-talon/src/cheatsheet/get_list.py +++ /dev/null @@ -1,97 +0,0 @@ -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 | None: - """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 if found, otherwise None. - """ - return next( - ( - spoken_form - for spoken_form, v in get_raw_list(list_name).items() - if v == value - ), - None, - ) - - -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, - ) diff --git a/cursorless-talon/src/cheatsheet/sections/actions.py b/cursorless-talon/src/cheatsheet/sections/actions.py deleted file mode 100644 index 99d1bdf1a9..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/actions.py +++ /dev/null @@ -1,144 +0,0 @@ -from typing import Callable - -from ...actions.actions import ACTION_LIST_NAMES -from ..get_list import ListItemDescriptor, get_raw_list, make_dict_readable - - -def get_actions() -> list[ListItemDescriptor]: - all_actions = {} - for name in ACTION_LIST_NAMES: - all_actions.update(get_raw_list(name)) - - complex_action_names = [ - "replaceWithTarget", - "moveToTarget", - "swapTargets", - "applyFormatter", - "callAsFunction", - "wrapWithPairedDelimiter", - "rewrap", - "pasteFromClipboard", - "insertSnippet", - ] - simple_actions = { - f"{key} ": value - for key, value in all_actions.items() - if value not in complex_action_names - } - complex_actions = { - value: key - for key, value in all_actions.items() - if value in complex_action_names - } - - swap_connectives = list(get_raw_list("swap_connective").keys()) - swap_connective = swap_connectives[0] if swap_connectives else None - - items = make_dict_readable( - "action", - simple_actions, - { - "editNewLineAfter": "Edit new line/scope after", - "editNewLineBefore": "Edit new line/scope before", - "experimental.setInstanceReference": "Set instance reference", - }, - ) - - complex_action_defs: dict[str, list[tuple[Callable, str]]] = { - "replaceWithTarget": [ - ( - lambda value: f"{value} ", - "Copy to ", - ), - ( - lambda value: f"{value} ", - "Insert copy of at selection", - ), - ], - "pasteFromClipboard": [ - ( - lambda value: f"{value} ", - "Paste from clipboard at ", - ) - ], - "moveToTarget": [ - ( - lambda value: f"{value} ", - "Move to ", - ), - ( - lambda value: f"{value} ", - "Move to selection", - ), - ], - "applyFormatter": [ - ( - lambda value: f"{value} at ", - "Reformat as ", - ) - ], - "callAsFunction": [ - ( - lambda value: f"{value} ", - "Insert call to on selection", - ), - ( - lambda value: f"{value} on ", - "Insert call to on ", - ), - ], - "wrapWithPairedDelimiter": [ - ( - lambda value: f" {value} ", - "Wrap with ", - ), - ( - lambda value: f" {value} ", - "Wrap with ", - ), - ], - "rewrap": [ - ( - lambda value: f" {value} ", - "Rewrap with ", - ) - ], - "insertSnippet": [ - ( - lambda value: f"{value} ", - "Insert snippet at ", - ) - ], - } - - if swap_connective: - complex_action_defs["swapTargets"] = [ - ( - lambda value: f"{value} {swap_connective} ", - "Swap with ", - ), - ( - lambda value: f"{value} {swap_connective} ", - "Swap selection with ", - ), - ] - - for action_id, variations in complex_action_defs.items(): - # This happens if the user has disabled the spoken form for a complex action - 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/cursorless-talon/src/cheatsheet/sections/compound_targets.py b/cursorless-talon/src/cheatsheet/sections/compound_targets.py deleted file mode 100644 index ce89cfbb61..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/compound_targets.py +++ /dev/null @@ -1,63 +0,0 @@ -from ..get_list import ListItemDescriptor, 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[ListItemDescriptor]: - list_connective_term = get_spoken_form_from_list( - "list_connective", "listConnective" - ) - vertical_range_term = get_spoken_form_from_list("range_type", "verticalRange") - - 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() - ] - ) - - if vertical_range_term: - items.append(get_entry(vertical_range_term, "verticalRange")) - - return items - - -def get_entry(spoken_form, id) -> ListItemDescriptor: - formatter = FORMATTERS[id] - - return { - "id": id, - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": f" {spoken_form} ", - "description": formatter("", ""), - }, - { - "spokenForm": f"{spoken_form} ", - "description": formatter("selection", ""), - }, - ], - } diff --git a/cursorless-talon/src/cheatsheet/sections/destinations.py b/cursorless-talon/src/cheatsheet/sections/destinations.py deleted file mode 100644 index 2c5fd1a832..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/destinations.py +++ /dev/null @@ -1,28 +0,0 @@ -from ..get_list import ListItemDescriptor, get_raw_list - - -def get_destinations() -> list[ListItemDescriptor]: - insertion_modes = { - **dict.fromkeys(get_raw_list("insertion_mode_to"), "to"), - **get_raw_list("insertion_mode_before_after"), - } - - descriptions = { - "to": "Replace ", - "before": "Insert before ", - "after": "Insert after ", - } - - return [ - { - "id": f"destination_{id}", - "type": "destination", - "variations": [ - { - "spokenForm": f"{spoken_form} ", - "description": descriptions[id], - } - ], - } - for spoken_form, id in insertion_modes.items() - ] diff --git a/cursorless-talon/src/cheatsheet/sections/get_scope_visualizer.py b/cursorless-talon/src/cheatsheet/sections/get_scope_visualizer.py deleted file mode 100644 index 8ae2cffc85..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/get_scope_visualizer.py +++ /dev/null @@ -1,47 +0,0 @@ -from ..get_list import ListItemDescriptor, get_list, get_raw_list, make_readable - - -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") - - 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} ", - "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", - "variations": [ - { - "spokenForm": "bar cursorless", - "description": "Show cursorless sidebar", - }, - ], - } - ) - - return items diff --git a/cursorless-talon/src/cheatsheet/sections/modifiers.py b/cursorless-talon/src/cheatsheet/sections/modifiers.py deleted file mode 100644 index 398f972808..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/modifiers.py +++ /dev/null @@ -1,301 +0,0 @@ -from itertools import chain -from typing import Callable, TypedDict - -from ..get_list import ListItemDescriptor, Variation, 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", -] - - -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)) - - 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 - } - - items = 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", - }, - ) - - if "extendThroughStartOf" in complex_modifiers: - items.append( - { - "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 ", - }, - ], - } - ) - - if "extendThroughEndOf" in complex_modifiers: - items.append( - { - "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 ", - }, - ], - } - ) - - items.append( - { - "id": "containingScope", - "type": "modifier", - "variations": [ - { - "spokenForm": "", - "description": "Containing instance of ", - }, - ], - } - ) - - if "every" in complex_modifiers: - items.append( - { - "id": "every", - "type": "modifier", - "variations": [ - { - "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['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", - } - ) - - 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]: - 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 - ) - ) diff --git a/cursorless-talon/src/cheatsheet/sections/scopes.py b/cursorless-talon/src/cheatsheet/sections/scopes.py deleted file mode 100644 index 5eedc2eb51..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/scopes.py +++ /dev/null @@ -1,44 +0,0 @@ -from ..get_list import ListItemDescriptor, get_lists, get_spoken_form_from_list - - -def get_scopes() -> list[ListItemDescriptor]: - glyph_spoken_form = get_spoken_form_from_list("glyph_scope_type", "glyph") - - items = get_lists( - ["scope_type"], - "scopeType", - { - "argumentOrParameter": "Argument", - "boundedNonWhitespaceSequence": "Non-whitespace sequence bounded by surrounding pair delimiters", - "boundedParagraph": "Paragraph bounded by surrounding pair delimiters", - }, - ) - - 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", - "variations": [ - { - "spokenForm": "", - "description": "Paired delimiters", - }, - ], - }, - ) - - return items diff --git a/cursorless-talon/src/cheatsheet/sections/special_marks.py b/cursorless-talon/src/cheatsheet/sections/special_marks.py deleted file mode 100644 index cd388756b7..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/special_marks.py +++ /dev/null @@ -1,20 +0,0 @@ -from ..get_list import ListItemDescriptor, get_lists, get_raw_list, make_dict_readable - - -def get_special_marks() -> list[ListItemDescriptor]: - line_direction_marks = make_dict_readable( - "mark", - { - f"{key} ": 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, - ] diff --git a/cursorless-talon/src/cheatsheet/sections/tutorial.py b/cursorless-talon/src/cheatsheet/sections/tutorial.py deleted file mode 100644 index 1bc2727d75..0000000000 --- a/cursorless-talon/src/cheatsheet/sections/tutorial.py +++ /dev/null @@ -1,86 +0,0 @@ -from ..get_list import ListItemDescriptor - - -def get_tutorial_entries() -> list[ListItemDescriptor]: - 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 ", - "description": "Start a specific tutorial by number", - }, - ], - }, - ] diff --git a/cursorless-talon/src/marks/decorated_mark.py b/cursorless-talon/src/marks/decorated_mark.py index 4ab6650b0a..f8abb3abf1 100644 --- a/cursorless-talon/src/marks/decorated_mark.py +++ b/cursorless-talon/src/marks/decorated_mark.py @@ -1,9 +1,9 @@ from pathlib import Path -from typing import Any +from typing import Any, Callable, Sequence from talon import Module, actions, cron, fs -from ..csv_overrides import init_csv_and_watch_changes +from ..csv_overrides import SpokenFormEntry, init_csv_and_watch_changes from .mark_types import DecoratedSymbol mod = Module() @@ -88,7 +88,11 @@ def cursorless_decorated_symbol(m) -> DecoratedSymbol: unsubscribe_hat_styles: Any = None -def setup_hat_styles_csv(hat_colors: dict[str, str], hat_shapes: dict[str, str]): +def setup_hat_styles_csv( + hat_colors: dict[str, str], + hat_shapes: dict[str, str], + handle_new_values: Callable[[Sequence[SpokenFormEntry]], None], +): global unsubscribe_hat_styles ( @@ -140,6 +144,7 @@ def setup_hat_styles_csv(hat_colors: dict[str, str], hat_shapes: dict[str, str]) "hat_color": active_hat_colors, "hat_shape": active_hat_shapes, }, + handle_new_values, extra_ignored_values=[*hat_colors.values(), *hat_shapes.values()], no_update_file=is_shape_error or is_color_error, ) @@ -152,8 +157,12 @@ def setup_hat_styles_csv(hat_colors: dict[str, str], hat_shapes: dict[str, str]) 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) +def init_hats( + hat_colors: dict[str, str], + hat_shapes: dict[str, str], + handle_new_values: Callable[[Sequence[SpokenFormEntry]], None], +): + setup_hat_styles_csv(hat_colors, hat_shapes, handle_new_values) vscode_settings_path: Path | None = None @@ -167,16 +176,23 @@ def on_watch(path, flags): 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) + "500ms", + lambda: setup_hat_styles_csv(hat_colors, hat_shapes, handle_new_values), ) slow_reload_job = cron.after( - "10s", lambda: setup_hat_styles_csv(hat_colors, hat_shapes) + "10s", + lambda: setup_hat_styles_csv(hat_colors, hat_shapes, handle_new_values), ) if vscode_settings_path is not None: fs.watch(vscode_settings_path, on_watch) def unsubscribe(): + global fast_reload_job, slow_reload_job + cron.cancel(fast_reload_job) + cron.cancel(slow_reload_job) + fast_reload_job = None + slow_reload_job = None if vscode_settings_path is not None: fs.unwatch(vscode_settings_path, on_watch) if unsubscribe_hat_styles is not None: diff --git a/cursorless-talon/src/modifiers/position.py b/cursorless-talon/src/modifiers/position.py index 800ca1f25b..fa864956a0 100644 --- a/cursorless-talon/src/modifiers/position.py +++ b/cursorless-talon/src/modifiers/position.py @@ -4,9 +4,9 @@ mod = Module() -mod.list("cursorless_position", desc='Positions such as "before", "after" etc') +mod.list("cursorless_position_modifier", desc='Positions such as "startOf" and "endOf"') -@mod.capture(rule="{user.cursorless_position}") +@mod.capture(rule="{user.cursorless_position_modifier}") def cursorless_position_modifier(m) -> dict[str, Any]: - return {"type": "startOf" if m.cursorless_position == "start" else "endOf"} + return {"type": "startOf" if m.cursorless_position_modifier == "start" else "endOf"} diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index c11fb9d3d6..ba30376933 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -113,7 +113,7 @@ } }, "positions.csv": { - "position": { + "position_modifier": { "start of": "start", "end of": "end" }, diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py index 2452a762df..8aff20d8f8 100644 --- a/cursorless-talon/src/spoken_forms.py +++ b/cursorless-talon/src/spoken_forms.py @@ -82,12 +82,50 @@ def ret(filename: str, *args: P.args, **kwargs: P.kwargs) -> R: "scope_type": "simpleScopeTypeType", "glyph_scope_type": "complexScopeTypeType", "custom_regex_scope_type": "customRegex", + "simple_modifier": "simpleModifier", + "interior_modifier": "simpleModifier", + "head_tail_modifier": "simpleModifier", + "position_modifier": "simpleModifier", + "every_scope_modifier": "simpleModifier", + "previous_next_modifier": "modifierExtra", + "forward_backward_modifier": "modifierExtra", + "first_modifier": "modifierExtra", + "last_modifier": "modifierExtra", + "ancestor_scope_modifier": "modifierExtra", + "range_connective": "connective", + "list_connective": "connective", + "swap_connective": "connective", + "range_type": "connective", + "insertion_mode_before_after": "insertionMode", + "insertion_mode_to": "insertionMode", + "simple_mark": "specialMark", + "unknown_symbol": "specialMark", + "line_direction": "specialMark", + "hat_color": "hatColor", + "hat_shape": "hatShape", + "show_scope_visualizer": "scopeVisualizer", + "hide_scope_visualizer": "scopeVisualizer", + "visualization_type": "scopeVisualizer", + "custom_action": "customAction", **{ action_list_name: "action" for action_list_name in ACTION_LIST_NAMES if action_list_name != "custom_action" }, - "custom_action": "customAction", +} + +ID_REWRITE_MAP = { + ("insertion_mode_to", "sourceDestinationConnective"): "to", + ("every_scope_modifier", "every"): "everyScope", + ("position_modifier", "start"): "startOf", + ("position_modifier", "end"): "endOf", + ("wrap_action", "rewrap"): "rewrapWithPairedDelimiter", +} + +LITERALS = { + "at": "connective", + "on": "connective", + "bar": "sidebar", } @@ -114,13 +152,21 @@ def update_spoken_forms_output(): *[ { "type": LIST_TO_TYPE_MAP[entry.list_name], - "id": entry.id, + "id": ID_REWRITE_MAP.get((entry.list_name, entry.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 ], + *[ + { + "type": LITERALS[literal], + "id": literal, + "spokenForms": [literal], + } + for literal in LITERALS + ], *get_grapheme_spoken_form_entries(graphemes_talon_list), ] ) @@ -186,6 +232,7 @@ def handle_new_values(csv_name: str, values: Sequence[SpokenFormEntry]): init_hats( spoken_forms["hat_styles.csv"]["hat_color"], spoken_forms["hat_styles.csv"]["hat_shape"], + lambda values: handle_new_values("hat_styles.csv", values), ), ] diff --git a/cursorless-talon/src/spoken_forms_output.py b/cursorless-talon/src/spoken_forms_output.py index 6961c98ec2..283825fd29 100644 --- a/cursorless-talon/src/spoken_forms_output.py +++ b/cursorless-talon/src/spoken_forms_output.py @@ -5,7 +5,7 @@ from talon import app SPOKEN_FORMS_OUTPUT_PATH = Path.home() / ".cursorless" / "state.json" -STATE_JSON_VERSION_NUMBER = 0 +STATE_JSON_VERSION_NUMBER = 1 class SpokenFormOutputEntry(TypedDict): diff --git a/packages/app-neovim/src/registerCommands.ts b/packages/app-neovim/src/registerCommands.ts index 37dd30252a..d676cb28a1 100644 --- a/packages/app-neovim/src/registerCommands.ts +++ b/packages/app-neovim/src/registerCommands.ts @@ -70,7 +70,6 @@ export function registerCommands( // Cheatsheet commands "cursorless.showCheatsheet": dummyCommandHandler, - "cursorless.internal.updateCheatsheetDefaults": dummyCommandHandler, // Testcase recorder commands "cursorless.recordTestCase": dummyCommandHandler, diff --git a/packages/app-vscode/package.json b/packages/app-vscode/package.json index 21c4614c3b..04a571a18e 100644 --- a/packages/app-vscode/package.json +++ b/packages/app-vscode/package.json @@ -177,11 +177,6 @@ "title": "Cursorless: Display the cursorless cheatsheet", "enablement": "false" }, - { - "command": "cursorless.internal.updateCheatsheetDefaults", - "title": "Cursorless: Update the default values of the cheatsheet payload used on the website and for local development. Be sure to run this on stock community and cursorless.", - "enablement": "false" - }, { "command": "cursorless.private.logQuickActions", "title": "Cursorless: Log the quick actions available at the current cursor position", diff --git a/packages/app-vscode/src/extension.ts b/packages/app-vscode/src/extension.ts index 44c5d4fe5b..26a39e2d10 100644 --- a/packages/app-vscode/src/extension.ts +++ b/packages/app-vscode/src/extension.ts @@ -166,6 +166,7 @@ export async function activate( vscodeTutorial, installationDependencies, storedTargets, + talonSpokenForms, ); void new ReleaseNotes(vscodeApi, context, normalizedIde.messages).maybeShow(); diff --git a/packages/app-vscode/src/keyboard/KeyboardCommandHandler.ts b/packages/app-vscode/src/keyboard/KeyboardCommandHandler.ts index 0c920e32dc..0327f6c145 100644 --- a/packages/app-vscode/src/keyboard/KeyboardCommandHandler.ts +++ b/packages/app-vscode/src/keyboard/KeyboardCommandHandler.ts @@ -5,7 +5,7 @@ import type { PartialMark, SurroundingPairName, } from "@cursorless/lib-common"; -import { surroundingPairsDelimiters } from "@cursorless/lib-engine"; +import { pairedDelimiterReferences } from "@cursorless/lib-common"; import type { HatColor, HatShape } from "../ide/vscode/hatStyles.types"; import type { SimpleKeyboardActionDescriptor, @@ -87,13 +87,16 @@ export class KeyboardCommandHandler { actionDescriptor, delimiter, }: WrapActionArg) { - const [left, right] = surroundingPairsDelimiters[delimiter]!; + const delimiters = pairedDelimiterReferences[delimiter].delimiters; + if (delimiters == null) { + throw new Error(`Unknown surrounding pair delimiters for '${delimiter}'`); + } await this.targeted.performActionOnTarget( (target) => ({ name: "wrapWithPairedDelimiter", target, - left, - right, + left: delimiters[0], + right: delimiters[1], }), actionDescriptor, ); diff --git a/packages/app-vscode/src/registerCommands.ts b/packages/app-vscode/src/registerCommands.ts index 3109b3216e..0f9299b79b 100644 --- a/packages/app-vscode/src/registerCommands.ts +++ b/packages/app-vscode/src/registerCommands.ts @@ -7,8 +7,11 @@ import type { import { CURSORLESS_COMMAND_ID } from "@cursorless/lib-common"; import type { CommandApi, StoredTargetMap } from "@cursorless/lib-engine"; import { analyzeCommandHistory } from "@cursorless/lib-engine"; -import { showCheatsheet, updateDefaults } from "@cursorless/lib-node-common"; -import type { CheatSheetCommandArg } from "@cursorless/lib-node-common"; +import type { + CheatSheetCommandArg, + FileSystemTalonSpokenForms, +} from "@cursorless/lib-node-common"; +import { showCheatsheet } from "@cursorless/lib-node-common"; import type { ScopeTestRecorder, TestCaseRecorder, @@ -42,6 +45,7 @@ export function registerCommands( tutorial: VscodeTutorial, installationDependencies: InstallationDependencies, storedTargets: StoredTargetMap, + talonSpokenForms: FileSystemTalonSpokenForms, ): void { const runCommandWrapper = async (run: () => Promise) => { try { @@ -68,9 +72,9 @@ export function registerCommands( }, // Cheatsheet commands - "cursorless.showCheatsheet": (arg: CheatSheetCommandArg) => - showCheatsheet(vscodeIde, arg), - "cursorless.internal.updateCheatsheetDefaults": updateDefaults, + "cursorless.showCheatsheet": (arg: CheatSheetCommandArg) => { + return showCheatsheet(vscodeIde, talonSpokenForms, arg); + }, // Testcase recorder commands "cursorless.recordTestCase": testCaseRecorder.toggle, diff --git a/packages/app-web-docs/src/docs/contributing/CONTRIBUTING.mdx b/packages/app-web-docs/src/docs/contributing/CONTRIBUTING.mdx index 774b0d47a9..39588754de 100644 --- a/packages/app-web-docs/src/docs/contributing/CONTRIBUTING.mdx +++ b/packages/app-web-docs/src/docs/contributing/CONTRIBUTING.mdx @@ -214,17 +214,6 @@ pnpm -F @cursorless/app-vscode uninstall-local ## Regular manual maintenance tasks -### Updating the cheatsheet - -We need to keep the default cheatsheet up-to-date for use with local development and for https://www.cursorless.org/cheatsheet. - -1. Switch to vanilla community + cursorless_talon -2. `"debug extension"` -3. `"cursorless update cheatsheet"` -4. See diff and cleanup if necessary. In particular, if you have shapes enabled, you'll need to remove these from the generated cheatsheet as they're not on by default - -This should be done every time we do something that will change the custom cheatsheet, but in practice we don't, so it rots a bit and needs to be redone regularly. - ## Changing SVGs ### SVG preprocessing script diff --git a/packages/app-web-docs/src/docs/contributing/cheatsheet.md b/packages/app-web-docs/src/docs/contributing/cheatsheet.md index e5355539a8..f3f1cfcd8a 100644 --- a/packages/app-web-docs/src/docs/contributing/cheatsheet.md +++ b/packages/app-web-docs/src/docs/contributing/cheatsheet.md @@ -2,13 +2,11 @@ The cheatsheet can be activated locally to show your custom cheatsheet by saying `"cursorless cheatsheet"`, or visited on the web at https://www.cursorless.org/cheatsheet, which will show the default spoken forms. -The implementation of the local version of the cheatsheet is split between the Talon side and the extension side. +The extension constructs the local cheatsheet from the canonical reference definitions in `lib-common` and the user's spoken-form lists in Talon's `state.json`. Talon only asks the extension to display it. Older Talon versions remain supported: the extension applies the spoken forms from their cheatsheet payload to the current structure, descriptions, and syntax. ## Adding a new spoken form -When you add a new scope type, action, modifier, etc, you'll need to ensure that it shows up both locally and on the website. It will usually automatically show up in the local cheatsheet. You can verify this by saying `"cursorless cheatsheet"` with your development version of `cursorless-talon` active in your Talon user directory, and inspecting the cheatsheet that appears. If it does not, you'll need to make fixes to [the Talon side of the cheatsheet](../../../../../cursorless-talon/src/cheatsheet). - -In either case, to get your changes to appear on the website, you need to update the defaults in [`defaults.json`](../../../../../packages/lib-cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json). First make sure you have the `cursorless-talon-dev` user file set in your Talon home directory, as indicated in the [initial contributor setup instructions](CONTRIBUTING.mdx#initial-setup). Then you can say `"cursorless update cheatsheet"` to update the default spoken forms. Note that this will use your custom spoken forms, so you may need to do some manual cleanup. +When you add a new scope type, action, modifier, etc, you'll need to ensure that it shows up both locally and on the website. Both cheatsheets are constructed from the reference definitions in `lib-common`, so reference changes appear automatically. The local version then applies the user's spoken forms from `state.json`. You can verify it by saying `"cursorless cheatsheet"` with your development version of `cursorless-talon` active in your Talon user directory. If a kind of spoken form is missing, add its raw Talon list to `state.json` rather than adding cheatsheet-specific assembly to Talon. ## Running the cheatsheet in development mode diff --git a/packages/app-web-docs/src/docs/user/README.md b/packages/app-web-docs/src/docs/user/README.md index 2816ad1059..99dbb49563 100644 --- a/packages/app-web-docs/src/docs/user/README.md +++ b/packages/app-web-docs/src/docs/user/README.md @@ -8,7 +8,7 @@ Welcome to Cursorless! If you're new, start with the [tutorial videos](https://w This guide assumes you've already [installed Cursorless](./installation.md). -Once you understand the concepts, you can pull up a cheatsheet by saying `"cursorless reference"` or `"cursorless cheatsheet"` with VSCode focused. You can return to these docs by saying `"cursorless docs"` or `"cursorless help"` within VSCode. +Once you understand the concepts, you can browse the [online cheatsheet](https://www.cursorless.org/cheatsheet), or pull up a local cheatsheet with your custom spoken forms by saying `"cursorless cheatsheet"` or `"cursorless reference"` with VSCode focused. You can return to these docs by saying `"cursorless docs"` or `"cursorless help"` within VSCode. To change any spoken forms, see [Customization](./customization.md). diff --git a/packages/app-web-docs/src/docs/user/actions/README.md b/packages/app-web-docs/src/docs/user/actions/README.md index bf9020f03d..9c9f107067 100644 --- a/packages/app-web-docs/src/docs/user/actions/README.md +++ b/packages/app-web-docs/src/docs/user/actions/README.md @@ -2,7 +2,7 @@ ## Cursor movement -- [`"take "`](./setSelection.mdx) - Set selection to ``. +- [`"take "`](./setSelection.mdx) - Select ``. - [`"pre "`](./setSelectionBefore.mdx) - Set empty selection before ``. - [`"post "`](./setSelectionAfter.mdx) - Set empty selection after ``. - [`"append "`](./addSelection.mdx) - Adds `` to the current selection set. diff --git a/packages/app-web-docs/src/docs/user/actions/rewrapWithPairedDelimiter.mdx b/packages/app-web-docs/src/docs/user/actions/rewrapWithPairedDelimiter.mdx index 05b4731282..30da712713 100644 --- a/packages/app-web-docs/src/docs/user/actions/rewrapWithPairedDelimiter.mdx +++ b/packages/app-web-docs/src/docs/user/actions/rewrapWithPairedDelimiter.mdx @@ -6,7 +6,7 @@ sidebar_label: Repack See [paired delimiters](../paired-delimiters.md) for the available pairs. -Cursorless ID: `rewrapWithPairedDelimiter` +Cursorless ID: `rewrap` ## Spoken form diff --git a/packages/app-web-docs/src/docs/user/actions/setSelection.mdx b/packages/app-web-docs/src/docs/user/actions/setSelection.mdx index 0b5d7909b0..9facbe794c 100644 --- a/packages/app-web-docs/src/docs/user/actions/setSelection.mdx +++ b/packages/app-web-docs/src/docs/user/actions/setSelection.mdx @@ -2,7 +2,7 @@ sidebar_label: Take --- -# Take (Set selection) +# Take (Select) When used with a list target, this action creates one cursor for each target. @@ -14,7 +14,7 @@ Default: `take` ## Syntax -- take `` - Set selection to ``. +- take `` - Select ``. ## Examples diff --git a/packages/app-web/src/Cheatsheet.tsx b/packages/app-web/src/Cheatsheet.tsx index e57c4952f4..bc1ed32ef3 100644 --- a/packages/app-web/src/Cheatsheet.tsx +++ b/packages/app-web/src/Cheatsheet.tsx @@ -1,12 +1,12 @@ import { Cheatsheet as OriginalCheatsheet } from "@cursorless/lib-cheatsheet"; -import defaultCheatsheetInfo from "@cursorless/lib-cheatsheet/defaultSpokenForms"; +import { getDefaultCheatsheetInfo } from "@cursorless/lib-common"; import { Title } from "./Title"; export function Cheatsheet() { return ( <> Cursorless cheatsheet - + ); } diff --git a/packages/lib-cheatsheet-local/README.md b/packages/lib-cheatsheet-local/README.md index 0e9663a771..616e5ad700 100644 --- a/packages/lib-cheatsheet-local/README.md +++ b/packages/lib-cheatsheet-local/README.md @@ -1,6 +1,6 @@ # Local cheatsheet -This app just bundles up the cheatsheet into a single file to be used when the user says `"cursorless cheatsheet"`. The file inlines all css and js so that it can be opened as a single file by the end user. During actual production use, Talon will send the user's custom spoken forms to the Cursorless engine, which will [inject them](../lib-engine/src/core/Cheatsheet.ts) into the cheatsheet using a global variable. +This app just bundles up the cheatsheet into a single file to be used when the user says `"cursorless cheatsheet"`. The file inlines all css and js so that it can be opened as a single file by the end user. During actual production use, the extension constructs the cheatsheet from the canonical reference definitions and the user's spoken-form lists in Talon's `state.json`, then injects it into the bundled page using a global variable. Note that there is no development server for this app. It is just a bundle step. If you want a live development environment for the cheatsheet, you should use the cheatsheet page in [the `app-web` package](../app-web). diff --git a/packages/lib-cheatsheet-local/package.json b/packages/lib-cheatsheet-local/package.json index bb44e99da5..ba096f11a9 100644 --- a/packages/lib-cheatsheet-local/package.json +++ b/packages/lib-cheatsheet-local/package.json @@ -21,7 +21,7 @@ "preact": "^10.29.8" }, "devDependencies": { - "@types/jest": "^30.0.0", + "@jest/globals": "^30.4.1", "@types/node": "^24.13.3", "jest": "^30.4.2", "typescript": "^6.0.3", diff --git a/packages/lib-cheatsheet-local/src/app.tsx b/packages/lib-cheatsheet-local/src/app.tsx index efc338c031..3e05ee56ef 100644 --- a/packages/lib-cheatsheet-local/src/app.tsx +++ b/packages/lib-cheatsheet-local/src/app.tsx @@ -1,5 +1,5 @@ -import type { CheatsheetInfo } from "@cursorless/lib-cheatsheet"; import { Cheatsheet } from "@cursorless/lib-cheatsheet"; +import type { CheatsheetInfo } from "@cursorless/lib-common"; import "./styles.css"; declare global { diff --git a/packages/lib-cheatsheet-local/src/test/app.spec.tsx b/packages/lib-cheatsheet-local/src/test/app.spec.tsx index 05915170ed..97c69a647f 100644 --- a/packages/lib-cheatsheet-local/src/test/app.spec.tsx +++ b/packages/lib-cheatsheet-local/src/test/app.spec.tsx @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; import { render } from "preact"; import { act } from "preact/test-utils"; import { fakeCheatsheetInfo } from "@cursorless/lib-cheatsheet"; diff --git a/packages/lib-cheatsheet-local/tsconfig.jest.json b/packages/lib-cheatsheet-local/tsconfig.jest.json index f7e35d10ae..188d7a5b44 100644 --- a/packages/lib-cheatsheet-local/tsconfig.jest.json +++ b/packages/lib-cheatsheet-local/tsconfig.jest.json @@ -3,6 +3,7 @@ "compilerOptions": { "ignoreDeprecations": "6.0", "module": "commonjs", - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["node"] } } diff --git a/packages/lib-cheatsheet-local/tsconfig.json b/packages/lib-cheatsheet-local/tsconfig.json index bb689757d2..20c49f2c53 100644 --- a/packages/lib-cheatsheet-local/tsconfig.json +++ b/packages/lib-cheatsheet-local/tsconfig.json @@ -1,12 +1,13 @@ { "extends": "../../tsconfig.web.json", "compilerOptions": { - "types": ["jest"] + "types": [] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.json", "../../resources/typings/**/*.d.ts" - ] + ], + "exclude": ["src/test"] } diff --git a/packages/lib-cheatsheet-local/vite.config.ts b/packages/lib-cheatsheet-local/vite.config.ts index 572edcab3e..0ffb901916 100644 --- a/packages/lib-cheatsheet-local/vite.config.ts +++ b/packages/lib-cheatsheet-local/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vite"; import type { UserConfig } from "vite"; import purgeCss from "vite-plugin-purgecss"; import { viteSingleFile } from "vite-plugin-singlefile"; -import defaultCheatsheetInfo from "@cursorless/lib-cheatsheet/defaultSpokenForms"; +import { getDefaultCheatsheetInfo } from "@cursorless/lib-common"; import { purgeCssOptions, viteHtmlParams, @@ -24,7 +24,7 @@ export default defineConfig((): UserConfig => { purgeCss(purgeCssOptions), viteSingleFile(), viteHtmlParams({ - FAKE_CHEATSHEET_INFO: JSON.stringify(defaultCheatsheetInfo), + FAKE_CHEATSHEET_INFO: JSON.stringify(getDefaultCheatsheetInfo()), }), ], }; diff --git a/packages/lib-cheatsheet/package.json b/packages/lib-cheatsheet/package.json index 2687c97df8..862692add0 100644 --- a/packages/lib-cheatsheet/package.json +++ b/packages/lib-cheatsheet/package.json @@ -5,8 +5,7 @@ "license": "MIT", "type": "module", "exports": { - ".": "./src/index.ts", - "./defaultSpokenForms": "./src/lib/sampleSpokenFormInfos/defaults.json" + ".": "./src/index.ts" }, "sideEffects": [ "*.css" @@ -22,7 +21,8 @@ "react-bootstrap-icons": "^1.11.6" }, "devDependencies": { - "@types/jest": "^30.0.0", + "@jest/globals": "^30.4.1", + "@types/node": "^24.13.3", "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", "ts-jest": "^29.4.12", diff --git a/packages/lib-cheatsheet/src/lib/Cheatsheet.tsx b/packages/lib-cheatsheet/src/lib/Cheatsheet.tsx index 8b8be08f76..66243d2048 100644 --- a/packages/lib-cheatsheet/src/lib/Cheatsheet.tsx +++ b/packages/lib-cheatsheet/src/lib/Cheatsheet.tsx @@ -1,7 +1,7 @@ import { useEffect } from "preact/hooks"; import { QuestionCircleFill } from "react-bootstrap-icons"; import "./cheatsheet.css"; -import type { CheatsheetInfo } from "./cheatsheet.types"; +import type { CheatsheetInfo } from "@cursorless/lib-common"; import { CheatsheetLegendSection } from "./CheatsheetLegendSection"; import { CheatsheetListSection } from "./CheatsheetListSection"; import { CheatsheetNotesSection } from "./CheatsheetNotesSection"; diff --git a/packages/lib-cheatsheet/src/lib/CheatsheetListSection.tsx b/packages/lib-cheatsheet/src/lib/CheatsheetListSection.tsx index 5d8d33b1e0..654f103fb1 100644 --- a/packages/lib-cheatsheet/src/lib/CheatsheetListSection.tsx +++ b/packages/lib-cheatsheet/src/lib/CheatsheetListSection.tsx @@ -1,5 +1,8 @@ import type { JSX } from "preact"; -import type { CheatsheetSection, Variation } from "./cheatsheet.types"; +import type { + CheatsheetSection, + CheatsheetVariation, +} from "@cursorless/lib-common"; import { formatCaptures } from "./utils/formatCaptures"; import { useIsHighlighted } from "./utils/useIsHighlighted"; @@ -47,7 +50,7 @@ export function CheatsheetListSection({ section }: Props): JSX.Element { } type CheatsheetListEntryProps = { - variation: Variation; + variation: CheatsheetVariation; }; function CheatsheetListEntry({ diff --git a/packages/lib-cheatsheet/src/lib/cheatsheet.types.tsx b/packages/lib-cheatsheet/src/lib/cheatsheet.types.tsx index 4770cac56c..1b3b5f763b 100644 --- a/packages/lib-cheatsheet/src/lib/cheatsheet.types.tsx +++ b/packages/lib-cheatsheet/src/lib/cheatsheet.types.tsx @@ -1,22 +1,3 @@ -export interface Variation { - spokenForm: string; - description: string; -} - -export interface CheatsheetSection { - name: string; - id: string; - items: { - id: string; - type: string; - variations: Variation[]; - }[]; -} - -export interface CheatsheetInfo { - sections: CheatsheetSection[]; -} - interface CheatsheetLegendEntry { term: string; definition: string; diff --git a/packages/lib-cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json b/packages/lib-cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json deleted file mode 100644 index a65a1c5957..0000000000 --- a/packages/lib-cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json +++ /dev/null @@ -1,2023 +0,0 @@ -{ - "sections": [ - { - "name": "Actions", - "id": "actions", - "items": [ - { - "id": "addSelection", - "type": "action", - "variations": [ - { - "spokenForm": "append ", - "description": "Add selection" - } - ] - }, - { - "id": "addSelectionAfter", - "type": "action", - "variations": [ - { - "spokenForm": "append post ", - "description": "Add selection after" - } - ] - }, - { - "id": "addSelectionBefore", - "type": "action", - "variations": [ - { - "spokenForm": "append pre ", - "description": "Add selection before" - } - ] - }, - { - "id": "applyFormatter", - "type": "action", - "variations": [ - { - "spokenForm": "format at ", - "description": "Reformat as " - } - ] - }, - { - "id": "breakLine", - "type": "action", - "variations": [ - { - "spokenForm": "break ", - "description": "Break line" - } - ] - }, - { - "id": "callAsFunction", - "type": "action", - "variations": [ - { - "spokenForm": "call ", - "description": "Insert call to on selection" - }, - { - "spokenForm": "call on ", - "description": "Insert call to on " - } - ] - }, - { - "id": "clearAndSetSelection", - "type": "action", - "variations": [ - { - "spokenForm": "change ", - "description": "Clear and set selection" - } - ] - }, - { - "id": "copyToClipboard", - "type": "action", - "variations": [ - { - "spokenForm": "copy ", - "description": "Copy to clipboard" - } - ] - }, - { - "id": "cutToClipboard", - "type": "action", - "variations": [ - { - "spokenForm": "carve ", - "description": "Cut to clipboard" - } - ] - }, - { - "id": "decrement", - "type": "action", - "variations": [ - { - "spokenForm": "decrement ", - "description": "Decrement" - } - ] - }, - { - "id": "deselect", - "type": "action", - "variations": [ - { - "spokenForm": "give ", - "description": "Deselect" - } - ] - }, - { - "id": "editNewLineAfter", - "type": "action", - "variations": [ - { - "spokenForm": "pour ", - "description": "Edit new line/scope after" - } - ] - }, - { - "id": "editNewLineBefore", - "type": "action", - "variations": [ - { - "spokenForm": "drink ", - "description": "Edit new line/scope before" - } - ] - }, - { - "id": "experimental.setInstanceReference", - "type": "action", - "variations": [ - { - "spokenForm": "from ", - "description": "Set instance reference" - } - ] - }, - { - "id": "extractVariable", - "type": "action", - "variations": [ - { - "spokenForm": "extract ", - "description": "Extract variable" - } - ] - }, - { - "id": "findInDocument", - "type": "action", - "variations": [ - { - "spokenForm": "scout ", - "description": "Find in document" - } - ] - }, - { - "id": "findInWorkspace", - "type": "action", - "variations": [ - { - "spokenForm": "scout all ", - "description": "Find in workspace" - } - ] - }, - { - "id": "flashTargets", - "type": "action", - "variations": [ - { - "spokenForm": "flash ", - "description": "Flash targets" - } - ] - }, - { - "id": "foldRegion", - "type": "action", - "variations": [ - { - "spokenForm": "fold ", - "description": "Fold region" - } - ] - }, - { - "id": "followLink", - "type": "action", - "variations": [ - { - "spokenForm": "follow ", - "description": "Follow link" - } - ] - }, - { - "id": "followLinkAside", - "type": "action", - "variations": [ - { - "spokenForm": "follow split ", - "description": "Follow link aside" - } - ] - }, - { - "id": "generateSnippet", - "type": "action", - "variations": [ - { - "spokenForm": "snip make ", - "description": "Generate snippet" - } - ] - }, - { - "id": "gitAccept", - "type": "action", - "variations": [ - { - "spokenForm": "git accept ", - "description": "Git accept" - } - ] - }, - { - "id": "gitRevert", - "type": "action", - "variations": [ - { - "spokenForm": "git revert ", - "description": "Git revert" - } - ] - }, - { - "id": "gitStage", - "type": "action", - "variations": [ - { - "spokenForm": "git stage ", - "description": "Git stage" - } - ] - }, - { - "id": "gitUnstage", - "type": "action", - "variations": [ - { - "spokenForm": "git unstage ", - "description": "Git unstage" - } - ] - }, - { - "id": "highlight", - "type": "action", - "variations": [ - { - "spokenForm": "highlight ", - "description": "Highlight" - } - ] - }, - { - "id": "increment", - "type": "action", - "variations": [ - { - "spokenForm": "increment ", - "description": "Increment" - } - ] - }, - { - "id": "indentLine", - "type": "action", - "variations": [ - { - "spokenForm": "indent ", - "description": "Indent line" - } - ] - }, - { - "id": "insertCopyAfter", - "type": "action", - "variations": [ - { - "spokenForm": "clone ", - "description": "Insert copy after" - } - ] - }, - { - "id": "insertCopyBefore", - "type": "action", - "variations": [ - { - "spokenForm": "clone up ", - "description": "Insert copy before" - } - ] - }, - { - "id": "insertEmptyLineAfter", - "type": "action", - "variations": [ - { - "spokenForm": "float ", - "description": "Insert empty line after" - } - ] - }, - { - "id": "insertEmptyLineBefore", - "type": "action", - "variations": [ - { - "spokenForm": "drop ", - "description": "Insert empty line before" - } - ] - }, - { - "id": "insertEmptyLinesAround", - "type": "action", - "variations": [ - { - "spokenForm": "puff ", - "description": "Insert empty lines around" - } - ] - }, - { - "id": "insertSnippet", - "type": "action", - "variations": [ - { - "spokenForm": "snip ", - "description": "Insert snippet at " - } - ] - }, - { - "id": "joinLines", - "type": "action", - "variations": [ - { - "spokenForm": "join ", - "description": "Join lines" - } - ] - }, - { - "id": "moveToTarget", - "type": "action", - "variations": [ - { - "spokenForm": "move ", - "description": "Move to " - }, - { - "spokenForm": "move ", - "description": "Move to selection" - } - ] - }, - { - "id": "nextHomophone", - "type": "action", - "variations": [ - { - "spokenForm": "phones ", - "description": "Next homophone" - } - ] - }, - { - "id": "outdentLine", - "type": "action", - "variations": [ - { - "spokenForm": "dedent ", - "description": "Outdent line" - } - ] - }, - { - "id": "pasteFromClipboard", - "type": "action", - "variations": [ - { - "spokenForm": "paste ", - "description": "Paste from clipboard at " - } - ] - }, - { - "id": "randomizeTargets", - "type": "action", - "variations": [ - { - "spokenForm": "shuffle ", - "description": "Randomize targets" - } - ] - }, - { - "id": "remove", - "type": "action", - "variations": [ - { - "spokenForm": "chuck ", - "description": "Remove" - } - ] - }, - { - "id": "rename", - "type": "action", - "variations": [ - { - "spokenForm": "rename ", - "description": "Rename" - } - ] - }, - { - "id": "replaceWithTarget", - "type": "action", - "variations": [ - { - "spokenForm": "bring ", - "description": "Copy to " - }, - { - "spokenForm": "bring ", - "description": "Insert copy of at selection" - } - ] - }, - { - "id": "revealDefinition", - "type": "action", - "variations": [ - { - "spokenForm": "define ", - "description": "Reveal definition" - } - ] - }, - { - "id": "revealTypeDefinition", - "type": "action", - "variations": [ - { - "spokenForm": "type deaf ", - "description": "Reveal type definition" - } - ] - }, - { - "id": "reverseTargets", - "type": "action", - "variations": [ - { - "spokenForm": "reverse ", - "description": "Reverse targets" - } - ] - }, - { - "id": "rewrap", - "type": "action", - "variations": [ - { - "spokenForm": " repack ", - "description": "Rewrap with " - } - ] - }, - { - "id": "scrollToBottom", - "type": "action", - "variations": [ - { - "spokenForm": "bottom ", - "description": "Scroll to bottom" - } - ] - }, - { - "id": "scrollToCenter", - "type": "action", - "variations": [ - { - "spokenForm": "center ", - "description": "Scroll to center" - } - ] - }, - { - "id": "scrollToTop", - "type": "action", - "variations": [ - { - "spokenForm": "crown ", - "description": "Scroll to top" - } - ] - }, - { - "id": "setSelection", - "type": "action", - "variations": [ - { - "spokenForm": "take ", - "description": "Set selection" - } - ] - }, - { - "id": "setSelectionAfter", - "type": "action", - "variations": [ - { - "spokenForm": "post ", - "description": "Set selection after" - } - ] - }, - { - "id": "setSelectionBefore", - "type": "action", - "variations": [ - { - "spokenForm": "pre ", - "description": "Set selection before" - } - ] - }, - { - "id": "showDebugHover", - "type": "action", - "variations": [ - { - "spokenForm": "inspect ", - "description": "Show debug hover" - } - ] - }, - { - "id": "showHover", - "type": "action", - "variations": [ - { - "spokenForm": "hover ", - "description": "Show hover" - } - ] - }, - { - "id": "showQuickFix", - "type": "action", - "variations": [ - { - "spokenForm": "quick fix ", - "description": "Show quick fix" - } - ] - }, - { - "id": "showReferences", - "type": "action", - "variations": [ - { - "spokenForm": "reference ", - "description": "Show references" - } - ] - }, - { - "id": "sortTargets", - "type": "action", - "variations": [ - { - "spokenForm": "sort ", - "description": "Sort targets" - } - ] - }, - { - "id": "swapTargets", - "type": "action", - "variations": [ - { - "spokenForm": "swap with ", - "description": "Swap with " - }, - { - "spokenForm": "swap with ", - "description": "Swap selection with " - } - ] - }, - { - "id": "toggleLineBreakpoint", - "type": "action", - "variations": [ - { - "spokenForm": "break point ", - "description": "Toggle line breakpoint" - } - ] - }, - { - "id": "toggleLineComment", - "type": "action", - "variations": [ - { - "spokenForm": "comment ", - "description": "Toggle line comment" - } - ] - }, - { - "id": "unfoldRegion", - "type": "action", - "variations": [ - { - "spokenForm": "unfold ", - "description": "Unfold region" - } - ] - }, - { - "id": "wrapWithPairedDelimiter", - "type": "action", - "variations": [ - { - "spokenForm": " wrap ", - "description": "Wrap with " - }, - { - "spokenForm": " wrap ", - "description": "Wrap with " - } - ] - } - ] - }, - { - "name": "Colors", - "id": "colors", - "items": [ - { - "id": "blue", - "type": "hatColor", - "variations": [ - { - "spokenForm": "blue", - "description": "Blue" - } - ] - }, - { - "id": "green", - "type": "hatColor", - "variations": [ - { - "spokenForm": "green", - "description": "Green" - } - ] - }, - { - "id": "pink", - "type": "hatColor", - "variations": [ - { - "spokenForm": "pink", - "description": "Pink" - } - ] - }, - { - "id": "red", - "type": "hatColor", - "variations": [ - { - "spokenForm": "red", - "description": "Red" - } - ] - }, - { - "id": "yellow", - "type": "hatColor", - "variations": [ - { - "spokenForm": "yellow", - "description": "Yellow" - } - ] - } - ] - }, - { - "name": "Compound targets", - "id": "compoundTargets", - "items": [ - { - "id": "listConnective", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": " and ", - "description": " and " - } - ] - }, - { - "id": "rangeExcludingEnd", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": " until ", - "description": " until start of " - }, - { - "spokenForm": "until ", - "description": "selection until start of " - } - ] - }, - { - "id": "rangeExclusive", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": " between ", - "description": "between and " - }, - { - "spokenForm": "between ", - "description": "between selection and " - } - ] - }, - { - "id": "rangeInclusive", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": " past ", - "description": " through " - }, - { - "spokenForm": "past ", - "description": "selection through " - } - ] - }, - { - "id": "verticalRange", - "type": "compoundTargetConnective", - "variations": [ - { - "spokenForm": " slice ", - "description": " vertically through " - }, - { - "spokenForm": "slice ", - "description": "selection vertically through " - } - ] - } - ] - }, - { - "name": "Destinations", - "id": "destinations", - "items": [ - { - "id": "destination_after", - "type": "destination", - "variations": [ - { - "spokenForm": "after ", - "description": "Insert after " - } - ] - }, - { - "id": "destination_before", - "type": "destination", - "variations": [ - { - "spokenForm": "before ", - "description": "Insert before " - } - ] - }, - { - "id": "destination_to", - "type": "destination", - "variations": [ - { - "spokenForm": "to ", - "description": "Replace " - } - ] - } - ] - }, - { - "name": "Modifiers", - "id": "modifiers", - "items": [ - { - "id": "ancestor", - "type": "modifier", - "variations": [ - { - "spokenForm": "grand ", - "description": "Grandparent containing instance of " - } - ] - }, - { - "id": "containingScope", - "type": "modifier", - "variations": [ - { - "spokenForm": "", - "description": "Containing instance of " - } - ] - }, - { - "id": "end", - "type": "modifier", - "variations": [ - { - "spokenForm": "end of", - "description": "Empty position at end of target" - } - ] - }, - { - "id": "every", - "type": "modifier", - "variations": [ - { - "spokenForm": "every ", - "description": "Every instance of " - } - ] - }, - { - "id": "excludeInterior", - "type": "modifier", - "variations": [ - { - "spokenForm": "bounds", - "description": "Bounding paired delimiters" - } - ] - }, - { - "id": "extendThroughEndOf", - "type": "modifier", - "variations": [ - { - "spokenForm": "tail", - "description": "Extend through end of line/pair" - }, - { - "spokenForm": "tail ", - "description": "Extend through end of " - } - ] - }, - { - "id": "extendThroughStartOf", - "type": "modifier", - "variations": [ - { - "spokenForm": "head", - "description": "Extend through start of line/pair" - }, - { - "spokenForm": "head ", - "description": "Extend through start of " - } - ] - }, - { - "id": "inferPreviousMark", - "type": "modifier", - "variations": [ - { - "spokenForm": "its", - "description": "Infer previous mark" - } - ] - }, - { - "id": "interiorOnly", - "type": "modifier", - "variations": [ - { - "spokenForm": "inside", - "description": "Interior only" - } - ] - }, - { - "id": "keepContentFilter", - "type": "modifier", - "variations": [ - { - "spokenForm": "content", - "description": "Keep content filter" - } - ] - }, - { - "id": "keepEmptyFilter", - "type": "modifier", - "variations": [ - { - "spokenForm": "empty", - "description": "Keep empty filter" - } - ] - }, - { - "id": "leading", - "type": "modifier", - "variations": [ - { - "spokenForm": "leading", - "description": "Leading delimiter range" - } - ] - }, - { - "id": "ordinalScope", - "type": "modifier", - "variations": [ - { - "spokenForm": " ", - "description": " instance of in iteration scope" - }, - { - "spokenForm": " last ", - "description": "-to-last instance of in iteration scope" - }, - { - "spokenForm": "first s", - "description": "first instances of in iteration scope, as contiguous range" - }, - { - "spokenForm": "every first s", - "description": "first instances of in iteration scope, as individual targets" - }, - { - "spokenForm": "last s", - "description": "last instances of in iteration scope, as contiguous range" - }, - { - "spokenForm": "every last s", - "description": "last instances of in iteration scope, as individual targets" - } - ] - }, - { - "id": "relativeScope", - "type": "modifier", - "variations": [ - { - "spokenForm": "previous ", - "description": "Previous instance of " - }, - { - "spokenForm": " previous ", - "description": " instance of before target" - }, - { - "spokenForm": "next ", - "description": "Next instance of " - }, - { - "spokenForm": " next ", - "description": " instance of after target" - }, - { - "spokenForm": " backward", - "description": "single instance of including target, going backwards" - }, - { - "spokenForm": " forward", - "description": "single instance of including target, going forwards" - }, - { - "spokenForm": " s backward", - "description": " instances of including target, going backwards, as contiguous range" - }, - { - "spokenForm": "every s backward", - "description": " instances of including target, going backwards, as individual targets" - }, - { - "spokenForm": " s", - "description": " instances of including target, going forwards, as contiguous range" - }, - { - "spokenForm": "every s", - "description": " instances of including target, going forwards, as individual targets" - }, - { - "spokenForm": "previous s", - "description": "previous instances of , as contiguous range" - }, - { - "spokenForm": "every previous s", - "description": "previous instances of , as individual targets" - }, - { - "spokenForm": "next s", - "description": "next instances of , as contiguous range" - }, - { - "spokenForm": "every next s", - "description": "next instances of , as individual targets" - } - ] - }, - { - "id": "start", - "type": "modifier", - "variations": [ - { - "spokenForm": "start of", - "description": "Empty position at start of target" - } - ] - }, - { - "id": "toRawSelection", - "type": "modifier", - "variations": [ - { - "spokenForm": "just", - "description": "No inference" - } - ] - }, - { - "id": "trailing", - "type": "modifier", - "variations": [ - { - "spokenForm": "trailing", - "description": "Trailing delimiter range" - } - ] - }, - { - "id": "visible", - "type": "modifier", - "variations": [ - { - "spokenForm": "visible", - "description": "Visible" - } - ] - } - ] - }, - { - "name": "Paired delimiters", - "id": "pairedDelimiters", - "items": [ - { - "id": "angleBrackets", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "diamond", - "description": "Angle brackets" - } - ] - }, - { - "id": "any", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "pair", - "description": "Any" - } - ] - }, - { - "id": "backtickQuotes", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "skis", - "description": "Backtick quotes" - } - ] - }, - { - "id": "curlyBrackets", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "curly", - "description": "Curly brackets" - } - ] - }, - { - "id": "doubleQuotes", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "quad", - "description": "Double quotes" - } - ] - }, - { - "id": "escapedDoubleQuotes", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "escaped quad", - "description": "Escaped double quotes" - } - ] - }, - { - "id": "escapedParentheses", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "escaped round", - "description": "Escaped parentheses" - } - ] - }, - { - "id": "escapedSingleQuotes", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "escaped twin", - "description": "Escaped single quotes" - } - ] - }, - { - "id": "escapedSquareBrackets", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "escaped box", - "description": "Escaped square brackets" - } - ] - }, - { - "id": "parentheses", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "round", - "description": "Parentheses" - } - ] - }, - { - "id": "singleQuotes", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "twin", - "description": "Single quotes" - } - ] - }, - { - "id": "squareBrackets", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "box", - "description": "Square brackets" - } - ] - }, - { - "id": "string", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "string", - "description": "String" - } - ] - }, - { - "id": "whitespace", - "type": "pairedDelimiter", - "variations": [ - { - "spokenForm": "void", - "description": "Whitespace" - } - ] - } - ] - }, - { - "name": "Scope visualizer", - "id": "scopeVisualizer", - "items": [ - { - "id": "hideScopeVisualizer", - "type": "command", - "variations": [ - { - "spokenForm": "visualize nothing", - "description": "Hide scope visualizer" - } - ] - }, - { - "id": "show_scope_sidebar", - "type": "command", - "variations": [ - { - "spokenForm": "bar cursorless", - "description": "Show cursorless sidebar" - } - ] - }, - { - "id": "show_scope_visualizer", - "type": "command", - "variations": [ - { - "spokenForm": "visualize ", - "description": "Visualize " - }, - { - "spokenForm": "visualize removal", - "description": "Visualize removal range" - }, - { - "spokenForm": "visualize iteration", - "description": "Visualize iteration range" - } - ] - } - ] - }, - { - "name": "Scopes", - "id": "scopes", - "items": [ - { - "id": "anonymousFunction", - "type": "scopeType", - "variations": [ - { - "spokenForm": "lambda", - "description": "Anonymous function" - } - ] - }, - { - "id": "argumentList", - "type": "scopeType", - "variations": [ - { - "spokenForm": "arg list", - "description": "Argument list" - } - ] - }, - { - "id": "argumentOrParameter", - "type": "scopeType", - "variations": [ - { - "spokenForm": "arg", - "description": "Argument" - } - ] - }, - { - "id": "attribute", - "type": "scopeType", - "variations": [ - { - "spokenForm": "attribute", - "description": "Attribute" - } - ] - }, - { - "id": "boundedNonWhitespaceSequence", - "type": "scopeType", - "variations": [ - { - "spokenForm": "short paint", - "description": "Non-whitespace sequence bounded by surrounding pair delimiters" - } - ] - }, - { - "id": "boundedParagraph", - "type": "scopeType", - "variations": [ - { - "spokenForm": "short block", - "description": "Paragraph bounded by surrounding pair delimiters" - } - ] - }, - { - "id": "branch", - "type": "scopeType", - "variations": [ - { - "spokenForm": "branch", - "description": "Branch" - } - ] - }, - { - "id": "chapter", - "type": "scopeType", - "variations": [ - { - "spokenForm": "chapter", - "description": "Chapter" - } - ] - }, - { - "id": "character", - "type": "scopeType", - "variations": [ - { - "spokenForm": "char", - "description": "Character" - } - ] - }, - { - "id": "class", - "type": "scopeType", - "variations": [ - { - "spokenForm": "class", - "description": "Class" - } - ] - }, - { - "id": "className", - "type": "scopeType", - "variations": [ - { - "spokenForm": "class name", - "description": "Class name" - } - ] - }, - { - "id": "collectionItem", - "type": "scopeType", - "variations": [ - { - "spokenForm": "item", - "description": "Collection item" - } - ] - }, - { - "id": "collectionKey", - "type": "scopeType", - "variations": [ - { - "spokenForm": "key", - "description": "Collection key" - } - ] - }, - { - "id": "command", - "type": "scopeType", - "variations": [ - { - "spokenForm": "command", - "description": "Command" - } - ] - }, - { - "id": "comment", - "type": "scopeType", - "variations": [ - { - "spokenForm": "comment", - "description": "Comment" - } - ] - }, - { - "id": "condition", - "type": "scopeType", - "variations": [ - { - "spokenForm": "condition", - "description": "Condition" - } - ] - }, - { - "id": "document", - "type": "scopeType", - "variations": [ - { - "spokenForm": "file", - "description": "Document" - } - ] - }, - { - "id": "environment", - "type": "scopeType", - "variations": [ - { - "spokenForm": "environment", - "description": "Environment" - } - ] - }, - { - "id": "fullLine", - "type": "scopeType", - "variations": [ - { - "spokenForm": "full line", - "description": "Full line" - } - ] - }, - { - "id": "functionCall", - "type": "scopeType", - "variations": [ - { - "spokenForm": "call", - "description": "Function call" - } - ] - }, - { - "id": "functionCallee", - "type": "scopeType", - "variations": [ - { - "spokenForm": "callee", - "description": "Function callee" - } - ] - }, - { - "id": "functionName", - "type": "scopeType", - "variations": [ - { - "spokenForm": "funk name", - "description": "Function name" - } - ] - }, - { - "id": "glyph", - "type": "scopeType", - "variations": [ - { - "spokenForm": "glyph ", - "description": "Instance of single character " - } - ] - }, - { - "id": "identifier", - "type": "scopeType", - "variations": [ - { - "spokenForm": "identifier", - "description": "Identifier" - } - ] - }, - { - "id": "ifStatement", - "type": "scopeType", - "variations": [ - { - "spokenForm": "if state", - "description": "If statement" - } - ] - }, - { - "id": "instance", - "type": "scopeType", - "variations": [ - { - "spokenForm": "instance", - "description": "Instance" - } - ] - }, - { - "id": "line", - "type": "scopeType", - "variations": [ - { - "spokenForm": "line", - "description": "Line" - } - ] - }, - { - "id": "list", - "type": "scopeType", - "variations": [ - { - "spokenForm": "list", - "description": "List" - } - ] - }, - { - "id": "map", - "type": "scopeType", - "variations": [ - { - "spokenForm": "map", - "description": "Map" - } - ] - }, - { - "id": "name", - "type": "scopeType", - "variations": [ - { - "spokenForm": "name", - "description": "Name" - } - ] - }, - { - "id": "namedFunction", - "type": "scopeType", - "variations": [ - { - "spokenForm": "funk", - "description": "Named function" - } - ] - }, - { - "id": "namedParagraph", - "type": "scopeType", - "variations": [ - { - "spokenForm": "paragraph", - "description": "Named paragraph" - } - ] - }, - { - "id": "nonWhitespaceSequence", - "type": "scopeType", - "variations": [ - { - "spokenForm": "paint", - "description": "Non whitespace sequence" - } - ] - }, - { - "id": "notebookCell", - "type": "scopeType", - "variations": [ - { - "spokenForm": "cell", - "description": "Notebook cell" - } - ] - }, - { - "id": "pair", - "type": "scopeType", - "variations": [ - { - "spokenForm": "", - "description": "Paired delimiters" - } - ] - }, - { - "id": "paragraph", - "type": "scopeType", - "variations": [ - { - "spokenForm": "block", - "description": "Paragraph" - } - ] - }, - { - "id": "part", - "type": "scopeType", - "variations": [ - { - "spokenForm": "part", - "description": "Part" - } - ] - }, - { - "id": "regularExpression", - "type": "scopeType", - "variations": [ - { - "spokenForm": "regex", - "description": "Regular expression" - } - ] - }, - { - "id": "section", - "type": "scopeType", - "variations": [ - { - "spokenForm": "section", - "description": "Section" - } - ] - }, - { - "id": "selector", - "type": "scopeType", - "variations": [ - { - "spokenForm": "selector", - "description": "Selector" - } - ] - }, - { - "id": "sentence", - "type": "scopeType", - "variations": [ - { - "spokenForm": "sentence", - "description": "Sentence" - } - ] - }, - { - "id": "statement", - "type": "scopeType", - "variations": [ - { - "spokenForm": "state", - "description": "Statement" - } - ] - }, - { - "id": "subParagraph", - "type": "scopeType", - "variations": [ - { - "spokenForm": "subparagraph", - "description": "Sub paragraph" - } - ] - }, - { - "id": "subSection", - "type": "scopeType", - "variations": [ - { - "spokenForm": "subsection", - "description": "Sub section" - } - ] - }, - { - "id": "subSubSection", - "type": "scopeType", - "variations": [ - { - "spokenForm": "subsubsection", - "description": "Sub sub section" - } - ] - }, - { - "id": "token", - "type": "scopeType", - "variations": [ - { - "spokenForm": "token", - "description": "Token" - } - ] - }, - { - "id": "type", - "type": "scopeType", - "variations": [ - { - "spokenForm": "type", - "description": "Type" - } - ] - }, - { - "id": "unit", - "type": "scopeType", - "variations": [ - { - "spokenForm": "unit", - "description": "Unit" - } - ] - }, - { - "id": "url", - "type": "scopeType", - "variations": [ - { - "spokenForm": "link", - "description": "Url" - } - ] - }, - { - "id": "value", - "type": "scopeType", - "variations": [ - { - "spokenForm": "value", - "description": "Value" - } - ] - }, - { - "id": "word", - "type": "scopeType", - "variations": [ - { - "spokenForm": "sub", - "description": "Word" - } - ] - }, - { - "id": "xmlBothTags", - "type": "scopeType", - "variations": [ - { - "spokenForm": "tags", - "description": "Xml both tags" - } - ] - }, - { - "id": "xmlElement", - "type": "scopeType", - "variations": [ - { - "spokenForm": "element", - "description": "Xml element" - } - ] - }, - { - "id": "xmlEndTag", - "type": "scopeType", - "variations": [ - { - "spokenForm": "end tag", - "description": "Xml end tag" - } - ] - }, - { - "id": "xmlStartTag", - "type": "scopeType", - "variations": [ - { - "spokenForm": "start tag", - "description": "Xml start tag" - } - ] - } - ] - }, - { - "name": "Shapes", - "id": "shapes", - "items": [] - }, - { - "name": "Special marks", - "id": "specialMarks", - "items": [ - { - "id": "currentSelection", - "type": "mark", - "variations": [ - { - "spokenForm": "this", - "description": "Current selection" - } - ] - }, - { - "id": "lineNumberModulo100", - "type": "mark", - "variations": [ - { - "spokenForm": "row ", - "description": "Line number modulo 100" - } - ] - }, - { - "id": "lineNumberRelativeDown", - "type": "mark", - "variations": [ - { - "spokenForm": "down ", - "description": "Line number down from cursor" - } - ] - }, - { - "id": "lineNumberRelativeUp", - "type": "mark", - "variations": [ - { - "spokenForm": "up ", - "description": "Line number up from cursor" - } - ] - }, - { - "id": "nothing", - "type": "mark", - "variations": [ - { - "spokenForm": "nothing", - "description": "Nothing" - } - ] - }, - { - "id": "previousSource", - "type": "mark", - "variations": [ - { - "spokenForm": "source", - "description": "Previous source" - } - ] - }, - { - "id": "previousTarget", - "type": "mark", - "variations": [ - { - "spokenForm": "that", - "description": "Previous target" - } - ] - }, - { - "id": "unknownSymbol", - "type": "mark", - "variations": [ - { - "spokenForm": "special", - "description": "Unknown symbol" - } - ] - } - ] - }, - { - "name": "Tutorial", - "id": "tutorial", - "items": [ - { - "id": "start_tutorial", - "type": "command", - "variations": [ - { - "spokenForm": "cursorless tutorial", - "description": "Start the introductory Cursorless tutorial" - } - ] - }, - { - "id": "tutorial_close", - "type": "command", - "variations": [ - { - "spokenForm": "tutorial close", - "description": "Close the tutorial" - } - ] - }, - { - "id": "tutorial_list", - "type": "command", - "variations": [ - { - "spokenForm": "tutorial list", - "description": "List all available tutorials" - } - ] - }, - { - "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_start_by_number", - "type": "command", - "variations": [ - { - "spokenForm": "tutorial ", - "description": "Start a specific tutorial by number" - } - ] - } - ] - } - ] -} diff --git a/packages/lib-cheatsheet/src/test/cheatsheet.spec.tsx b/packages/lib-cheatsheet/src/test/cheatsheet.spec.tsx index a99a265998..0933f7a89f 100644 --- a/packages/lib-cheatsheet/src/test/cheatsheet.spec.tsx +++ b/packages/lib-cheatsheet/src/test/cheatsheet.spec.tsx @@ -1,3 +1,4 @@ +import { afterEach, describe, expect, it } from "@jest/globals"; import { render } from "preact"; import { act } from "preact/test-utils"; import { Cheatsheet } from "../lib/Cheatsheet"; diff --git a/packages/lib-cheatsheet/src/test/formatCaptures.spec.tsx b/packages/lib-cheatsheet/src/test/formatCaptures.spec.tsx index 5cffc56ca1..f5f68d719d 100644 --- a/packages/lib-cheatsheet/src/test/formatCaptures.spec.tsx +++ b/packages/lib-cheatsheet/src/test/formatCaptures.spec.tsx @@ -1,3 +1,4 @@ +import { afterEach, describe, expect, it } from "@jest/globals"; import { render } from "preact"; import { act } from "preact/test-utils"; import { formatCaptures } from "../lib/utils/formatCaptures"; diff --git a/packages/lib-cheatsheet/src/test/getDefaultCheatsheetInfo.spec.ts b/packages/lib-cheatsheet/src/test/getDefaultCheatsheetInfo.spec.ts new file mode 100644 index 0000000000..c47eabee6d --- /dev/null +++ b/packages/lib-cheatsheet/src/test/getDefaultCheatsheetInfo.spec.ts @@ -0,0 +1,419 @@ +import { describe, expect, test } from "@jest/globals"; +import type { CheatsheetInfo } from "@cursorless/lib-common/cheatsheet"; +import { + getCheatsheetInfo, + getDefaultCheatsheetInfo, +} from "@cursorless/lib-common/cheatsheet"; + +describe("getDefaultCheatsheetInfo", () => { + const cheatsheetInfo = getDefaultCheatsheetInfo(); + + test("constructs action syntax from the reference definition", () => { + expect(getItem("actions", "swapTargets").variations).toEqual([ + { + spokenForm: "swap with ", + description: "Swap selection with ", + }, + { + spokenForm: "swap with ", + description: "Swap with ", + }, + ]); + }); + + test("omits private and disabled-by-default references", () => { + expect(getSection("actions").items).not.toContainEqual( + expect.objectContaining({ id: "private.showParseTree" }), + ); + expect(getSection("scopes").items).not.toContainEqual( + expect.objectContaining({ id: "private.fieldAccess" }), + ); + expect(getSection("scopes").items).not.toContainEqual( + expect.objectContaining({ id: "sectionLevelOne" }), + ); + }); + + test("includes private references with actual spoken forms", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { + type: "action", + id: "private.showParseTree", + spokenForms: ["inspect parse tree"], + }, + { + type: "simpleScopeTypeType", + id: "private.fieldAccess", + spokenForms: ["access"], + }, + ]); + + expect( + getItem("actions", "private.showParseTree", customCheatsheetInfo) + .variations[0]?.description, + ).toMatch(/\(PRIVATE\)$/u); + expect( + getItem("scopes", "private.fieldAccess", customCheatsheetInfo) + .variations[0]?.description, + ).toMatch(/\(PRIVATE\)$/u); + }); + + test("uses canonical reference ids", () => { + expect(getItem("modifiers", "everyScope")).toBeDefined(); + expect(getItem("scopes", "surroundingPair")).toBeDefined(); + expect(getItem("actions", "rewrapWithPairedDelimiter")).toBeDefined(); + }); + + test("constructs default destinations", () => { + expect(getSection("destinations").items).toEqual([ + { + id: "destination_after", + type: "destination", + variations: [ + { + spokenForm: "after ", + description: "Insert after ", + }, + ], + }, + { + id: "destination_before", + type: "destination", + variations: [ + { + spokenForm: "before ", + description: "Insert before ", + }, + ], + }, + { + id: "destination_to", + type: "destination", + variations: [ + { + spokenForm: "to ", + description: "Replace ", + }, + ], + }, + ]); + }); + + test("includes only colors and shapes that are enabled by default", () => { + expect(getSection("colors").items.map(({ id }) => id)).toEqual([ + "blue", + "green", + "red", + "pink", + "yellow", + ]); + expect(getSection("shapes").items).toEqual([]); + }); + + test("applies Talon spoken-form entries to the current syntax", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { + type: "action", + id: "editNewLineBefore", + spokenForms: ["gulp"], + }, + { type: "action", id: "swapTargets", spokenForms: ["swap"] }, + { + type: "action", + id: "pasteFromClipboard", + spokenForms: ["paste", "plop"], + }, + { type: "action", id: "callAsFunction", spokenForms: ["call"] }, + { + type: "modifierExtra", + id: "ancestor", + spokenForms: ["parental", "ancestral"], + }, + { + type: "simpleModifier", + id: "interiorOnly", + spokenForms: ["within"], + }, + { type: "connective", id: "on", spokenForms: ["onto", "upon"] }, + { + type: "simpleScopeTypeType", + id: "token", + spokenForms: ["word unit", "lexeme"], + }, + { + type: "simpleScopeTypeType", + id: "sectionLevelOne", + spokenForms: ["one section"], + }, + { + type: "connective", + id: "rangeExcludingStart", + spokenForms: ["from end"], + }, + { + type: "connective", + id: "swapConnective", + spokenForms: ["versus"], + }, + ]); + + expect( + getItem("actions", "editNewLineBefore", customCheatsheetInfo).variations, + ).toEqual([ + { spokenForm: "gulp ", description: "Edit new line before" }, + { + spokenForm: "gulp ", + description: "Edit new before", + }, + ]); + expect( + getItem("modifiers", "interiorOnly", customCheatsheetInfo).variations[0] + ?.spokenForm, + ).toBe("within"); + expect( + getItem("modifiers", "ancestor", customCheatsheetInfo).variations.map( + ({ spokenForm }) => spokenForm, + ), + ).toEqual(["parental "]); + expect( + getItem("scopes", "token", customCheatsheetInfo).variations.map( + ({ spokenForm }) => spokenForm, + ), + ).toEqual(["word unit"]); + expect( + getItem("scopes", "sectionLevelOne", customCheatsheetInfo).variations[0] + ?.spokenForm, + ).toBe("one section"); + expect( + getItem("compoundTargets", "rangeExcludingStart", customCheatsheetInfo) + .variations[0]?.spokenForm, + ).toBe(" from end "); + expect( + getItem("actions", "swapTargets", customCheatsheetInfo).variations[0] + ?.spokenForm, + ).toBe("swap versus "); + expect( + getItem("actions", "pasteFromClipboard", customCheatsheetInfo).variations, + ).toEqual([ + { + spokenForm: "paste ", + description: "Paste from clipboard at ", + }, + ]); + expect( + getItem("actions", "callAsFunction", customCheatsheetInfo).variations.map( + ({ spokenForm }) => spokenForm, + ), + ).toEqual(["call ", "call onto "]); + }); + + test("does not apply replacements to customized spoken forms", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { type: "action", id: "swapTargets", spokenForms: ["swap"] }, + { + type: "connective", + id: "swapConnective", + spokenForms: ["next"], + }, + { type: "modifierExtra", id: "next", spokenForms: ["afterward"] }, + ]); + + expect( + getItem("actions", "swapTargets", customCheatsheetInfo).variations[0] + ?.spokenForm, + ).toBe("swap next "); + }); + + test("includes only the first spoken form for custom actions", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { + type: "customAction", + id: "editor.action.moveLinesDownAction", + spokenForms: ["push down", "shove down"], + }, + { + type: "customAction", + id: "disabled.action", + spokenForms: [], + }, + ]); + + expect( + getItem( + "actions", + "editor.action.moveLinesDownAction", + customCheatsheetInfo, + ), + ).toEqual({ + id: "editor.action.moveLinesDownAction", + type: "action", + variations: [ + { + spokenForm: "push down ", + description: "Editor action move lines down action", + }, + ], + }); + expect( + getSection("actions", customCheatsheetInfo).items, + ).not.toContainEqual(expect.objectContaining({ id: "disabled.action" })); + }); + + test("includes custom regex scopes", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { + type: "customRegex", + id: String.raw`[\w.]+`, + spokenForms: ["dotted", "dotty"], + }, + { + type: "customRegex", + id: "disabled", + spokenForms: [], + }, + ]); + + expect( + getItem("scopes", "customRegex.dotted", customCheatsheetInfo), + ).toEqual({ + id: "customRegex.dotted", + type: "scopeType", + variations: [ + { + spokenForm: "dotted", + description: String.raw`/[\w.]+/`, + }, + ], + }); + expect(getSection("scopes", customCheatsheetInfo).items).not.toContainEqual( + expect.objectContaining({ id: "customRegex.disabled" }), + ); + }); + + test("an empty spoken-form entry disables only the corresponding item", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { type: "simpleScopeTypeType", id: "token", spokenForms: [] }, + { type: "action", id: "swapTargets", spokenForms: ["swap"] }, + { + type: "connective", + id: "swapConnective", + spokenForms: ["with"], + }, + ]); + + expect(getSection("scopes", customCheatsheetInfo).items).not.toContainEqual( + expect.objectContaining({ id: "token" }), + ); + expect( + getItem("actions", "swapTargets", customCheatsheetInfo), + ).toBeDefined(); + }); + + test("omits syntax examples whose spoken form is missing or disabled", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { type: "action", id: "callAsFunction", spokenForms: ["call"] }, + { type: "action", id: "swapTargets", spokenForms: ["swap"] }, + { type: "connective", id: "swapConnective", spokenForms: [] }, + ]); + + expect( + getItem("actions", "callAsFunction", customCheatsheetInfo).variations, + ).toEqual([ + { + spokenForm: "call ", + description: "Insert call to on selection", + }, + ]); + expect( + getSection("actions", customCheatsheetInfo).items, + ).not.toContainEqual(expect.objectContaining({ id: "swapTargets" })); + }); + + test("constructs destinations only from enabled spoken forms", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { + type: "insertionMode", + id: "before", + spokenForms: ["ahead of", "prior to"], + }, + { type: "insertionMode", id: "to", spokenForms: ["toward"] }, + ]); + + expect(getSection("destinations", customCheatsheetInfo).items).toEqual([ + { + id: "destination_before", + type: "destination", + variations: [ + { + spokenForm: "ahead of ", + description: "Insert before ", + }, + ], + }, + { + id: "destination_to", + type: "destination", + variations: [ + { + spokenForm: "toward ", + description: "Replace ", + }, + ], + }, + ]); + }); + + test("constructs scope visualizer commands from enabled spoken forms", () => { + const customCheatsheetInfo = getCheatsheetInfo([ + { + type: "scopeVisualizer", + id: "showScopeVisualizer", + spokenForms: ["inspect", "visualize"], + }, + { + type: "scopeVisualizer", + id: "hideScopeVisualizer", + spokenForms: [], + }, + { + type: "scopeVisualizer", + id: "removal", + spokenForms: ["deletion"], + }, + ]); + + expect( + getItem("scopeVisualizer", "show_scope_visualizer", customCheatsheetInfo) + .variations, + ).toEqual([ + { + spokenForm: "inspect ", + description: "Visualize ", + }, + { + spokenForm: "inspect deletion", + description: "Visualize removal range", + }, + ]); + expect( + getSection("scopeVisualizer", customCheatsheetInfo).items, + ).not.toContainEqual( + expect.objectContaining({ id: "hideScopeVisualizer" }), + ); + }); + + // oxlint-disable-next-line unicorn/consistent-function-scoping + function getSection( + sectionId: string, + info: CheatsheetInfo = cheatsheetInfo, + ) { + return info.sections.find(({ id }) => id === sectionId)!; + } + + function getItem( + sectionId: string, + itemId: string, + info: CheatsheetInfo = cheatsheetInfo, + ) { + return getSection(sectionId, info).items.find(({ id }) => id === itemId)!; + } +}); diff --git a/packages/lib-cheatsheet/tsconfig.jest.json b/packages/lib-cheatsheet/tsconfig.jest.json index f7e35d10ae..188d7a5b44 100644 --- a/packages/lib-cheatsheet/tsconfig.jest.json +++ b/packages/lib-cheatsheet/tsconfig.jest.json @@ -3,6 +3,7 @@ "compilerOptions": { "ignoreDeprecations": "6.0", "module": "commonjs", - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["node"] } } diff --git a/packages/lib-cheatsheet/tsconfig.json b/packages/lib-cheatsheet/tsconfig.json index bb689757d2..20c49f2c53 100644 --- a/packages/lib-cheatsheet/tsconfig.json +++ b/packages/lib-cheatsheet/tsconfig.json @@ -1,12 +1,13 @@ { "extends": "../../tsconfig.web.json", "compilerOptions": { - "types": ["jest"] + "types": [] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.json", "../../resources/typings/**/*.d.ts" - ] + ], + "exclude": ["src/test"] } diff --git a/packages/lib-common/package.json b/packages/lib-common/package.json index fd96e847d3..342b599d1d 100644 --- a/packages/lib-common/package.json +++ b/packages/lib-common/package.json @@ -9,6 +9,7 @@ }, "exports": { ".": "./src/index.ts", + "./cheatsheet": "./src/cheatsheet/index.ts", "./jest": "./src/tooling/jest.ts", "./vite": "./src/tooling/vite.ts" }, diff --git a/packages/lib-common/src/cheatsheet/cheatsheet.types.ts b/packages/lib-common/src/cheatsheet/cheatsheet.types.ts new file mode 100644 index 0000000000..9a1c716b52 --- /dev/null +++ b/packages/lib-common/src/cheatsheet/cheatsheet.types.ts @@ -0,0 +1,20 @@ +export interface CheatsheetVariation { + spokenForm: string; + description: string; +} + +export interface CheatsheetItem { + id: string; + type: string; + variations: CheatsheetVariation[]; +} + +export interface CheatsheetSection { + name: string; + id: string; + items: CheatsheetItem[]; +} + +export interface CheatsheetInfo { + sections: CheatsheetSection[]; +} diff --git a/packages/lib-common/src/cheatsheet/getCheatsheetInfo.ts b/packages/lib-common/src/cheatsheet/getCheatsheetInfo.ts new file mode 100644 index 0000000000..87049b715c --- /dev/null +++ b/packages/lib-common/src/cheatsheet/getCheatsheetInfo.ts @@ -0,0 +1,791 @@ +import { + actionReferences, + connectiveDefaultSpokenForms, + defaultSpokenFormMapCore, + hatColorReferences, + hatShapeReferences, + modifierExtraReferences, + modifierReferences, + pairedDelimiterReferences, + scopeReferences, +} from "../references"; +import type { SpokenFormReference } from "../references"; +import type { + DefaultSpokenFormMapDefinition, + DefaultSpokenFormMapEntry, +} from "../types/DefaultSpokenFormMap"; +import type { SpokenFormEntry } from "../types/TalonSpokenForms"; +import { camelCaseToAllDown, capitalize } from "../util/stringUtils"; +import type { CheatsheetInfo, CheatsheetSection } from "./cheatsheet.types"; + +interface CheatsheetReference extends SpokenFormReference { + syntaxes: readonly { + pattern: string; + cheatsheet: string; + }[]; +} + +type ReferenceMap = Readonly>; + +const REFERENCE_SPOKEN_FORM = ""; + +const actionTypes = ["action"] as const; + +type ReferenceKind = "action" | "modifier" | "scope"; + +/** Construct the stock cheatsheet directly from the canonical references. */ +export function getDefaultCheatsheetInfo(): CheatsheetInfo { + return constructCheatsheetInfo( + getDefaultSpokenFormEntries(defaultSpokenFormMapCore), + ); +} + +/** Construct a cheatsheet using optional customized Talon spoken forms. */ +export function getCheatsheetInfo( + spokenFormEntries: readonly SpokenFormEntry[], +): CheatsheetInfo { + return constructCheatsheetInfo(spokenFormEntries); +} + +function constructCheatsheetInfo( + spokenFormEntries: readonly SpokenFormResolverEntry[], +): CheatsheetInfo { + const resolver = new SpokenFormResolver(spokenFormEntries); + + return { + sections: [ + actionsSection(resolver, spokenFormEntries), + destinationsSection(resolver), + scopesSection(resolver, spokenFormEntries), + scopeVisualizerSection(resolver), + referenceSection( + resolver, + "Modifiers", + "modifiers", + "modifier", + "modifier", + modifierReferences, + ), + pairedDelimitersSection(resolver), + specialMarksSection(resolver), + compoundTargetsSection(resolver), + colorsSection(resolver), + shapesSection(resolver), + tutorialSection, + ], + }; +} + +function actionsSection( + resolver: SpokenFormResolver, + spokenFormEntries: readonly SpokenFormResolverEntry[], +): CheatsheetSection { + const section = referenceSection( + resolver, + "Actions", + "actions", + "action", + "action", + actionReferences, + ); + + return { + ...section, + items: [ + ...section.items, + ...spokenFormEntries + .filter(({ type }) => type === "customAction") + .map(({ id, spokenForms }) => + items( + id, + "action", + spokenForms + .slice(0, 1) + .map((spokenForm) => `${spokenForm} `), + makeReadable(id), + ), + ) + .filter(({ variations }) => variations.length > 0), + ], + }; +} + +function scopesSection( + resolver: SpokenFormResolver, + spokenFormEntries: readonly SpokenFormResolverEntry[], +): CheatsheetSection { + const section = referenceSection( + resolver, + "Scopes", + "scopes", + "scopeType", + "scope", + scopeReferences, + ); + + return { + ...section, + items: [ + ...section.items, + ...spokenFormEntries + .filter(({ type }) => type === "customRegex") + .flatMap(({ id }) => { + const spokenForms = resolver.get(["customRegex"], id); + if (spokenForms.length === 0) { + return []; + } + return [ + items( + `customRegex.${spokenForms[0]}`, + "scopeType", + spokenForms, + `/${id}/`, + ), + ]; + }), + ], + }; +} + +function referenceSection( + resolver: SpokenFormResolver, + name: string, + id: string, + type: string, + referenceKind: ReferenceKind, + references: ReferenceMap, +): CheatsheetSection { + return { + name, + id, + items: Object.entries(references) + .map(([referenceId, reference]) => { + const spokenForms = getReferenceSpokenForms( + resolver, + referenceKind, + referenceId, + reference.defaultSpokenForm, + ); + const replacements = getSyntaxReplacements(resolver); + + return { + id: referenceId, + type, + variations: reference.syntaxes.flatMap(({ pattern, cheatsheet }) => + spokenForms.flatMap((spokenForm) => + applyReplacements( + pattern.replaceAll( + REFERENCE_SPOKEN_FORM, + reference.defaultSpokenForm ?? REFERENCE_SPOKEN_FORM, + ), + [ + ...replacements, + ...(reference.defaultSpokenForm == null || spokenForm == null + ? [] + : ([[reference.defaultSpokenForm, [spokenForm]]] as const)), + ], + ).map((customPattern) => ({ + spokenForm: customPattern, + description: + reference.visibility === "private" + ? `${cheatsheet} (PRIVATE)` + : cheatsheet, + })), + ), + ), + }; + }) + .filter(({ variations }) => variations.length > 0), + }; +} + +class SpokenFormResolver { + private entries = new Map(); + + constructor(spokenFormEntries: readonly SpokenFormResolverEntry[]) { + for (const { type, id, spokenForms } of spokenFormEntries) { + this.entries.set(`${type}\0${id}`, spokenForms); + } + } + + get(types: readonly string[], id: string): readonly string[] { + return types + .flatMap((type) => this.entries.get(`${type}\0${id}`) ?? []) + .slice(0, 1); + } +} + +interface SpokenFormResolverEntry { + type: string; + id: string; + spokenForms: readonly string[]; +} + +function getDefaultSpokenFormEntries( + spokenFormMap: DefaultSpokenFormMapDefinition, +): SpokenFormResolverEntry[] { + return Object.entries(spokenFormMap).flatMap(([type, entries]) => + Object.entries( + entries as Readonly>, + ).map(([id, value]) => ({ + type, + id, + spokenForms: getEnabledDefaultSpokenForms(value), + })), + ); +} + +function getEnabledDefaultSpokenForms( + value: string | DefaultSpokenFormMapEntry, +): readonly string[] { + if (typeof value === "string") { + return [value]; + } + + return value.visibility == null ? value.defaultSpokenForms : []; +} + +function getReferenceSpokenForms( + resolver: SpokenFormResolver, + kind: ReferenceKind, + id: string, + defaultSpokenForm: string | undefined, +): readonly (string | undefined)[] { + if (defaultSpokenForm == null) { + return [undefined]; + } + + if (kind === "action") { + return resolver.get(actionTypes, id); + } + + if (kind === "scope") { + const types = + id === "glyph" ? ["complexScopeTypeType"] : ["simpleScopeTypeType"]; + return resolver.get(types, id); + } + + const definition = modifierSpokenFormDefinitions[id]; + if (definition == null) { + return []; + } + + return resolver.get(definition.types, definition.id); +} + +const modifierSpokenFormDefinitions: Readonly< + Record +> = { + everyScope: { types: ["simpleModifier"], id: "everyScope" }, + ancestor: { types: ["modifierExtra"], id: "ancestor" }, + interiorOnly: { types: ["simpleModifier"], id: "interiorOnly" }, + excludeInterior: { types: ["simpleModifier"], id: "excludeInterior" }, + leading: { types: ["simpleModifier"], id: "leading" }, + trailing: { types: ["simpleModifier"], id: "trailing" }, + extendThroughStartOf: { + types: ["simpleModifier"], + id: "extendThroughStartOf", + }, + extendThroughEndOf: { + types: ["simpleModifier"], + id: "extendThroughEndOf", + }, + startOf: { types: ["simpleModifier"], id: "startOf" }, + endOf: { types: ["simpleModifier"], id: "endOf" }, + visible: { types: ["simpleModifier"], id: "visible" }, + keepContentFilter: { + types: ["simpleModifier"], + id: "keepContentFilter", + }, + keepEmptyFilter: { + types: ["simpleModifier"], + id: "keepEmptyFilter", + }, + toRawSelection: { types: ["simpleModifier"], id: "toRawSelection" }, + inferPreviousMark: { + types: ["simpleModifier"], + id: "inferPreviousMark", + }, +}; + +type SyntaxReplacement = readonly [string, readonly string[]]; + +const syntaxReplacementDefinitions = { + swapConnective: { + type: "connective", + syntaxTerm: connectiveDefaultSpokenForms.swapConnective, + }, + first: { + type: "modifierExtra", + syntaxTerm: modifierExtraReferences.first.defaultSpokenForm, + }, + last: { + type: "modifierExtra", + syntaxTerm: modifierExtraReferences.last.defaultSpokenForm, + }, + previous: { + type: "modifierExtra", + syntaxTerm: modifierExtraReferences.previous.defaultSpokenForm, + }, + next: { + type: "modifierExtra", + syntaxTerm: modifierExtraReferences.next.defaultSpokenForm, + }, + forward: { + type: "modifierExtra", + syntaxTerm: modifierExtraReferences.forward.defaultSpokenForm, + }, + backward: { + type: "modifierExtra", + syntaxTerm: modifierExtraReferences.backward.defaultSpokenForm, + }, + everyScope: { + type: "simpleModifier", + syntaxTerm: modifierReferences.everyScope.defaultSpokenForm, + }, + token: { + type: "simpleScopeTypeType", + syntaxTerm: scopeReferences.token.defaultSpokenForm, + }, + at: { + type: "connective", + syntaxTerm: connectiveDefaultSpokenForms.at, + }, + on: { + type: "connective", + syntaxTerm: connectiveDefaultSpokenForms.on, + }, +} as const; + +type SyntaxReplacementId = keyof typeof syntaxReplacementDefinitions; + +function getSyntaxReplacements( + resolver: SpokenFormResolver, +): readonly SyntaxReplacement[] { + return [ + replacement(resolver, "swapConnective"), + replacement(resolver, "first"), + replacement(resolver, "last"), + replacement(resolver, "previous"), + replacement(resolver, "next"), + replacement(resolver, "forward"), + replacement(resolver, "backward"), + replacement(resolver, "everyScope"), + replacement(resolver, "token"), + replacement(resolver, "at"), + replacement(resolver, "on"), + ]; +} + +function replacement( + resolver: SpokenFormResolver, + id: SyntaxReplacementId, +): SyntaxReplacement { + const { type, syntaxTerm } = syntaxReplacementDefinitions[id]; + return [syntaxTerm, resolver.get([type], id)]; +} + +function applyReplacements( + pattern: string, + replacements: readonly SyntaxReplacement[], +): string[] { + let protectedPattern: (string | number)[] = [pattern]; + + for (const [ + replacementIndex, + [defaultSpokenForm], + ] of replacements.entries()) { + protectedPattern = protectedPattern.flatMap((part) => + typeof part === "string" + ? intersperse( + part.split(termRegex(defaultSpokenForm)), + replacementIndex, + ) + : [part], + ); + } + + let patterns = [protectedPattern]; + + for (const [replacementIndex, [, spokenForms]] of replacements.entries()) { + if (!protectedPattern.includes(replacementIndex)) { + continue; + } + + const replacedPatterns: (string | number)[][] = []; + for (const currentPattern of patterns) { + for (const spokenForm of spokenForms) { + replacedPatterns.push( + currentPattern.map((part) => + part === replacementIndex ? spokenForm : part, + ), + ); + } + } + patterns = replacedPatterns; + } + + return patterns.map((parts) => parts.join("")); +} + +function intersperse(items: readonly T[], separator: U): (T | U)[] { + const result: (T | U)[] = []; + for (const [index, item] of items.entries()) { + result.push(item); + if (index < items.length - 1) { + result.push(separator); + } + } + return result; +} + +function termRegex(term: string): RegExp { + const escaped = term.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`); + return new RegExp(`(? ({ + id, + type: "pairedDelimiter", + variations: resolver.get(["pairedDelimiter"], id).map((spokenForm) => ({ + spokenForm, + description: capitalize(reference.name), + })), + })) + .filter(({ variations }) => variations.length > 0), + }; +} + +function makeReadable(value: string): string { + return capitalize(camelCaseToAllDown(value.replaceAll(".", " "))); +} + +function colorsSection(resolver: SpokenFormResolver): CheatsheetSection { + return { + name: "Colors", + id: "colors", + items: Object.entries(hatColorReferences) + .map(([id, reference]) => ({ + id, + type: "hatColor", + variations: resolver.get(["hatColor"], id).map((spokenForm) => ({ + spokenForm, + description: reference.name, + })), + })) + .filter(({ variations }) => variations.length > 0), + }; +} + +function compoundTargetsSection( + resolver: SpokenFormResolver, +): CheatsheetSection { + return { + name: "Compound targets", + id: "compoundTargets", + items: [ + { + id: "listConnective", + types: ["connective"], + descriptions: [" and "], + }, + { + id: "rangeExclusive", + types: ["connective"], + descriptions: [ + "between and ", + "between selection and ", + ], + }, + { + id: "rangeInclusive", + types: ["connective"], + descriptions: [ + " through ", + "selection through ", + ], + }, + { + id: "rangeExcludingStart", + types: ["connective"], + descriptions: [ + "end of through ", + "end of selection through ", + ], + }, + { + id: "rangeExcludingEnd", + types: ["connective"], + descriptions: [ + " until start of ", + "selection until start of ", + ], + }, + { + id: "verticalRange", + types: ["connective"], + descriptions: [ + " vertically through ", + "selection vertically through ", + ], + }, + ] + .map(({ id, types, descriptions }) => { + return { + id, + type: "compoundTargetConnective", + variations: resolver.get(types, id).flatMap((customSpokenForm) => + descriptions.map((description, index) => ({ + spokenForm: + index === 0 + ? ` ${customSpokenForm} ` + : `${customSpokenForm} `, + description, + })), + ), + }; + }) + .filter(({ variations }) => variations.length > 0), + }; +} + +function destinationsSection(resolver: SpokenFormResolver): CheatsheetSection { + return { + name: "Destinations", + id: "destinations", + items: [ + { + id: "destination_after", + valueId: "after", + description: "Insert after ", + }, + { + id: "destination_before", + valueId: "before", + description: "Insert before ", + }, + { + id: "destination_to", + valueId: "to", + description: "Replace ", + }, + ] + .map(({ id, valueId, description }) => ({ + id, + type: "destination", + variations: resolver + .get(["insertionMode"], valueId) + .map((customSpokenForm) => ({ + spokenForm: `${customSpokenForm} `, + description, + })), + })) + .filter(({ variations }) => variations.length > 0), + }; +} + +function scopeVisualizerSection( + resolver: SpokenFormResolver, +): CheatsheetSection { + const showSpokenForms = resolver.get( + ["scopeVisualizer"], + "showScopeVisualizer", + ); + return { + name: "Scope visualizer", + id: "scopeVisualizer", + items: [ + items( + "hideScopeVisualizer", + "command", + resolver.get(["scopeVisualizer"], "hideScopeVisualizer"), + "Hide scope visualizer", + ), + items( + "show_scope_sidebar", + "command", + resolver + .get(["sidebar"], "bar") + .map((spokenForm) => `${spokenForm} cursorless`), + "Show cursorless sidebar", + ), + { + id: "show_scope_visualizer", + type: "command", + variations: showSpokenForms.flatMap((showSpokenForm) => [ + { + spokenForm: `${showSpokenForm} `, + description: "Visualize ", + }, + ...["removal", "iteration"].flatMap((visualizationType) => + resolver + .get(["scopeVisualizer"], visualizationType) + .map((spokenForm) => ({ + spokenForm: `${showSpokenForm} ${spokenForm}`, + description: `Visualize ${visualizationType} range`, + })), + ), + ]), + }, + ].filter(({ variations }) => variations.length > 0), + }; +} + +function shapesSection(resolver: SpokenFormResolver): CheatsheetSection { + return { + name: "Shapes", + id: "shapes", + items: Object.entries(hatShapeReferences) + .map(([id, reference]) => ({ + id, + type: "hatShape", + variations: resolver.get(["hatShape"], id).map((spokenForm) => ({ + spokenForm, + description: reference.name, + })), + })) + .filter(({ variations }) => variations.length > 0), + }; +} + +function specialMarksSection(resolver: SpokenFormResolver): CheatsheetSection { + return { + name: "Special marks", + id: "specialMarks", + items: [ + items( + "currentSelection", + "mark", + resolver.get(["specialMark"], "currentSelection"), + "Current selection", + ), + items( + "lineNumberModulo100", + "mark", + resolver + .get(["specialMark"], "lineNumberModulo100") + .map((spokenForm) => `${spokenForm} `), + "Line number modulo 100", + ), + items( + "lineNumberRelativeDown", + "mark", + resolver + .get(["specialMark"], "lineNumberRelativeDown") + .map((spokenForm) => `${spokenForm} `), + "Line number down from cursor", + ), + items( + "lineNumberRelativeUp", + "mark", + resolver + .get(["specialMark"], "lineNumberRelativeUp") + .map((spokenForm) => `${spokenForm} `), + "Line number up from cursor", + ), + items( + "nothing", + "mark", + resolver.get(["specialMark"], "nothing"), + "Nothing", + ), + items( + "previousSource", + "mark", + resolver.get(["specialMark"], "previousSource"), + "Previous source", + ), + items( + "previousTarget", + "mark", + resolver.get(["specialMark"], "previousTarget"), + "Previous target", + ), + items( + "unknownSymbol", + "mark", + resolver.get(["specialMark"], "unknownSymbol"), + "Unknown symbol", + ), + ].filter(({ variations }) => variations.length > 0), + }; +} + +const tutorialSection: CheatsheetSection = { + name: "Tutorial", + id: "tutorial", + items: [ + item( + "start_tutorial", + "command", + "cursorless tutorial", + "Start the introductory Cursorless tutorial", + ), + item("tutorial_close", "command", "tutorial close", "Close the tutorial"), + item( + "tutorial_list", + "command", + "tutorial list", + "List all available tutorials", + ), + item( + "tutorial_next", + "command", + "tutorial next", + "Advance to next step in tutorial", + ), + item( + "tutorial_previous", + "command", + "tutorial previous", + "Go back to previous step in tutorial", + ), + item( + "tutorial_restart", + "command", + "tutorial restart", + "Restart the tutorial", + ), + item( + "tutorial_resume", + "command", + "tutorial resume", + "Resume the tutorial", + ), + item( + "tutorial_start_by_number", + "command", + "tutorial ", + "Start a specific tutorial by number", + ), + ], +}; + +function item( + id: string, + type: string, + spokenForm: string, + description: string, +): CheatsheetSection["items"][number] { + return { id, type, variations: [{ spokenForm, description }] }; +} + +function items( + id: string, + type: string, + spokenForms: readonly string[], + description: string, +): CheatsheetSection["items"][number] { + return { + id, + type, + variations: spokenForms.map((spokenForm) => ({ spokenForm, description })), + }; +} diff --git a/packages/lib-common/src/cheatsheet/index.ts b/packages/lib-common/src/cheatsheet/index.ts new file mode 100644 index 0000000000..14a161d2a6 --- /dev/null +++ b/packages/lib-common/src/cheatsheet/index.ts @@ -0,0 +1,2 @@ +export * from "./cheatsheet.types"; +export * from "./getCheatsheetInfo"; diff --git a/packages/lib-common/src/cursorlessCommandIds.ts b/packages/lib-common/src/cursorlessCommandIds.ts index 6f366540b2..bc64fda55b 100644 --- a/packages/lib-common/src/cursorlessCommandIds.ts +++ b/packages/lib-common/src/cursorlessCommandIds.ts @@ -25,7 +25,6 @@ class VisibleCommand extends Command implements CommandDescription { export const cursorlessCommandIds = [ "cursorless.command", "cursorless.repeatPreviousCommand", - "cursorless.internal.updateCheatsheetDefaults", "cursorless.private.logQuickActions", "cursorless.keyboard.escape", "cursorless.keyboard.modal.modeOff", @@ -125,9 +124,6 @@ export const cursorlessCommandDescriptions: Record< "cursorless.showCheatsheet": new HiddenCommand( "Display the cursorless cheatsheet", ), - "cursorless.internal.updateCheatsheetDefaults": new HiddenCommand( - "Update the default values of the cheatsheet payload used on the website and for local development. Be sure to run this on stock community and cursorless.", - ), "cursorless.private.logQuickActions": new HiddenCommand( "Log the quick actions available at the current cursor position", ), diff --git a/packages/lib-common/src/index.ts b/packages/lib-common/src/index.ts index 1b88c6acc6..a003910b68 100644 --- a/packages/lib-common/src/index.ts +++ b/packages/lib-common/src/index.ts @@ -1,3 +1,4 @@ +export * from "./cheatsheet/index"; export * from "./constants"; export * from "./cursorlessCommandIds"; export * from "./cursorlessSideBarIds"; @@ -70,6 +71,7 @@ export * from "./types/command/legacy/targetDescriptorV2.types"; export * from "./types/command/PartialTargetDescriptor.types"; export * from "./types/commandHistory"; export * from "./types/CommandServerApi"; +export * from "./types/DefaultSpokenFormMap"; export * from "./types/Edit"; export * from "./types/GeneralizedRange"; export * from "./types/HatTokenMap"; @@ -84,6 +86,7 @@ export * from "./types/ScopeProvider"; export * from "./types/Selection"; export * from "./types/SpokenForm"; export * from "./types/SpokenFormType"; +export * from "./types/SpokenFormVisibility"; export * from "./types/StringRecord"; export * from "./types/TalonSpokenForms"; export * from "./types/TestCaseFixture"; diff --git a/packages/lib-common/src/references/ReferenceEntry.ts b/packages/lib-common/src/references/ReferenceEntry.ts index bb1d09d627..b8a11c435b 100644 --- a/packages/lib-common/src/references/ReferenceEntry.ts +++ b/packages/lib-common/src/references/ReferenceEntry.ts @@ -1,11 +1,13 @@ +import type { SpokenFormVisibility } from "../types/SpokenFormVisibility"; + export interface SpokenFormReference { defaultSpokenForm?: string; - private?: boolean; - disabledByDefault?: boolean; + visibility?: SpokenFormVisibility; } export interface ReferenceEntry extends SpokenFormReference { name: string; + csv_id?: string; legacySpokenForms?: string[]; description?: string; group: GroupDefinition; diff --git a/packages/lib-common/src/references/actionReferences.ts b/packages/lib-common/src/references/actionReferences.ts index 884568a41f..8b91c4b2bb 100644 --- a/packages/lib-common/src/references/actionReferences.ts +++ b/packages/lib-common/src/references/actionReferences.ts @@ -22,32 +22,38 @@ import { VAR_TARGET_1, VAR_TARGET_2, } from "./constants"; -import { modifierReferences } from "./modifierReferences"; +import { + modifierExtraReferences, + modifierReferences, +} from "./modifierReferences"; import { pairedDelimiterReferences } from "./pairedDelimiterReferences"; import type { ReferenceEntry } from "./ReferenceEntry"; import { scopeReferences } from "./scopeReferences"; -import { connectiveDefaultSpokenForms } from "./spokenForms/connectiveDefaultSpokenForms"; +import { + connectiveDefaultSpokenForms, + insertionModeDefaultSpokenForms, +} from "./spokenForms/connectiveDefaultSpokenForms"; import { markDefaultSpokenForms } from "./spokenForms/markDefaultSpokenForms"; const DEFAULT_PATTERN = `${VAR_SPOKEN_FORM} ${VAR_TARGET}`; const DEFAULT_COMMAND = `${VAR_SPOKEN_FORM} ${TARGET}`; +const CURLY = pairedDelimiterReferences.curlyBrackets.defaultSpokenForm; +const SQUARE = pairedDelimiterReferences.squareBrackets.defaultSpokenForm; const ITEM = scopeReferences.collectionItem.defaultSpokenForm; const VALUE = scopeReferences.value.defaultSpokenForm; const INSTANCE = scopeReferences.instance.defaultSpokenForm; const FUNCTION = scopeReferences.namedFunction.defaultSpokenForm; const TOKEN = scopeReferences.token.defaultSpokenForm; const EVERY = modifierReferences.everyScope.defaultSpokenForm; -const NEXT = connectiveDefaultSpokenForms.next; +const NEXT = modifierExtraReferences.next.defaultSpokenForm; const WITH = connectiveDefaultSpokenForms.swapConnective; -const AFTER = connectiveDefaultSpokenForms.after; -const TO = connectiveDefaultSpokenForms.sourceDestinationConnective; -const CURLY = pairedDelimiterReferences.curlyBrackets.defaultSpokenForm; -const SQUARE = pairedDelimiterReferences.squareBrackets.defaultSpokenForm; const AT = connectiveDefaultSpokenForms.at; const ON = connectiveDefaultSpokenForms.on; const SLICE = connectiveDefaultSpokenForms.verticalRange; const CURRENT_SELECTION = markDefaultSpokenForms.cursor; +const AFTER = insertionModeDefaultSpokenForms.after; +const TO = insertionModeDefaultSpokenForms.to; const MULTI_TARGET_CURSOR_DESCRIPTION = "When used with a list target, this action creates one cursor for each target."; @@ -60,20 +66,25 @@ const REORDER_DESCRIPTION = const PAIRED_DELIMITER_DESCRIPTION = "See [paired delimiters](../paired-delimiters.md) for the available pairs."; -type TalonSideActionType = "applyFormatter" | "nextHomophone"; +export const talonSideActionNames = [ + "applyFormatter", + "nextHomophone", +] as const; + +export type TalonSideActionType = (typeof talonSideActionNames)[number]; export const actionReferences = { // Group: cursor setSelection: { - name: "Set selection", + name: "Select", defaultSpokenForm: SET_SELECTION, group: { id: "cursor", index: 0 }, description: MULTI_TARGET_CURSOR_DESCRIPTION, syntaxes: [ { pattern: DEFAULT_PATTERN, - description: `Set selection to ${VAR_TARGET}.`, - cheatsheet: "Set selection", + description: `Select ${VAR_TARGET}.`, + cheatsheet: "Select", }, ], examples: [ @@ -726,6 +737,7 @@ export const actionReferences = { }, rewrapWithPairedDelimiter: { name: "Rewrap with paired delimiter", + csv_id: "rewrap", defaultSpokenForm: "repack", group: { id: "wrap", index: 1 }, description: PAIRED_DELIMITER_DESCRIPTION, @@ -1414,7 +1426,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Show parse tree", defaultSpokenForm: "parse tree", group: { id: "private", index: 0 }, - private: true, + visibility: "private", syntaxes: [ { pattern: DEFAULT_PATTERN, @@ -1432,7 +1444,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading parsed: { name: "Parsed", defaultSpokenForm: "parsed", - private: true, + visibility: "private", group: { id: "private", index: 1 }, syntaxes: [], examples: [], @@ -1441,7 +1453,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Get targets", defaultSpokenForm: "get targets", group: { id: "private", index: 2 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, @@ -1449,7 +1461,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Set keyboard target", defaultSpokenForm: "set keyboard target", group: { id: "private", index: 3 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, @@ -1457,7 +1469,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Execute command", defaultSpokenForm: "execute command", group: { id: "private", index: 4 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, @@ -1465,7 +1477,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Edit new", defaultSpokenForm: "edit new", group: { id: "private", index: 5 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, @@ -1473,7 +1485,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Get text", defaultSpokenForm: "get text", group: { id: "private", index: 6 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, @@ -1481,7 +1493,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Replace", defaultSpokenForm: "replace", group: { id: "private", index: 7 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, @@ -1489,7 +1501,7 @@ Older Cursorless installations may have \`"from"\` disabled. Remove the leading name: "Wrap with snippet", defaultSpokenForm: "wrap", group: { id: "private", index: 8 }, - private: true, + visibility: "private", syntaxes: [], examples: [], }, diff --git a/packages/lib-common/src/references/constants.ts b/packages/lib-common/src/references/constants.ts index 6c1a14397f..823d664a45 100644 --- a/packages/lib-common/src/references/constants.ts +++ b/packages/lib-common/src/references/constants.ts @@ -1,5 +1,5 @@ +import { hatColorReferences } from "./hatStyleReferences"; import { graphemeDefaultSpokenForms } from "./spokenForms/graphemeDefaultSpokenForms"; -import { hatColorDefaultSpokenForms } from "./spokenForms/markDefaultSpokenForms"; // Template variables export const VAR_SPOKEN_FORM = ""; @@ -17,11 +17,11 @@ export const VAR_MODIFIER = ""; export const VAR_CHARACTER = ""; // Targets -export const TARGET = `${hatColorDefaultSpokenForms.blue} ${graphemeDefaultSpokenForms.a}`; +export const TARGET = `${hatColorReferences.blue.defaultSpokenForm} ${graphemeDefaultSpokenForms.a}`; export const TARGET_DESC = "token containing letter 'a' with a blue hat"; -export const TARGET_2 = `${hatColorDefaultSpokenForms.green} ${graphemeDefaultSpokenForms.b}`; +export const TARGET_2 = `${hatColorReferences.green.defaultSpokenForm} ${graphemeDefaultSpokenForms.b}`; export const TARGET_2_DESC = "token containing letter 'b' with a green hat"; -export const TARGET_NUMBER = `${hatColorDefaultSpokenForms.blue} ${graphemeDefaultSpokenForms["5"]}`; +export const TARGET_NUMBER = `${hatColorReferences.blue.defaultSpokenForm} ${graphemeDefaultSpokenForms["5"]}`; export const TARGET_NUMBER_DESC = "token containing number '5' with a blue hat"; // Actions diff --git a/packages/lib-common/src/references/hatStyleReferences.ts b/packages/lib-common/src/references/hatStyleReferences.ts new file mode 100644 index 0000000000..d1227a7383 --- /dev/null +++ b/packages/lib-common/src/references/hatStyleReferences.ts @@ -0,0 +1,102 @@ +import type { SpokenFormReference } from "./ReferenceEntry"; + +type HatStyleReference = SpokenFormReference & { + name: string; + defaultSpokenForm: string; +}; + +export const hatColorReferences = { + blue: { + name: "Blue", + defaultSpokenForm: "blue", + }, + green: { + name: "Green", + defaultSpokenForm: "green", + }, + red: { + name: "Red", + defaultSpokenForm: "red", + }, + pink: { + name: "Pink", + defaultSpokenForm: "pink", + }, + yellow: { + name: "Yellow", + defaultSpokenForm: "yellow", + }, + userColor1: { + name: "User color 1", + defaultSpokenForm: "navy", + visibility: "disabledByDefault", + }, + userColor2: { + name: "User color 2", + defaultSpokenForm: "apricot", + visibility: "disabledByDefault", + }, + userColor3: { + name: "User color 3", + defaultSpokenForm: "user color three", + visibility: "disabledByDefault", + }, + userColor4: { + name: "User color 4", + defaultSpokenForm: "user color four", + visibility: "disabledByDefault", + }, +} as const satisfies Record; + +export const hatShapeReferences = { + ex: { + name: "Ex", + defaultSpokenForm: "ex", + visibility: "disabledByDefault", + }, + fox: { + name: "Fox", + defaultSpokenForm: "fox", + visibility: "disabledByDefault", + }, + wing: { + name: "Wing", + defaultSpokenForm: "wing", + visibility: "disabledByDefault", + }, + hole: { + name: "Hole", + defaultSpokenForm: "hole", + visibility: "disabledByDefault", + }, + frame: { + name: "Frame", + defaultSpokenForm: "frame", + visibility: "disabledByDefault", + }, + curve: { + name: "Curve", + defaultSpokenForm: "curve", + visibility: "disabledByDefault", + }, + eye: { + name: "Eye", + defaultSpokenForm: "eye", + visibility: "disabledByDefault", + }, + play: { + name: "Play", + defaultSpokenForm: "play", + visibility: "disabledByDefault", + }, + crosshairs: { + name: "Crosshairs", + defaultSpokenForm: "cross", + visibility: "disabledByDefault", + }, + bolt: { + name: "Bolt", + defaultSpokenForm: "bolt", + visibility: "disabledByDefault", + }, +} as const satisfies Record; diff --git a/packages/lib-common/src/references/index.ts b/packages/lib-common/src/references/index.ts index 514298b5ae..f8874fde02 100644 --- a/packages/lib-common/src/references/index.ts +++ b/packages/lib-common/src/references/index.ts @@ -1,5 +1,6 @@ export * from "./actionReferenceGroups"; export * from "./actionReferences"; +export * from "./hatStyleReferences"; export * from "./modifierReferenceGroups"; export * from "./modifierReferences"; export * from "./pairedDelimiterReferences"; @@ -7,6 +8,7 @@ export * from "./ReferenceEntry"; export * from "./scopeReferenceGroups"; export * from "./scopeReferences"; export * from "./spokenForms/connectiveDefaultSpokenForms"; +export * from "./spokenForms/defaultSpokenFormMapCore"; export * from "./spokenForms/graphemeDefaultSpokenForms"; export * from "./spokenForms/markDefaultSpokenForms"; export * from "./spokenForms/numberDefaultSpokenForms"; diff --git a/packages/lib-common/src/references/modifierReferences.ts b/packages/lib-common/src/references/modifierReferences.ts index 82affa3028..2c9fac5300 100644 --- a/packages/lib-common/src/references/modifierReferences.ts +++ b/packages/lib-common/src/references/modifierReferences.ts @@ -1,4 +1,5 @@ import type { ModifierType } from "../types/command/PartialTargetDescriptor.types"; +import type { SpokenFormMapKeyTypes } from "../types/SpokenFormType"; import { EVERY, ITEM, @@ -16,8 +17,7 @@ import { } from "./constants"; import type { ModifierReferenceGroupId } from "./modifierReferenceGroups"; import { pairedDelimiterReferences } from "./pairedDelimiterReferences"; -import type { ReferenceEntry } from "./ReferenceEntry"; -import { connectiveDefaultSpokenForms } from "./spokenForms/connectiveDefaultSpokenForms"; +import type { ReferenceEntry, SpokenFormReference } from "./ReferenceEntry"; import { graphemeDefaultSpokenForms } from "./spokenForms/graphemeDefaultSpokenForms"; import { ordinalDefaultSpokenForms } from "./spokenForms/numberDefaultSpokenForms"; @@ -26,17 +26,18 @@ const PARENTHESES = pairedDelimiterReferences.parentheses.defaultSpokenForm; const THREE = graphemeDefaultSpokenForms["3"]; const EXTEND_THROUGH_END_OF = "past end of"; const THIRD = ordinalDefaultSpokenForms[3]; -const PREVIOUS = connectiveDefaultSpokenForms.previous; -const NEXT = connectiveDefaultSpokenForms.next; -const BACKWARD = connectiveDefaultSpokenForms.backward; -const FORWARD = connectiveDefaultSpokenForms.forward; -const FIRST = connectiveDefaultSpokenForms.first; -const LAST = connectiveDefaultSpokenForms.last; +const PREVIOUS = "previous"; +const NEXT = "next"; +const BACKWARD = "backward"; +const FORWARD = "forward"; +const FIRST = "first"; +const LAST = "last"; const PASTE = "paste"; const BRING = "bring"; const FROM = "from"; -const AFTER = connectiveDefaultSpokenForms.after; -const TO = connectiveDefaultSpokenForms.sourceDestinationConnective; +const GRAND = "grand"; +const AFTER = "after"; +const TO = "to"; const INSTANCE = "instance"; const VALUE = "value"; @@ -45,6 +46,30 @@ const ITERATION_SCOPE_DESCRIPTION = type AdditionalModifierReferenceType = "ancestor"; +export const modifierExtraReferences = { + first: { + defaultSpokenForm: FIRST, + }, + last: { + defaultSpokenForm: LAST, + }, + previous: { + defaultSpokenForm: PREVIOUS, + }, + next: { + defaultSpokenForm: NEXT, + }, + forward: { + defaultSpokenForm: FORWARD, + }, + backward: { + defaultSpokenForm: BACKWARD, + }, + ancestor: { + defaultSpokenForm: GRAND, + }, +} satisfies Record; + export const modifierReferences = { // Group: containing containingScope: { @@ -88,7 +113,7 @@ export const modifierReferences = { }, ancestor: { name: "Ancestor", - defaultSpokenForm: "grand", + defaultSpokenForm: GRAND, group: { id: "containing", index: 2 }, description: 'Selects the parent of a containing scope. Repeat `"grand"` to walk up more than one parent level.', @@ -627,39 +652,28 @@ Relative modifiers select scopes before or after the input target. Without \`"ev // Group: private preferredScope: { name: "Preferred scope", - private: true, + visibility: "private", group: { id: "private", index: 0 }, - syntaxes: [ - { - pattern: VAR_SCOPE, - description: `Preferred instance of ${VAR_SCOPE}.`, - cheatsheet: `Preferred instance of ${VAR_SCOPE}`, - }, - ], - examples: [ - { - command: `${SET_SELECTION} ${ITEM} ${TARGET}`, - description: `Selects the closest item to the ${TARGET_DESC}.`, - }, - ], + syntaxes: [], + examples: [], }, modifyIfUntyped: { name: "Modify if untyped", - private: true, + visibility: "private", group: { id: "private", index: 1 }, syntaxes: [], examples: [], }, fallback: { name: "Fallback", - private: true, + visibility: "private", group: { id: "private", index: 2 }, syntaxes: [], examples: [], }, range: { name: "Range", - private: true, + visibility: "private", group: { id: "private", index: 3 }, syntaxes: [], examples: [], diff --git a/packages/lib-common/src/references/pairedDelimiterReferences.ts b/packages/lib-common/src/references/pairedDelimiterReferences.ts index 44f59605e5..db900df61d 100644 --- a/packages/lib-common/src/references/pairedDelimiterReferences.ts +++ b/packages/lib-common/src/references/pairedDelimiterReferences.ts @@ -177,8 +177,7 @@ export const pairedDelimiterReferences = { tripleDoubleQuotes: { name: "triple double quotes", defaultSpokenForm: "triple quad", - private: true, - disabledByDefault: true, + visibility: "private", index: null, isSingleLine: false, delimiters: ['"""', '"""'], @@ -195,8 +194,7 @@ export const pairedDelimiterReferences = { tripleSingleQuotes: { name: "triple single quotes", defaultSpokenForm: "triple twin", - private: true, - disabledByDefault: true, + visibility: "private", index: null, isSingleLine: false, delimiters: ["'''", "'''"], @@ -210,8 +208,7 @@ export const pairedDelimiterReferences = { tripleBacktickQuotes: { name: "triple backtick quotes", defaultSpokenForm: "triple skis", - private: true, - disabledByDefault: true, + visibility: "private", index: null, isSingleLine: false, delimiters: ["```", "```"], @@ -233,8 +230,7 @@ export const pairedDelimiterReferences = { collectionBoundary: { name: "collection boundary", defaultSpokenForm: "collection boundary", - private: true, - disabledByDefault: true, + visibility: "private", index: null, isSingleLine: false, delimiters: null, diff --git a/packages/lib-common/src/references/scopeReferences.ts b/packages/lib-common/src/references/scopeReferences.ts index 682707edb2..67f3232394 100644 --- a/packages/lib-common/src/references/scopeReferences.ts +++ b/packages/lib-common/src/references/scopeReferences.ts @@ -18,6 +18,7 @@ import { VAR_PAIR, VAR_SPOKEN_FORM, } from "./constants"; +import { modifierExtraReferences } from "./modifierReferences"; import { pairedDelimiterReferences } from "./pairedDelimiterReferences"; import type { ReferenceEntry } from "./ReferenceEntry"; import type { ScopeReferenceGroupId } from "./scopeReferenceGroups"; @@ -40,9 +41,9 @@ const DEFAULT_PATTERN = VAR_SPOKEN_FORM; const AIR = graphemeDefaultSpokenForms.a; const PARENTHESIS = pairedDelimiterReferences.parentheses.defaultSpokenForm; -const NEXT = connectiveDefaultSpokenForms.next; -const FIRST = connectiveDefaultSpokenForms.first; -const LAST = connectiveDefaultSpokenForms.last; +const NEXT = modifierExtraReferences.next.defaultSpokenForm; +const FIRST = modifierExtraReferences.first.defaultSpokenForm; +const LAST = modifierExtraReferences.last.defaultSpokenForm; const PAST = connectiveDefaultSpokenForms.rangeInclusive; const SECOND = ordinalDefaultSpokenForms[2]; const FOURTH = ordinalDefaultSpokenForms[4]; @@ -1162,7 +1163,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Section level one", group: { id: "sections", index: 1 }, defaultSpokenForm: "one section", - disabledByDefault: true, + visibility: "disabledByDefault", isLanguageSpecific: true, defaultInsertionDelimiter: "\n\n", syntaxes: [ @@ -1183,7 +1184,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Section level two", group: { id: "sections", index: 2 }, defaultSpokenForm: "two section", - disabledByDefault: true, + visibility: "disabledByDefault", isLanguageSpecific: true, defaultInsertionDelimiter: "\n\n", syntaxes: [ @@ -1204,7 +1205,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Section level three", group: { id: "sections", index: 3 }, defaultSpokenForm: "three section", - disabledByDefault: true, + visibility: "disabledByDefault", isLanguageSpecific: true, defaultInsertionDelimiter: "\n\n", syntaxes: [ @@ -1225,7 +1226,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Section level four", group: { id: "sections", index: 4 }, defaultSpokenForm: "four section", - disabledByDefault: true, + visibility: "disabledByDefault", isLanguageSpecific: true, defaultInsertionDelimiter: "\n\n", syntaxes: [ @@ -1246,7 +1247,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Section level five", group: { id: "sections", index: 5 }, defaultSpokenForm: "five section", - disabledByDefault: true, + visibility: "disabledByDefault", isLanguageSpecific: true, defaultInsertionDelimiter: "\n\n", syntaxes: [ @@ -1267,7 +1268,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Section level six", group: { id: "sections", index: 6 }, defaultSpokenForm: "six section", - disabledByDefault: true, + visibility: "disabledByDefault", isLanguageSpecific: true, defaultInsertionDelimiter: "\n\n", syntaxes: [ @@ -1311,7 +1312,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Field access", group: { id: "private", index: 0 }, defaultSpokenForm: "access", - private: true, + visibility: "private", isLanguageSpecific: true, syntaxes: [ { @@ -1331,7 +1332,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "String", group: { id: "private", index: 1 }, defaultSpokenForm: "parse tree string", - private: true, + visibility: "private", isLanguageSpecific: true, syntaxes: [ { @@ -1351,7 +1352,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Text fragment", group: { id: "private", index: 2 }, defaultSpokenForm: "text fragment", - private: true, + visibility: "private", isLanguageSpecific: true, syntaxes: [ { @@ -1371,7 +1372,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Disqualify delimiter", group: { id: "private", index: 3 }, defaultSpokenForm: "disqualify delimiter", - private: true, + visibility: "private", isLanguageSpecific: true, syntaxes: [ { @@ -1391,7 +1392,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Pair delimiter", group: { id: "private", index: 4 }, defaultSpokenForm: "pair delimiter", - private: true, + visibility: "private", isLanguageSpecific: true, syntaxes: [ { @@ -1411,7 +1412,7 @@ For a range target, the entire range becomes the search text. Without an explici name: "Interior", group: { id: "private", index: 5 }, defaultSpokenForm: "interior", - private: true, + visibility: "private", isLanguageSpecific: true, syntaxes: [ { @@ -1430,7 +1431,7 @@ For a range target, the entire range becomes the search text. Without an explici surroundingPairInterior: { name: "Surrounding pair interior", group: { id: "private", index: 6 }, - private: true, + visibility: "private", isLanguageSpecific: false, syntaxes: [], examples: [], @@ -1438,7 +1439,7 @@ For a range target, the entire range becomes the search text. Without an explici customRegex: { name: "Custom regex", group: { id: "private", index: 7 }, - private: true, + visibility: "private", isLanguageSpecific: false, syntaxes: [], examples: [], diff --git a/packages/lib-common/src/references/spokenForms/connectiveDefaultSpokenForms.ts b/packages/lib-common/src/references/spokenForms/connectiveDefaultSpokenForms.ts index 773a61eb7f..9b03d188f6 100644 --- a/packages/lib-common/src/references/spokenForms/connectiveDefaultSpokenForms.ts +++ b/packages/lib-common/src/references/spokenForms/connectiveDefaultSpokenForms.ts @@ -6,18 +6,13 @@ export const connectiveDefaultSpokenForms = { rangeExcludingEnd: "until", listConnective: "and", swapConnective: "with", - sourceDestinationConnective: "to", - before: "before", - after: "after", verticalRange: "slice", - - first: "first", - last: "last", - previous: "previous", - next: "next", - forward: "forward", - backward: "backward", - at: "at", on: "on", } as const; + +export const insertionModeDefaultSpokenForms = { + to: "to", + before: "before", + after: "after", +} as const; diff --git a/packages/lib-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/lib-common/src/references/spokenForms/defaultSpokenFormMapCore.ts similarity index 55% rename from packages/lib-engine/src/spokenForms/defaultSpokenFormMapCore.ts rename to packages/lib-common/src/references/spokenForms/defaultSpokenFormMapCore.ts index ed2045ece2..afac7289c2 100644 --- a/packages/lib-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/lib-common/src/references/spokenForms/defaultSpokenFormMapCore.ts @@ -1,21 +1,30 @@ -import { - actionNames, - actionReferences, - graphemeDefaultSpokenForms, - modifierReferences, - pairedDelimiterReferences, - simpleScopeTypeTypes, - scopeReferences, - connectiveDefaultSpokenForms, -} from "@cursorless/lib-common"; -import type { - SpokenFormMapKeyTypes, - SpokenFormReference, -} from "@cursorless/lib-common"; +import { actionNames } from "../../types/command/ActionDescriptor"; +import { simpleScopeTypeTypes } from "../../types/command/PartialTargetDescriptor.types"; import type { DefaultSpokenFormMapDefinition, DefaultSpokenFormMapEntry, -} from "./defaultSpokenFormMap.types"; +} from "../../types/DefaultSpokenFormMap"; +import type { SpokenFormMapKeyTypes } from "../../types/SpokenFormType"; +import { actionReferences, talonSideActionNames } from "../actionReferences"; +import { hatColorReferences, hatShapeReferences } from "../hatStyleReferences"; +import { + modifierExtraReferences, + modifierReferences, +} from "../modifierReferences"; +import { pairedDelimiterReferences } from "../pairedDelimiterReferences"; +import type { SpokenFormReference } from "../ReferenceEntry"; +import { scopeReferences } from "../scopeReferences"; +import { + connectiveDefaultSpokenForms, + insertionModeDefaultSpokenForms, +} from "./connectiveDefaultSpokenForms"; +import { graphemeDefaultSpokenForms } from "./graphemeDefaultSpokenForms"; +import { + lineDirectionDefaultSpokenForms, + markDefaultSpokenForms, + unknownSymbolMarkDefaultSpokenForm, +} from "./markDefaultSpokenForms"; +import { isDisabledByDefault } from "./spokenFormMapUtil"; type DefaultSpokenForm = string | DefaultSpokenFormMapEntry; @@ -46,28 +55,6 @@ const modifierExtraReferenceIds = [ "ancestor", ] as const satisfies readonly SpokenFormMapKeyTypes["modifierExtra"][]; -const modifierExtraReferences = { - first: { - defaultSpokenForm: connectiveDefaultSpokenForms.first, - }, - last: { - defaultSpokenForm: connectiveDefaultSpokenForms.last, - }, - previous: { - defaultSpokenForm: connectiveDefaultSpokenForms.previous, - }, - next: { - defaultSpokenForm: connectiveDefaultSpokenForms.next, - }, - forward: { - defaultSpokenForm: connectiveDefaultSpokenForms.forward, - }, - backward: { - defaultSpokenForm: connectiveDefaultSpokenForms.backward, - }, - ancestor: modifierReferences.ancestor, -} satisfies Record; - function getDefaultSpokenForm( reference: SpokenFormReference, ): DefaultSpokenForm { @@ -77,17 +64,15 @@ function getDefaultSpokenForm( throw new Error("Reference has no default spoken form"); } - const isDisabledByDefault = reference.disabledByDefault ?? false; - const isPrivate = reference.private ?? false; + const { visibility } = reference; - if (!isDisabledByDefault && !isPrivate) { + if (visibility == null) { return defaultSpokenForm; } return { defaultSpokenForms: [defaultSpokenForm], - isDisabledByDefault, - isPrivate, + visibility, }; } @@ -120,7 +105,10 @@ function getCompleteDefaultSpokenFormMap< */ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { pairedDelimiter: getCompleteDefaultSpokenFormMap(pairedDelimiterReferences), - + action: getDefaultSpokenFormMap( + [...actionNames, ...talonSideActionNames], + actionReferences, + ), simpleScopeTypeType: getDefaultSpokenFormMap( simpleScopeTypeTypes, scopeReferences, @@ -128,7 +116,6 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { complexScopeTypeType: { glyph: getDefaultSpokenForm(scopeReferences.glyph), }, - simpleModifier: getDefaultSpokenFormMap( simpleModifierReferenceIds, modifierReferences, @@ -137,9 +124,33 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { modifierExtraReferenceIds, modifierExtraReferences, ), - + hatColor: getCompleteDefaultSpokenFormMap(hatColorReferences), + hatShape: getCompleteDefaultSpokenFormMap(hatShapeReferences), + grapheme: graphemeDefaultSpokenForms, + insertionMode: insertionModeDefaultSpokenForms, + connective: { + ...connectiveDefaultSpokenForms, + rangeExcludingStart: isDisabledByDefault(), + }, + specialMark: { + currentSelection: markDefaultSpokenForms.cursor, + previousTarget: markDefaultSpokenForms.that, + previousSource: markDefaultSpokenForms.source, + nothing: markDefaultSpokenForms.nothing, + lineNumberModulo100: lineDirectionDefaultSpokenForms.modulo100, + lineNumberRelativeUp: lineDirectionDefaultSpokenForms.relativeUp, + lineNumberRelativeDown: lineDirectionDefaultSpokenForms.relativeDown, + unknownSymbol: unknownSymbolMarkDefaultSpokenForm, + }, + scopeVisualizer: { + showScopeVisualizer: "visualize", + hideScopeVisualizer: "visualize nothing", + removal: "removal", + iteration: "iteration", + }, + sidebar: { + bar: "bar", + }, customRegex: {}, - action: getDefaultSpokenFormMap(actionNames, actionReferences), customAction: {}, - grapheme: graphemeDefaultSpokenForms, }; diff --git a/packages/lib-common/src/references/spokenForms/graphemeDefaultSpokenForms.ts b/packages/lib-common/src/references/spokenForms/graphemeDefaultSpokenForms.ts index af68dbbf24..8437cc3ac7 100644 --- a/packages/lib-common/src/references/spokenForms/graphemeDefaultSpokenForms.ts +++ b/packages/lib-common/src/references/spokenForms/graphemeDefaultSpokenForms.ts @@ -84,8 +84,6 @@ const symbols = { "]": "right square", "<": "angle", ">": "right angle", - - "\uFFFD": "special", }; export const graphemeDefaultSpokenForms: Record = { diff --git a/packages/lib-common/src/references/spokenForms/markDefaultSpokenForms.ts b/packages/lib-common/src/references/spokenForms/markDefaultSpokenForms.ts index 9a9fb9b1d3..42a4daee3a 100644 --- a/packages/lib-common/src/references/spokenForms/markDefaultSpokenForms.ts +++ b/packages/lib-common/src/references/spokenForms/markDefaultSpokenForms.ts @@ -2,35 +2,6 @@ import type { PartialMark } from "../../types/command/PartialTargetDescriptor.ty type MarkType = PartialMark["type"]; -export const hatColorDefaultSpokenForms: Record = { - blue: "blue", - green: "green", - red: "red", - pink: "pink", - yellow: "yellow", - userColor1: "navy", - userColor2: "apricot", - userColor3: "user color three", - userColor4: "user color four", - - default: null, -}; - -export const hatShapeDefaultSpokenForms: Record = { - ex: "ex", - fox: "fox", - wing: "wing", - hole: "hole", - frame: "frame", - curve: "curve", - eye: "eye", - play: "play", - crosshairs: "cross", - bolt: "bolt", - - default: null, -}; - export const markDefaultSpokenForms = { cursor: "this", that: "that", @@ -45,6 +16,8 @@ export const markDefaultSpokenForms = { target: null, } as const satisfies Record; +export const unknownSymbolMarkDefaultSpokenForm = "special"; + export const lineDirectionDefaultSpokenForms = { modulo100: "row", relativeUp: "up", diff --git a/packages/lib-engine/src/spokenForms/spokenFormMapUtil.ts b/packages/lib-common/src/references/spokenForms/spokenFormMapUtil.ts similarity index 50% rename from packages/lib-engine/src/spokenForms/spokenFormMapUtil.ts rename to packages/lib-common/src/references/spokenForms/spokenFormMapUtil.ts index 37f260f623..dea609265d 100644 --- a/packages/lib-engine/src/spokenForms/spokenFormMapUtil.ts +++ b/packages/lib-common/src/references/spokenForms/spokenFormMapUtil.ts @@ -1,20 +1,17 @@ -import type { DefaultSpokenFormMapEntry } from "./defaultSpokenFormMap.types"; +import type { DefaultSpokenFormMapEntry } from "../../types/DefaultSpokenFormMap"; /** * Used to construct entities that should not be speakable by default. * * @param spokenForms The default spoken forms for this entity - * @returns A DefaultSpokenFormMapEntry with the given spoken forms, and - * {@link DefaultSpokenFormMapEntry.isDisabledByDefault|isDisabledByDefault} set - * to true + * @returns A disabled-by-default entry with the given spoken forms */ export function isDisabledByDefault( ...spokenForms: string[] ): DefaultSpokenFormMapEntry { return { defaultSpokenForms: spokenForms, - isDisabledByDefault: true, - isPrivate: false, + visibility: "disabledByDefault", }; } @@ -22,14 +19,11 @@ export function isDisabledByDefault( * Used to construct entities that are only for internal experimentation. * * @param spokenForms The default spoken forms for this entity - * @returns A DefaultSpokenFormMapEntry with the given spoken forms, and - * {@link DefaultSpokenFormMapEntry.isDisabledByDefault|isDisabledByDefault} and - * {@link DefaultSpokenFormMapEntry.isPrivate|isPrivate} set to true + * @returns A private entry with the given spoken forms */ export function isPrivate(...spokenForms: string[]): DefaultSpokenFormMapEntry { return { defaultSpokenForms: spokenForms, - isDisabledByDefault: true, - isPrivate: true, + visibility: "private", }; } diff --git a/packages/lib-common/src/types/DefaultSpokenFormMap.ts b/packages/lib-common/src/types/DefaultSpokenFormMap.ts new file mode 100644 index 0000000000..935a3d7353 --- /dev/null +++ b/packages/lib-common/src/types/DefaultSpokenFormMap.ts @@ -0,0 +1,13 @@ +import type { SpokenFormMapKeyTypes } from "./SpokenFormType"; +import type { SpokenFormVisibility } from "./SpokenFormVisibility"; + +export interface DefaultSpokenFormMapEntry { + defaultSpokenForms: string[]; + visibility?: SpokenFormVisibility; +} + +export type DefaultSpokenFormMapDefinition = { + readonly [K in keyof SpokenFormMapKeyTypes]: Readonly< + Record + >; +}; diff --git a/packages/lib-common/src/types/SpokenFormType.ts b/packages/lib-common/src/types/SpokenFormType.ts index 683d2234a7..77889c73ae 100644 --- a/packages/lib-common/src/types/SpokenFormType.ts +++ b/packages/lib-common/src/types/SpokenFormType.ts @@ -1,10 +1,42 @@ +import type { TalonSideActionType } from "../references/actionReferences"; import type { ActionType } from "./command/ActionDescriptor"; +import type { InsertionMode } from "./command/DestinationDescriptor.types"; import type { ModifierType, SimpleScopeTypeType, SurroundingPairName, } from "./command/PartialTargetDescriptor.types"; +type SpecialMark = + | "previousSource" + | "previousTarget" + | "currentSelection" + | "lineNumberRelativeDown" + | "lineNumberModulo100" + | "lineNumberRelativeUp" + | "nothing" + | "unknownSymbol"; + +type Connective = + | "rangeExclusive" + | "rangeInclusive" + | "rangeExcludingStart" + | "rangeExcludingEnd" + | "listConnective" + | "swapConnective" + | "verticalRange" + | "at" + | "on"; + +type ScopeVisualizer = + | "showScopeVisualizer" + | "hideScopeVisualizer" + | "removal" + | "iteration"; + +type ComplexScopeTypeType = "glyph"; +type Sidebar = "bar"; + /** * This interface is the source of truth for the types used in our spoken form * map. The keys of this interface are the types of spoken forms that we @@ -13,9 +45,17 @@ import type { * spoken form. */ export interface SpokenFormMapKeyTypes { + action: ActionType | TalonSideActionType; pairedDelimiter: SpeakableSurroundingPairName; simpleScopeTypeType: SimpleScopeTypeType; - complexScopeTypeType: "glyph"; + complexScopeTypeType: ComplexScopeTypeType; + insertionMode: InsertionMode; + specialMark: SpecialMark; + connective: Connective; + hatColor: string; + hatShape: string; + sidebar: Sidebar; + scopeVisualizer: ScopeVisualizer; /** * These modifier types are spoken by directly saying the spoken form for the @@ -32,9 +72,11 @@ export interface SpokenFormMapKeyTypes { * modifiers, but `next` itself isn't a modifier type. */ modifierExtra: ModifierExtra; - customRegex: string; - action: ActionType; + /** + * These are customizable spoken forms that correspond to a regex pattern. + */ + customRegex: string; /** * These actions correspond to id's of app commands. Eg in VSCode, you can have diff --git a/packages/lib-common/src/types/SpokenFormVisibility.ts b/packages/lib-common/src/types/SpokenFormVisibility.ts new file mode 100644 index 0000000000..c4baf37f3d --- /dev/null +++ b/packages/lib-common/src/types/SpokenFormVisibility.ts @@ -0,0 +1,14 @@ +/** + * Controls whether a spoken form is exposed and enabled by default. + * + * - `"disabledByDefault"` indicates that the spoken form is available for + * customization but is not enabled by default. It appears in user CSV files + * with a `-` prefix. + * - `"private"` indicates that the spoken form is intended only for internal + * experimentation or a targeted working group. Private spoken forms are + * also disabled by default. + * + * An omitted visibility indicates a public spoken form that is enabled by + * default. + */ +export type SpokenFormVisibility = "disabledByDefault" | "private"; diff --git a/packages/lib-common/src/types/TalonSpokenForms.ts b/packages/lib-common/src/types/TalonSpokenForms.ts index a55ab61023..477311cf38 100644 --- a/packages/lib-common/src/types/TalonSpokenForms.ts +++ b/packages/lib-common/src/types/TalonSpokenForms.ts @@ -6,7 +6,7 @@ import type { SpokenFormMapKeyTypes, SpokenFormType } from "./SpokenFormType"; * the user's custom spoken forms to the Cursorless engine. */ export interface TalonSpokenForms { - getSpokenFormEntries(): Promise; + getSpokenForms(): Promise; onDidChange: Notifier["registerListener"]; } @@ -21,6 +21,15 @@ export const SUPPORTED_ENTRY_TYPES = [ "pairedDelimiter", "action", "customAction", + "simpleModifier", + "modifierExtra", + "connective", + "insertionMode", + "specialMark", + "hatColor", + "hatShape", + "sidebar", + "scopeVisualizer", "grapheme", ] as const; @@ -36,6 +45,11 @@ export type SpokenFormEntry = { [K in SpokenFormType]: SpokenFormEntryForType; }[SupportedEntryType]; +export interface TalonSpokenFormsPayload { + version: number; + spokenForms: SpokenFormEntry[]; +} + export class NeedsInitialTalonUpdateError extends Error { constructor(message: string) { super(message); diff --git a/packages/lib-engine/src/customCommandGrammar/lexer.ts b/packages/lib-engine/src/customCommandGrammar/lexer.ts index a8fdab3010..2378dd245b 100644 --- a/packages/lib-engine/src/customCommandGrammar/lexer.ts +++ b/packages/lib-engine/src/customCommandGrammar/lexer.ts @@ -3,7 +3,7 @@ import type { InsertionMode, } from "@cursorless/lib-common"; import { - connectiveDefaultSpokenForms, + insertionModeDefaultSpokenForms, markDefaultSpokenForms, simpleActionNames, simpleScopeTypeTypes, @@ -49,10 +49,7 @@ for (const bringMoveActionName of bringMoveActionNames) { const insertionModes: InsertionMode[] = ["before", "after", "to"]; for (const insertionMode of insertionModes) { - const spokenForm = - connectiveDefaultSpokenForms[ - insertionMode === "to" ? "sourceDestinationConnective" : insertionMode - ]; + const spokenForm = insertionModeDefaultSpokenForms[insertionMode]; tokens[spokenForm] = { type: "insertionMode", value: insertionMode, diff --git a/packages/lib-engine/src/disabledComponents/DisabledTalonSpokenForms.ts b/packages/lib-engine/src/disabledComponents/DisabledTalonSpokenForms.ts index f9fe109464..a4e4cf299b 100644 --- a/packages/lib-engine/src/disabledComponents/DisabledTalonSpokenForms.ts +++ b/packages/lib-engine/src/disabledComponents/DisabledTalonSpokenForms.ts @@ -1,8 +1,11 @@ -import type { SpokenFormEntry, TalonSpokenForms } from "@cursorless/lib-common"; +import type { + TalonSpokenForms, + TalonSpokenFormsPayload, +} from "@cursorless/lib-common"; import { DisabledCustomSpokenFormsError } from "@cursorless/lib-common"; export class DisabledTalonSpokenForms implements TalonSpokenForms { - getSpokenFormEntries(): Promise { + getSpokenForms(): Promise { throw new DisabledCustomSpokenFormsError(); } diff --git a/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts b/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts index 648266e30a..50fad298d2 100644 --- a/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts +++ b/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts @@ -4,27 +4,35 @@ import { CustomSpokenFormGeneratorImpl } from "./CustomSpokenFormGeneratorImpl"; suite("CustomSpokenFormGeneratorImpl", () => { test( - "basic", + "uses custom spoken forms and defaults for types omitted by Talon", asyncSafety(async () => { const generator = new CustomSpokenFormGeneratorImpl(new FakeIDE(), { - getSpokenFormEntries() { - return Promise.resolve([ - { - type: "complexScopeTypeType", - id: "glyph", - spokenForms: ["foo"], - }, - { - type: "action", - id: "setSelection", - spokenForms: ["bar"], - }, - { - type: "grapheme", - id: "a", - spokenForms: ["alabaster"], - }, - ]); + getSpokenForms() { + return Promise.resolve({ + version: 0, + spokenForms: [ + { + type: "complexScopeTypeType", + id: "glyph", + spokenForms: ["foo"], + }, + { + type: "action", + id: "setSelection", + spokenForms: ["bar"], + }, + { + type: "grapheme", + id: "a", + spokenForms: ["alabaster"], + }, + { + type: "specialMark", + id: "currentSelection", + spokenForms: ["this"], + }, + ], + }); }, onDidChange: () => ({ dispose() { @@ -64,6 +72,75 @@ suite("CustomSpokenFormGeneratorImpl", () => { spokenForms: ["bar this"], }, ); + + assert.deepEqual( + generator.commandToSpokenForm({ + version: LATEST_VERSION, + action: { + name: "setSelection", + target: { + type: "primitive", + mark: { + type: "decoratedSymbol", + symbolColor: "blue", + character: "a", + }, + }, + }, + usePrePhraseSnapshot: false, + }), + { + type: "success", + spokenForms: ["bar blue alabaster"], + }, + ); + }), + ); + + test( + "requires a Talon update when version 1 omits an entry type", + asyncSafety(async () => { + const generator = new CustomSpokenFormGeneratorImpl(new FakeIDE(), { + getSpokenForms() { + return Promise.resolve({ + version: 1, + spokenForms: [ + { + type: "action", + id: "setSelection", + spokenForms: ["take"], + }, + ], + }); + }, + onDidChange: () => ({ + dispose() { + // no-op + }, + }), + }); + + await generator.customSpokenFormsInitialized; + + const spokenForm = generator.commandToSpokenForm({ + version: LATEST_VERSION, + action: { + name: "setSelection", + target: { + type: "primitive", + mark: { + type: "decoratedSymbol", + symbolColor: "blue", + character: "a", + }, + }, + }, + usePrePhraseSnapshot: false, + }); + + assert.equal(spokenForm.type, "error"); + assert.equal(spokenForm.requiresTalonUpdate, true); + assert.match(spokenForm.reason, /hat color with id blue/u); }), ); }); diff --git a/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/marks.ts b/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/marks.ts deleted file mode 100644 index 30ddc9a136..0000000000 --- a/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/marks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - hatColorDefaultSpokenForms, - hatShapeDefaultSpokenForms, -} from "@cursorless/lib-common"; - -export function hatColorToSpokenForm(color: string): string { - const result = hatColorDefaultSpokenForms[color]; - if (result == null) { - throw new Error(`Unknown hat color '${color}'`); - } - return result; -} - -export function hatShapeToSpokenForm(shape: string): string { - const result = hatShapeDefaultSpokenForms[shape]; - if (result == null) { - throw new Error(`Unknown hat shape '${shape}'`); - } - return result; -} diff --git a/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/modifiers.ts b/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/modifiers.ts index 7d72f2d5a8..984befaf2a 100644 --- a/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/modifiers.ts +++ b/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/modifiers.ts @@ -1,18 +1,22 @@ -import { CompositeKeyMap } from "@cursorless/lib-common"; +import { + CompositeKeyMap, + pairedDelimiterReferences, +} from "@cursorless/lib-common"; import type { SpeakableSurroundingPairName } from "@cursorless/lib-common"; import type { SpokenFormComponentMap } from "../getSpokenFormComponentMap"; import type { CustomizableSpokenFormComponentForType } from "../SpokenFormComponent"; -import { surroundingPairsDelimiters } from "./surroundingPairsDelimiters"; const surroundingPairDelimiterToName = new CompositeKeyMap< [string, string], SpeakableSurroundingPairName >((pair) => pair); -for (const [name, pair] of Object.entries(surroundingPairsDelimiters)) { - if (pair != null) { +for (const [name, { delimiters }] of Object.entries( + pairedDelimiterReferences, +)) { + if (delimiters != null) { surroundingPairDelimiterToName.set( - pair, + delimiters, name as SpeakableSurroundingPairName, ); } diff --git a/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts b/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts deleted file mode 100644 index f69c631a1e..0000000000 --- a/packages/lib-engine/src/generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { SpeakableSurroundingPairName } from "@cursorless/lib-common"; - -export const surroundingPairsDelimiters: Record< - SpeakableSurroundingPairName, - [string, string] | null -> = { - curlyBrackets: ["{", "}"], - angleBrackets: ["<", ">"], - escapedDoubleQuotes: [String.raw`\"`, String.raw`\"`], - escapedSingleQuotes: [String.raw`\'`, String.raw`\'`], - escapedParentheses: [String.raw`\(`, String.raw`\)`], - escapedSquareBrackets: [String.raw`\[`, String.raw`\]`], - doubleQuotes: ['"', '"'], - parentheses: ["(", ")"], - backtickQuotes: ["`", "`"], - squareBrackets: ["[", "]"], - singleQuotes: ["'", "'"], - tripleBacktickQuotes: ["```", "```"], - tripleDoubleQuotes: ['"""', '"""'], - tripleSingleQuotes: ["'''", "'''"], - whitespace: [" ", " "], - - any: null, - string: null, - collectionBoundary: null, -}; diff --git a/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.test.ts b/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.test.ts index f944fba623..a23e1d5c1e 100644 --- a/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.test.ts +++ b/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.test.ts @@ -18,13 +18,16 @@ import { getHatMapCommand } from "./getHatMapCommand"; */ const spokenFormMap: SpokenFormMap = mapSpokenForms( defaultSpokenFormInfoMap, - ({ defaultSpokenForms, isPrivate }) => ({ - spokenForms: isPrivate ? [] : defaultSpokenForms, - isCustom: false, - defaultSpokenForms, - requiresTalonUpdate: false, - isPrivate, - }), + ({ defaultSpokenForms, visibility }) => { + const isPrivate = visibility === "private"; + return { + spokenForms: isPrivate ? [] : defaultSpokenForms, + isCustom: false, + defaultSpokenForms, + requiresTalonUpdate: false, + isPrivate, + }; + }, ); suite("Generate spoken forms", () => { diff --git a/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.ts b/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.ts index b71300aad4..b156b486ce 100644 --- a/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.ts +++ b/packages/lib-engine/src/generateSpokenForm/generateSpokenForm.ts @@ -9,11 +9,7 @@ import type { SpokenFormMapKeyTypes, SpokenFormType, } from "@cursorless/lib-common"; -import { - camelCaseToAllDown, - connectiveDefaultSpokenForms, - DOCS_URL, -} from "@cursorless/lib-common"; +import { camelCaseToAllDown, DOCS_URL } from "@cursorless/lib-common"; import type { SpokenFormMap } from "../spokenForms/SpokenFormMap"; import { surroundingPairDelimitersToSpokenForm } from "./defaultSpokenForms/modifiers"; import { @@ -139,7 +135,7 @@ export class SpokenFormGenerator { return [ this.spokenFormMap.action[action.name], this.handleTarget(action.target1), - connectiveDefaultSpokenForms.swapConnective, + this.spokenFormMap.connective.swapConnective, this.handleTarget(action.target2), ]; @@ -153,7 +149,7 @@ export class SpokenFormGenerator { return [ this.spokenFormMap.action[action.name], this.handleTarget(action.callee), - connectiveDefaultSpokenForms.on, + this.spokenFormMap.connective.on, this.handleTarget(action.argument), ]; @@ -228,7 +224,7 @@ export class SpokenFormGenerator { i === 0 ? this.handleTarget(element) : [ - connectiveDefaultSpokenForms.listConnective, + this.spokenFormMap.connective.listConnective, this.handleTarget(element), ], ); @@ -237,6 +233,7 @@ export class SpokenFormGenerator { const anchor = this.handleTarget(target.anchor); const active = this.handleTarget(target.active); const connective = getRangeConnective( + this.spokenFormMap, target.excludeAnchor, target.excludeActive, target.rangeType, @@ -267,7 +264,7 @@ export class SpokenFormGenerator { i === 0 ? this.handleDestination(destination) : [ - connectiveDefaultSpokenForms.listConnective, + this.spokenFormMap.connective.listConnective, this.handleDestination(destination), ], ); @@ -285,14 +282,16 @@ export class SpokenFormGenerator { } } - private handleInsertionMode(insertionMode: InsertionMode): string { + private handleInsertionMode( + insertionMode: InsertionMode, + ): SpokenFormComponent { switch (insertionMode) { case "to": - return connectiveDefaultSpokenForms.sourceDestinationConnective; + return this.spokenFormMap.insertionMode.to; case "before": - return connectiveDefaultSpokenForms.before; + return this.spokenFormMap.insertionMode.before; case "after": - return connectiveDefaultSpokenForms.after; + return this.spokenFormMap.insertionMode.after; // No default } } @@ -324,7 +323,7 @@ function constructSpokenForms(component: SpokenFormComponent): string[] { helpInfo = "this is a private spoken form currently only for internal experimentation"; } else if (component.spokenForms.requiresTalonUpdate) { - helpInfo = `please update talon to the latest version (see ${DOCS_URL}/user/updating)`; + helpInfo = `please update cursorless-talon to the latest version (see ${DOCS_URL}/user/updating)`; } else { helpInfo = `please see ${DOCS_URL}/user/customization for more information`; } diff --git a/packages/lib-engine/src/generateSpokenForm/getRangeConnective.ts b/packages/lib-engine/src/generateSpokenForm/getRangeConnective.ts index 00df734b14..0420fd652a 100644 --- a/packages/lib-engine/src/generateSpokenForm/getRangeConnective.ts +++ b/packages/lib-engine/src/generateSpokenForm/getRangeConnective.ts @@ -1,26 +1,33 @@ import type { PartialRangeType } from "@cursorless/lib-common"; -import { connectiveDefaultSpokenForms } from "@cursorless/lib-common"; -import { NoSpokenFormError } from "./NoSpokenFormError"; +import type { SpokenFormComponentMap } from "./getSpokenFormComponentMap"; +import type { SpokenFormComponent } from "./SpokenFormComponent"; export function getRangeConnective( + spokenFormMap: SpokenFormComponentMap, excludeAnchor: boolean, excludeActive: boolean, type?: PartialRangeType, -): string { +): SpokenFormComponent { const prefix = - type === "vertical" ? `${connectiveDefaultSpokenForms.verticalRange} ` : ""; + type === "vertical" ? spokenFormMap.connective.verticalRange : undefined; if (excludeAnchor && excludeActive) { - return prefix + connectiveDefaultSpokenForms.rangeExclusive; + return prefix != null + ? [prefix, spokenFormMap.connective.rangeExclusive] + : spokenFormMap.connective.rangeExclusive; } if (excludeAnchor) { - throw new NoSpokenFormError("Range exclude anchor"); + return prefix != null + ? [prefix, spokenFormMap.connective.rangeExcludingStart] + : spokenFormMap.connective.rangeExcludingStart; } if (excludeActive) { - return prefix + connectiveDefaultSpokenForms.rangeExcludingEnd; + return prefix != null + ? [prefix, spokenFormMap.connective.rangeExcludingEnd] + : spokenFormMap.connective.rangeExcludingEnd; } if (type === "vertical") { // "slice", but could have been "slice past" - return connectiveDefaultSpokenForms.verticalRange; + return spokenFormMap.connective.verticalRange; } - return connectiveDefaultSpokenForms.rangeInclusive; + return spokenFormMap.connective.rangeInclusive; } diff --git a/packages/lib-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts b/packages/lib-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts index 4b39be7d96..e2a3ab58b6 100644 --- a/packages/lib-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts +++ b/packages/lib-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts @@ -6,15 +6,6 @@ import type { RelativeScopeModifier, ScopeType, } from "@cursorless/lib-common"; -import { - connectiveDefaultSpokenForms, - lineDirectionDefaultSpokenForms, - markDefaultSpokenForms, -} from "@cursorless/lib-common"; -import { - hatColorToSpokenForm, - hatShapeToSpokenForm, -} from "./defaultSpokenForms/marks"; import { numberToSpokenForm, ordinalToSpokenForm, @@ -142,6 +133,7 @@ export class PrimitiveTargetSpokenFormGenerator { : ordinalToSpokenForm(modifier.anchor.start + 1); const active = this.handleModifier(modifier.active); const connective = getRangeConnective( + this.spokenFormMap, modifier.excludeAnchor, modifier.excludeActive, ); @@ -169,8 +161,8 @@ export class PrimitiveTargetSpokenFormGenerator { if (modifier.length === 1) { const direction = modifier.direction === "forward" - ? connectiveDefaultSpokenForms.forward - : connectiveDefaultSpokenForms.backward; + ? this.spokenFormMap.modifierExtra.forward + : this.spokenFormMap.modifierExtra.backward; // token forward/backward return [isEvery, scope, direction]; @@ -190,7 +182,7 @@ export class PrimitiveTargetSpokenFormGenerator { isEvery, length, scopePlural, - connectiveDefaultSpokenForms.backward, + this.spokenFormMap.modifierExtra.backward, ]; } @@ -200,8 +192,8 @@ export class PrimitiveTargetSpokenFormGenerator { const scope = this.handleScopeType(modifier.scopeType); const direction = modifier.direction === "forward" - ? connectiveDefaultSpokenForms.next - : connectiveDefaultSpokenForms.previous; + ? this.spokenFormMap.modifierExtra.next + : this.spokenFormMap.modifierExtra.previous; const isEvery = modifier.isEvery ? this.spokenFormMap.simpleModifier.everyScope : []; @@ -278,18 +270,30 @@ export class PrimitiveTargetSpokenFormGenerator { const [color, shape] = mark.symbolColor.split("-"); const components: SpokenFormComponent[] = []; if (color !== "default") { - components.push(hatColorToSpokenForm(color)); + const result = this.spokenFormMap.hatColor[color]; + if (result == null) { + throw new Error(`Unknown hat color '${color}'`); + } + components.push(result); } if (shape != null) { - components.push(hatShapeToSpokenForm(shape)); + const result = this.spokenFormMap.hatShape[shape]; + if (result == null) { + throw new Error(`Unknown hat shape '${shape}'`); + } + components.push(result); + } + if (mark.character === "\uFFFD") { + components.push(this.spokenFormMap.specialMark.unknownSymbol); + } else { + components.push( + getSpokenFormStrict( + this.spokenFormMap.grapheme, + "grapheme", + mark.character, + ), + ); } - components.push( - getSpokenFormStrict( - this.spokenFormMap.grapheme, - "grapheme", - mark.character, - ), - ); return components; } @@ -310,6 +314,7 @@ export class PrimitiveTargetSpokenFormGenerator { ); if (typeAnchor === typeActive) { const connective = getRangeConnective( + this.spokenFormMap, mark.excludeAnchor, mark.excludeActive, ); @@ -321,24 +326,35 @@ export class PrimitiveTargetSpokenFormGenerator { // a spoken form for these; we may deprecate this construct entirely throw new Error(`Mark '${mark.type}' is not fully implemented`); } + + case "cursor": + return this.spokenFormMap.specialMark.currentSelection; + case "that": + return this.spokenFormMap.specialMark.previousTarget; + case "source": + return this.spokenFormMap.specialMark.previousSource; + case "nothing": + return this.spokenFormMap.specialMark.nothing; + case "explicit": case "keyboard": case "target": throw new NoSpokenFormError(`Mark '${mark.type}'`); - default: - return [markDefaultSpokenForms[mark.type]]; + // No default } } - private handleLineNumberMark(mark: LineNumberMark): [string, string] { + private handleLineNumberMark( + mark: LineNumberMark, + ): [SpokenFormComponent, string] { switch (mark.lineNumberType) { case "absolute": throw new NoSpokenFormError("Absolute line numbers"); case "modulo100": { // row/ five return [ - lineDirectionDefaultSpokenForms.modulo100, + this.spokenFormMap.specialMark.lineNumberModulo100, numberToSpokenForm(mark.lineNumber + 1), ]; } @@ -346,8 +362,8 @@ export class PrimitiveTargetSpokenFormGenerator { // up/down five return [ mark.lineNumber < 0 - ? lineDirectionDefaultSpokenForms.relativeUp - : lineDirectionDefaultSpokenForms.relativeDown, + ? this.spokenFormMap.specialMark.lineNumberRelativeUp + : this.spokenFormMap.specialMark.lineNumberRelativeDown, numberToSpokenForm(Math.abs(mark.lineNumber)), ]; } diff --git a/packages/lib-engine/src/index.ts b/packages/lib-engine/src/index.ts index b4bcc4cdc1..b5409b402d 100644 --- a/packages/lib-engine/src/index.ts +++ b/packages/lib-engine/src/index.ts @@ -7,7 +7,6 @@ export * from "./core/Snippets"; export * from "./core/StoredTargets"; export * from "./cursorlessEngine"; export * from "./customCommandGrammar/parseCommand"; -export * from "./generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters"; export * from "./generateSpokenForm/generateSpokenForm"; export * from "./languages/TreeSitterQuery/TreeSitterQueryCache"; export * from "./processTargets/modifiers/scopeHandlers/ScopeHandlerCache"; diff --git a/packages/lib-engine/src/spokenForms/CustomSpokenForms.ts b/packages/lib-engine/src/spokenForms/CustomSpokenForms.ts index f7af2ac88a..a4b68deaf7 100644 --- a/packages/lib-engine/src/spokenForms/CustomSpokenForms.ts +++ b/packages/lib-engine/src/spokenForms/CustomSpokenForms.ts @@ -1,6 +1,7 @@ import { isEqual } from "lodash-es"; import type { CustomRegexScopeType, + DefaultSpokenFormMapEntry, Disposable, IDE, SpokenFormEntry, @@ -20,7 +21,6 @@ import { defaultSpokenFormInfoMap, defaultSpokenFormMap, } from "./defaultSpokenFormMap"; -import type { DefaultSpokenFormMapEntry } from "./defaultSpokenFormMap.types"; import type { SpokenFormMap, SpokenFormMapEntry } from "./SpokenFormMap"; type Writable = { @@ -78,13 +78,16 @@ export class CustomSpokenForms { private async updateSpokenFormMaps(): Promise { let allCustomEntries: SpokenFormEntry[]; + let spokenFormsVersion: number; // We successfully loaded spoken forms, so any previous "needs update" // state is no longer relevant. this.needsInitialTalonUpdate_ = false; try { - allCustomEntries = await this.talonSpokenForms.getSpokenFormEntries(); + const payload = await this.talonSpokenForms.getSpokenForms(); + allCustomEntries = payload.spokenForms; + spokenFormsVersion = payload.version; if (allCustomEntries.length === 0) { throw new Error("Custom spoken forms list empty"); } @@ -110,15 +113,26 @@ export class CustomSpokenForms { return; } + this.spokenFormMap_ = { ...defaultSpokenFormMap }; + for (const entryType of SUPPORTED_ENTRY_TYPES) { + const entriesForType = allCustomEntries.filter( + (entry) => entry.type === entryType, + ); + + // Older Talon versions don't provide every entry type. In that case, + // retain the defaults for the entire type instead of treating each entry + // as a newly added spoken form that requires a Talon update. + if (entriesForType.length === 0 && spokenFormsVersion === 0) { + continue; + } + updateEntriesForType( this.spokenFormMap_, entryType, defaultSpokenFormInfoMap[entryType], Object.fromEntries( - allCustomEntries - .filter((entry) => entry.type === entryType) - .map(({ id, spokenForms }) => [id, spokenForms]), + entriesForType.map(({ id, spokenForms }) => [id, spokenForms]), ), ); } @@ -159,8 +173,8 @@ function updateEntriesForType( const obj: Partial> = {}; for (const id of ids) { - const { defaultSpokenForms = [], isPrivate = false } = - defaultEntries[id] ?? {}; + const { defaultSpokenForms = [], visibility } = defaultEntries[id] ?? {}; + const isPrivate = visibility === "private"; const customSpokenForms = customEntries[id]; // No entry for the given id. This either means that the user needs to diff --git a/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.ts b/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.ts index d6c6d216a5..aeb3c7a229 100644 --- a/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.ts +++ b/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.ts @@ -1,8 +1,11 @@ -import type { DefaultSpokenFormInfoMap } from "./defaultSpokenFormMap.types"; -import { defaultSpokenFormMapCore } from "./defaultSpokenFormMapCore"; -import type { SpokenFormMap } from "./SpokenFormMap"; +import { defaultSpokenFormMapCore } from "@cursorless/lib-common"; +import type { DefaultSpokenFormMapEntry } from "@cursorless/lib-common"; +import type { SpokenFormMap, SpokenFormMappingType } from "./SpokenFormMap"; import { mapSpokenForms } from "./SpokenFormMap"; +export type DefaultSpokenFormInfoMap = + SpokenFormMappingType; + /** * This map contains information about the default spoken forms for all our * speakable entities, including scope types, paired delimiters, etc. Note that @@ -14,8 +17,6 @@ export const defaultSpokenFormInfoMap: DefaultSpokenFormInfoMap = typeof value === "string" ? { defaultSpokenForms: [value], - isDisabledByDefault: false, - isPrivate: false, } : value, ); @@ -26,11 +27,14 @@ export const defaultSpokenFormInfoMap: DefaultSpokenFormInfoMap = */ export const defaultSpokenFormMap: SpokenFormMap = mapSpokenForms( defaultSpokenFormInfoMap, - ({ defaultSpokenForms, isDisabledByDefault, isPrivate }) => ({ - spokenForms: isDisabledByDefault ? [] : defaultSpokenForms, - isCustom: false, - defaultSpokenForms, - requiresTalonUpdate: false, - isPrivate, - }), + ({ defaultSpokenForms, visibility }) => { + const isPrivate = visibility === "private"; + return { + spokenForms: visibility == null ? defaultSpokenForms : [], + isCustom: false, + defaultSpokenForms, + requiresTalonUpdate: false, + isPrivate, + }; + }, ); diff --git a/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.types.ts b/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.types.ts deleted file mode 100644 index ffe714d50c..0000000000 --- a/packages/lib-engine/src/spokenForms/defaultSpokenFormMap.types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { SpokenFormMapKeyTypes } from "@cursorless/lib-common"; -import type { SpokenFormMappingType } from "./SpokenFormMap"; - -export type DefaultSpokenFormMapDefinition = { - readonly [K in keyof SpokenFormMapKeyTypes]: Readonly< - Record - >; -}; - -export interface DefaultSpokenFormMapEntry { - defaultSpokenForms: string[]; - - /** - * If `true`, indicates that the entry may have a default spoken form, but - * it should not be enabled by default. These will show up in user csv's with - * a `-` at the beginning. - */ - isDisabledByDefault: boolean; - - /** - * If `true`, indicates that the entry is only for internal experimentation, - * and should not be exposed to users except within a targeted working group. - */ - isPrivate: boolean; -} - -export type DefaultSpokenFormInfoMap = - SpokenFormMappingType; diff --git a/packages/lib-node-common/src/Cheatsheet.ts b/packages/lib-node-common/src/Cheatsheet.ts index d1d89fb08b..9b7295b71d 100644 --- a/packages/lib-node-common/src/Cheatsheet.ts +++ b/packages/lib-node-common/src/Cheatsheet.ts @@ -1,91 +1,90 @@ import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import { produce } from "immer"; -import { sortBy } from "lodash-es"; import { parse } from "node-html-parser"; -import type { IDE } from "@cursorless/lib-common"; -import { getCursorlessRepoRoot } from "./getCursorlessRepoRoot"; +import type { CheatsheetInfo, IDE } from "@cursorless/lib-common"; +import { + getCheatsheetInfo, + getDefaultCheatsheetInfo, + getErrorMessage, + showWarning, +} from "@cursorless/lib-common"; +import type { FileSystemTalonSpokenForms } from "./FileSystemTalonSpokenForms"; -/** - * The argument expected by the cheatsheet command. - */ -export interface CheatSheetCommandArg { - /** - * The version of the cheatsheet command. - */ +interface CheatSheetCommandArgV0 { version: 0; + /** The file to write the cheatsheet to. */ + outputPath: string; + /** * A representation of all spoken forms that is used to generate the * cheatsheet. */ spokenFormInfo: CheatsheetInfo; +} - /** - * The file to write the cheatsheet to - */ +/** The extension assembles the cheatsheet from the Talon state file. */ +interface CheatSheetCommandArgV1 { + version: 1; + + /** The file to write the cheatsheet to. */ outputPath: string; } +export type CheatSheetCommandArg = + | CheatSheetCommandArgV0 + | CheatSheetCommandArgV1; + export async function showCheatsheet( ide: IDE, - { version, spokenFormInfo, outputPath }: CheatSheetCommandArg, + talonSpokenForms: FileSystemTalonSpokenForms, + arg: CheatSheetCommandArg, ) { - if (version !== 0) { - throw new Error(`Unsupported cheatsheet api version: ${version}`); - } - + const cheatsheetInfo = await getCheatsheetInfoForCommand( + ide, + talonSpokenForms, + arg, + ); const cheatsheetPath = path.join(ide.assetsRoot, "cheatsheet.html"); - const cheatsheetContent = await readFile(cheatsheetPath, "utf8"); - const root = parse(cheatsheetContent); root.getElementById("cheatsheet-data")!.textContent = - `document.cheatsheetInfo = ${JSON.stringify(spokenFormInfo)};`; + `document.cheatsheetInfo = ${JSON.stringify(cheatsheetInfo)};`; - await writeFile(outputPath, root.toString()); + await writeFile(arg.outputPath, root.toString()); } -/** - * Updates the default spoken forms stored in `defaults.json` for - * development. - * @param spokenFormInfo The new value to use for default spoken forms. - */ -export async function updateDefaults(spokenFormInfo: CheatsheetInfo) { - const defaultsPath = path.join( - getCursorlessRepoRoot(), - "packages/lib-cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json", - ); - - const outputObject = produce(spokenFormInfo, (draft) => { - draft.sections = sortBy(draft.sections, "id"); - for (const section of draft.sections) { - section.items = sortBy(section.items, "id"); - } - }); - - const json = JSON.stringify(outputObject, null, 2); - await writeFile(defaultsPath, `${json}\n`); -} +async function getCheatsheetInfoForCommand( + ide: IDE, + talonSpokenForms: FileSystemTalonSpokenForms, + arg: CheatSheetCommandArg, +): Promise { + const version = arg.version; -// FIXME: Stop duplicating these types once we have #945 -// The source of truth is at /cursorless-nx/libs/cheatsheet/src/lib/CheatsheetInfo.tsx -interface Variation { - spokenForm: string; - description: string; -} + if (version === 0) { + // DEPRECATED: 2026-08-31 + void showWarning( + ide.messages, + "cheatSheetV0Deprecated", + "Cheat sheet command version 0 is deprecated. Please update cursorless-talon.", + ); + return arg.spokenFormInfo; + } -interface CheatsheetSection { - name: string; - id: string; - items: { - id: string; - type: string; - variations: Variation[]; - }[]; -} + if (version === 1) { + try { + const { spokenForms } = await talonSpokenForms.getSpokenForms(); + return getCheatsheetInfo(spokenForms); + } catch (error) { + void showWarning( + ide.messages, + "cheatsheetSpokenFormsFallback", + `Unable to load custom spoken forms: ${getErrorMessage(error)}. Using default spoken forms.`, + ); + return getDefaultCheatsheetInfo(); + } + } -interface CheatsheetInfo { - sections: CheatsheetSection[]; + throw new Error(`Unsupported cheatsheet command version: ${version}`); } diff --git a/packages/lib-node-common/src/FileSystemTalonSpokenForms.ts b/packages/lib-node-common/src/FileSystemTalonSpokenForms.ts index 471374d40a..f3ebce6fc8 100644 --- a/packages/lib-node-common/src/FileSystemTalonSpokenForms.ts +++ b/packages/lib-node-common/src/FileSystemTalonSpokenForms.ts @@ -4,18 +4,13 @@ import type { Disposable, FileSystem, Listener, - SpokenFormEntry, TalonSpokenForms, + TalonSpokenFormsPayload, } from "@cursorless/lib-common"; import { NeedsInitialTalonUpdateError, Notifier } from "@cursorless/lib-common"; import { isEnoentError } from "./isError"; -interface TalonSpokenFormsPayload { - version: number; - spokenForms: SpokenFormEntry[]; -} - -const LATEST_SPOKEN_FORMS_JSON_VERSION = 0; +const LATEST_SPOKEN_FORMS_JSON_VERSION = 1; export class FileSystemTalonSpokenForms implements TalonSpokenForms { private disposable: Disposable; @@ -37,7 +32,7 @@ export class FileSystemTalonSpokenForms implements TalonSpokenForms { return this.notifier.registerListener(listener); } - async getSpokenFormEntries(): Promise { + async getSpokenForms(): Promise { let payload: TalonSpokenFormsPayload; try { payload = JSON.parse( @@ -53,14 +48,13 @@ export class FileSystemTalonSpokenForms implements TalonSpokenForms { throw error; } - if (payload.version !== LATEST_SPOKEN_FORMS_JSON_VERSION) { - // In the future, we'll need to handle migrations. Not sure exactly how yet. + if (payload.version > LATEST_SPOKEN_FORMS_JSON_VERSION) { throw new Error( - `Invalid spoken forms version. Expected ${LATEST_SPOKEN_FORMS_JSON_VERSION} but got ${payload.version}`, + `Unsupported spoken forms version ${payload.version}. Supported versions: 0-${LATEST_SPOKEN_FORMS_JSON_VERSION}`, ); } - return payload.spokenForms; + return payload; } dispose() { diff --git a/packages/lib-tutorial/src/setupStep.ts b/packages/lib-tutorial/src/setupStep.ts index 0379d84607..61c4d8b319 100644 --- a/packages/lib-tutorial/src/setupStep.ts +++ b/packages/lib-tutorial/src/setupStep.ts @@ -41,7 +41,7 @@ export async function setupStep( state: TutorialState, currentTutorial: TutorialContent | undefined, ): Promise { - if (state.type !== "doingTutorial") { + if (state.type !== "doingTutorial" || state.hasErrors) { return { editor: undefined, highlightRanges: [] }; } diff --git a/packages/test-vscode-e2e/src/suite/scopeProvider/runCustomSpokenFormScopeInfoTest.ts b/packages/test-vscode-e2e/src/suite/scopeProvider/runCustomSpokenFormScopeInfoTest.ts index 2d2eeb4f51..623eb4959e 100644 --- a/packages/test-vscode-e2e/src/suite/scopeProvider/runCustomSpokenFormScopeInfoTest.ts +++ b/packages/test-vscode-e2e/src/suite/scopeProvider/runCustomSpokenFormScopeInfoTest.ts @@ -120,7 +120,7 @@ const squareMissing: ScopeTypeInfo = { scopeType: { type: "surroundingPair", delimiter: "squareBrackets" }, spokenForm: { isPrivate: false, - reason: `paired delimiter with id squareBrackets; please update talon to the latest version (see ${DOCS_URL}/user/updating)`, + reason: `paired delimiter with id squareBrackets; please update cursorless-talon to the latest version (see ${DOCS_URL}/user/updating)`, requiresTalonUpdate: true, type: "error", }, @@ -184,7 +184,7 @@ const statementMissing: ScopeTypeInfo = { scopeType: { type: "statement" }, spokenForm: { isPrivate: false, - reason: `simple scope type type with id statement; please update talon to the latest version (see ${DOCS_URL}/user/updating)`, + reason: `simple scope type type with id statement; please update cursorless-talon to the latest version (see ${DOCS_URL}/user/updating)`, requiresTalonUpdate: true, type: "error", }, diff --git a/packages/tool-meta-updater/src/metaUpdater.ts b/packages/tool-meta-updater/src/metaUpdater.ts index d781ab02d9..e6e7eb535c 100644 --- a/packages/tool-meta-updater/src/metaUpdater.ts +++ b/packages/tool-meta-updater/src/metaUpdater.ts @@ -138,5 +138,5 @@ export const updater = async (workspaceDir: string) => { }; function isPrivate(entry: object): boolean { - return "private" in entry && entry.private === true; + return "visibility" in entry && entry.visibility === "private"; } diff --git a/packages/tool-meta-updater/src/scopeFixtureGroups.ts b/packages/tool-meta-updater/src/scopeFixtureGroups.ts index cf8bde4419..402e7f069c 100644 --- a/packages/tool-meta-updater/src/scopeFixtureGroups.ts +++ b/packages/tool-meta-updater/src/scopeFixtureGroups.ts @@ -185,7 +185,8 @@ function groupFixtures(fixtures: FixtureMetadata[]): ScopeFixtureGroup[] { name: prettifyScopeType( getFacetInfo(fixture.languageId, fixture.facet).scopeType, ), - private: "private" in reference && reference.private === true, + private: + "visibility" in reference && reference.visibility === "private", scopeTypeType: fixture.scopeTypeType, }; scopeMap.set(fixture.scopeTypeType, scope); diff --git a/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts b/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts index 4242c09385..a672046ef2 100644 --- a/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts +++ b/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts @@ -23,12 +23,12 @@ export function updatePairedDelimitersMd( const rows = Object.values(entries) .filter( (entry): entry is PairedDelimiterReference & { index: number } => - !entry.private && entry.index != null, + entry.visibility !== "private" && entry.index != null, ) .toSorted((a, b) => a.index - b.index) .map((entry) => { const [before, after] = entry.delimiters ?? [null, null]; - const spokenForm = `${code(`"${entry.defaultSpokenForm}"`)}${entry.disabledByDefault ? ` (${DISABLED_BY_DEFAULT})` : ""}`; + const spokenForm = `${code(`"${entry.defaultSpokenForm}"`)}${entry.visibility === "disabledByDefault" ? ` (${DISABLED_BY_DEFAULT})` : ""}`; return [ spokenForm, diff --git a/packages/tool-meta-updater/src/updateReferenceMdx.ts b/packages/tool-meta-updater/src/updateReferenceMdx.ts index 8c061538f7..74aeeba485 100644 --- a/packages/tool-meta-updater/src/updateReferenceMdx.ts +++ b/packages/tool-meta-updater/src/updateReferenceMdx.ts @@ -49,7 +49,7 @@ export function updateReferenceMdx( expected.push(entry.description, ""); } - expected.push(`Cursorless ID: ${code(id)}`, ""); + expected.push(`Cursorless ID: ${code(entry.csv_id ?? id)}`, ""); const spokenFormLines: string[] = []; @@ -63,7 +63,7 @@ export function updateReferenceMdx( ); } - if (entry.disabledByDefault) { + if (entry.visibility === "disabledByDefault") { spokenFormLines.push(DISABLED_BY_DEFAULT); } diff --git a/packages/tool-meta-updater/src/updateReferenceReadmeMd.ts b/packages/tool-meta-updater/src/updateReferenceReadmeMd.ts index 2062977c15..81b71846e0 100644 --- a/packages/tool-meta-updater/src/updateReferenceReadmeMd.ts +++ b/packages/tool-meta-updater/src/updateReferenceReadmeMd.ts @@ -31,9 +31,10 @@ export function updateReferenceReadmeMd( for (const [rawId, entry] of groupEntries) { const id = cleanId(rawId); - const disabledByDefault = entry.disabledByDefault - ? ` (${DISABLED_BY_DEFAULT})` - : ""; + const disabledByDefault = + entry.visibility === "disabledByDefault" + ? ` (${DISABLED_BY_DEFAULT})` + : ""; for (const syntax of entry.syntaxes) { const pattern = injectSpokenForm( syntax.pattern, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aceb98e4cc..b6b852a513 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,9 +310,12 @@ importers: specifier: ^1.11.6 version: 1.11.6(react@19.2.8) devDependencies: - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 + '@jest/globals': + specifier: ^30.4.1 + version: 30.4.1 + '@types/node': + specifier: ^24.13.3 + version: 24.13.3 jest: specifier: ^30.4.2 version: 30.4.2(@types/node@24.13.3)(ts-node@10.9.2(@swc/core@1.16.1)(@types/node@24.13.3)(typescript@6.0.3)) @@ -341,9 +344,9 @@ importers: specifier: ^10.29.8 version: 10.29.8 devDependencies: - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 + '@jest/globals': + specifier: ^30.4.1 + version: 30.4.1 '@types/node': specifier: ^24.13.3 version: 24.13.3 @@ -4925,9 +4928,6 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@30.0.0': - resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} - '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -16955,11 +16955,6 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@30.0.0': - dependencies: - expect: 30.4.1 - pretty-format: 30.4.1 - '@types/js-yaml@4.0.9': {} '@types/jsdom@21.1.7': diff --git a/resources/fixtures/recorded/everyRange/clearEveryFunkNameSkipPastBlueDrum.yml b/resources/fixtures/recorded/everyRange/clearEveryFunkNameSkipPastBlueDrum.yml index ed39103d74..5fbfb492c3 100644 --- a/resources/fixtures/recorded/everyRange/clearEveryFunkNameSkipPastBlueDrum.yml +++ b/resources/fixtures/recorded/everyRange/clearEveryFunkNameSkipPastBlueDrum.yml @@ -17,7 +17,9 @@ command: excludeAnchor: true excludeActive: false usePrePhraseSnapshot: true -spokenFormError: Range exclude anchor +spokenFormError: >- + connective with id rangeExcludingStart; please see + https://www.cursorless.org/docs/user/customization for more information initialState: documentContents: | def aaa(): diff --git a/resources/fixtures/recorded/everyRange/clearEveryTokenBatSkipPastEach.yml b/resources/fixtures/recorded/everyRange/clearEveryTokenBatSkipPastEach.yml index 4a4eb2c5ee..1a62f7044e 100644 --- a/resources/fixtures/recorded/everyRange/clearEveryTokenBatSkipPastEach.yml +++ b/resources/fixtures/recorded/everyRange/clearEveryTokenBatSkipPastEach.yml @@ -18,7 +18,9 @@ command: excludeAnchor: true excludeActive: false usePrePhraseSnapshot: true -spokenFormError: Range exclude anchor +spokenFormError: >- + connective with id rangeExcludingStart; please see + https://www.cursorless.org/docs/user/customization for more information initialState: documentContents: | aaa bbb ccc ddd eee fff diff --git a/resources/fixtures/recorded/everyRange/postEveryTokenFunkNameSkipPastToken.yml b/resources/fixtures/recorded/everyRange/postEveryTokenFunkNameSkipPastToken.yml index a8dc2dca8f..5e3d3b0c72 100644 --- a/resources/fixtures/recorded/everyRange/postEveryTokenFunkNameSkipPastToken.yml +++ b/resources/fixtures/recorded/everyRange/postEveryTokenFunkNameSkipPastToken.yml @@ -21,7 +21,9 @@ command: excludeAnchor: true excludeActive: false usePrePhraseSnapshot: true -spokenFormError: Range exclude anchor +spokenFormError: >- + connective with id rangeExcludingStart; please see + https://www.cursorless.org/docs/user/customization for more information initialState: documentContents: |- def aaa():