diff --git a/cursorless-talon-dev/src/cursorless_test.talon b/cursorless-talon-dev/src/cursorless_test.talon index 561d620ee0..339c642110 100644 --- a/cursorless-talon-dev/src/cursorless_test.talon +++ b/cursorless-talon-dev/src/cursorless_test.talon @@ -25,12 +25,8 @@ test api insert snippet: user.cursorless_insert_snippet("Hello, $foo! My name is $bar!") test api insert snippet : user.cursorless_insert_snippet("Hello, $foo! My name is $bar!", cursorless_destination, "statement") -test api insert snippet by name: - user.cursorless_insert_snippet_by_name("functionDeclaration") test api wrap with snippet : user.cursorless_wrap_with_snippet("Hello, $foo! My name is $bar!", cursorless_target, "foo", "statement") -test api wrap with snippet by name : - user.cursorless_wrap_with_snippet_by_name("functionDeclaration", "body", cursorless_target) test api extract decorated marks : user.private_cursorless_test_extract_decorated_marks(cursorless_target) test api alternate highlight nothing: diff --git a/cursorless-talon-dev/src/spoken_form_test.py b/cursorless-talon-dev/src/spoken_form_test.py index 275c8c3a7e..6c531ef2c2 100644 --- a/cursorless-talon-dev/src/spoken_form_test.py +++ b/cursorless-talon-dev/src/spoken_form_test.py @@ -39,8 +39,6 @@ mockedGetValue = "" -community_snippets_tag_name = "user.cursorless_use_community_snippets" - @ctx.action_class("user") class UserActions: @@ -99,20 +97,6 @@ def private_cursorless_spoken_form_test_mode(enable: bool): # pyright: ignore [ "Cursorless spoken form tests are done. Talon microphone is re-enabled." ) - def private_cursorless_use_community_snippets(enable: bool): # pyright: ignore [reportGeneralTypeIssues] - """Enable/disable cursorless community snippets in test mode""" - if enable: - tags = set(ctx.tags) - tags.add(community_snippets_tag_name) - ctx.tags = list(tags) - else: - tags = set(ctx.tags) - tags.remove(community_snippets_tag_name) - ctx.tags = list(tags) - # Note: Test harness hangs if we don't print anything because it's - # waiting for stdout - print(f"Set community snippet enablement to {enable}") - def private_cursorless_spoken_form_test( phrase: str, # pyright: ignore [reportGeneralTypeIssues] mockedGetValue_: Optional[str], diff --git a/cursorless-talon/src/actions/generate_snippet.py b/cursorless-talon/src/actions/generate_snippet.py index 54835bd66f..7d9b66a4f3 100644 --- a/cursorless-talon/src/actions/generate_snippet.py +++ b/cursorless-talon/src/actions/generate_snippet.py @@ -1,17 +1,12 @@ import glob from pathlib import Path -from talon import Context, Module, actions, registry, settings +from talon import Module, actions, registry, settings from ..targets.target_types import CursorlessExplicitTarget mod = Module() -ctx = Context() -ctx.matches = r""" -tag: user.cursorless_use_community_snippets -""" - @mod.action_class class Actions: @@ -33,17 +28,6 @@ def private_cursorless_migrate_snippets(): def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget): # pyright: ignore [reportGeneralTypeIssues] """Generate a snippet from the given target""" - actions.user.private_cursorless_command_no_wait( - { - "name": "generateSnippet", - "target": target, - } - ) - - -@ctx.action_class("user") -class UserActions: - def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget): # pyright: ignore [reportGeneralTypeIssues] actions.user.private_cursorless_command_no_wait( { "name": "generateSnippet", diff --git a/cursorless-talon/src/check_community_repo.py b/cursorless-talon/src/check_community_repo.py index ef776de3ae..79d0a6a677 100644 --- a/cursorless-talon/src/check_community_repo.py +++ b/cursorless-talon/src/check_community_repo.py @@ -9,6 +9,7 @@ required_actions = [ "user.homophones_get", + "user.insert_snippet_by_name", "user.reformat_text", ] diff --git a/cursorless-talon/src/snippet_cursorless.talon b/cursorless-talon/src/snippet_cursorless.talon deleted file mode 100644 index ffc28e20b4..0000000000 --- a/cursorless-talon/src/snippet_cursorless.talon +++ /dev/null @@ -1,14 +0,0 @@ -mode: command -mode: user.cursorless_spoken_form_test -tag: user.cursorless -and not tag: user.cursorless_use_community_snippets -- - -{user.cursorless_insert_snippet_action} : - user.private_cursorless_insert_snippet(cursorless_insertion_snippet) - -{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} [{user.cursorless_phrase_terminator}]: - user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text) - -{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} : - user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet) diff --git a/cursorless-talon/src/snippet_types.py b/cursorless-talon/src/snippet_types.py new file mode 100644 index 0000000000..3107bf040d --- /dev/null +++ b/cursorless-talon/src/snippet_types.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass + +from .targets.target_types import CursorlessDestination, CursorlessTarget + + +@dataclass +class ScopeType: + type: str + + +# Insertion snippets + + +@dataclass +class CustomInsertionSnippet: + type = "custom" + body: str + scopeTypes: list[ScopeType] | None + languages: list[str] | None + substitutions: dict[str, str] | None + + +@dataclass +class ListInsertionSnippet: + type = "list" + substitutions: dict[str, str] | None + snippets: list[CustomInsertionSnippet] + + +@dataclass +class InsertSnippetAction: + name = "insertSnippet" + snippetDescription: CustomInsertionSnippet | ListInsertionSnippet + destination: CursorlessDestination + + +# Wrapper snippets + + +@dataclass +class CustomWrapperSnippet: + type = "custom" + body: str + variableName: str | None + scopeType: ScopeType | None + languages: list[str] | None + + +@dataclass +class ListWrapperSnippet: + type = "list" + snippets: list[CustomWrapperSnippet] + + +@dataclass +class WrapperSnippetAction: + name = "wrapWithSnippet" + snippetDescription: CustomWrapperSnippet | ListWrapperSnippet + target: CursorlessTarget + + +# Community types + + +@dataclass +class CommunityInsertionSnippet: + body: str + languages: list[str] | None = None + scopes: list[str] | None = None + + +@dataclass +class CommunityWrapperSnippet: + body: str + variable_name: str + languages: list[str] | None + scope: str | None diff --git a/cursorless-talon/src/snippets.py b/cursorless-talon/src/snippets.py index 1a28fb1df8..9ef9211f5c 100644 --- a/cursorless-talon/src/snippets.py +++ b/cursorless-talon/src/snippets.py @@ -1,187 +1,114 @@ -from dataclasses import dataclass -from typing import Any, Optional, Union - -from talon import Module, actions - +from typing import Optional, Union + +from talon import Context, Module, actions + +from .snippet_types import ( + CommunityInsertionSnippet, + CommunityWrapperSnippet, + CustomInsertionSnippet, + CustomWrapperSnippet, + InsertSnippetAction, + ListInsertionSnippet, + ListWrapperSnippet, + ScopeType, + WrapperSnippetAction, +) from .targets.target_types import ( CursorlessDestination, CursorlessTarget, ImplicitDestination, ) - -@dataclass -class InsertionSnippet: - name: str - destination: CursorlessDestination - - -@dataclass -class CommunityInsertionSnippet: - body: str - scopes: list[str] | None = None - - -@dataclass -class CommunityWrapperSnippet: - body: str - variable_name: str - scope: str | None = None - - mod = Module() +ctx = Context() mod.list("cursorless_insert_snippet_action", desc="Cursorless insert snippet action") -# Deprecated tag; we should probably remove this and notify users that they -# should get rid of it, but I don't think it's worth the effort right now -mod.tag( - "cursorless_experimental_snippets", - desc="tag for enabling experimental snippet support", -) - -mod.tag( - "cursorless_use_community_snippets", - "If active use community snippets instead of Cursorless snippets", -) - -mod.list("cursorless_wrapper_snippet", desc="Cursorless wrapper snippet") -mod.list( - "cursorless_insertion_snippet_no_phrase", - desc="Cursorless insertion snippets that don't accept a phrase", -) -mod.list( - "cursorless_insertion_snippet_single_phrase", - desc="Cursorless insertion snippet that can accept a single phrase", -) -mod.list("cursorless_phrase_terminator", "Contains term used to terminate a phrase") - -@mod.capture( - rule="({user.cursorless_insertion_snippet_no_phrase} | {user.cursorless_insertion_snippet_single_phrase}) []" -) -def cursorless_insertion_snippet(m) -> InsertionSnippet: - try: - name = m.cursorless_insertion_snippet_no_phrase - except AttributeError: - name = m.cursorless_insertion_snippet_single_phrase.split(".")[0] - - try: - destination = m.cursorless_destination - except AttributeError: - destination = ImplicitDestination() - - return InsertionSnippet(name, destination) - - -def wrap_with_snippet(snippet_description: dict, target: CursorlessTarget): +def insert_snippet( + snippet: CustomInsertionSnippet | ListInsertionSnippet, + destination: CursorlessDestination, +): actions.user.private_cursorless_command_and_wait( - { - "name": "wrapWithSnippet", - "snippetDescription": snippet_description, - "target": target, - }, + InsertSnippetAction(snippet, destination), ) -def insert_snippet(snippet_description: dict, destination: CursorlessDestination): +def wrap_with_snippet( + snippet: CustomWrapperSnippet | ListWrapperSnippet, + target: CursorlessTarget, +): actions.user.private_cursorless_command_and_wait( - { - "name": "insertSnippet", - "snippetDescription": snippet_description, - "destination": destination, - }, + WrapperSnippetAction(snippet, target), ) -def insert_named_snippet( +def insert_community_snippet( name: str, + substitutions: dict[str, str] | None, destination: CursorlessDestination, - substitutions: Optional[dict] = None, ): - snippet: dict = { - "type": "named", - "name": name, - } - if substitutions is not None: - snippet["substitutions"] = substitutions + snippets: list[CommunityInsertionSnippet] = get_insertion_snippets(name) + snippet = ListInsertionSnippet( + substitutions, + [ + CustomInsertionSnippet( + s.body, + to_scope_types(s.scopes), + # languages will be missing if the user has an older version of community + s.languages if hasattr(s, "languages") else None, + substitutions=None, + ) + for s in snippets + ], + ) insert_snippet(snippet, destination) -def insert_custom_snippet( - body: str, - destination: CursorlessDestination, - scope_types: Optional[list[dict]] = None, -): - snippet: dict = { - "type": "custom", - "body": body, - } - - if scope_types: - snippet["scopeTypes"] = scope_types - - insert_snippet(snippet, destination) - +def insert_community_wrapper_snippet(name: str, target: CursorlessTarget): + snippets: list[CommunityWrapperSnippet] = get_wrapper_snippets(name) + snippet = ListWrapperSnippet( + [ + CustomWrapperSnippet( + s.body, + s.variable_name, + ScopeType(s.scope) if s.scope else None, + s.languages if hasattr(s, "languages") else None, + ) + for s in snippets + ], + ) + wrap_with_snippet(snippet, target) -@mod.action_class -class Actions: - def private_cursorless_insert_snippet(insertion_snippet: InsertionSnippet): # pyright: ignore [reportGeneralTypeIssues] - """Execute Cursorless insert snippet action""" - insert_named_snippet( - insertion_snippet.name, - insertion_snippet.destination, - ) - def private_cursorless_insert_snippet_with_phrase( - snippet_description: str, # pyright: ignore [reportGeneralTypeIssues] - text: str, +@ctx.action_class("user") +class UserActions: + def insert_snippet_by_name( + name: str, # pyright: ignore [reportGeneralTypeIssues] + substitutions: dict[str, str] = None, ): - """Cursorless: Insert snippet with phrase """ - snippet_name, snippet_variable = snippet_description.split(".") - insert_named_snippet( - snippet_name, - ImplicitDestination(), - {snippet_variable: text}, - ) - - def cursorless_insert_snippet_by_name(name: str): # pyright: ignore [reportGeneralTypeIssues] - """Cursorless: Insert named snippet """ - insert_named_snippet( + insert_community_snippet( name, + substitutions, ImplicitDestination(), ) + +@mod.action_class +class Actions: def cursorless_insert_snippet( body: str, # pyright: ignore [reportGeneralTypeIssues] destination: CursorlessDestination = ImplicitDestination(), scope_type: Optional[Union[str, list[str]]] = None, ): """Cursorless: Insert custom snippet """ - if isinstance(scope_type, str): - scope_type = [scope_type] - - if scope_type is not None: - scope_types = [{"type": st} for st in scope_type] - else: - scope_types = None - - insert_custom_snippet(body, destination, scope_types) - - def cursorless_wrap_with_snippet_by_name( - name: str, # pyright: ignore [reportGeneralTypeIssues] - variable_name: str, - target: CursorlessTarget, - ): - """Cursorless: Wrap target with a named snippet """ - wrap_with_snippet( - { - "type": "named", - "name": name, - "variableName": variable_name, - }, - target, + snippet = CustomInsertionSnippet( + body, + to_scope_types(scope_type), + languages=None, + substitutions=None, ) + insert_snippet(snippet, destination) def cursorless_wrap_with_snippet( body: str, # pyright: ignore [reportGeneralTypeIssues] @@ -190,27 +117,23 @@ def cursorless_wrap_with_snippet( scope: Optional[str] = None, ): """Cursorless: Wrap target with custom snippet """ - snippet_arg: dict[str, Any] = { - "type": "custom", - "body": body, - } - if scope is not None: - snippet_arg["scopeType"] = {"type": scope} - if variable_name is not None: - snippet_arg["variableName"] = variable_name - wrap_with_snippet( - snippet_arg, - target, + snippet = CustomWrapperSnippet( + body, + variable_name, + ScopeType(scope) if scope else None, + languages=None, ) + wrap_with_snippet(snippet, target) def private_cursorless_insert_community_snippet( name: str, # pyright: ignore [reportGeneralTypeIssues] destination: CursorlessDestination, ): """Cursorless: Insert community snippet """ - snippet: CommunityInsertionSnippet = actions.user.get_insertion_snippet(name) - actions.user.cursorless_insert_snippet( - snippet.body, destination, snippet.scopes + insert_community_snippet( + name, + substitutions=None, + destination=destination, ) def private_cursorless_wrap_with_community_snippet( @@ -218,7 +141,32 @@ def private_cursorless_wrap_with_community_snippet( target: CursorlessTarget, ): """Cursorless: Wrap target with community snippet """ - snippet: CommunityWrapperSnippet = actions.user.get_wrapper_snippet(name) - actions.user.cursorless_wrap_with_snippet( - snippet.body, target, snippet.variable_name, snippet.scope - ) + insert_community_wrapper_snippet(name, target) + + +def to_scope_types(scope_types: str | list[str] | None) -> list[ScopeType] | None: + if isinstance(scope_types, str): + return [ScopeType(scope_types)] + elif scope_types is not None: + return [ScopeType(st) for st in scope_types] + return None + + +def get_insertion_snippets(name: str) -> list[CommunityInsertionSnippet]: + try: + return actions.user.get_insertion_snippets(name) + except Exception as ex: + if isinstance(ex, KeyError): + snippet = actions.user.get_insertion_snippet(name) + return [snippet] + raise + + +def get_wrapper_snippets(name: str) -> list[CommunityWrapperSnippet]: + try: + return actions.user.get_wrapper_snippets(name) + except Exception as ex: + if isinstance(ex, KeyError): + snippet = actions.user.get_wrapper_snippet(name) + return [snippet] + raise diff --git a/cursorless-talon/src/snippets_community.talon b/cursorless-talon/src/snippets.talon similarity index 90% rename from cursorless-talon/src/snippets_community.talon rename to cursorless-talon/src/snippets.talon index f94e491f83..f5f1d98ac0 100644 --- a/cursorless-talon/src/snippets_community.talon +++ b/cursorless-talon/src/snippets.talon @@ -1,7 +1,6 @@ mode: command mode: user.cursorless_spoken_form_test tag: user.cursorless -and tag: user.cursorless_use_community_snippets - # These snippets are defined in community diff --git a/cursorless-talon/src/snippets_deprecated.py b/cursorless-talon/src/snippets_deprecated.py new file mode 100644 index 0000000000..2f32fbc48f --- /dev/null +++ b/cursorless-talon/src/snippets_deprecated.py @@ -0,0 +1,53 @@ +from typing import Any + +from talon import Module, app, registry + +mod = Module() + +# DEPRECATED @ 2025-02-01 + +tags = [ + "cursorless_experimental_snippets", + "cursorless_use_community_snippets", +] + +lists = [ + "cursorless_insertion_snippet_no_phrase", + "cursorless_insertion_snippet_single_phrase", + "cursorless_phrase_terminator", +] + +for tag in tags: + mod.tag(tag, desc="DEPRECATED") + +for list in lists: + mod.list(list, desc="DEPRECATED") + + +@mod.action_class +class Actions: + def cursorless_insert_snippet_by_name(name: str): # pyright: ignore [reportGeneralTypeIssues] + """[DEPRECATED] Cursorless: Insert named snippet """ + raise NotImplementedError( + "Cursorless snippets are deprecated. Please use community snippets." + ) + + def cursorless_wrap_with_snippet_by_name( + name: str, # pyright: ignore [reportGeneralTypeIssues] + variable_name: str, + target: Any, + ): + """[DEPRECATED] Cursorless: Wrap target with a named snippet """ + raise NotImplementedError( + "Cursorless snippets are deprecated. Please use community snippets." + ) + + +def on_ready(): + for tag in tags: + name = f"user.{tag}" + if name in registry.tags: + print(f"WARNING tag: '{name}' is deprecated and should not be used anymore") + + +app.register("ready", on_ready) diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py index 044978b95e..92896f3719 100644 --- a/cursorless-talon/src/spoken_forms.py +++ b/cursorless-talon/src/spoken_forms.py @@ -147,7 +147,6 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): handle_csv("special_marks.csv"), handle_csv("scope_visualizer.csv"), handle_csv("experimental/experimental_actions.csv"), - handle_csv("experimental/miscellaneous.csv"), handle_csv( "modifier_scope_types.csv", pluralize_lists=[ @@ -165,6 +164,7 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): ], default_list_name="scope_type", ), + # DEPRECATED @ 2025-02-01 handle_csv( "experimental/wrapper_snippets.csv", allow_unknown_values=True, @@ -180,6 +180,8 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): allow_unknown_values=True, default_list_name="insertion_snippet_single_phrase", ), + handle_csv("experimental/miscellaneous.csv"), + # --- handle_csv( "experimental/actions_custom.csv", headers=[SPOKEN_FORM_HEADER, "VSCode command"], diff --git a/data/fixtures/recorded/actions/snippets/customInsert.yml b/data/fixtures/recorded/actions/snippets/customInsert.yml index 50b82a4dd2..4648ba14cd 100644 --- a/data/fixtures/recorded/actions/snippets/customInsert.yml +++ b/data/fixtures/recorded/actions/snippets/customInsert.yml @@ -7,7 +7,7 @@ command: snippetDescription: {type: custom, body: 'dummy snippet hole1: ($hole1), hole2: ($hole2)'} destination: {type: implicit} usePrePhraseSnapshot: true -spokenFormError: Custom insertion snippet +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/customInsertAfterWhale.yml b/data/fixtures/recorded/actions/snippets/customInsertAfterWhale.yml index aecd21b1db..0895eb3b23 100644 --- a/data/fixtures/recorded/actions/snippets/customInsertAfterWhale.yml +++ b/data/fixtures/recorded/actions/snippets/customInsertAfterWhale.yml @@ -12,7 +12,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: w} usePrePhraseSnapshot: true -spokenFormError: Custom insertion snippet +spokenFormError: custom insertion snippet initialState: documentContents: hello world selections: diff --git a/data/fixtures/recorded/actions/snippets/customInsertAfterWhale2.yml b/data/fixtures/recorded/actions/snippets/customInsertAfterWhale2.yml index fc02893614..aa352d2e02 100644 --- a/data/fixtures/recorded/actions/snippets/customInsertAfterWhale2.yml +++ b/data/fixtures/recorded/actions/snippets/customInsertAfterWhale2.yml @@ -16,7 +16,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: w} usePrePhraseSnapshot: true -spokenFormError: Custom insertion snippet +spokenFormError: custom insertion snippet initialState: documentContents: hello world selections: diff --git a/data/fixtures/recorded/actions/snippets/customInsertHelloWorld.yml b/data/fixtures/recorded/actions/snippets/customInsertHelloWorld.yml index 7d85bdc525..25015f5205 100644 --- a/data/fixtures/recorded/actions/snippets/customInsertHelloWorld.yml +++ b/data/fixtures/recorded/actions/snippets/customInsertHelloWorld.yml @@ -10,7 +10,7 @@ command: substitutions: {hole2: hello world} destination: {type: implicit} usePrePhraseSnapshot: true -spokenFormError: Custom insertion snippet +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/customWrapHarp.yml b/data/fixtures/recorded/actions/snippets/customWrapHarp.yml index 02c26c8a23..ea856689bd 100644 --- a/data/fixtures/recorded/actions/snippets/customWrapHarp.yml +++ b/data/fixtures/recorded/actions/snippets/customWrapHarp.yml @@ -13,7 +13,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: h} usePrePhraseSnapshot: true -spokenFormError: Custom wrap with snippet +spokenFormError: custom wrap with snippet initialState: documentContents: | hello world diff --git a/data/fixtures/recorded/actions/snippets/customWrapLine.yml b/data/fixtures/recorded/actions/snippets/customWrapLine.yml index 4e016823c1..e26290d238 100644 --- a/data/fixtures/recorded/actions/snippets/customWrapLine.yml +++ b/data/fixtures/recorded/actions/snippets/customWrapLine.yml @@ -11,7 +11,7 @@ command: - type: containingScope scopeType: {type: line} usePrePhraseSnapshot: true -spokenFormError: Custom wrap with snippet +spokenFormError: custom wrap with snippet initialState: documentContents: hello world selections: diff --git a/data/fixtures/recorded/actions/snippets/customWrapLine2.yml b/data/fixtures/recorded/actions/snippets/customWrapLine2.yml index 22a4900bca..f681a363ae 100644 --- a/data/fixtures/recorded/actions/snippets/customWrapLine2.yml +++ b/data/fixtures/recorded/actions/snippets/customWrapLine2.yml @@ -11,7 +11,7 @@ command: - type: containingScope scopeType: {type: line} usePrePhraseSnapshot: true -spokenFormError: Custom wrap with snippet +spokenFormError: custom wrap with snippet initialState: documentContents: hello world selections: diff --git a/data/fixtures/recorded/actions/snippets/duplicatedDuplicatedWrapThis.yml b/data/fixtures/recorded/actions/snippets/duplicatedDuplicatedWrapThis.yml index b635ff16cc..04425e9bea 100644 --- a/data/fixtures/recorded/actions/snippets/duplicatedDuplicatedWrapThis.yml +++ b/data/fixtures/recorded/actions/snippets/duplicatedDuplicatedWrapThis.yml @@ -4,12 +4,12 @@ command: spokenForm: duplicated duplicated wrap this action: name: wrapWithSnippet - snippetDescription: {type: named, name: duplicatedVariableTest, variableName: duplicated} + snippetDescription: {type: custom, variableName: duplicated, body: 'This variable: ''$duplicated'' is duplicated here: ''$duplicated'', but ''$unique'' is unique!'} target: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true -spokenFormError: Named wrap with snippet 'duplicatedVariableTest.duplicated' +spokenFormError: custom wrap with snippet initialState: documentContents: Hello world selections: diff --git a/data/fixtures/recorded/actions/snippets/duplicatedUniqueWrapThis.yml b/data/fixtures/recorded/actions/snippets/duplicatedUniqueWrapThis.yml index b58d8875dc..37cf7d3509 100644 --- a/data/fixtures/recorded/actions/snippets/duplicatedUniqueWrapThis.yml +++ b/data/fixtures/recorded/actions/snippets/duplicatedUniqueWrapThis.yml @@ -4,12 +4,12 @@ command: spokenForm: duplicated unique wrap this action: name: wrapWithSnippet - snippetDescription: {type: named, name: duplicatedVariableTest, variableName: unique} + snippetDescription: {type: custom, variableName: unique, body: 'This variable: ''$duplicated'' is duplicated here: ''$duplicated'', but ''$unique'' is unique!'} target: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true -spokenFormError: Named wrap with snippet 'duplicatedVariableTest.unique' +spokenFormError: custom wrap with snippet initialState: documentContents: Hello world selections: diff --git a/data/fixtures/recorded/actions/snippets/funkWrapClass.yml b/data/fixtures/recorded/actions/snippets/funkWrapClass.yml index 326f31f954..de61136506 100644 --- a/data/fixtures/recorded/actions/snippets/funkWrapClass.yml +++ b/data/fixtures/recorded/actions/snippets/funkWrapClass.yml @@ -4,13 +4,14 @@ command: spokenForm: funk wrap class action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: {type: custom, variableName: body, body: "def $name($parameterList):\n\t$body"} target: type: primitive modifiers: - type: containingScope scopeType: {type: class} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: |- class Aaa: diff --git a/data/fixtures/recorded/actions/snippets/ifWrapCap.yml b/data/fixtures/recorded/actions/snippets/ifWrapCap.yml index dcdbd5eaee..b747cb1221 100644 --- a/data/fixtures/recorded/actions/snippets/ifWrapCap.yml +++ b/data/fixtures/recorded/actions/snippets/ifWrapCap.yml @@ -13,7 +13,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: c} usePrePhraseSnapshot: true -spokenFormError: Custom wrap with snippet +spokenFormError: custom wrap with snippet initialState: documentContents: const value = 2; selections: diff --git a/data/fixtures/recorded/actions/snippets/snipSpaghetti.yml b/data/fixtures/recorded/actions/snippets/snipCode.yml similarity index 57% rename from data/fixtures/recorded/actions/snippets/snipSpaghetti.yml rename to data/fixtures/recorded/actions/snippets/snipCode.yml index c7398e762f..cc85d58c03 100644 --- a/data/fixtures/recorded/actions/snippets/snipSpaghetti.yml +++ b/data/fixtures/recorded/actions/snippets/snipCode.yml @@ -1,13 +1,16 @@ languageId: plaintext command: - version: 6 - spokenForm: snippet spaghetti + version: 7 + spokenForm: snip code action: name: insertSnippet - snippetDescription: {type: named, name: spaghetti} + snippetDescription: + type: list + snippets: + - {type: custom, body: "```\n$0\n```"} destination: {type: implicit} usePrePhraseSnapshot: false -spokenFormError: Named insertion snippet 'spaghetti' +spokenFormError: list insertion snippet initialState: documentContents: "" selections: @@ -15,14 +18,17 @@ initialState: active: {line: 0, character: 0} marks: {} finalState: - documentContents: My friend likes to eat spaghetti! + documentContents: |- + ``` + + ``` selections: - - anchor: {line: 0, character: 10} - active: {line: 0, character: 10} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} thatMark: - type: UntypedTarget contentRange: start: {line: 0, character: 0} - end: {line: 0, character: 34} + end: {line: 2, character: 3} isReversed: false hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicated.yml b/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicated.yml index 4f9c0c86fa..0e4ae2bcc2 100644 --- a/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicated.yml +++ b/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicated.yml @@ -4,10 +4,10 @@ command: spokenForm: snippet duplicated duplicated action: name: insertSnippet - snippetDescription: {type: named, name: duplicatedVariableTest} + snippetDescription: {type: custom, body: 'This variable: ''$duplicated'' is duplicated here: ''$duplicated'', but ''$unique'' is unique!'} destination: {type: implicit} usePrePhraseSnapshot: true -spokenFormError: Named insertion snippet 'duplicatedVariableTest' +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicatedHelloWorld.yml b/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicatedHelloWorld.yml index 5a36d7daf9..da2c15d73d 100644 --- a/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicatedHelloWorld.yml +++ b/data/fixtures/recorded/actions/snippets/snipDuplicatedDuplicatedHelloWorld.yml @@ -5,12 +5,14 @@ command: action: name: insertSnippet snippetDescription: - type: named - name: duplicatedVariableTest - substitutions: {duplicated: hello world} + type: custom + substitutions: {duplicated: hello_world} + body: >- + This variable: '$duplicated' is duplicated here: '$duplicated', but + '$unique' is unique! destination: {type: implicit} usePrePhraseSnapshot: true -spokenFormError: Named insertion snippet 'duplicatedVariableTest' +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipDuplicatedUniqueHelloWorld.yml b/data/fixtures/recorded/actions/snippets/snipDuplicatedUniqueHelloWorld.yml index dc7108f9de..3264a39935 100644 --- a/data/fixtures/recorded/actions/snippets/snipDuplicatedUniqueHelloWorld.yml +++ b/data/fixtures/recorded/actions/snippets/snipDuplicatedUniqueHelloWorld.yml @@ -5,12 +5,15 @@ command: action: name: insertSnippet snippetDescription: - type: named - name: duplicatedVariableTest - substitutions: {unique: hello world} + type: custom + substitutions: {unique: helloWorld} + Name: duplicated + body: >- + This variable: '$duplicated' is duplicated here: '$duplicated', but + '$unique' is unique! destination: {type: implicit} usePrePhraseSnapshot: true -spokenFormError: Named insertion snippet 'duplicatedVariableTest' +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunk.yml b/data/fixtures/recorded/actions/snippets/snipFunk.yml index f5fb9aebd5..272c890bb3 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunk.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunk.yml @@ -4,9 +4,10 @@ command: spokenForm: snippet funk action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "function $name($parameterList) {\n\t$body\n}"} destination: {type: implicit} usePrePhraseSnapshot: false +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunk2.yml b/data/fixtures/recorded/actions/snippets/snipFunk2.yml index 4d8ccb35c3..639d045f95 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunk2.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunk2.yml @@ -4,9 +4,10 @@ command: spokenForm: snippet funk action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "function $name($parameterList) {\n\t$body\n}"} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunk3.yml b/data/fixtures/recorded/actions/snippets/snipFunk3.yml index 4e2bf78d42..91e662020a 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunk3.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunk3.yml @@ -4,9 +4,10 @@ command: spokenForm: snippet funk action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "$name($parameterList) {\n\t$body\n}"} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunk4.yml b/data/fixtures/recorded/actions/snippets/snipFunk4.yml index db9ac84ca2..40b288fa3e 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunk4.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunk4.yml @@ -4,9 +4,10 @@ command: spokenForm: snippet funk action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "function $name($parameterList) {\n\t$body\n}"} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunk5.yml b/data/fixtures/recorded/actions/snippets/snipFunk5.yml index 5059994512..e8186e8124 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunk5.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunk5.yml @@ -4,9 +4,10 @@ command: spokenForm: snippet funk action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "$name($parameterList) {\n\t$body\n}"} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- function aaa() { diff --git a/data/fixtures/recorded/actions/snippets/snipFunk6.yml b/data/fixtures/recorded/actions/snippets/snipFunk6.yml index 12a7187419..7f4bebe005 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunk6.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunk6.yml @@ -4,9 +4,10 @@ command: spokenForm: snippet funk action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "$name($parameterList) {\n\t$body\n}"} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterClass.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterClass.yml index 7bc740df73..b89d65130d 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterClass.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterClass.yml @@ -4,7 +4,7 @@ command: spokenForm: snippet funk after class action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: {type: custom, body: "function $name($parameterList) {\n\t$body\n}"} destination: type: primitive insertionMode: after @@ -14,6 +14,7 @@ command: - type: containingScope scopeType: {type: class} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterFineAndZip.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterFineAndZip.yml index 7c5b951b54..a4b4ddc614 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterFineAndZip.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterFineAndZip.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk after fine and zip action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: after @@ -16,6 +20,7 @@ command: - type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: z} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- const foo = "bar"; diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterMadeAndBeforeFineAndZip.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterMadeAndBeforeFineAndZip.yml index cb83372b77..eda4cc3bfb 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterMadeAndBeforeFineAndZip.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterMadeAndBeforeFineAndZip.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk after made and before fine and zip action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: list destinations: @@ -23,6 +27,7 @@ command: - type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: z} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: | const foo = "bar"; diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis.yml index 80146831cc..fef8f9bd59 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +16,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: " const foo = \"bar\";" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis2.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis2.yml index 17338f69cc..d7af366172 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis2.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis2.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +16,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: const foo = "bar"; selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis3.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis3.yml index cac2137eeb..2b7129bda8 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis3.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis3.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- function helloWorld() { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis4.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis4.yml index 99baeccc41..754366fbf1 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis4.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis4.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +16,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: false +spokenFormError: custom insertion snippet initialState: documentContents: const foo = "bar"; selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis5.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis5.yml index aa8a47c57e..75d7eadce1 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterThis5.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterThis5.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "$name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkAfterZipAndBeforeFine.yml b/data/fixtures/recorded/actions/snippets/snipFunkAfterZipAndBeforeFine.yml index 268f9c53fb..604cb1b555 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkAfterZipAndBeforeFine.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkAfterZipAndBeforeFine.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk after zip and before fine action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: list destinations: @@ -19,6 +23,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: f} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- const foo = "bar"; diff --git a/data/fixtures/recorded/actions/snippets/snipFunkBeforeClass.yml b/data/fixtures/recorded/actions/snippets/snipFunkBeforeClass.yml index ee2e73118e..c7e451c64b 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkBeforeClass.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkBeforeClass.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk before class action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: before @@ -14,6 +18,7 @@ command: - type: containingScope scopeType: {type: class} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis.yml b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis.yml index 9adbfc936c..5a51ee1ec6 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk before this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: before @@ -12,6 +16,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: const foo = "bar"; selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis2.yml b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis2.yml index 63bbf52fcf..7731e04161 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis2.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis2.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk before this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: before @@ -12,6 +16,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: " const foo = \"bar\";" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis3.yml b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis3.yml index 028d9ae24c..37bc03c0b6 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis3.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis3.yml @@ -4,7 +4,11 @@ command: spokenForm: snippet funk before this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} destination: type: primitive insertionMode: before @@ -12,6 +16,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: const foo = "bar"; selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis4.yml b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis4.yml index fe6f711972..aa74af2eca 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis4.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkBeforeThis4.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk before this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "$name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: before @@ -12,6 +17,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkCelloWorld.yml b/data/fixtures/recorded/actions/snippets/snipFunkCelloWorld.yml index 179fbeaebd..46fc3370de 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkCelloWorld.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkCelloWorld.yml @@ -5,11 +5,14 @@ command: action: name: insertSnippet snippetDescription: - type: named - name: functionDeclaration - substitutions: {name: cello world} + type: custom + body: "$name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} + substitutions: {name: celloWorld} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- class Aaa { diff --git a/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld.yml b/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld.yml index 7c51d5e316..be0eb3bf47 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld.yml @@ -5,11 +5,14 @@ command: action: name: insertSnippet snippetDescription: - type: named - name: functionDeclaration - substitutions: {name: hello world} + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} + substitutions: {name: helloWorld} destination: {type: implicit} usePrePhraseSnapshot: false +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld2.yml b/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld2.yml index 5f8c395a6a..925e671338 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld2.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld2.yml @@ -5,11 +5,14 @@ command: action: name: insertSnippet snippetDescription: - type: named - name: functionDeclaration - substitutions: {name: hello world} + type: custom + body: "def $name($parameterList):\n\t$body" + scopeTypes: + - {type: statement} + substitutions: {name: hello_world} destination: {type: implicit} usePrePhraseSnapshot: false +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld3.yml b/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld3.yml index 1018dcff70..c325d30959 100644 --- a/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld3.yml +++ b/data/fixtures/recorded/actions/snippets/snipFunkHelloWorld3.yml @@ -5,11 +5,14 @@ command: action: name: insertSnippet snippetDescription: - type: named - name: functionDeclaration - substitutions: {name: hello world} + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: statement} + substitutions: {name: helloWorld} destination: {type: implicit} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: "" selections: diff --git a/data/fixtures/recorded/actions/snippets/snipFunkToClass.yml b/data/fixtures/recorded/actions/snippets/snipFunkToClass.yml deleted file mode 100644 index 67bf80cd09..0000000000 --- a/data/fixtures/recorded/actions/snippets/snipFunkToClass.yml +++ /dev/null @@ -1,40 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: snippet funk to class - action: - name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} - destination: - type: primitive - insertionMode: to - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: class} - usePrePhraseSnapshot: true -initialState: - documentContents: |- - class Aaa { - - } - selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} - marks: {} -finalState: - documentContents: |- - function () { - - } - selections: - - anchor: {line: 0, character: 9} - active: {line: 0, character: 9} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 0, character: 0} - end: {line: 2, character: 1} - isReversed: false - hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/snipFunkToThis.yml b/data/fixtures/recorded/actions/snippets/snipFunkToThis.yml deleted file mode 100644 index cd540d7878..0000000000 --- a/data/fixtures/recorded/actions/snippets/snipFunkToThis.yml +++ /dev/null @@ -1,42 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: snippet funk to this - action: - name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} - destination: - type: primitive - insertionMode: to - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: true -initialState: - documentContents: |- - class Aaa { - bbb() { - - } - } - selections: - - anchor: {line: 2, character: 8} - active: {line: 2, character: 8} - marks: {} -finalState: - documentContents: |- - class Aaa { - () { - - } - } - selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 1, character: 4} - end: {line: 3, character: 5} - isReversed: false - hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/snipIf.yml b/data/fixtures/recorded/actions/snippets/snipIf.yml index 37c6f597a7..dc78b01315 100644 --- a/data/fixtures/recorded/actions/snippets/snipIf.yml +++ b/data/fixtures/recorded/actions/snippets/snipIf.yml @@ -1,35 +1,48 @@ languageId: typescript command: - version: 6 - spokenForm: snippet if + version: 7 + spokenForm: snip if action: name: insertSnippet - snippetDescription: {type: named, name: ifStatement} + snippetDescription: + type: list + snippets: + - type: custom + body: "if ($1) {\n\t$0\n}" + scopeTypes: + - {type: statement} + languages: [c, cpp, csharp, java, javascript, typescript, javascriptreact, typescriptreact] + - type: custom + body: "if $1:\n\t$0" + scopeTypes: + - {type: statement} + languages: [python] + - type: custom + body: "if $1 then\n\t$0\nend" + scopeTypes: + - {type: statement} + languages: [lua] destination: {type: implicit} usePrePhraseSnapshot: false +spokenFormError: list insertion snippet initialState: - documentContents: |- - function whatever() { - - } + documentContents: "" selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} marks: {} finalState: documentContents: |- - function whatever() { - if () { - - } + if () { + } selections: - - anchor: {line: 1, character: 8} - active: {line: 1, character: 8} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} thatMark: - type: UntypedTarget contentRange: - start: {line: 1, character: 4} - end: {line: 3, character: 5} + start: {line: 0, character: 0} + end: {line: 2, character: 1} isReversed: false hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/snipIfAfterAirAndBat.yml b/data/fixtures/recorded/actions/snippets/snipIfAfterAirAndBat.yml new file mode 100644 index 0000000000..93398664d8 --- /dev/null +++ b/data/fixtures/recorded/actions/snippets/snipIfAfterAirAndBat.yml @@ -0,0 +1,83 @@ +languageId: typescript +command: + version: 7 + spokenForm: snip if after air and bat + action: + name: insertSnippet + snippetDescription: + type: list + snippets: + - type: custom + body: "if ($1) {\n\t$0\n}" + scopeTypes: + - {type: statement} + - {type: line} + languages: [c, cpp, csharp, java, javascript, typescript, javascriptreact, typescriptreact] + - type: custom + body: "if $1:\n\t$0" + scopeTypes: + - {type: statement} + - {type: line} + languages: [python] + - type: custom + body: "if $1 then\n\t$0\nend" + scopeTypes: + - {type: statement} + - {type: line} + languages: [lua] + destination: + type: primitive + insertionMode: after + target: + type: list + elements: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + usePrePhraseSnapshot: false +spokenFormError: list insertion snippet +initialState: + documentContents: | + const aaa = 2; + + // bbb + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + marks: + default.a: + start: {line: 0, character: 6} + end: {line: 0, character: 9} + default.b: + start: {line: 2, character: 3} + end: {line: 2, character: 6} +finalState: + documentContents: | + const aaa = 2; + if () { + + } + + // bbb + if () { + + } + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + - anchor: {line: 6, character: 4} + active: {line: 6, character: 4} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 1, character: 0} + end: {line: 3, character: 1} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 6, character: 0} + end: {line: 8, character: 1} + isReversed: false + hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/snipMakeFunk.yml b/data/fixtures/recorded/actions/snippets/snipMakeFunk.yml index d56f3b0fc4..36f94a2e94 100644 --- a/data/fixtures/recorded/actions/snippets/snipMakeFunk.yml +++ b/data/fixtures/recorded/actions/snippets/snipMakeFunk.yml @@ -4,6 +4,7 @@ command: spokenForm: snippet make funk action: name: generateSnippet + directory: "" snippetName: snippetTest1 target: type: primitive @@ -28,37 +29,23 @@ initialState: active: {line: 5, character: 9} marks: {} finalState: - documentContents: |- - { - "snippetTest1": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript" - ] - }, - "body": [ - "function $variable1() {", - "\tconst whatever = \"hello\";", - "", - "\t$variable2", - "}" - ] - } - ], - "description": "", - "variables": { - "variable1": {}, - "variable2": {} - } - } + documentContents: | + name: snippetTest1 + language: typescript + phrase: + + $1.wrapperPhrase: + $0.wrapperPhrase: + - + function $1() { + const whatever = "hello"; + + $0 } + --- selections: - - anchor: {line: 10, character: 21} - active: {line: 10, character: 30} - - anchor: {line: 20, character: 7} - active: {line: 20, character: 16} + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} thatMark: - type: UntypedTarget contentRange: diff --git a/data/fixtures/recorded/actions/snippets/snipMakeState.yml b/data/fixtures/recorded/actions/snippets/snipMakeState.yml index c0503658af..d12dfc5a5d 100644 --- a/data/fixtures/recorded/actions/snippets/snipMakeState.yml +++ b/data/fixtures/recorded/actions/snippets/snipMakeState.yml @@ -4,6 +4,7 @@ command: spokenForm: snippet make state action: name: generateSnippet + directory: "" snippetName: snippetTest1 target: type: primitive @@ -22,34 +23,20 @@ initialState: active: {line: 0, character: 4} marks: {} finalState: - documentContents: |- - { - "snippetTest1": { - "definitions": [ - { - "scope": { - "langIds": [ - "typescript" - ] - }, - "body": [ - "if ($variable1) {", - "\tconsole.log(\"hello\")", - "}" - ] - } - ], - "description": "", - "variables": { - "variable1": {} - } - } + documentContents: | + name: snippetTest1 + language: typescript + phrase: + + $0.wrapperPhrase: + - + if ($0) { + console.log("hello") } + --- selections: - - anchor: {line: 10, character: 16} - active: {line: 10, character: 25} - - anchor: {line: 18, character: 7} - active: {line: 18, character: 16} + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} thatMark: - type: UntypedTarget contentRange: diff --git a/data/fixtures/recorded/actions/snippets/snipPrintAfterPit.yml b/data/fixtures/recorded/actions/snippets/snipPrintAfterPit.yml index 19c2f19ca9..5524e40cbe 100644 --- a/data/fixtures/recorded/actions/snippets/snipPrintAfterPit.yml +++ b/data/fixtures/recorded/actions/snippets/snipPrintAfterPit.yml @@ -17,7 +17,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: p} usePrePhraseSnapshot: true -spokenFormError: Custom insertion snippet +spokenFormError: custom insertion snippet initialState: documentContents: | def my_funk(): diff --git a/data/fixtures/recorded/actions/snippets/snipSpaghettiGraceHopper.yml b/data/fixtures/recorded/actions/snippets/snipSpaghettiGraceHopper.yml deleted file mode 100644 index c2ec079a34..0000000000 --- a/data/fixtures/recorded/actions/snippets/snipSpaghettiGraceHopper.yml +++ /dev/null @@ -1,31 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: snippet spaghetti grace hopper - action: - name: insertSnippet - snippetDescription: - type: named - name: spaghetti - substitutions: {foo: grace hopper} - destination: {type: implicit} - usePrePhraseSnapshot: false -spokenFormError: Named insertion snippet 'spaghetti' -initialState: - documentContents: "" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: My friend grace_hopper likes to eat spaghetti! - selections: - - anchor: {line: 0, character: 46} - active: {line: 0, character: 46} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 0, character: 0} - end: {line: 0, character: 46} - isReversed: false - hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/spaghettiWrapPastGust.yml b/data/fixtures/recorded/actions/snippets/spaghettiWrapPastGust.yml deleted file mode 100644 index a21370da14..0000000000 --- a/data/fixtures/recorded/actions/snippets/spaghettiWrapPastGust.yml +++ /dev/null @@ -1,38 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: spaghetti wrap past gust - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: spaghetti, variableName: foo} - target: - type: range - anchor: {type: implicit} - active: - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: g} - excludeAnchor: false - excludeActive: false - usePrePhraseSnapshot: false -spokenFormError: Named wrap with snippet 'spaghetti.foo' -initialState: - documentContents: grace hopper - selections: - - anchor: {line: 0, character: 12} - active: {line: 0, character: 12} - marks: - default.g: - start: {line: 0, character: 0} - end: {line: 0, character: 5} -finalState: - documentContents: My friend grace hopper likes to eat spaghetti! - selections: - - anchor: {line: 0, character: 46} - active: {line: 0, character: 46} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 0, character: 0} - end: {line: 0, character: 46} - isReversed: true - hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine.yml b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine.yml index 828d06aa2b..88ba687d97 100644 --- a/data/fixtures/recorded/actions/snippets/testSnippetMakeLine.yml +++ b/data/fixtures/recorded/actions/snippets/testSnippetMakeLine.yml @@ -4,6 +4,7 @@ command: spokenForm: test snippet make line action: name: generateSnippet + directory: "" snippetName: testSnippet target: type: primitive @@ -19,32 +20,18 @@ initialState: active: {line: 0, character: 12} marks: {} finalState: - documentContents: |- - { - "testSnippet": { - "definitions": [ - { - "scope": { - "langIds": [ - "plaintext" - ] - }, - "body": [ - "\\textbf{\\$$variable1}" - ] - } - ], - "description": "", - "variables": { - "variable1": {} - } - } - } + documentContents: | + name: testSnippet + language: plaintext + phrase: + + $0.wrapperPhrase: + - + \textbf{\$$0} + --- selections: - - anchor: {line: 10, character: 24} - active: {line: 10, character: 33} - - anchor: {line: 16, character: 7} - active: {line: 16, character: 16} + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} thatMark: - type: UntypedTarget contentRange: diff --git a/data/fixtures/recorded/actions/snippets/tryWrapThis.yml b/data/fixtures/recorded/actions/snippets/tryWrapThis.yml index 3c1c4fe8de..26b2ec3145 100644 --- a/data/fixtures/recorded/actions/snippets/tryWrapThis.yml +++ b/data/fixtures/recorded/actions/snippets/tryWrapThis.yml @@ -4,11 +4,16 @@ command: spokenForm: try wrap this action: name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} + snippetDescription: + type: custom + variableName: try + body: "try {\n\t$try\n} catch (err) {\n\t$catch\n}" + scopeType: {type: statement} target: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: const foo = "bar"; selections: @@ -25,10 +30,3 @@ finalState: selections: - anchor: {line: 3, character: 4} active: {line: 3, character: 4} - thatMark: - - type: UntypedTarget - contentRange: - start: {line: 0, character: 0} - end: {line: 4, character: 1} - isReversed: false - hasExplicitRange: true diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapPastInk.yml b/data/fixtures/recorded/implicitExpansion/funkWrapPastInk.yml index 8b4a728473..035506421d 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapPastInk.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapPastInk.yml @@ -4,7 +4,11 @@ command: spokenForm: funk wrap past sit action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: range anchor: {type: implicit} @@ -14,6 +18,7 @@ command: excludeAnchor: false excludeActive: false usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: |- const whatever = "hello"; diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapThat.yml b/data/fixtures/recorded/implicitExpansion/funkWrapThat.yml index bca75a3f8f..bcf931e2a3 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapThat.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapThat.yml @@ -4,11 +4,16 @@ command: spokenForm: funk wrap that action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: primitive mark: {type: that} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: | const whatever = "hello"; diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapThat2.yml b/data/fixtures/recorded/implicitExpansion/funkWrapThat2.yml index a504308100..4f6923d8d7 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapThat2.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapThat2.yml @@ -4,11 +4,16 @@ command: spokenForm: funk wrap that action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: primitive mark: {type: that} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: | const whatever = "hello"; diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapThis.yml b/data/fixtures/recorded/implicitExpansion/funkWrapThis.yml index 97ac715f4f..40ef4294e0 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapThis.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapThis.yml @@ -4,11 +4,16 @@ command: spokenForm: funk wrap this action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: const whatever = "hello"; selections: diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapThis2.yml b/data/fixtures/recorded/implicitExpansion/funkWrapThis2.yml index 16bcb03420..74ff0b6344 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapThis2.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapThis2.yml @@ -4,11 +4,16 @@ command: spokenForm: funk wrap this action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: const whatever = "hello"; selections: diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapToken.yml b/data/fixtures/recorded/implicitExpansion/funkWrapToken.yml index 2f306b0132..0b2fe517e8 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapToken.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapToken.yml @@ -4,13 +4,18 @@ command: spokenForm: funk wrap token action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: primitive modifiers: - type: containingScope scopeType: {type: token} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: const whatever = "hello"; selections: diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapWhale.yml b/data/fixtures/recorded/implicitExpansion/funkWrapWhale.yml index c898c850a4..33928e80fb 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapWhale.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapWhale.yml @@ -4,11 +4,16 @@ command: spokenForm: funk wrap whale action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: w} usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: | const whatever = "hello"; diff --git a/data/fixtures/recorded/implicitExpansion/funkWrapWhalePastBat.yml b/data/fixtures/recorded/implicitExpansion/funkWrapWhalePastBat.yml index 74f5834c2d..ca5059557d 100644 --- a/data/fixtures/recorded/implicitExpansion/funkWrapWhalePastBat.yml +++ b/data/fixtures/recorded/implicitExpansion/funkWrapWhalePastBat.yml @@ -4,7 +4,11 @@ command: spokenForm: funk wrap whale past bat action: name: wrapWithSnippet - snippetDescription: {type: named, name: functionDeclaration, variableName: body} + snippetDescription: + type: custom + variableName: body + scopeType: {type: statement} + body: "function $name($parameterList) {\n\t$body\n}" target: type: range anchor: @@ -16,6 +20,7 @@ command: excludeAnchor: false excludeActive: false usePrePhraseSnapshot: true +spokenFormError: custom wrap with snippet initialState: documentContents: | const whatever = "hello"; diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat.yml b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat.yml index f6263bd0c1..0ca28adaee 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after that action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: that} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: | function myFunction() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat2.yml b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat2.yml index b234a12c98..9c3a50fc2f 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat2.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThat2.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after that action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: that} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- function whatever() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis.yml b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis.yml index 229979cf89..a2975cf354 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |+ function myFunction() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis2.yml b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis2.yml index 7407d87130..1ec65d4ef2 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis2.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkAfterThis2.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after this action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: cursor} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |+ function myFunction() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkAfterToken.yml b/data/fixtures/recorded/implicitExpansion/snipFunkAfterToken.yml index 24b8e0df7e..e31f4b9bbd 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkAfterToken.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkAfterToken.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after token action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -14,6 +19,7 @@ command: - type: containingScope scopeType: {type: token} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |+ function myFunction() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkAfterWhale.yml b/data/fixtures/recorded/implicitExpansion/snipFunkAfterWhale.yml index cac59a6e92..65676b2878 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkAfterWhale.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkAfterWhale.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk after whale action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: after @@ -12,6 +17,7 @@ command: type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: w} usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |+ function myFunction() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkToAirPastWhale.yml b/data/fixtures/recorded/implicitExpansion/snipFunkToAirPastWhale.yml index 6921751272..24bee9eb56 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkToAirPastWhale.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkToAirPastWhale.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk to air past whale action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: to @@ -19,6 +24,7 @@ command: excludeAnchor: false excludeActive: false usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |+ function myFunction() { diff --git a/data/fixtures/recorded/implicitExpansion/snipFunkToThisPastEach.yml b/data/fixtures/recorded/implicitExpansion/snipFunkToThisPastEach.yml index b7609a3210..d4e5fdc204 100644 --- a/data/fixtures/recorded/implicitExpansion/snipFunkToThisPastEach.yml +++ b/data/fixtures/recorded/implicitExpansion/snipFunkToThisPastEach.yml @@ -4,7 +4,12 @@ command: spokenForm: snippet funk to this past each action: name: insertSnippet - snippetDescription: {type: named, name: functionDeclaration} + snippetDescription: + type: custom + body: "function $name($parameterList) {\n\t$body\n}" + scopeTypes: + - {type: namedFunction} + - {type: statement} destination: type: primitive insertionMode: to @@ -19,6 +24,7 @@ command: excludeAnchor: false excludeActive: false usePrePhraseSnapshot: true +spokenFormError: custom insertion snippet initialState: documentContents: |- function whatever() { diff --git a/data/fixtures/recorded/inference/ifWrapTokenFine.yml b/data/fixtures/recorded/inference/ifWrapTokenFine.yml index 885ba79886..05b0435411 100644 --- a/data/fixtures/recorded/inference/ifWrapTokenFine.yml +++ b/data/fixtures/recorded/inference/ifWrapTokenFine.yml @@ -4,7 +4,11 @@ command: spokenForm: if wrap token fine action: name: wrapWithSnippet - snippetDescription: {type: named, name: ifStatement, variableName: consequence} + snippetDescription: + type: custom + variableName: consequence + body: "if ($condition) {\n\t$consequence\n}" + scopeType: {type: statement} target: type: primitive modifiers: @@ -12,6 +16,7 @@ command: scopeType: {type: token} mark: {type: decoratedSymbol, symbolColor: default, character: f} usePrePhraseSnapshot: false +spokenFormError: custom wrap with snippet initialState: documentContents: | const foo = "hello"; diff --git a/data/fixtures/recorded/languages/cpp/elseStateWrapThis.yml b/data/fixtures/recorded/languages/cpp/elseStateWrapThis.yml deleted file mode 100644 index bc925fec3e..0000000000 --- a/data/fixtures/recorded/languages/cpp/elseStateWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: cpp -command: - version: 6 - spokenForm: else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: alternative} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - - } else { - int foo = 0; - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/cpp/ifElseWrapThis.yml b/data/fixtures/recorded/languages/cpp/ifElseWrapThis.yml deleted file mode 100644 index d1e44da734..0000000000 --- a/data/fixtures/recorded/languages/cpp/ifElseWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: cpp -command: - version: 6 - spokenForm: if else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: consequence} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - int foo = 0; - } else { - - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/cpp/ifStateWrapThis.yml b/data/fixtures/recorded/languages/cpp/ifStateWrapThis.yml deleted file mode 100644 index 7e2aabb36b..0000000000 --- a/data/fixtures/recorded/languages/cpp/ifStateWrapThis.yml +++ /dev/null @@ -1,25 +0,0 @@ -languageId: cpp -command: - version: 6 - spokenForm: if wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifStatement, variableName: consequence} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - int foo = 0; - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/cpp/tryCatchWrapThis.yml b/data/fixtures/recorded/languages/cpp/tryCatchWrapThis.yml deleted file mode 100644 index 3dc87af8ac..0000000000 --- a/data/fixtures/recorded/languages/cpp/tryCatchWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: cpp -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - try { - int foo = 0; - } catch () { - - } - selections: - - anchor: {line: 2, character: 9} - active: {line: 2, character: 9} diff --git a/data/fixtures/recorded/languages/cpp/tryCatchWrapThis2.yml b/data/fixtures/recorded/languages/cpp/tryCatchWrapThis2.yml deleted file mode 100644 index cdf200b099..0000000000 --- a/data/fixtures/recorded/languages/cpp/tryCatchWrapThis2.yml +++ /dev/null @@ -1,44 +0,0 @@ -languageId: cpp -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - if (true) { - int foo = 0; - } - - int bar = 1; - selections: - - anchor: {line: 4, character: 0} - active: {line: 4, character: 0} - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: |- - try { - if (true) { - int foo = 0; - } - } catch () { - - } - - try { - int bar = 1; - } catch () { - - } - selections: - - anchor: {line: 10, character: 9} - active: {line: 10, character: 9} - - anchor: {line: 4, character: 9} - active: {line: 4, character: 9} diff --git a/data/fixtures/recorded/languages/csharp/elseStateWrapThis.yml b/data/fixtures/recorded/languages/csharp/elseStateWrapThis.yml deleted file mode 100644 index e6e550b326..0000000000 --- a/data/fixtures/recorded/languages/csharp/elseStateWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: csharp -command: - version: 6 - spokenForm: else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: alternative} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - - } else { - int foo = 0; - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/csharp/ifElseWrapThis.yml b/data/fixtures/recorded/languages/csharp/ifElseWrapThis.yml deleted file mode 100644 index 8acf803b31..0000000000 --- a/data/fixtures/recorded/languages/csharp/ifElseWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: csharp -command: - version: 6 - spokenForm: if else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: consequence} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - int foo = 0; - } else { - - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/csharp/ifStateWrapThis.yml b/data/fixtures/recorded/languages/csharp/ifStateWrapThis.yml deleted file mode 100644 index 26b6825557..0000000000 --- a/data/fixtures/recorded/languages/csharp/ifStateWrapThis.yml +++ /dev/null @@ -1,25 +0,0 @@ -languageId: csharp -command: - version: 6 - spokenForm: if wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifStatement, variableName: consequence} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - int foo = 0; - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/csharp/tryCatchWrapThis.yml b/data/fixtures/recorded/languages/csharp/tryCatchWrapThis.yml deleted file mode 100644 index f0cdd28d08..0000000000 --- a/data/fixtures/recorded/languages/csharp/tryCatchWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: csharp -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - try { - int foo = 0; - } catch () { - - } - selections: - - anchor: {line: 2, character: 9} - active: {line: 2, character: 9} diff --git a/data/fixtures/recorded/languages/csharp/tryCatchWrapThis2.yml b/data/fixtures/recorded/languages/csharp/tryCatchWrapThis2.yml deleted file mode 100644 index 4c61de1dde..0000000000 --- a/data/fixtures/recorded/languages/csharp/tryCatchWrapThis2.yml +++ /dev/null @@ -1,44 +0,0 @@ -languageId: csharp -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - if (true) { - int foo = 0; - } - - int bar = 1; - selections: - - anchor: {line: 4, character: 0} - active: {line: 4, character: 0} - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: |- - try { - if (true) { - int foo = 0; - } - } catch () { - - } - - try { - int bar = 1; - } catch () { - - } - selections: - - anchor: {line: 10, character: 9} - active: {line: 10, character: 9} - - anchor: {line: 4, character: 9} - active: {line: 4, character: 9} diff --git a/data/fixtures/recorded/languages/java/elseStateWrapThis.yml b/data/fixtures/recorded/languages/java/elseStateWrapThis.yml deleted file mode 100644 index c1a5e476bf..0000000000 --- a/data/fixtures/recorded/languages/java/elseStateWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: java -command: - version: 6 - spokenForm: else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: alternative} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - - } else { - int foo = 0; - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/java/ifElseWrapThis.yml b/data/fixtures/recorded/languages/java/ifElseWrapThis.yml deleted file mode 100644 index e042e38aec..0000000000 --- a/data/fixtures/recorded/languages/java/ifElseWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: java -command: - version: 6 - spokenForm: if else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: consequence} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - int foo = 0; - } else { - - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/java/ifStateWrapThis.yml b/data/fixtures/recorded/languages/java/ifStateWrapThis.yml deleted file mode 100644 index c183a7ea41..0000000000 --- a/data/fixtures/recorded/languages/java/ifStateWrapThis.yml +++ /dev/null @@ -1,25 +0,0 @@ -languageId: java -command: - version: 6 - spokenForm: if wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifStatement, variableName: consequence} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - if () { - int foo = 0; - } - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/java/tryCatchWrapThis.yml b/data/fixtures/recorded/languages/java/tryCatchWrapThis.yml deleted file mode 100644 index db29bfdd6a..0000000000 --- a/data/fixtures/recorded/languages/java/tryCatchWrapThis.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: java -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: int foo = 0; - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} - marks: {} -finalState: - documentContents: |- - try { - int foo = 0; - } catch () { - - } - selections: - - anchor: {line: 2, character: 9} - active: {line: 2, character: 9} diff --git a/data/fixtures/recorded/languages/java/tryCatchWrapThis2.yml b/data/fixtures/recorded/languages/java/tryCatchWrapThis2.yml deleted file mode 100644 index d9534338f4..0000000000 --- a/data/fixtures/recorded/languages/java/tryCatchWrapThis2.yml +++ /dev/null @@ -1,44 +0,0 @@ -languageId: java -command: - version: 6 - spokenForm: try wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: tryCatchStatement, variableName: body} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - if (true) { - int foo = 0; - } - - int bar = 1; - selections: - - anchor: {line: 4, character: 0} - active: {line: 4, character: 0} - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: |- - try { - if (true) { - int foo = 0; - } - } catch () { - - } - - try { - int bar = 1; - } catch () { - - } - selections: - - anchor: {line: 10, character: 9} - active: {line: 10, character: 9} - - anchor: {line: 4, character: 9} - active: {line: 4, character: 9} diff --git a/data/fixtures/recorded/languages/php/elseWrapThis.yml b/data/fixtures/recorded/languages/php/elseWrapThis.yml deleted file mode 100644 index ea60f4b585..0000000000 --- a/data/fixtures/recorded/languages/php/elseWrapThis.yml +++ /dev/null @@ -1,32 +0,0 @@ -languageId: php -command: - version: 6 - spokenForm: else wrap this - action: - name: wrapWithSnippet - snippetDescription: {type: named, name: ifElseStatement, variableName: alternative} - target: - type: primitive - mark: {type: cursor} - usePrePhraseSnapshot: false -initialState: - documentContents: |- - ; } -interface CustomInsertSnippetArg { + +export interface CustomInsertSnippetArg { type: "custom"; body: string; + languages?: string[]; scopeTypes?: ScopeType[]; substitutions?: Record; } -export type InsertSnippetArg = NamedInsertSnippetArg | CustomInsertSnippetArg; + +interface ListInsertSnippetArg { + type: "list"; + substitutions?: Record; + snippets: CustomInsertSnippetArg[]; +} + +export type InsertSnippetArg = + | NamedInsertSnippetArg + | CustomInsertSnippetArg + | ListInsertSnippetArg; export interface InsertSnippetActionDescriptor { name: "insertSnippet"; @@ -165,15 +177,24 @@ interface NamedWrapWithSnippetArg { name: string; variableName: string; } -interface CustomWrapWithSnippetArg { + +export interface CustomWrapWithSnippetArg { type: "custom"; body: string; variableName?: string; scopeType?: ScopeType; + languages?: string[]; +} + +interface ListWrapWithSnippetArg { + type: "list"; + snippets: CustomWrapWithSnippetArg[]; } + export type WrapWithSnippetArg = | NamedWrapWithSnippetArg - | CustomWrapWithSnippetArg; + | CustomWrapWithSnippetArg + | ListWrapWithSnippetArg; export interface WrapWithSnippetActionDescriptor { name: "wrapWithSnippet"; diff --git a/packages/common/src/types/snippet.types.ts b/packages/common/src/types/snippet.types.ts index 4262e13736..88777ae9de 100644 --- a/packages/common/src/types/snippet.types.ts +++ b/packages/common/src/types/snippet.types.ts @@ -1,4 +1,3 @@ -import type { TextFormatterName } from "../util/textFormatters"; import type { SimpleScopeTypeType } from "./command/PartialTargetDescriptor.types"; export interface SnippetScope { @@ -58,7 +57,7 @@ export interface SnippetVariable { /** * Format text inserted into this variable using the given formatter */ - formatter?: TextFormatterName; + formatter?: string; } export interface Snippet { diff --git a/packages/common/src/util/textFormatters.ts b/packages/common/src/util/textFormatters.ts deleted file mode 100644 index d5e6dc9581..0000000000 --- a/packages/common/src/util/textFormatters.ts +++ /dev/null @@ -1,35 +0,0 @@ -type TextFormatter = (tokens: string[]) => string; - -export const textFormatters: Record = { - camelCase(tokens: string[]) { - if (tokens.length === 0) { - return ""; - } - - const [first, ...rest] = tokens; - - return first + rest.map(capitalizeToken).join(""); - }, - - snakeCase(tokens: string[]) { - return tokens.join("_"); - }, - - upperSnakeCase(tokens: string[]) { - return tokens.map((token) => token.toUpperCase()).join("_"); - }, - - pascalCase(tokens: string[]) { - return tokens.map(capitalizeToken).join(""); - }, -}; - -function capitalizeToken(token: string): string { - return token.length === 0 ? "" : token[0].toUpperCase() + token.substr(1); -} - -export type TextFormatterName = - | "camelCase" - | "pascalCase" - | "snakeCase" - | "upperSnakeCase"; diff --git a/packages/cursorless-engine/src/CommandHistory.ts b/packages/cursorless-engine/src/CommandHistory.ts index 4162f03b3a..b1f83a0a15 100644 --- a/packages/cursorless-engine/src/CommandHistory.ts +++ b/packages/cursorless-engine/src/CommandHistory.ts @@ -114,9 +114,16 @@ function sanitizeActionInPlace(action: ActionDescriptor): void { // Remove substitutions and custom body case "insertSnippet": - delete action.snippetDescription.substitutions; if (action.snippetDescription.type === "custom") { action.snippetDescription.body = ""; + delete action.snippetDescription.substitutions; + } else if (action.snippetDescription.type === "list") { + for (const snippet of action.snippetDescription.snippets) { + snippet.body = ""; + delete snippet.substitutions; + } + } else { + delete action.snippetDescription.substitutions; } break; diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts index b7e0aacde2..f3eca4e4d8 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts @@ -1,9 +1,57 @@ +import { + FlashStyle, + Range, + matchAll, + type EditableTextEditor, + type Selection, + type TextEditor, +} from "@cursorless/common"; +import { + parseSnippetFile, + serializeSnippetFile, + type Snippet, + type SnippetFile, + type SnippetHeader, + type SnippetVariable, +} from "talon-snippets"; import type { Snippets } from "../../core/Snippets"; +import { ide } from "../../singletons/ide.singleton"; import type { Target } from "../../typings/target.types"; +import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; import type { ActionReturnValue } from "../actions.types"; -import GenerateSnippetCommunity from "./GenerateSnippetCommunity"; -import GenerateSnippetLegacy from "./GenerateSnippetLegacy"; +import { constructSnippetBody } from "./constructSnippetBody"; +import { editText } from "./editText"; +import type { Offsets } from "./Offsets"; +/** + * This action can be used to automatically create a snippet from a target. Any + * cursor selections inside the target will become placeholders in the final + * snippet. This action creates a new file, and inserts a snippet that the user + * can fill out to construct their desired snippet. + * + * Note that there are two snippets involved in this implementation: + * + * - The snippet that the user is trying to create. We refer to this snippet as + * the user snippet. + * - The snippet that we insert that the user can use to build their snippet. We + * refer to this as the meta snippet. + * + * We proceed as follows: + * + * 1. Ask user for snippet name if not provided as arg + * 2. Find all cursor selections inside target - these will become the user + * snippet variables + * 3. Extract text of target + * 4. Replace cursor selections in text with snippet variables + * 4. Construct the user snippet body as a list of strings + * 5. Construct a javascript object that will be serialized to become the meta + * snippet + * 6. Serialize the javascript object + * 7. Escape dollar signs and replace placeholder text with snippet placeholders. + * This modified json output is the meta snippet. + * 8. Open a new document in the snippets dir to hold the new snippet. + * 9. Insert the meta snippet so that the user can construct their snippet. + */ export default class GenerateSnippet { constructor(private snippets: Snippets) { this.run = this.run.bind(this); @@ -11,15 +59,189 @@ export default class GenerateSnippet { async run( targets: Target[], - directory?: string, + directory: string, snippetName?: string, ): Promise { if (directory == null) { - const action = new GenerateSnippetLegacy(this.snippets); - return action.run(targets, snippetName); + throw new Error( + "Directory argument is required for GenerateSnippet action. Please update Cursorless Talon", + ); } - const action = new GenerateSnippetCommunity(this.snippets); - return action.run(targets, directory, snippetName); + const target = ensureSingleTarget(targets); + const editor = target.editor; + + // NB: We don't await the pending edit decoration so that if the user + // immediately starts saying the name of the snippet (eg command chain + // "snippet make funk camel my function"), we're more likely to + // win the race and have the input box ready for them + void flashTargets(ide(), targets, FlashStyle.referenced); + + if (snippetName == null) { + snippetName = await ide().showInputBox({ + prompt: "Name of snippet", + placeHolder: "helloWorld", + }); + + // User cancelled; do nothing + if (!snippetName) { + return {}; + } + } + + const baseOffset = editor.document.offsetAt(target.contentRange.start); + + /** + * The variables that will appear in the user snippet. + */ + const selections = getsSnippetSelections(editor, target.contentRange); + const variables = selections.map( + (selection, index): Variable => ({ + offsets: { + start: editor.document.offsetAt(selection.start) - baseOffset, + end: editor.document.offsetAt(selection.end) - baseOffset, + }, + name: index === selections.length - 1 ? "0" : `${index + 1}`, + }), + ); + + /** + * Text before the start of the snippet in the snippet start line. We need + * to pass this to {@link constructSnippetBody} so that it knows the + * baseline indentation of the snippet + */ + const linePrefix = editor.document.getText( + new Range( + target.contentRange.start.with(undefined, 0), + target.contentRange.start, + ), + ); + + const originalText = editor.document.getText(target.contentRange); + + const snippetBodyText = editText(originalText, [ + ...matchAll(originalText, /\$|\\/g, (match) => ({ + offsets: { + start: match.index!, + end: match.index! + match[0].length, + }, + text: `\\${match[0]}`, + })), + ...variables.map(({ offsets, name }) => ({ + offsets, + text: `$${name}`, + })), + ]); + + const snippetLines = constructSnippetBody(snippetBodyText, linePrefix); + + let editableEditor: EditableTextEditor; + let snippetFile: SnippetFile = { snippets: [] }; + + if (ide().runMode === "test") { + // If we're testing, we just overwrite the current document + editableEditor = ide().getEditableTextEditor(editor); + } else { + // Otherwise, we create and open a new document for the snippet + editableEditor = ide().getEditableTextEditor( + await this.snippets.openNewSnippetFile(snippetName, directory), + ); + snippetFile = parseSnippetFile(editableEditor.document.getText()); + } + + await editableEditor.setSelections([ + editableEditor.document.range.toSelection(false), + ]); + + /** The next placeholder index to use for the meta snippet */ + let currentPlaceholderIndex = 1; + + const { header } = snippetFile; + + const phrases = + snippetFile.header?.phrases != null + ? undefined + : [`${PLACEHOLDER}${currentPlaceholderIndex++}`]; + + const createVariable = (variable: Variable): SnippetVariable => { + const hasPhrase = header?.variables?.some( + (v) => v.name === variable.name && v.wrapperPhrases != null, + ); + return { + name: variable.name, + wrapperPhrases: hasPhrase + ? undefined + : [`${PLACEHOLDER}${currentPlaceholderIndex++}`], + }; + }; + + const snippet: Snippet = { + name: header?.name === snippetName ? undefined : snippetName, + phrases, + languages: getSnippetLanguages(editor, header), + body: snippetLines, + variables: variables.map(createVariable), + }; + + snippetFile.snippets.push(snippet); + + /** + * This is the text of the meta-snippet in Textmate format that we will + * insert into the new document where the user will fill out their snippet + * definition + */ + const metaSnippetText = serializeSnippetFile(snippetFile) + // Escape dollar signs in the snippet text so that they don't get used as + // placeholders in the meta snippet + .replace(/\$/g, "\\$") + // Replace constant with dollar sign for meta snippet placeholders + .replaceAll(PLACEHOLDER, "$"); + + // Insert the meta-snippet + await editableEditor.insertSnippet(metaSnippetText); + + return { + thatSelections: targets.map(({ editor, contentSelection }) => ({ + editor, + selection: contentSelection, + })), + }; + } +} + +function getSnippetLanguages( + editor: TextEditor, + header: SnippetHeader | undefined, +): string[] | undefined { + if (header?.languages?.includes(editor.document.languageId)) { + return undefined; } + return [editor.document.languageId]; +} + +function getsSnippetSelections(editor: TextEditor, range: Range): Selection[] { + const selections = editor.selections.filter((selection) => + range.contains(selection), + ); + selections.sort((a, b) => a.start.compareTo(b.start)); + return selections; +} + +// Used to temporarily escape the $1, $2 snippet holes (the "meta snippet" holes +// that become live snippets when the user edits) so we can use traditional +// backslash escaping for the holes in the underlying snippet itself (the "user +// snippet" holes that will be saved as part of their new template). +const PLACEHOLDER = "PLACEHOLDER_VFA77zcbLD6wXNmfMAay"; + +interface Variable { + /** + * The start an end offsets of the variable relative to the text of the + * snippet that contains it + */ + offsets: Offsets; + + /** + * The name for the variable + */ + name: string; } diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts deleted file mode 100644 index 898c15db7d..0000000000 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetCommunity.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { - FlashStyle, - matchAll, - Range, - type EditableTextEditor, - type Selection, - type TextEditor, -} from "@cursorless/common"; -import { - parseSnippetFile, - serializeSnippetFile, - type Snippet, - type SnippetFile, - type SnippetHeader, - type SnippetVariable, -} from "talon-snippets"; -import type { Snippets } from "../../core/Snippets"; -import { ide } from "../../singletons/ide.singleton"; -import type { Target } from "../../typings/target.types"; -import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; -import type { ActionReturnValue } from "../actions.types"; -import { constructSnippetBody } from "./constructSnippetBody"; -import { editText } from "./editText"; -import type { Offsets } from "./Offsets"; - -/** - * This action can be used to automatically create a snippet from a target. Any - * cursor selections inside the target will become placeholders in the final - * snippet. This action creates a new file, and inserts a snippet that the user - * can fill out to construct their desired snippet. - * - * Note that there are two snippets involved in this implementation: - * - * - The snippet that the user is trying to create. We refer to this snippet as - * the user snippet. - * - The snippet that we insert that the user can use to build their snippet. We - * refer to this as the meta snippet. - * - * We proceed as follows: - * - * 1. Ask user for snippet name if not provided as arg - * 2. Find all cursor selections inside target - these will become the user - * snippet variables - * 3. Extract text of target - * 4. Replace cursor selections in text with snippet variables - * 4. Construct the user snippet body as a list of strings - * 5. Construct a javascript object that will be serialized to become the meta - * snippet - * 6. Serialize the javascript object - * 7. Escape dollar signs and replace placeholder text with snippet placeholders. - * This modified json output is the meta snippet. - * 8. Open a new document in the snippets dir to hold the new snippet. - * 9. Insert the meta snippet so that the user can construct their snippet. - */ -export default class GenerateSnippetCommunity { - constructor(private snippets: Snippets) { - this.run = this.run.bind(this); - } - - async run( - targets: Target[], - directory: string, - snippetName?: string, - ): Promise { - const target = ensureSingleTarget(targets); - const editor = target.editor; - - // NB: We don't await the pending edit decoration so that if the user - // immediately starts saying the name of the snippet (eg command chain - // "snippet make funk camel my function"), we're more likely to - // win the race and have the input box ready for them - void flashTargets(ide(), targets, FlashStyle.referenced); - - if (snippetName == null) { - snippetName = await ide().showInputBox({ - prompt: "Name of snippet", - placeHolder: "helloWorld", - }); - - // User cancelled; do nothing - if (!snippetName) { - return {}; - } - } - - const baseOffset = editor.document.offsetAt(target.contentRange.start); - - /** - * The variables that will appear in the user snippet. - */ - const selections = getsSnippetSelections(editor, target.contentRange); - const variables = selections.map( - (selection, index): Variable => ({ - offsets: { - start: editor.document.offsetAt(selection.start) - baseOffset, - end: editor.document.offsetAt(selection.end) - baseOffset, - }, - name: index === selections.length - 1 ? "0" : `${index + 1}`, - }), - ); - - /** - * Text before the start of the snippet in the snippet start line. We need - * to pass this to {@link constructSnippetBody} so that it knows the - * baseline indentation of the snippet - */ - const linePrefix = editor.document.getText( - new Range( - target.contentRange.start.with(undefined, 0), - target.contentRange.start, - ), - ); - - const originalText = editor.document.getText(target.contentRange); - - const snippetBodyText = editText(originalText, [ - ...matchAll(originalText, /\$|\\/g, (match) => ({ - offsets: { - start: match.index!, - end: match.index! + match[0].length, - }, - text: `\\${match[0]}`, - })), - ...variables.map(({ offsets, name }) => ({ - offsets, - text: `$${name}`, - })), - ]); - - const snippetLines = constructSnippetBody(snippetBodyText, linePrefix); - - let editableEditor: EditableTextEditor; - let snippetFile: SnippetFile = { snippets: [] }; - - if (ide().runMode === "test") { - // If we're testing, we just overwrite the current document - editableEditor = ide().getEditableTextEditor(editor); - } else { - // Otherwise, we create and open a new document for the snippet - editableEditor = ide().getEditableTextEditor( - await this.snippets.openNewSnippetFile(snippetName, directory), - ); - snippetFile = parseSnippetFile(editableEditor.document.getText()); - } - - await editableEditor.setSelections([ - editableEditor.document.range.toSelection(false), - ]); - - /** The next placeholder index to use for the meta snippet */ - let currentPlaceholderIndex = 1; - - const { header } = snippetFile; - - const phrases = - snippetFile.header?.phrases != null - ? undefined - : [`${PLACEHOLDER}${currentPlaceholderIndex++}`]; - - const createVariable = (variable: Variable): SnippetVariable => { - const hasPhrase = header?.variables?.some( - (v) => v.name === variable.name && v.wrapperPhrases != null, - ); - return { - name: variable.name, - wrapperPhrases: hasPhrase - ? undefined - : [`${PLACEHOLDER}${currentPlaceholderIndex++}`], - }; - }; - - const snippet: Snippet = { - name: header?.name === snippetName ? undefined : snippetName, - phrases, - languages: getSnippetLanguages(editor, header), - body: snippetLines, - variables: variables.map(createVariable), - }; - - snippetFile.snippets.push(snippet); - - /** - * This is the text of the meta-snippet in Textmate format that we will - * insert into the new document where the user will fill out their snippet - * definition - */ - const metaSnippetText = serializeSnippetFile(snippetFile) - // Escape dollar signs in the snippet text so that they don't get used as - // placeholders in the meta snippet - .replace(/\$/g, "\\$") - // Replace constant with dollar sign for meta snippet placeholders - .replaceAll(PLACEHOLDER, "$"); - - // Insert the meta-snippet - await editableEditor.insertSnippet(metaSnippetText); - - return { - thatSelections: targets.map(({ editor, contentSelection }) => ({ - editor, - selection: contentSelection, - })), - }; - } -} - -function getSnippetLanguages( - editor: TextEditor, - header: SnippetHeader | undefined, -): string[] | undefined { - if (header?.languages?.includes(editor.document.languageId)) { - return undefined; - } - return [editor.document.languageId]; -} - -function getsSnippetSelections(editor: TextEditor, range: Range): Selection[] { - const selections = editor.selections.filter((selection) => - range.contains(selection), - ); - selections.sort((a, b) => a.start.compareTo(b.start)); - return selections; -} - -// Used to temporarily escape the $1, $2 snippet holes (the "meta snippet" holes -// that become live snippets when the user edits) so we can use traditional -// backslash escaping for the holes in the underlying snippet itself (the "user -// snippet" holes that will be saved as part of their new template). -const PLACEHOLDER = "PLACEHOLDER_VFA77zcbLD6wXNmfMAay"; - -interface Variable { - /** - * The start an end offsets of the variable relative to the text of the - * snippet that contains it - */ - offsets: Offsets; - - /** - * The name for the variable - */ - name: string; -} diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts deleted file mode 100644 index 76410fbba2..0000000000 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippetLegacy.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { FlashStyle, matchAll, Range } from "@cursorless/common"; -import type { Snippets } from "../../core/Snippets"; -import { ide } from "../../singletons/ide.singleton"; -import type { Target } from "../../typings/target.types"; -import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; -import type { ActionReturnValue } from "../actions.types"; -import { constructSnippetBody } from "./constructSnippetBody"; -import { editText } from "./editText"; -import type { Offsets } from "./Offsets"; -import Substituter from "./Substituter"; - -/** - * This action can be used to automatically create a snippet from a target. Any - * cursor selections inside the target will become placeholders in the final - * snippet. This action creates a new file, and inserts a snippet that the user - * can fill out to construct their desired snippet. - * - * Note that there are two snippets involved in this implementation: - * - * - The snippet that the user is trying to create. We refer to this snippet as - * the user snippet. - * - The snippet that we insert that the user can use to build their snippet. We - * refer to this as the meta snippet. - * - * We proceed as follows: - * - * 1. Ask user for snippet name if not provided as arg - * 2. Find all cursor selections inside target - these will become the user - * snippet variables - * 3. Extract text of target - * 4. Replace cursor selections in text with random ids that won't be affected - * by json serialization. After serialization we'll replace these id's by - * snippet placeholders. - * 4. Construct the user snippet body as a list of strings - * 5. Construct a javascript object that will be json-ified to become the meta - * snippet - * 6. Serialize the javascript object to json - * 7. Perform replacements on the random id's appearing in this json to get the - * text we desire. This modified json output is the meta snippet. - * 8. Open a new document in user custom snippets dir to hold the new snippet. - * 9. Insert the meta snippet so that the user can construct their snippet. - * - * Note that we avoid using JS interpolation strings here because the syntax is - * very similar to snippet placeholders, so we would end up with lots of - * confusing escaping. - */ -export default class GenerateSnippetLegacy { - constructor(private snippets: Snippets) { - this.run = this.run.bind(this); - } - - async run( - targets: Target[], - snippetName?: string, - ): Promise { - const target = ensureSingleTarget(targets); - const editor = target.editor; - - // NB: We don't await the pending edit decoration so that if the user - // immediately starts saying the name of the snippet (eg command chain - // "snippet make funk camel my function"), we're more likely to - // win the race and have the input box ready for them - void flashTargets(ide(), targets, FlashStyle.referenced); - - if (snippetName == null) { - snippetName = await ide().showInputBox({ - prompt: "Name of snippet", - placeHolder: "helloWorld", - }); - } - - // User cancelled; don't do anything - if (snippetName == null) { - return {}; - } - - /** The next placeholder index to use for the meta snippet */ - let currentPlaceholderIndex = 1; - - const baseOffset = editor.document.offsetAt(target.contentRange.start); - - /** - * The variables that will appear in the user snippet. Note that - * `placeholderIndex` here is the placeholder index in the meta snippet not - * the user snippet. - */ - const variables: Variable[] = editor.selections - .filter((selection) => target.contentRange.contains(selection)) - .map((selection, index) => ({ - offsets: { - start: editor.document.offsetAt(selection.start) - baseOffset, - end: editor.document.offsetAt(selection.end) - baseOffset, - }, - defaultName: `variable${index + 1}`, - placeholderIndex: currentPlaceholderIndex++, - })); - - /** - * Constructs random ids that can be put into the text that won't be - * modified by json serialization. - */ - const substituter = new Substituter(); - - /** - * Text before the start of the snippet in the snippet start line. We need - * to pass this to {@link constructSnippetBody} so that it knows the - * baseline indentation of the snippet - */ - const linePrefix = editor.document.getText( - new Range( - target.contentRange.start.with(undefined, 0), - target.contentRange.start, - ), - ); - - const originalText = editor.document.getText(target.contentRange); - - /** - * The text of the snippet, with placeholders inserted for variables and - * special characters `$`, `\`, and `}` escaped twice to make it through - * both meta snippet and user snippet. - */ - const snippetBodyText = editText(originalText, [ - ...matchAll(originalText, /\$|\\/g, (match) => ({ - offsets: { - start: match.index!, - end: match.index! + match[0].length, - }, - text: match[0] === "\\" ? `\\${match[0]}` : `\\\\${match[0]}`, - })), - ...variables.map(({ offsets, defaultName, placeholderIndex }) => ({ - offsets, - // Note that the reason we use the substituter here is primarily so - // that the `\` below doesn't get escaped upon conversion to json. - text: substituter.addSubstitution( - [ - // This `\$` will end up being a `$` in the final document. It - // indicates the start of a variable in the user snippet. We need - // the `\` so that the meta-snippet doesn't see it as one of its - // placeholders. - "\\$", - - // The remaining text here is a placeholder in the meta-snippet - // that the user can use to name their snippet variable that will - // be in the user snippet. - "${", - placeholderIndex, - ":", - defaultName, - "}", - ].join(""), - ), - })), - ]); - - const snippetLines = constructSnippetBody(snippetBodyText, linePrefix); - - /** - * Constructs a key-value entry for use in the variable description section - * of the user snippet definition. It contains tabstops for use in the - * meta-snippet. - * @param variable The variable - * @returns A [key, value] pair for use in the meta-snippet - */ - const constructVariableDescriptionEntry = ({ - placeholderIndex, - }: Variable): [string, string] => { - // The key will have the same placeholder index as the other location - // where this variable appears. - const key = "$" + placeholderIndex; - - // The value will end up being an empty object with a tabstop in the - // middle so that the user can add information about the variable, such - // as wrapperScopeType. Ie the output will look like `{|}` (with the `|` - // representing a tabstop in the meta-snippet) - // - // NB: We use the substituter here, with `isQuoted=true` because in order - // to make this work for the meta-snippet, we want to end up with - // something like `{$3}`, which is not valid json. So we instead arrange - // to end up with json like `"hgidfsivhs"`, and then replace the whole - // string (including quotes) with `{$3}` after json-ification - const value = substituter.addSubstitution( - "{$" + currentPlaceholderIndex++ + "}", - true, - ); - - return [key, value]; - }; - - /** An object that will be json-ified to become the meta-snippet */ - const snippet = { - [snippetName]: { - definitions: [ - { - scope: { - langIds: [editor.document.languageId], - }, - body: snippetLines, - }, - ], - description: "$" + currentPlaceholderIndex++, - variables: - variables.length === 0 - ? undefined - : Object.fromEntries( - variables.map(constructVariableDescriptionEntry), - ), - }, - }; - - /** - * This is the text of the meta-snippet in Textmate format that we will - * insert into the new document where the user will fill out their snippet - * definition - */ - const snippetText = substituter.makeSubstitutions( - JSON.stringify(snippet, null, 2), - ); - - const editableEditor = ide().getEditableTextEditor(editor); - - if (ide().runMode === "test") { - // If we're testing, we just overwrite the current document - await editableEditor.setSelections([ - editor.document.range.toSelection(false), - ]); - } else { - // Otherwise, we create and open a new document for the snippet in the - // user snippets dir - await this.snippets.openNewSnippetFile(snippetName); - } - - // Insert the meta-snippet - await editableEditor.insertSnippet(snippetText); - - return { - thatSelections: targets.map(({ editor, contentSelection }) => ({ - editor, - selection: contentSelection, - })), - }; - } -} - -interface Variable { - /** - * The start an end offsets of the variable relative to the text of the - * snippet that contains it - */ - offsets: Offsets; - - /** - * The default name for the given variable that will appear as the placeholder - * text in the meta snippet - */ - defaultName: string; - - /** - * The placeholder to use when filling out the name of this variable in the - * meta snippet. - */ - placeholderIndex: number; -} diff --git a/packages/cursorless-engine/src/actions/InsertSnippet.ts b/packages/cursorless-engine/src/actions/InsertSnippet.ts index f7ca4c9e27..126dabe45a 100644 --- a/packages/cursorless-engine/src/actions/InsertSnippet.ts +++ b/packages/cursorless-engine/src/actions/InsertSnippet.ts @@ -1,23 +1,15 @@ -import type { - InsertSnippetArg, - ScopeType, - Snippet, - SnippetDefinition, -} from "@cursorless/common"; -import { RangeExpansionBehavior, textFormatters } from "@cursorless/common"; +import type { InsertSnippetArg } from "@cursorless/common"; +import { RangeExpansionBehavior } from "@cursorless/common"; import type { Snippets } from "../core/Snippets"; +import { getPreferredSnippet } from "../core/getPreferredSnippet"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import type { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedExplicitStage } from "../processTargets/modifiers/ConditionalModifierStages"; -import { UntypedTarget } from "../processTargets/targets"; import { ide } from "../singletons/ide.singleton"; -import { - findMatchingSnippetDefinitionStrict, - transformSnippetVariables, -} from "../snippets/snippet"; +import { transformSnippetVariables } from "../snippets/transformSnippetVariables"; import { SnippetParser } from "../snippets/vendor/vscodeSnippet/snippetParser"; -import type { Destination, Target } from "../typings/target.types"; +import type { Destination } from "../typings/target.types"; import { ensureSingleEditor } from "../util/targetUtils"; import type { Actions } from "./Actions"; import type { ActionReturnValue } from "./actions.types"; @@ -34,9 +26,16 @@ export default class InsertSnippet { this.run = this.run.bind(this); } - getFinalStages(snippetDescription: InsertSnippetArg) { - const defaultScopeTypes = this.getScopeTypes(snippetDescription); - + getFinalStages( + destinations: Destination[], + snippetDescription: InsertSnippetArg, + ) { + const editor = ensureSingleEditor(destinations); + const snippet = getPreferredSnippet( + snippetDescription, + editor.document.languageId, + ); + const defaultScopeTypes = snippet.scopeTypes ?? []; return defaultScopeTypes.length === 0 ? [] : [ @@ -50,58 +49,6 @@ export default class InsertSnippet { ]; } - private getScopeTypes(snippetDescription: InsertSnippetArg): ScopeType[] { - if (snippetDescription.type === "named") { - const { name } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const scopeTypeTypes = snippet.insertionScopeTypes; - return scopeTypeTypes == null - ? [] - : scopeTypeTypes.map((scopeTypeType) => ({ - type: scopeTypeType, - })); - } else { - return snippetDescription.scopeTypes ?? []; - } - } - - private getSnippetInfo( - snippetDescription: InsertSnippetArg, - targets: Target[], - ) { - if (snippetDescription.type === "named") { - const { name } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const definition = findMatchingSnippetDefinitionStrict( - this.modifierStageFactory, - targets, - snippet.definitions, - ); - - return { - body: definition.body.join("\n"), - - formatSubstitutions(substitutions: Record | undefined) { - return substitutions == null - ? undefined - : formatSubstitutions(snippet, definition, substitutions); - }, - }; - } else { - return { - body: snippetDescription.body, - - formatSubstitutions(substitutions: Record | undefined) { - return substitutions; - }, - }; - } - } - async run( destinations: Destination[], snippetDescription: InsertSnippetArg, @@ -109,34 +56,22 @@ export default class InsertSnippet { const editor = ide().getEditableTextEditor( ensureSingleEditor(destinations), ); - - await this.actions.editNew.run(destinations); - - const { body, formatSubstitutions } = this.getSnippetInfo( + const snippet = getPreferredSnippet( snippetDescription, - // Use new selection locations instead of original targets because - // that's where we'll be doing the snippet insertion - editor.selections.map( - (selection) => - new UntypedTarget({ - editor, - contentRange: selection, - isReversed: false, - hasExplicitRange: true, - }), - ), + editor.document.languageId, ); - const parsedSnippet = this.snippetParser.parse(body); + const parsedSnippet = this.snippetParser.parse(snippet.body); - transformSnippetVariables( - parsedSnippet, - null, - formatSubstitutions(snippetDescription.substitutions), - ); + const substitutions = + snippet.substitutions ?? snippetDescription.substitutions; + + transformSnippetVariables(parsedSnippet, null, substitutions); const snippetString = parsedSnippet.toTextmateString(); + await this.actions.editNew.run(destinations); + const { editorSelections: updatedThatSelections } = await performEditsAndUpdateSelections({ rangeUpdater: this.rangeUpdater, @@ -159,43 +94,3 @@ export default class InsertSnippet { }; } } - -/** - * Applies the appropriate formatters to the given variable substitution values - * in {@link substitutions} based on the formatter specified for the given - * variables as defined in {@link snippet} and {@link definition}. - * @param snippet The full snippet info - * @param definition The specific definition chosen for the given target context - * @param substitutions The original unformatted substitution strings - * @returns A new map of substitution strings with the values formatted - */ -function formatSubstitutions( - snippet: Snippet, - definition: SnippetDefinition, - substitutions: Record, -): Record { - return Object.fromEntries( - Object.entries(substitutions).map(([variableName, value]) => { - // We prefer the variable formatters from the contextually relevant - // snippet definition if they exist, otherwise we fall back to the - // global definitions for the given snippet. - const formatterName = - (definition.variables ?? {})[variableName]?.formatter ?? - (snippet.variables ?? {})[variableName]?.formatter; - - if (formatterName == null) { - return [variableName, value]; - } - - const formatter = textFormatters[formatterName]; - - if (formatter == null) { - throw new Error( - `Couldn't find formatter ${formatterName} for variable ${variableName}`, - ); - } - - return [variableName, formatter(value.split(" "))]; - }), - ); -} diff --git a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts index 07c33d3f91..4610d69701 100644 --- a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts +++ b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts @@ -1,15 +1,13 @@ -import type { ScopeType, WrapWithSnippetArg } from "@cursorless/common"; +import type { WrapWithSnippetArg } from "@cursorless/common"; import { FlashStyle } from "@cursorless/common"; import type { Snippets } from "../core/Snippets"; +import { getPreferredSnippet } from "../core/getPreferredSnippet"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import type { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedStage } from "../processTargets/modifiers/ConditionalModifierStages"; import { ide } from "../singletons/ide.singleton"; -import { - findMatchingSnippetDefinitionStrict, - transformSnippetVariables, -} from "../snippets/snippet"; +import { transformSnippetVariables } from "../snippets/transformSnippetVariables"; import { SnippetParser } from "../snippets/vendor/vscodeSnippet/snippetParser"; import type { Target } from "../typings/target.types"; import { ensureSingleEditor, flashTargets } from "../util/targetUtils"; @@ -26,10 +24,14 @@ export default class WrapWithSnippet { this.run = this.run.bind(this); } - getFinalStages(snippet: WrapWithSnippetArg) { - const defaultScopeType = this.getScopeType(snippet); + getFinalStages(targets: Target[], snippetDescription: WrapWithSnippetArg) { + const editor = ensureSingleEditor(targets); + const snippet = getPreferredSnippet( + snippetDescription, + editor.document.languageId, + ); - if (defaultScopeType == null) { + if (snippet.scopeType == null) { return []; } @@ -38,64 +40,25 @@ export default class WrapWithSnippet { type: "modifyIfUntyped", modifier: { type: "containingScope", - scopeType: defaultScopeType, + scopeType: snippet.scopeType, }, }), ]; } - private getScopeType( - snippetDescription: WrapWithSnippetArg, - ): ScopeType | undefined { - if (snippetDescription.type === "named") { - const { name, variableName } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const variables = snippet.variables ?? {}; - const scopeTypeType = variables[variableName]?.wrapperScopeType; - return scopeTypeType == null - ? undefined - : { - type: scopeTypeType, - }; - } else { - return snippetDescription.scopeType; - } - } - - private getBody( - snippetDescription: WrapWithSnippetArg, - targets: Target[], - ): string { - if (snippetDescription.type === "named") { - const { name } = snippetDescription; - - const snippet = this.snippets.getSnippetStrict(name); - - const definition = findMatchingSnippetDefinitionStrict( - this.modifierStageFactory, - targets, - snippet.definitions, - ); - - return definition.body.join("\n"); - } else { - return snippetDescription.body; - } - } - async run( targets: Target[], snippetDescription: WrapWithSnippetArg, ): Promise { const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); + const snippet = getPreferredSnippet( + snippetDescription, + editor.document.languageId, + ); - const body = this.getBody(snippetDescription, targets); + const parsedSnippet = this.snippetParser.parse(snippet.body); - const parsedSnippet = this.snippetParser.parse(body); - - transformSnippetVariables(parsedSnippet, snippetDescription.variableName); + transformSnippetVariables(parsedSnippet, snippet.variableName); const snippetString = parsedSnippet.toTextmateString(); @@ -103,14 +66,11 @@ export default class WrapWithSnippet { const targetSelections = targets.map((target) => target.contentSelection); - const callback = () => - editor.insertSnippet(snippetString, targetSelections); - const { targetSelections: updatedTargetSelections } = await performEditsAndUpdateSelections({ rangeUpdater: this.rangeUpdater, editor, - callback, + callback: () => editor.insertSnippet(snippetString, targetSelections), preserveCursorSelections: true, selections: { targetSelections, diff --git a/packages/cursorless-engine/src/actions/actions.types.ts b/packages/cursorless-engine/src/actions/actions.types.ts index a0eeef4810..e1c93967c0 100644 --- a/packages/cursorless-engine/src/actions/actions.types.ts +++ b/packages/cursorless-engine/src/actions/actions.types.ts @@ -136,7 +136,10 @@ export interface ActionRecord extends Record { destinations: Destination[], snippetDescription: InsertSnippetArg, ): Promise; - getFinalStages(snippetDescription: InsertSnippetArg): ModifierStage[]; + getFinalStages( + destinations: Destination[], + snippetDescription: InsertSnippetArg, + ): ModifierStage[]; }; wrapWithSnippet: { @@ -144,7 +147,10 @@ export interface ActionRecord extends Record { targets: Target[], snippetDescription: WrapWithSnippetArg, ): Promise; - getFinalStages(snippetDescription: WrapWithSnippetArg): ModifierStage[]; + getFinalStages( + targets: Target[], + snippetDescription: WrapWithSnippetArg, + ): ModifierStage[]; }; editNew: { diff --git a/packages/cursorless-engine/src/core/Snippets.ts b/packages/cursorless-engine/src/core/Snippets.ts index 190cb1e9ab..b6f2311d57 100644 --- a/packages/cursorless-engine/src/core/Snippets.ts +++ b/packages/cursorless-engine/src/core/Snippets.ts @@ -1,4 +1,4 @@ -import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; +import type { TextEditor } from "@cursorless/common"; /** * Handles all cursorless snippets, including core, third-party and @@ -6,28 +6,6 @@ import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; * name. */ export interface Snippets { - updateUserSnippets(): Promise; - - /** - * Allows extensions to register third-party snippets. Calling this function - * twice with the same extensionId will replace the older snippets. - * - * Note that third-party snippets take precedence over core snippets, but - * user snippets take precedence over both. - * @param extensionId The id of the extension registering the snippets. - * @param snippets The snippets to be registered. - */ - registerThirdPartySnippets(extensionId: string, snippets: SnippetMap): void; - - /** - * Looks in merged collection of snippets for a snippet with key - * `snippetName`. Throws an exception if the snippet of the given name could - * not be found - * @param snippetName The name of the snippet to look up - * @returns The named snippet - */ - getSnippetStrict(snippetName: string): Snippet; - /** * Opens a new snippet file * @param snippetName The name of the snippet @@ -36,6 +14,6 @@ export interface Snippets { */ openNewSnippetFile( snippetName: string, - directory?: string, + directory: string, ): Promise; } diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index ede4ea49c4..812359dcbf 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -175,6 +175,7 @@ export class CommandRunnerImpl implements CommandRunner { case "insertSnippet": this.finalStages = this.actions.insertSnippet.getFinalStages( + this.getDestinations(actionDescriptor.destination), actionDescriptor.snippetDescription, ); return this.actions.insertSnippet.run( @@ -184,6 +185,7 @@ export class CommandRunnerImpl implements CommandRunner { case "wrapWithSnippet": this.finalStages = this.actions.wrapWithSnippet.getFinalStages( + this.getTargets(actionDescriptor.target), actionDescriptor.snippetDescription, ); return this.actions.wrapWithSnippet.run( diff --git a/packages/cursorless-engine/src/core/compareSnippetDefinitions.ts b/packages/cursorless-engine/src/core/compareSnippetDefinitions.ts index b10f5e8c56..1b167f0ae7 100644 --- a/packages/cursorless-engine/src/core/compareSnippetDefinitions.ts +++ b/packages/cursorless-engine/src/core/compareSnippetDefinitions.ts @@ -1,9 +1,9 @@ import type { + CustomInsertSnippetArg, + CustomWrapWithSnippetArg, SimpleScopeTypeType, - SnippetDefinition, SnippetScope, } from "@cursorless/common"; -import type { SnippetOrigin } from "./mergeSnippets"; /** * Compares two snippet definitions by how specific their scope, breaking @@ -12,23 +12,26 @@ import type { SnippetOrigin } from "./mergeSnippets"; * @param b The other snippet definition to compare * @returns A negative number if a should come before b, a positive number if b */ -export function compareSnippetDefinitions( - a: SnippetDefinitionWithOrigin, - b: SnippetDefinitionWithOrigin, -): number { - const scopeComparision = compareSnippetScopes( - a.definition.scope, - b.definition.scope, +export function compareSnippetDefinitions< + T extends CustomInsertSnippetArg | CustomWrapWithSnippetArg, +>(a: T, b: T): number { + return compareSnippetScopes( + getScopeFromSnippetDescription(a), + getScopeFromSnippetDescription(b), ); +} - // Prefer the more specific snippet definition, no matter the origin - if (scopeComparision !== 0) { - return scopeComparision; +function getScopeFromSnippetDescription( + snippet: CustomInsertSnippetArg | CustomWrapWithSnippetArg, +): SnippetScope | undefined { + if (snippet.languages != null) { + return { + langIds: snippet.languages, + // Note what is called scopeTypes in the snippet arg is the + // insertion scope. Not scope to match against like with the + // function/method snippet example. + }; } - - // If the scopes are the same, prefer the snippet from the higher priority - // origin - return a.origin - b.origin; } function compareSnippetScopes( @@ -88,8 +91,3 @@ function compareScopeTypes( return 0; } - -interface SnippetDefinitionWithOrigin { - origin: SnippetOrigin; - definition: SnippetDefinition; -} diff --git a/packages/cursorless-engine/src/core/getPreferredSnippet.ts b/packages/cursorless-engine/src/core/getPreferredSnippet.ts new file mode 100644 index 0000000000..9e11ab72fd --- /dev/null +++ b/packages/cursorless-engine/src/core/getPreferredSnippet.ts @@ -0,0 +1,63 @@ +import type { + CustomInsertSnippetArg, + CustomWrapWithSnippetArg, + InsertSnippetArg, + WrapWithSnippetArg, +} from "@cursorless/common"; +import { compareSnippetDefinitions } from "./compareSnippetDefinitions"; + +export function getPreferredSnippet( + snippetDescription: InsertSnippetArg, + languageId: string, +): CustomInsertSnippetArg; + +export function getPreferredSnippet( + snippetDescription: WrapWithSnippetArg, + languageId: string, +): CustomWrapWithSnippetArg; + +export function getPreferredSnippet( + snippetDescription: InsertSnippetArg | WrapWithSnippetArg, + languageId: string, +) { + const allSnippets = getSnippets(snippetDescription); + const filteredSnippets = filterSnippetDefinitions(allSnippets, languageId); + filteredSnippets.sort(compareSnippetDefinitions); + const preferredSnippet = filteredSnippets[0]; + + if (preferredSnippet == null) { + throw new Error("No snippet available for the current language"); + } + + return preferredSnippet; +} + +function getSnippets( + snippetDescription: InsertSnippetArg | WrapWithSnippetArg, +) { + if (snippetDescription.type === "named") { + throw new Error( + "Cursorless snippets are deprecated. Please use community snippets.", + ); + } + if (snippetDescription.type === "custom") { + return [snippetDescription]; + } + return snippetDescription.snippets; +} + +/** + * Filter snippet definitions by language. + * @param snippetDescriptions The snippets to filter + * @returns The snippets that are relevant to the current language + */ +function filterSnippetDefinitions< + T extends CustomInsertSnippetArg | CustomWrapWithSnippetArg, +>(snippetDescriptions: T[], languageId: string): T[] { + return snippetDescriptions.filter((snippetDescription) => { + if (snippetDescription.languages == null) { + return true; + } + return snippetDescription.languages.includes(languageId); + }); +} diff --git a/packages/cursorless-engine/src/core/mergeSnippets.test.ts b/packages/cursorless-engine/src/core/mergeSnippets.test.ts deleted file mode 100644 index 31a6c8d41d..0000000000 --- a/packages/cursorless-engine/src/core/mergeSnippets.test.ts +++ /dev/null @@ -1,348 +0,0 @@ -import type { SnippetMap } from "@cursorless/common"; -import { mergeSnippets } from "./mergeSnippets"; -import assert from "assert"; - -interface TestCase { - name: string; - coreSnippets?: SnippetMap; - thirdPartySnippets?: Record; - userSnippets?: SnippetMap[]; - expected: SnippetMap; -} - -const testCases: TestCase[] = [ - { - name: "should handle simple case", - coreSnippets: { - aaa: { - definitions: [ - { - body: ["aaa"], - }, - ], - }, - }, - thirdPartySnippets: { - someThirdParty: { - bbb: { - definitions: [ - { - body: ["bbb"], - }, - ], - }, - }, - }, - userSnippets: [ - { - ccc: { - definitions: [ - { - body: ["ccc"], - }, - ], - }, - }, - ], - expected: { - aaa: { - definitions: [ - { - body: ["aaa"], - }, - ], - }, - bbb: { - definitions: [ - { - body: ["bbb"], - }, - ], - }, - ccc: { - definitions: [ - { - body: ["ccc"], - }, - ], - }, - }, - }, - - { - name: "should prefer user snippets", - coreSnippets: { - aaa: { - definitions: [ - { - body: ["core aaa"], - }, - ], - description: "core snippet", - }, - }, - thirdPartySnippets: { - someThirdParty: { - aaa: { - definitions: [ - { - body: ["someThirdParty aaa"], - }, - ], - description: "someThirdParty snippet", - }, - }, - }, - userSnippets: [ - { - aaa: { - definitions: [ - { - body: ["user aaa"], - }, - ], - description: "user snippet", - }, - }, - ], - expected: { - aaa: { - definitions: [ - { - body: ["user aaa"], - }, - { - body: ["someThirdParty aaa"], - }, - { - body: ["core aaa"], - }, - ], - description: "user snippet", - }, - }, - }, - - { - name: "should prefer user snippets when scopes are the same", - coreSnippets: { - aaa: { - definitions: [ - { - body: ["core aaa"], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - ], - description: "core snippet", - }, - }, - thirdPartySnippets: { - someThirdParty: { - aaa: { - definitions: [ - { - body: ["someThirdParty aaa"], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - ], - description: "someThirdParty snippet", - }, - }, - }, - userSnippets: [ - { - aaa: { - definitions: [ - { - body: ["user aaa"], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - ], - description: "user snippet", - }, - }, - ], - expected: { - aaa: { - definitions: [ - { - body: ["user aaa"], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: ["someThirdParty aaa"], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: ["core aaa"], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - ], - description: "user snippet", - }, - }, - }, - - { - name: "should prefer more specific snippets, even if they are from a lower priority origin", - coreSnippets: { - aaa: { - definitions: [ - { - body: ["core aaa"], - scope: { - langIds: ["typescript"], - }, - }, - ], - description: "core snippet", - }, - }, - userSnippets: [ - { - aaa: { - definitions: [ - { - body: ["user aaa"], - }, - ], - description: "user snippet", - }, - }, - ], - expected: { - aaa: { - definitions: [ - { - body: ["core aaa"], - scope: { - langIds: ["typescript"], - }, - }, - { - body: ["user aaa"], - }, - ], - description: "user snippet", - }, - }, - }, - - { - name: "should prefer snippets based on specificity", - coreSnippets: { - aaa: { - definitions: [ - { - body: [""], - }, - { - body: [""], - scope: { - langIds: ["typescript"], - }, - }, - { - body: [""], - scope: { - langIds: ["typescript", "javascript"], - }, - }, - { - body: [""], - scope: { - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: [""], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: [""], - scope: { - langIds: ["typescript", "javascript"], - scopeTypes: ["anonymousFunction"], - }, - }, - ], - }, - }, - expected: { - aaa: { - definitions: [ - { - body: [""], - scope: { - langIds: ["typescript"], - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: [""], - scope: { - langIds: ["typescript", "javascript"], - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: [""], - scope: { - langIds: ["typescript"], - }, - }, - { - body: [""], - scope: { - langIds: ["typescript", "javascript"], - }, - }, - { - body: [""], - scope: { - scopeTypes: ["anonymousFunction"], - }, - }, - { - body: [""], - }, - ], - }, - }, - }, -]; - -suite("mergeSnippets", function () { - for (const testCase of testCases) { - test(testCase.name, function () { - const actual = mergeSnippets( - testCase.coreSnippets ?? {}, - testCase.thirdPartySnippets ?? {}, - testCase.userSnippets ?? [], - ); - - assert.deepStrictEqual(actual, testCase.expected); - }); - } -}); diff --git a/packages/cursorless-engine/src/core/mergeSnippets.ts b/packages/cursorless-engine/src/core/mergeSnippets.ts deleted file mode 100644 index e00bdb088e..0000000000 --- a/packages/cursorless-engine/src/core/mergeSnippets.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Snippet, SnippetMap } from "@cursorless/common"; -import { cloneDeep, groupBy, mapValues, merge } from "lodash-es"; -import { compareSnippetDefinitions } from "./compareSnippetDefinitions"; - -export function mergeSnippets( - coreSnippets: SnippetMap, - thirdPartySnippets: Record, - userSnippets: SnippetMap[], -): SnippetMap { - const mergedSnippets: SnippetMap = {}; - - // We make a merged map where we map every key to an array of all snippets - // with that key, whether they are core, third-party, or user snippets. - const mergedMap = mapValues( - groupBy( - [ - ...prepareSnippetsFromOrigin(SnippetOrigin.core, coreSnippets), - ...prepareSnippetsFromOrigin( - SnippetOrigin.thirdParty, - ...Object.values(thirdPartySnippets), - ), - ...prepareSnippetsFromOrigin(SnippetOrigin.user, ...userSnippets), - ], - ([key]) => key, - ), - (entries) => entries.map(([, value]) => value), - ); - - Object.entries(mergedMap).forEach(([key, snippets]) => { - const mergedSnippet: Snippet = merge( - {}, - // We sort the snippets by origin as (core, third-party, user) so that - // when we merge them, the user snippets will override the third-party - // snippets, which will override the core snippets. - ...snippets - .sort((a, b) => a.origin - b.origin) - .map(({ snippet }) => snippet), - ); - - // We sort the definitions by decreasing precedence, so that earlier - // definitions will be chosen before later definitions when we're choosing a - // definition for a given target context. - mergedSnippet.definitions = snippets - .flatMap(({ origin, snippet }) => - snippet.definitions.map((definition) => ({ origin, definition })), - ) - .sort((a, b) => -compareSnippetDefinitions(a, b)) - .map(({ definition }) => definition); - - mergedSnippets[key] = mergedSnippet; - }); - - return mergedSnippets; -} - -/** - * Prepares the given snippet maps for merging by adding the given origin to - * each snippet. - * @param origin The origin of the snippets - * @param snippetMaps The snippet maps from the given origin - * @returns An array of entries of the form [key, {origin, snippet}] - */ -function prepareSnippetsFromOrigin( - origin: SnippetOrigin, - ...snippetMaps: SnippetMap[] -) { - return snippetMaps - .map((snippetMap) => - mapValues(cloneDeep(snippetMap), (snippet) => ({ - origin, - snippet, - })), - ) - .flatMap((snippetMap) => Object.entries(snippetMap)); -} - -export enum SnippetOrigin { - core = 0, - thirdParty = 1, - user = 2, -} diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts index af33393be1..cf40f8f75f 100644 --- a/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts +++ b/packages/cursorless-engine/src/disabledComponents/DisabledSnippets.ts @@ -1,25 +1,10 @@ -import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; +import type { TextEditor } from "@cursorless/common"; import type { Snippets } from "../core/Snippets"; export class DisabledSnippets implements Snippets { - updateUserSnippets(): Promise { - throw new Error("Snippets are not implemented."); - } - - registerThirdPartySnippets( - _extensionId: string, - _snippets: SnippetMap, - ): void { - throw new Error("Snippets are not implemented."); - } - - getSnippetStrict(_snippetName: string): Snippet { - throw new Error("Snippets are not implemented."); - } - openNewSnippetFile( _snippetName: string, - _directory?: string, + _directory: string, ): Promise { throw new Error("Snippets are not implemented."); } diff --git a/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/snippets.ts b/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/snippets.ts index 808d36e52e..ab16d35e7c 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/snippets.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/snippets.ts @@ -1,52 +1,14 @@ import type { InsertSnippetArg, WrapWithSnippetArg } from "@cursorless/common"; import { NoSpokenFormError } from "../NoSpokenFormError"; -const insertionSnippets: Record = { - ifStatement: "if", - ifElseStatement: "if else", - tryCatchStatement: "try", - functionDeclaration: "funk", - link: "link", -}; - -const wrapperSnippets: Record = { - "ifElseStatement.alternative": "else", - "functionDeclaration.body": "funk", - "ifElseStatement.consequence": "if else", - "ifStatement.consequence": "if", - "tryCatchStatement.body": "try", - "link.text": "link", -}; - export function insertionSnippetToSpokenForm( snippetDescription: InsertSnippetArg, ): string { - if (snippetDescription.type === "custom") { - throw new NoSpokenFormError("Custom insertion snippet"); - } - const result = insertionSnippets[snippetDescription.name]; - if (result == null) { - throw new NoSpokenFormError( - `Named insertion snippet '${snippetDescription.name}'`, - ); - } - if (snippetDescription.substitutions != null) { - const values = Object.values(snippetDescription.substitutions); - return `${result} ${values.join(" ")}`; - } - return result; + throw new NoSpokenFormError(`${snippetDescription.type} insertion snippet`); } export function wrapperSnippetToSpokenForm( snippetDescription: WrapWithSnippetArg, ): string { - if (snippetDescription.type === "custom") { - throw new NoSpokenFormError("Custom wrap with snippet"); - } - const name = `${snippetDescription.name}.${snippetDescription.variableName}`; - const result = wrapperSnippets[name]; - if (result == null) { - throw new NoSpokenFormError(`Named wrap with snippet '${name}'`); - } - return result; + throw new NoSpokenFormError(`${snippetDescription.type} wrap with snippet`); } diff --git a/packages/cursorless-engine/src/index.ts b/packages/cursorless-engine/src/index.ts index 0fde5045ea..e00aaf5fce 100644 --- a/packages/cursorless-engine/src/index.ts +++ b/packages/cursorless-engine/src/index.ts @@ -3,7 +3,6 @@ export * from "./CommandHistory"; export * from "./CommandHistoryAnalyzer"; export * from "./CommandRunner"; export * from "./core/commandVersionUpgrades/canonicalizeAndValidateCommand"; -export * from "./core/mergeSnippets"; export * from "./core/Snippets"; export * from "./core/StoredTargets"; export * from "./cursorlessEngine"; diff --git a/packages/cursorless-engine/src/snippets/snippet.ts b/packages/cursorless-engine/src/snippets/snippet.ts deleted file mode 100644 index 5502a6a21f..0000000000 --- a/packages/cursorless-engine/src/snippets/snippet.ts +++ /dev/null @@ -1,207 +0,0 @@ -import type { - SimpleScopeTypeType, - SnippetDefinition, -} from "@cursorless/common"; -import type { Target } from "../typings/target.types"; -import type { TextmateSnippet } from "./vendor/vscodeSnippet/snippetParser"; -import { - Placeholder, - Text, - Variable, -} from "./vendor/vscodeSnippet/snippetParser"; -import { KnownSnippetVariableNames } from "./vendor/vscodeSnippet/snippetVariables"; -import type { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; - -/** - * Replaces the snippet variable with name `placeholderName` with - * TM_SELECTED_TEXT - * - * Also replaces any unknown variables with placeholders. We do this so it's - * easier to leave one of the placeholders blank. We may make it so that you can - * disable this with a setting in the future - * @param parsedSnippet The parsed textmate snippet to operate on - * @param placeholderName The variable name to replace with TM_SELECTED_TEXT - * @param substitutions A map from variable names to text values that will be - * substituted and the given variable will no longer be a placeholder in the - * final snippet - */ -export function transformSnippetVariables( - parsedSnippet: TextmateSnippet, - placeholderName?: string | null, - substitutions?: Record, -): void { - let nextPlaceholderIndex = getMaxPlaceholderIndex(parsedSnippet) + 1; - const placeholderIndexMap: Record = {}; - - parsedSnippet.walk((candidate) => { - if (candidate instanceof Variable) { - if (candidate.name === placeholderName) { - candidate.name = "TM_SELECTED_TEXT"; - } else if ( - substitutions != null && - Object.prototype.hasOwnProperty.call(substitutions, candidate.name) - ) { - candidate.parent.replace(candidate, [ - new Text(substitutions[candidate.name]), - ]); - } else if (!KnownSnippetVariableNames[candidate.name]) { - let placeholderIndex: number; - if (candidate.name in placeholderIndexMap) { - placeholderIndex = placeholderIndexMap[candidate.name]; - } else { - placeholderIndex = nextPlaceholderIndex++; - placeholderIndexMap[candidate.name] = placeholderIndex; - } - const placeholder = new Placeholder(placeholderIndex); - candidate.children.forEach((child) => placeholder.appendChild(child)); - candidate.parent.replace(candidate, [placeholder]); - } - } else if (candidate instanceof Placeholder) { - if (candidate.index.toString() === placeholderName) { - candidate.parent.replace(candidate, [new Variable("TM_SELECTED_TEXT")]); - } - } - return true; - }); -} - -/** - * Returns the highest placeholder index in the given snippet - * @param parsedSnippet The parsed textmate snippet - * @returns The highest placeholder index in the given snippet - */ -function getMaxPlaceholderIndex(parsedSnippet: TextmateSnippet): number { - let placeholderIndex = 0; - parsedSnippet.walk((candidate) => { - if (candidate instanceof Placeholder) { - placeholderIndex = Math.max(placeholderIndex, candidate.index); - } - return true; - }); - return placeholderIndex; -} - -/** - * Based on the context determined by {@link targets} (eg the file's language - * id and containing scope), finds the first snippet definition that matches the - * given context. Throws an error if different snippet definitions match for - * different targets or if matching snippet definition could not be found - * @param targets The target that defines the context to use for finding the - * right snippet definition - * @param definitions The list of snippet definitions to search - * @returns The snippet definition that matches the given context - */ -export function findMatchingSnippetDefinitionStrict( - modifierStageFactory: ModifierStageFactory, - targets: Target[], - definitions: SnippetDefinition[], -): SnippetDefinition { - const definitionIndices = targets.map((target) => - findMatchingSnippetDefinitionForSingleTarget( - modifierStageFactory, - target, - definitions, - ), - ); - - const definitionIndex = definitionIndices[0]; - - if (!definitionIndices.every((index) => index === definitionIndex)) { - throw new Error("Multiple snippet definitions match the given context"); - } - - if (definitionIndex === -1) { - throw new Error("Couldn't find matching snippet definition"); - } - - return definitions[definitionIndex]; -} - -/** - * Based on the context determined by {@link target} (eg the file's language id - * and containing scope), finds the best snippet definition that matches the - * given context. Returns -1 if no matching snippet definition could be found. - * - * We assume that the definitions are sorted in precedence order, so we just - * return the first match we find. - * - * @param modifierStageFactory For creating containing scope modifiers - * @param target The target to find a matching snippet definition for - * @param definitions The list of snippet definitions to search - * @returns The index of the best snippet definition that matches the given - * target, or -1 if no matching snippet definition could be found - */ -function findMatchingSnippetDefinitionForSingleTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - definitions: SnippetDefinition[], -): number { - const languageId = target.editor.document.languageId; - - // We want to find the first definition that matches the given context. - // Note that we just use the first match we find because the definitions are - // guaranteed to come sorted in precedence order. - return definitions.findIndex(({ scope }) => { - if (scope == null) { - return true; - } - - const { langIds, scopeTypes, excludeDescendantScopeTypes } = scope; - - if (langIds != null && !langIds.includes(languageId)) { - return false; - } - - if (scopeTypes != null) { - const allScopeTypes = scopeTypes.concat( - excludeDescendantScopeTypes ?? [], - ); - let matchingTarget: Target | undefined = undefined; - let matchingScopeType: SimpleScopeTypeType | undefined = undefined; - for (const scopeTypeType of allScopeTypes) { - try { - let containingTarget = modifierStageFactory - .create({ - type: "containingScope", - scopeType: { type: scopeTypeType }, - }) - .run(target)[0]; - - if (target.contentRange.isRangeEqual(containingTarget.contentRange)) { - // Skip this scope if the target is exactly the same as the - // containing scope, otherwise wrapping won't work, because we're - // really outside the containing scope when we're wrapping - containingTarget = modifierStageFactory - .create({ - type: "containingScope", - scopeType: { type: scopeTypeType }, - ancestorIndex: 1, - }) - .run(target)[0]; - } - - if ( - matchingTarget == null || - matchingTarget.contentRange.contains(containingTarget.contentRange) - ) { - matchingTarget = containingTarget; - matchingScopeType = scopeTypeType; - } - } catch (_e) { - continue; - } - } - - if (matchingScopeType == null) { - return false; - } - - return ( - matchingTarget != null && - !(excludeDescendantScopeTypes ?? []).includes(matchingScopeType) - ); - } - - return true; - }); -} diff --git a/packages/cursorless-engine/src/snippets/transformSnippetVariables.ts b/packages/cursorless-engine/src/snippets/transformSnippetVariables.ts new file mode 100644 index 0000000000..f8f213715c --- /dev/null +++ b/packages/cursorless-engine/src/snippets/transformSnippetVariables.ts @@ -0,0 +1,83 @@ +import type { TextmateSnippet } from "./vendor/vscodeSnippet/snippetParser"; +import { + Placeholder, + Text, + Variable, +} from "./vendor/vscodeSnippet/snippetParser"; +import { KnownSnippetVariableNames } from "./vendor/vscodeSnippet/snippetVariables"; + +/** + * Replaces the snippet variable with name `placeholderName` with + * TM_SELECTED_TEXT + * + * Also replaces any unknown variables with placeholders. We do this so it's + * easier to leave one of the placeholders blank. We may make it so that you can + * disable this with a setting in the future + * @param parsedSnippet The parsed textmate snippet to operate on + * @param placeholderName The variable name to replace with TM_SELECTED_TEXT + * @param substitutions A map from variable names to text values that will be + * substituted and the given variable will no longer be a placeholder in the + * final snippet + */ +export function transformSnippetVariables( + parsedSnippet: TextmateSnippet, + placeholderName?: string | null, + substitutions?: Record, +): void { + let nextPlaceholderIndex = getMaxPlaceholderIndex(parsedSnippet) + 1; + const placeholderIndexMap: Record = {}; + + parsedSnippet.walk((candidate) => { + if (candidate instanceof Variable) { + if (candidate.name === placeholderName) { + candidate.name = "TM_SELECTED_TEXT"; + } else if ( + substitutions != null && + Object.prototype.hasOwnProperty.call(substitutions, candidate.name) + ) { + candidate.parent.replace(candidate, [ + new Text(substitutions[candidate.name]), + ]); + } else if (!KnownSnippetVariableNames[candidate.name]) { + let placeholderIndex: number; + if (candidate.name in placeholderIndexMap) { + placeholderIndex = placeholderIndexMap[candidate.name]; + } else { + placeholderIndex = nextPlaceholderIndex++; + placeholderIndexMap[candidate.name] = placeholderIndex; + } + const placeholder = new Placeholder(placeholderIndex); + candidate.children.forEach((child) => placeholder.appendChild(child)); + candidate.parent.replace(candidate, [placeholder]); + } + } else if (candidate instanceof Placeholder) { + if (candidate.index.toString() === placeholderName) { + candidate.parent.replace(candidate, [new Variable("TM_SELECTED_TEXT")]); + } else if ( + substitutions != null && + Object.prototype.hasOwnProperty.call(substitutions, candidate.index) + ) { + candidate.parent.replace(candidate, [ + new Text(substitutions[candidate.index]), + ]); + } + } + return true; + }); +} + +/** + * Returns the highest placeholder index in the given snippet + * @param parsedSnippet The parsed textmate snippet + * @returns The highest placeholder index in the given snippet + */ +function getMaxPlaceholderIndex(parsedSnippet: TextmateSnippet): number { + let placeholderIndex = 0; + parsedSnippet.walk((candidate) => { + if (candidate instanceof Placeholder) { + placeholderIndex = Math.max(placeholderIndex, candidate.index); + } + return true; + }); + return placeholderIndex; +} diff --git a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts index d9ccddd191..d205429c71 100644 --- a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts +++ b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts @@ -1,7 +1,7 @@ import type { ActionDescriptor } from "@cursorless/common"; import { spokenFormTest } from "./spokenFormTest"; -const verticalRangeAction: ActionDescriptor = { +const snippetAfterAction: ActionDescriptor = { name: "insertSnippet", destination: { type: "primitive", @@ -16,8 +16,13 @@ const verticalRangeAction: ActionDescriptor = { }, }, snippetDescription: { - body: "```\n$0\n```", - type: "custom", + type: "list", + snippets: [ + { + type: "custom", + body: "```\n$0\n```", + }, + ], }, }; @@ -27,7 +32,5 @@ const verticalRangeAction: ActionDescriptor = { * Talon tests by relying on our recorded test fixtures alone. */ export const communitySnippetsSpokenFormsFixture = [ - spokenFormTest("snippet code after air", verticalRangeAction, undefined, { - useCommunitySnippets: true, - }), + spokenFormTest("snippet code after air", snippetAfterAction, undefined), ]; diff --git a/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts b/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts index fc12a4b850..4394cf0a66 100644 --- a/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts +++ b/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts @@ -23,24 +23,17 @@ export interface SpokenFormTest { * {@link spokenForm} is spoken. */ commands: CommandV6[]; - - /** - * If `true`, use community snippets instead of Cursorless snippets - */ - useCommunitySnippets: boolean; } export function spokenFormTest( spokenForm: string, action: ActionDescriptor, mockedGetValue?: unknown, - { useCommunitySnippets = false }: SpokenFormTestOpts = {}, ): SpokenFormTest { return { spokenForm, mockedGetValue: wrapMockedGetValue(mockedGetValue), commands: [command(spokenForm, action)], - useCommunitySnippets, }; } @@ -48,13 +41,11 @@ export function multiActionSpokenFormTest( spokenForm: string, actions: ActionDescriptor[], mockedGetValue?: unknown, - { useCommunitySnippets = false }: SpokenFormTestOpts = {}, ): SpokenFormTest { return { spokenForm, mockedGetValue: wrapMockedGetValue(mockedGetValue), commands: actions.map((action) => command(spokenForm, action)), - useCommunitySnippets, }; } @@ -72,7 +63,3 @@ function command(spokenForm: string, action: ActionDescriptor): CommandV6 { action, }; } - -export interface SpokenFormTestOpts { - useCommunitySnippets?: boolean; -} diff --git a/packages/cursorless-engine/src/test/fixtures/talonApi.fixture.ts b/packages/cursorless-engine/src/test/fixtures/talonApi.fixture.ts index fac4504e5d..71a8c17d57 100644 --- a/packages/cursorless-engine/src/test/fixtures/talonApi.fixture.ts +++ b/packages/cursorless-engine/src/test/fixtures/talonApi.fixture.ts @@ -60,14 +60,6 @@ const insertSnippetWithScopeAction: ActionDescriptor = { scopeTypes: [{ type: "statement" }], }, }; -const insertSnippetByNameAction: ActionDescriptor = { - name: "insertSnippet", - destination: { type: "implicit" }, - snippetDescription: { - type: "named", - name: "functionDeclaration", - }, -}; const wrapWithSnippetAction: ActionDescriptor = { name: "wrapWithSnippet", target: { @@ -81,18 +73,6 @@ const wrapWithSnippetAction: ActionDescriptor = { scopeType: { type: "statement" }, }, }; -const wrapWithSnippetByNameAction: ActionDescriptor = { - name: "wrapWithSnippet", - target: { - type: "primitive", - mark: { type: "cursor" }, - }, - snippetDescription: { - type: "named", - name: "functionDeclaration", - variableName: "body", - }, -}; const alternateHighlightAirAndBatAction: ActionDescriptor = { name: "highlight", target: { @@ -212,12 +192,7 @@ export const talonApiFixture = [ "test api insert snippet after air", insertSnippetWithScopeAction, ), - spokenFormTest("test api insert snippet by name", insertSnippetByNameAction), spokenFormTest("test api wrap with snippet this", wrapWithSnippetAction), - spokenFormTest( - "test api wrap with snippet by name this", - wrapWithSnippetByNameAction, - ), spokenFormTest( "test api get text air", getTextAction({ showDecorations: true, ensureSingleTarget: true }), diff --git a/packages/cursorless-engine/src/test/spokenForms.talon.test.ts b/packages/cursorless-engine/src/test/spokenForms.talon.test.ts index ccadecb3d8..e4a494d4ec 100644 --- a/packages/cursorless-engine/src/test/spokenForms.talon.test.ts +++ b/packages/cursorless-engine/src/test/spokenForms.talon.test.ts @@ -13,7 +13,6 @@ import { getHatMapCommand } from "../generateSpokenForm/getHatMapCommand"; import { TalonRepl } from "../testUtil/TalonRepl"; import { communitySnippetsSpokenFormsFixture } from "./fixtures/communitySnippets.fixture"; import { multiActionFixture } from "./fixtures/multiAction.fixture"; -import type { SpokenFormTestOpts } from "./fixtures/spokenFormTest"; import { synonymousSpokenFormsFixture } from "./fixtures/synonymousSpokenForms.fixture"; import { talonApiFixture } from "./fixtures/talonApi.fixture"; @@ -45,12 +44,8 @@ suite("Talon spoken forms", async function () { ...talonApiFixture, ...multiActionFixture, ...communitySnippetsSpokenFormsFixture, - ].forEach(({ spokenForm, commands, mockedGetValue, useCommunitySnippets }) => - test(spokenForm, () => - runTest(repl, spokenForm, commands, mockedGetValue, { - useCommunitySnippets, - }), - ), + ].forEach(({ spokenForm, commands, mockedGetValue }) => + test(spokenForm, () => runTest(repl, spokenForm, commands, mockedGetValue)), ); }); @@ -83,7 +78,6 @@ async function runTest( spokenForm: string, commandsLegacy: Command[], mockedGetValue?: unknown, - { useCommunitySnippets = false }: SpokenFormTestOpts = {}, ) { const commandsExpected = commandsLegacy.map((command) => ({ ...canonicalizeAndValidateCommand(command), @@ -109,37 +103,24 @@ async function runTest( ? "None" : JSON.stringify(JSON.stringify(mockedGetValue)); - if (useCommunitySnippets) { - await repl.action(`user.private_cursorless_use_community_snippets(True)`); - } + const result = await repl.action( + `user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`, + ); - try { - const result = await repl.action( - `user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`, - ); - - const commandsActual = (() => { - try { - return JSON.parse(result); - } catch (_e) { - throw Error(result); - } - })(); - - assert.deepStrictEqual(commandsActual, commandsExpected); - } finally { - if (useCommunitySnippets) { - await repl.action( - `user.private_cursorless_use_community_snippets(False)`, - ); + const commandsActual = (() => { + try { + return JSON.parse(result); + } catch (_e) { + throw Error(result); } - } + })(); + + assert.deepStrictEqual(commandsActual, commandsExpected); } async function setTestMode(repl: TalonRepl, enabled: boolean) { const arg = enabled ? "True" : "False"; await repl.action(`user.private_cursorless_spoken_form_test_mode(${arg})`); - await repl.action(`user.private_cursorless_use_community_snippets(False)`); // If you have warnings in your talon user files, they will be printed to the // repl when you run the above action. We need to eat them so that they don't diff --git a/packages/cursorless-vscode-e2e/src/suite/wrapWithSnippetAcrossSplit.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/wrapWithSnippetAcrossSplit.vscode.test.ts index 7cffec5001..413b80b7e9 100644 --- a/packages/cursorless-vscode-e2e/src/suite/wrapWithSnippetAcrossSplit.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/wrapWithSnippetAcrossSplit.vscode.test.ts @@ -25,10 +25,15 @@ async function runTest() { await hatTokenMap.allocateHats(); await runCursorlessCommand({ - version: 4, - action: { name: "wrapWithSnippet", args: ["spaghetti.foo"] }, - targets: [ - { + version: 7, + action: { + name: "wrapWithSnippet", + snippetDescription: { + type: "custom", + body: "My friend $foo likes to eat spaghetti!", + variableName: "foo", + }, + target: { type: "primitive", mark: { type: "decoratedSymbol", @@ -36,7 +41,7 @@ async function runTest() { character: "h", }, }, - ], + }, usePrePhraseSnapshot: false, }); diff --git a/packages/cursorless-vscode/src/VscodeSnippets.ts b/packages/cursorless-vscode/src/VscodeSnippets.ts index 4514809b3b..7ea83f8c93 100644 --- a/packages/cursorless-vscode/src/VscodeSnippets.ts +++ b/packages/cursorless-vscode/src/VscodeSnippets.ts @@ -1,18 +1,12 @@ -import type { Snippet, SnippetMap, TextEditor } from "@cursorless/common"; -import { mergeStrict, showError, type IDE } from "@cursorless/common"; -import { mergeSnippets, type Snippets } from "@cursorless/cursorless-engine"; +import type { TextEditor } from "@cursorless/common"; +import { type IDE } from "@cursorless/common"; +import { type Snippets } from "@cursorless/cursorless-engine"; import { walkFiles } from "@cursorless/node-common"; -import { max } from "lodash-es"; -import { open, readFile, stat } from "node:fs/promises"; +import { open } from "node:fs/promises"; import { join } from "node:path"; +// DEPRECATED @ 2025-02-01 export const CURSORLESS_SNIPPETS_SUFFIX = ".cursorless-snippets"; -const SNIPPET_DIR_REFRESH_INTERVAL_MS = 1000; - -interface DirectoryErrorMessage { - directory: string; - errorMessage: string; -} /** * Handles all cursorless snippets, including core, third-party and @@ -20,236 +14,29 @@ interface DirectoryErrorMessage { * name. */ export class VscodeSnippets implements Snippets { - private coreSnippets!: SnippetMap; - private thirdPartySnippets: Record = {}; - private userSnippets!: SnippetMap[]; - - private mergedSnippets!: SnippetMap; - private userSnippetsDir?: string; - /** - * The maximum modification time of any snippet in user snippets dir. - * - * This variable will be set to -1 if no user snippets have yet been read or - * if the user snippets path has changed. - * - * This variable will be set to 0 if the user has no snippets dir configured and - * we've already set userSnippets to {}. - */ - private maxSnippetMtimeMs: number = -1; - - /** - * If the user has misconfigured their snippet dir, then we keep track of it - * so that we can show them the error message if we can't find a snippet - * later, and so that we don't show them the same error message every time - * we try to poll the directory. - */ - private directoryErrorMessage: DirectoryErrorMessage | null | undefined = - null; - constructor(private ide: IDE) { this.updateUserSnippetsPath(); - this.updateUserSnippets = this.updateUserSnippets.bind(this); - this.registerThirdPartySnippets = - this.registerThirdPartySnippets.bind(this); - - const timer = setInterval( - this.updateUserSnippets, - SNIPPET_DIR_REFRESH_INTERVAL_MS, - ); - this.ide.disposeOnExit( - this.ide.configuration.onDidChangeConfiguration(() => { - if (this.updateUserSnippetsPath()) { - void this.updateUserSnippets(); - } - }), - { - dispose() { - clearInterval(timer); - }, - }, - ); - } - - async init() { - const extensionPath = this.ide.assetsRoot; - const snippetsDir = join(extensionPath, "cursorless-snippets"); - const snippetFiles = await this.getSnippetPaths(snippetsDir); - this.coreSnippets = mergeStrict( - ...(await Promise.all( - snippetFiles.map(async (path) => - JSON.parse(await readFile(path, "utf8")), - ), - )), + this.ide.configuration.onDidChangeConfiguration(() => + this.updateUserSnippetsPath(), + ), ); - await this.updateUserSnippets(); } - /** - * Updates the userSnippetsDir field if it has change, returning a boolean - * indicating whether there was an update. If there was an update, resets the - * maxSnippetMtime to -1 to ensure snippet update. - * @returns Boolean indicating whether path has changed - */ - private updateUserSnippetsPath(): boolean { - const newUserSnippetsDir = this.ide.configuration.getOwnConfiguration( + private updateUserSnippetsPath() { + this.userSnippetsDir = this.ide.configuration.getOwnConfiguration( "experimental.snippetsDir", ); - - if (newUserSnippetsDir === this.userSnippetsDir) { - return false; - } - - // Reset mtime to -1 so that next time we'll update the snippets - this.maxSnippetMtimeMs = -1; - - this.userSnippetsDir = newUserSnippetsDir; - - return true; - } - - async updateUserSnippets() { - let snippetFiles: string[]; - try { - snippetFiles = this.userSnippetsDir - ? await this.getSnippetPaths(this.userSnippetsDir) - : []; - } catch (err) { - if (this.directoryErrorMessage?.directory !== this.userSnippetsDir) { - // NB: We suppress error messages once we've shown it the first time - // because we poll the directory every second and want to make sure we - // don't show the same error message repeatedly - const errorMessage = `Error with cursorless snippets dir "${ - this.userSnippetsDir - }": ${(err as Error).message}`; - - void showError(this.ide.messages, "snippetsDirError", errorMessage); - - this.directoryErrorMessage = { - directory: this.userSnippetsDir!, - errorMessage, - }; - } - - this.userSnippets = []; - this.mergeSnippets(); - - return; - } - - this.directoryErrorMessage = null; - - const maxSnippetMtime = - max( - (await Promise.all(snippetFiles.map((file) => stat(file)))).map( - (stat) => stat.mtimeMs, - ), - ) ?? 0; - - if (maxSnippetMtime <= this.maxSnippetMtimeMs) { - return; - } - - this.maxSnippetMtimeMs = maxSnippetMtime; - - this.userSnippets = await Promise.all( - snippetFiles.map(async (path) => { - try { - const content = await readFile(path, "utf8"); - - if (content.length === 0) { - // Gracefully handle an empty file - return {}; - } - - return JSON.parse(content); - } catch (err) { - void showError( - this.ide.messages, - "snippetsFileError", - `Error with cursorless snippets file "${path}": ${ - (err as Error).message - }`, - ); - - // We don't want snippets from all files to stop working if there is - // a parse error in one file, so we just effectively ignore this file - // once we've shown an error message - return {}; - } - }), - ); - - this.mergeSnippets(); - } - - /** - * Allows extensions to register third-party snippets. Calling this function - * twice with the same extensionId will replace the older snippets. - * - * Note that third-party snippets take precedence over core snippets, but - * user snippets take precedence over both. - * @param extensionId The id of the extension registering the snippets. - * @param snippets The snippets to be registered. - */ - registerThirdPartySnippets(extensionId: string, snippets: SnippetMap) { - this.thirdPartySnippets[extensionId] = snippets; - this.mergeSnippets(); - } - - /** - * Merge core, third-party, and user snippets, with precedence user > third - * party > core. - */ - private mergeSnippets() { - this.mergedSnippets = mergeSnippets( - this.coreSnippets, - this.thirdPartySnippets, - this.userSnippets, - ); - } - - /** - * Looks in merged collection of snippets for a snippet with key - * `snippetName`. Throws an exception if the snippet of the given name could - * not be found - * @param snippetName The name of the snippet to look up - * @returns The named snippet - */ - getSnippetStrict(snippetName: string): Snippet { - const snippet = this.mergedSnippets[snippetName]; - - if (snippet == null) { - let errorMessage = `Couldn't find snippet ${snippetName}. `; - - if (this.directoryErrorMessage != null) { - errorMessage += `This could be due to: ${this.directoryErrorMessage.errorMessage}.`; - } - - throw Error(errorMessage); - } - - return snippet; } async openNewSnippetFile( snippetName: string, - directory?: string, + directory: string, ): Promise { - const path = (() => { - if (directory != null) { - return join(directory, `${snippetName}.snippet`); - } - - return join( - this.getUserDirectoryStrict(), - `${snippetName}.cursorless-snippets`, - ); - })(); - + const path = join(directory, `${snippetName}.snippet`); await touch(path); return this.ide.openTextDocument(path); } diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index a3995a33ce..d6ff7f83cd 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -97,10 +97,7 @@ export async function activate( const treeSitter = createTreeSitter(parseTreeApi); const talonSpokenForms = new FileSystemTalonSpokenForms(fileSystem); - // NOTE: do not await on snippet loading and hats initialization because we don't want to - // block extension activation const snippets = new VscodeSnippets(normalizedIde); - void snippets.init(); const treeSitterQueryProvider = new FileSystemRawTreeSitterQueryProvider( normalizedIde, @@ -217,10 +214,6 @@ export async function activate( vscodeTutorial, ) : undefined, - - experimental: { - registerThirdPartySnippets: snippets.registerThirdPartySnippets, - }, }; } diff --git a/packages/vscode-common/src/getExtensionApi.ts b/packages/vscode-common/src/getExtensionApi.ts index 4aefc4a6e0..2549c52ac0 100644 --- a/packages/vscode-common/src/getExtensionApi.ts +++ b/packages/vscode-common/src/getExtensionApi.ts @@ -1,17 +1,10 @@ -import type { CommandServerApi, SnippetMap } from "@cursorless/common"; +import type { CommandServerApi } from "@cursorless/common"; import * as vscode from "vscode"; import type { Language, SyntaxNode, Tree } from "web-tree-sitter"; import type { VscodeTestHelpers } from "./TestHelpers"; export interface CursorlessApi { testHelpers: VscodeTestHelpers | undefined; - - experimental: { - registerThirdPartySnippets: ( - extensionId: string, - snippets: SnippetMap, - ) => void; - }; } export interface ParseTreeApi {