diff --git a/.vscode/launch.json b/.vscode/launch.json index 7bfd69da44..9fa7d9f27f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -41,6 +41,27 @@ "!**/node_modules/**" ] }, + { + "name": "Run Test Subset", + "type": "extensionHost", + "request": "launch", + "env": { + "CURSORLESS_TEST": "true", + "CURSORLESS_RUN_TEST_SUBSET": "true" + }, + "args": [ + "--extensions-dir=${workspaceFolder}/.vscode-sandbox/extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "${defaultBuildTask}", + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!${workspaceFolder}/.vscode-sandbox/**", + "!**/node_modules/**" + ] + }, { "name": "Update fixtures", "type": "extensionHost", @@ -62,6 +83,28 @@ "!**/node_modules/**" ] }, + { + "name": "Update fixtures subset", + "type": "extensionHost", + "request": "launch", + "env": { + "CURSORLESS_TEST": "true", + "CURSORLESS_TEST_UPDATE_FIXTURES": "true", + "CURSORLESS_RUN_TEST_SUBSET": "true" + }, + "args": [ + "--extensions-dir=${workspaceFolder}/.vscode-sandbox/extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "${defaultBuildTask}", + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!${workspaceFolder}/.vscode-sandbox/**", + "!**/node_modules/**" + ] + }, { "name": "Docusaurus Start (Debug)", "type": "node", diff --git a/.vscodeignore b/.vscodeignore index 135d5e9483..bf66ead03b 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,16 +1,13 @@ -.vscode/** -.vscode-test/** -src/** -.gitignore -.yarnrc -vsc-extension-quickstart.md -**/tsconfig.json -**/.eslintrc.json -**/*.map -**/*.ts -node_modules/** -.vscode-sandbox/** -out/** -dist/** +** +!CHANGELOG.md +!cursorless-snippets/** !dist/extension.js -website/** +!images/hats/** +!images/icon.png +!LICENSE +!NOTICE.md +!package.json +!README.md +!schemas/** +!third-party-licenses.csv +!build-info.json diff --git a/cursorless-talon/src/actions/actions.py b/cursorless-talon/src/actions/actions.py index ef9d8b71d5..56bdeb0192 100644 --- a/cursorless-talon/src/actions/actions.py +++ b/cursorless-talon/src/actions/actions.py @@ -4,7 +4,7 @@ from .actions_callback import callback_action_defaults, callback_action_map from .actions_custom import custom_action_defaults from .actions_makeshift import makeshift_action_defaults, makeshift_action_map -from .actions_simple import simple_action_defaults +from .actions_simple import positional_action_defaults, simple_action_defaults mod = Module() @@ -79,6 +79,7 @@ def vscode_command_no_wait(command_id: str, target: dict, command_options: dict default_values = { "simple_action": simple_action_defaults, + "positional_action": positional_action_defaults, "callback_action": callback_action_defaults, "makeshift_action": makeshift_action_defaults, "custom_action": custom_action_defaults, diff --git a/cursorless-talon/src/actions/actions_makeshift.py b/cursorless-talon/src/actions/actions_makeshift.py index f1f84075c4..06c40569bb 100644 --- a/cursorless-talon/src/actions/actions_makeshift.py +++ b/cursorless-talon/src/actions/actions_makeshift.py @@ -39,7 +39,7 @@ class MakeshiftAction: "editor.action.rename", restore_selection=True, await_command=False, - post_command_sleep_ms=150, + post_command_sleep_ms=200, ), ] diff --git a/cursorless-talon/src/actions/actions_simple.py b/cursorless-talon/src/actions/actions_simple.py index eabd90bbdc..fc1e20297c 100644 --- a/cursorless-talon/src/actions/actions_simple.py +++ b/cursorless-talon/src/actions/actions_simple.py @@ -24,7 +24,6 @@ "give": "deselect", "highlight": "highlight", "indent": "indentLine", - "paste to": "pasteFromClipboard", "post": "setSelectionAfter", "pour": "editNewLineAfter", "pre": "setSelectionBefore", @@ -37,8 +36,18 @@ "unfold": "unfoldRegion", } +# NOTE: Please do not change these dicts. Use the CSVs for customization. +# See https://www.cursorless.org/docs/user/customization/ +positional_action_defaults = { + "paste": "pasteFromClipboard", +} + mod = Module() mod.list( "cursorless_simple_action", desc="Supported simple actions for cursorless navigation", ) +mod.list( + "cursorless_positional_action", + desc="Supported actions for cursorless that expect a positional target", +) diff --git a/cursorless-talon/src/actions/call.py b/cursorless-talon/src/actions/call.py index f29e61fab1..d12aff0172 100644 --- a/cursorless-talon/src/actions/call.py +++ b/cursorless-talon/src/actions/call.py @@ -6,5 +6,5 @@ def run_call_action(target: dict): - targets = [target, IMPLICIT_TARGET] + targets = [target, IMPLICIT_TARGET.copy()] actions.user.cursorless_multiple_target_command("callAsFunction", targets) diff --git a/cursorless-talon/src/actions/move_bring.py b/cursorless-talon/src/actions/move_bring.py index 021fffa174..30c7241452 100644 --- a/cursorless-talon/src/actions/move_bring.py +++ b/cursorless-talon/src/actions/move_bring.py @@ -3,24 +3,18 @@ from ..primitive_target import IMPLICIT_TARGET mod = Module() -mod.list( - "cursorless_source_destination_connective", - desc="The connective used to separate source and destination targets", -) mod.list("cursorless_move_bring_action", desc="Cursorless move or bring actions") -@mod.capture( - rule=( - " [{user.cursorless_source_destination_connective} ]" - ) -) +@mod.capture(rule=" []") def cursorless_move_bring_targets(m) -> list[dict]: target_list = m.cursorless_target_list - if len(target_list) == 1: - target_list = target_list + [IMPLICIT_TARGET] + try: + target_list += [m.cursorless_positional_target] + except AttributeError: + target_list += [IMPLICIT_TARGET.copy()] return target_list diff --git a/cursorless-talon/src/cheatsheet/sections/scopes.py b/cursorless-talon/src/cheatsheet/sections/scopes.py index a9df0cd2a5..1df251ac69 100644 --- a/cursorless-talon/src/cheatsheet/sections/scopes.py +++ b/cursorless-talon/src/cheatsheet/sections/scopes.py @@ -4,7 +4,7 @@ def get_scopes(): return { **get_lists( - ["scope_type", "selection_type", "subtoken_scope_type"], + ["scope_type", "subtoken_scope_type"], {"argumentOrParameter": "Argument"}, ), "

": "Paired delimiter", diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index 95d8d69a17..f596fa8006 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -127,11 +127,13 @@ def construct_cursorless_command_argument( use_pre_phrase_snapshot = False return { - "version": 1, + "version": 2, "spokenForm": get_spoken_form(), - "action": action, + "action": { + "name": action, + "args": args, + }, "targets": targets, - "extraArgs": args, "usePrePhraseSnapshot": use_pre_phrase_snapshot, } diff --git a/cursorless-talon/src/compound_targets.py b/cursorless-talon/src/compound_targets.py index 8c321bd200..63f8512fca 100644 --- a/cursorless-talon/src/compound_targets.py +++ b/cursorless-talon/src/compound_targets.py @@ -44,19 +44,19 @@ def cursorless_range(m) -> str: return primitive_targets[0] if len(primitive_targets) == 1: - start = BASE_TARGET.copy() + anchor = BASE_TARGET.copy() else: - start = primitive_targets[0] + anchor = primitive_targets[0] range_connective = range_connective_with_type["connective"] range_type = range_connective_with_type["type"] range = { "type": "range", - "start": start, - "end": primitive_targets[-1], - "excludeStart": not is_anchor_included(range_connective), - "excludeEnd": not is_active_included(range_connective), + "anchor": anchor, + "active": primitive_targets[-1], + "excludeAnchor": not is_anchor_included(range_connective), + "excludeActive": not is_active_included(range_connective), } if range_type: @@ -79,4 +79,7 @@ def is_active_included(range_connective: str): def cursorless_target(m) -> dict: if len(m.cursorless_range_list) == 1: return m.cursorless_range - return {"type": "list", "elements": m.cursorless_range_list} + return { + "type": "list", + "elements": m.cursorless_range_list, + } diff --git a/cursorless-talon/src/connective.py b/cursorless-talon/src/connective.py index 28e593e7de..05723e9cd6 100644 --- a/cursorless-talon/src/connective.py +++ b/cursorless-talon/src/connective.py @@ -1,7 +1,15 @@ -from talon import app +from talon import Module, app from .csv_overrides import init_csv_and_watch_changes +mod = Module() + +mod.list( + "cursorless_source_destination_connective", + desc="The connective used to separate source and destination targets", +) + + # NOTE: Please do not change these dicts. Use the CSVs for customization. # See https://www.cursorless.org/docs/user/customization/ range_connectives = { @@ -11,6 +19,7 @@ "until": "rangeExcludingEnd", } + default_range_connective = "rangeInclusive" diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index baf1f7880a..ed06601aec 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -159,6 +159,14 @@ def update_dicts( key = obj["key"] if not is_removed(key): for k in key.split("|"): + if value == "pasteFromClipboard" and k.endswith(" to"): + # FIXME: This is a hack to work around the fact that the + # spoken form of the `pasteFromClipboard` action used to be + # "paste to", but now the spoken form is just "paste" and + # the "to" is part of the positional target. Users who had + # cursorless before this change would have "paste to" as + # their spoken form and so would need to say "paste to to". + k = k[:-3] results[obj["list"]][k.strip()] = value # Assign result to talon context list diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index 1ebb807a85..52563585a8 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -4,6 +4,9 @@ app: vscode : user.cursorless_action_or_vscode_command(cursorless_action_or_vscode_command, cursorless_target) +{user.cursorless_positional_action} : + user.cursorless_single_target_command(cursorless_positional_action, cursorless_positional_target) + {user.cursorless_swap_action} : user.cursorless_multiple_target_command(cursorless_swap_action, cursorless_swap_targets) diff --git a/cursorless-talon/src/marks/lines_number.py b/cursorless-talon/src/marks/lines_number.py index 4c8e9049ba..a6ba61f156 100644 --- a/cursorless-talon/src/marks/lines_number.py +++ b/cursorless-talon/src/marks/lines_number.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Any, Callable from talon import Context, Module @@ -13,7 +14,7 @@ class CustomizableTerm: defaultSpokenForm: str cursorlessIdentifier: str type: str - formatter: callable + formatter: Callable # NOTE: Please do not change these dicts. Use the CSVs for customization. @@ -33,7 +34,7 @@ class CustomizableTerm: @mod.capture(rule="{user.cursorless_line_direction} ") -def cursorless_line_number(m) -> str: +def cursorless_line_number(m) -> dict[str, Any]: direction = directions_map[m.cursorless_line_direction] line_number = m.number_small line = { @@ -41,10 +42,7 @@ def cursorless_line_number(m) -> str: "type": direction.type, } return { - "selectionType": "line", - "mark": { - "type": "lineNumber", - "anchor": line, - "active": line, - }, + "type": "lineNumber", + "anchor": line, + "active": line, } diff --git a/cursorless-talon/src/marks/mark.py b/cursorless-talon/src/marks/mark.py index 999ceb8a62..83b951fbde 100644 --- a/cursorless-talon/src/marks/mark.py +++ b/cursorless-talon/src/marks/mark.py @@ -43,7 +43,7 @@ @mod.capture( rule="[{user.cursorless_hat_color}] [{user.cursorless_hat_shape}] " ) -def cursorless_decorated_symbol(m) -> dict[str, dict[str, Any]]: +def cursorless_decorated_symbol(m) -> dict[str, Any]: """A decorated symbol""" hat_color = getattr(m, "cursorless_hat_color", "default") try: @@ -51,11 +51,9 @@ def cursorless_decorated_symbol(m) -> dict[str, dict[str, Any]]: except AttributeError: hat_style_name = hat_color return { - "mark": { - "type": "decoratedSymbol", - "symbolColor": hat_style_name, - "character": m.any_alphanumeric_key, - } + "type": "decoratedSymbol", + "symbolColor": hat_style_name, + "character": m.any_alphanumeric_key, } @@ -69,10 +67,10 @@ class CustomizableTerm: # NOTE: Please do not change these dicts. Use the CSVs for customization. # See https://www.cursorless.org/docs/user/customization/ special_marks = [ - CustomizableTerm("this", "currentSelection", {"mark": {"type": "cursor"}}), - CustomizableTerm("that", "previousTarget", {"mark": {"type": "that"}}), - CustomizableTerm("source", "previousSource", {"mark": {"type": "source"}}), - CustomizableTerm("nothing", "nothing", {"mark": {"type": "nothing"}}), + CustomizableTerm("this", "currentSelection", {"type": "cursor"}), + CustomizableTerm("that", "previousTarget", {"type": "that"}), + CustomizableTerm("source", "previousSource", {"type": "source"}), + CustomizableTerm("nothing", "nothing", {"type": "nothing"}), # "last cursor": {"mark": {"type": "lastCursorPosition"}} # Not implemented ] @@ -93,7 +91,7 @@ class CustomizableTerm: "" # row (ie absolute mod 100), up, down ) ) -def cursorless_mark(m) -> str: +def cursorless_mark(m) -> dict[str, Any]: try: return m.cursorless_decorated_symbol except AttributeError: diff --git a/cursorless-talon/src/modifiers/containing_scope.py b/cursorless-talon/src/modifiers/containing_scope.py index 17956d9019..e8525be1a6 100644 --- a/cursorless-talon/src/modifiers/containing_scope.py +++ b/cursorless-talon/src/modifiers/containing_scope.py @@ -46,24 +46,7 @@ "tags": "xmlBothTags", "start tag": "xmlStartTag", "end tag": "xmlEndTag", -} - - -@mod.capture(rule="[every] {user.cursorless_scope_type}") -def cursorless_containing_scope(m) -> dict[str, dict[str, Any]]: - """Expand to containing scope""" - return { - "modifier": { - "type": "containingScope", - "scopeType": m.cursorless_scope_type, - "includeSiblings": m[0] == "every", - } - } - - -# NOTE: Please do not change these dicts. Use the CSVs for customization. -# See https://www.cursorless.org/docs/user/customization/ -selection_types = { + # Text-based scope types "block": "paragraph", "cell": "notebookCell", "file": "document", @@ -73,6 +56,18 @@ def cursorless_containing_scope(m) -> dict[str, dict[str, Any]]: "token": "token", } + +@mod.capture(rule="[every] {user.cursorless_scope_type}") +def cursorless_containing_scope(m) -> dict[str, Any]: + """Expand to containing scope""" + return { + "type": "everyScope" if m[0] == "every" else "containingScope", + "scopeType": { + "type": m.cursorless_scope_type, + }, + } + + # NOTE: Please do not change these dicts. Use the CSVs for customization. # See https://www.cursorless.org/docs/user/customization/ subtoken_scope_types = { @@ -90,7 +85,6 @@ def cursorless_containing_scope(m) -> dict[str, dict[str, Any]]: default_values = { "scope_type": scope_types, - "selection_type": selection_types, "subtoken_scope_type": subtoken_scope_types, "surrounding_pair_scope_type": surrounding_pair_scope_types, } diff --git a/cursorless-talon/src/modifiers/head_tail.py b/cursorless-talon/src/modifiers/head_tail.py deleted file mode 100644 index 8049a1e637..0000000000 --- a/cursorless-talon/src/modifiers/head_tail.py +++ /dev/null @@ -1,30 +0,0 @@ -from dataclasses import dataclass - -from talon import Module - -mod = Module() -mod.list("cursorless_head_tail", desc="Cursorless modifier for head or tail of line") - - -@dataclass -class HeadTail: - defaultSpokenForm: str - cursorlessIdentifier: str - type: str - - -head_tail_list = [ - HeadTail("head", "extendThroughStartOf", "head"), - HeadTail("tail", "extendThroughEndOf", "tail"), -] -head_tail_map = {i.cursorlessIdentifier: i.type for i in head_tail_list} -head_tail = {i.defaultSpokenForm: i.cursorlessIdentifier for i in head_tail_list} - - -@mod.capture(rule="{user.cursorless_head_tail}") -def cursorless_head_tail(m) -> dict: - return { - "modifier": { - "type": head_tail_map[m.cursorless_head_tail], - } - } diff --git a/cursorless-talon/src/modifiers/modifiers.py b/cursorless-talon/src/modifiers/modifiers.py index 238d18a9f3..217c6033f8 100644 --- a/cursorless-talon/src/modifiers/modifiers.py +++ b/cursorless-talon/src/modifiers/modifiers.py @@ -1,27 +1,42 @@ -from talon import app +from talon import Module, app from ..csv_overrides import init_csv_and_watch_changes -from .head_tail import head_tail from .range_type import range_types +mod = Module() + # NOTE: Please do not change these dicts. Use the CSVs for customization. # See https://www.cursorless.org/docs/user/customization/ - -delimiter_inclusions = { +simple_modifiers = { "inside": "interiorOnly", - "bound": "excludeInterior", + "bounds": "excludeInterior", + "just": "toRawSelection", + "head": "extendThroughStartOf", + "tail": "extendThroughEndOf", + "leading": "leading", + "trailing": "trailing", } -to_raw_selection = {"just": "toRawSelection"} + +mod.list( + "cursorless_simple_modifier", + desc="Simple cursorless modifiers that only need to specify their type", +) + + +@mod.capture(rule="{user.cursorless_simple_modifier}") +def cursorless_simple_modifier(m) -> dict[str, str]: + """Simple cursorless modifiers that only need to specify their type""" + return { + "type": m.cursorless_simple_modifier, + } def on_ready(): init_csv_and_watch_changes( "modifiers", { - "delimiter_inclusion": delimiter_inclusions, + "simple_modifier": simple_modifiers, "range_type": range_types, - "head_tail": head_tail, - "to_raw_selection": to_raw_selection, }, ) diff --git a/cursorless-talon/src/modifiers/position.py b/cursorless-talon/src/modifiers/position.py index 830d00b68a..508bdb2880 100644 --- a/cursorless-talon/src/modifiers/position.py +++ b/cursorless-talon/src/modifiers/position.py @@ -1,23 +1,40 @@ -from talon import Context, Module +from typing import Any + +from talon import Context, Module, app + +from ..csv_overrides import init_csv_and_watch_changes mod = Module() ctx = Context() +mod.list("cursorless_position", desc='Positions such as "before", "after" etc') +# NOTE: Please do not change these dicts. Use the CSVs for customization. +# See https://www.cursorless.org/docs/user/customization/ positions = { - "after": {"position": "after"}, - "before": {"position": "before"}, - "start of": {"position": "before", "insideOutsideType": "inside"}, - "end of": {"position": "after", "insideOutsideType": "inside"}, - # Disabled for now because "below" can misrecognize with "blue" and we may move away from allowing positional modifiers in arbitrary places anyway - # "above": {"position": "before", **LINE.json_repr}, - # "below": {"position": "after", **LINE.json_repr} + "start of": "start", + "end of": "end", + "before": "before", + "after": "after", } -mod.list("cursorless_position", desc="Types of positions") -ctx.lists["self.cursorless_position"] = positions.keys() +def construct_positional_modifier(position: str) -> dict: + return {"type": "position", "position": position} + +# Note that we allow positional connectives such as "before" and "after" to appear +# as modifiers. We may disallow this in the future. @mod.capture(rule="{user.cursorless_position}") -def cursorless_position(m) -> str: - return positions[m.cursorless_position] +def cursorless_position(m) -> dict[str, Any]: + return construct_positional_modifier(m.cursorless_position) + + +def on_ready(): + init_csv_and_watch_changes( + "positions", + {"position": positions}, + ) + + +app.register("ready", on_ready) diff --git a/cursorless-talon/src/modifiers/selection_type.py b/cursorless-talon/src/modifiers/selection_type.py deleted file mode 100644 index ec2296cd22..0000000000 --- a/cursorless-talon/src/modifiers/selection_type.py +++ /dev/null @@ -1,11 +0,0 @@ -from talon import Module - -mod = Module() - - -mod.list("cursorless_selection_type", desc="Types of selection_types") - - -@mod.capture(rule="{user.cursorless_selection_type}") -def cursorless_selection_type(m) -> str: - return {"selectionType": m.cursorless_selection_type} diff --git a/cursorless-talon/src/modifiers/sub_token.py b/cursorless-talon/src/modifiers/sub_token.py index 08ba72ad8f..419d2a75a1 100644 --- a/cursorless-talon/src/modifiers/sub_token.py +++ b/cursorless-talon/src/modifiers/sub_token.py @@ -1,3 +1,5 @@ +from typing import Any + from talon import Module from ..compound_targets import is_active_included, is_anchor_included @@ -47,21 +49,20 @@ def cursorless_first_last_range(m) -> str: @mod.capture( rule=( - "( | )" + "( | ) " "{user.cursorless_subtoken_scope_type}" ) ) -def cursorless_subtoken_scope(m) -> str: +def cursorless_subtoken_scope(m) -> dict[str, Any]: """Subtoken ranges such as subwords or characters""" try: range = m.cursorless_ordinal_range except AttributeError: range = m.cursorless_first_last_range return { - "selectionType": "token", - "modifier": { - "type": "subpiece", - "pieceType": m.cursorless_subtoken_scope_type, - **range, + "type": "ordinalRange", + "scopeType": { + "type": m.cursorless_subtoken_scope_type, }, + **range, } diff --git a/cursorless-talon/src/modifiers/surrounding_pair.py b/cursorless-talon/src/modifiers/surrounding_pair.py index 8fbf236179..92f990bb1a 100644 --- a/cursorless-talon/src/modifiers/surrounding_pair.py +++ b/cursorless-talon/src/modifiers/surrounding_pair.py @@ -1,3 +1,6 @@ +from contextlib import suppress +from typing import Any + from talon import Context, Module from ..paired_delimiter import paired_delimiters_map @@ -6,11 +9,6 @@ ctx = Context() -mod.list( - "cursorless_delimiter_inclusion", - desc="Whether to include delimiters in surrounding range", -) - mod.list( "cursorless_delimiter_force_direction", desc="Can be used to force an ambiguous delimiter to extend in one direction", @@ -20,8 +18,6 @@ "right", ] -# NB: This is a hack until we support having inside and outside on arbitrary -# scope types mod.list( "cursorless_surrounding_pair_scope_type", desc="Scope types that can function as surrounding pairs", @@ -30,48 +26,39 @@ @mod.capture( rule=( - "[{user.cursorless_delimiter_inclusion}] [{user.cursorless_delimiter_force_direction}] | " - "{user.cursorless_delimiter_inclusion} [{user.cursorless_delimiter_force_direction}]" + " |" + "{user.cursorless_surrounding_pair_scope_type}" ) ) -def cursorless_surrounding_pair(m) -> str: - """Surrounding pair modifier""" +def cursorless_surrounding_pair_scope_type(m) -> str: + """Surrounding pair scope type""" + try: + return m.cursorless_surrounding_pair_scope_type + except AttributeError: + return paired_delimiters_map[ + m.cursorless_selectable_paired_delimiter + ].cursorlessIdentifier + + +@mod.capture( + rule="[{user.cursorless_delimiter_force_direction}] " +) +def cursorless_surrounding_pair(m) -> dict[str, Any]: + """Expand to containing surrounding pair""" try: surrounding_pair_scope_type = m.cursorless_surrounding_pair_scope_type except AttributeError: surrounding_pair_scope_type = "any" - modifier = { + scope_type = { "type": "surroundingPair", "delimiter": surrounding_pair_scope_type, } - try: - modifier["delimiterInclusion"] = m.cursorless_delimiter_inclusion - except AttributeError: - pass - - try: - modifier["forceDirection"] = m.cursorless_delimiter_force_direction - except AttributeError: - pass + with suppress(AttributeError): + scope_type["forceDirection"] = m.cursorless_delimiter_force_direction return { - "modifier": modifier, + "type": "containingScope", + "scopeType": scope_type, } - - -@mod.capture( - rule=( - " |" - "{user.cursorless_surrounding_pair_scope_type}" - ) -) -def cursorless_surrounding_pair_scope_type(m) -> str: - """Surrounding pair scope type""" - try: - return m.cursorless_surrounding_pair_scope_type - except AttributeError: - return paired_delimiters_map[ - m.cursorless_selectable_paired_delimiter - ].cursorlessIdentifier diff --git a/cursorless-talon/src/modifiers/to_raw_selection.py b/cursorless-talon/src/modifiers/to_raw_selection.py index 791f775745..fc2b6d14ea 100644 --- a/cursorless-talon/src/modifiers/to_raw_selection.py +++ b/cursorless-talon/src/modifiers/to_raw_selection.py @@ -1,3 +1,5 @@ +from typing import Any + from talon import Module mod = Module() @@ -10,5 +12,5 @@ @mod.capture(rule="{user.cursorless_to_raw_selection}") -def cursorless_to_raw_selection(m) -> dict: - return {"modifier": {"type": "toRawSelection"}} +def cursorless_to_raw_selection(m) -> dict[str, Any]: + return {"type": "toRawSelection"} diff --git a/cursorless-talon/src/positional_target.py b/cursorless-talon/src/positional_target.py new file mode 100644 index 0000000000..3f5594a883 --- /dev/null +++ b/cursorless-talon/src/positional_target.py @@ -0,0 +1,42 @@ +from talon import Module + +from .modifiers.position import construct_positional_modifier + +mod = Module() + + +@mod.capture( + rule=( + "({user.cursorless_position} | {user.cursorless_source_destination_connective}) " + "" + ) +) +def cursorless_positional_target(m) -> list[dict]: + target = m.cursorless_target + try: + modifier = construct_positional_modifier(m.cursorless_position) + return update_first_primitive_target(target, modifier) + except AttributeError: + return target + + +def update_first_primitive_target(target: dict, modifier: dict): + if target["type"] == "primitive": + if "modifiers" not in target: + target["modifiers"] = [] + target["modifiers"].insert(0, modifier) + return target + elif target["type"] == "range": + return { + **target, + "anchor": update_first_primitive_target(target["anchor"], modifier), + } + else: + elements = target["elements"] + return { + **target, + "elements": [ + update_first_primitive_target(elements[0], modifier), + *elements[1:], + ], + } diff --git a/cursorless-talon/src/primitive_target.py b/cursorless-talon/src/primitive_target.py index 714e19de64..d7a769a46a 100644 --- a/cursorless-talon/src/primitive_target.py +++ b/cursorless-talon/src/primitive_target.py @@ -1,20 +1,19 @@ +from typing import Any + from talon import Module mod = Module() -BASE_TARGET = {"type": "primitive"} +BASE_TARGET: dict[str, Any] = {"type": "primitive"} IMPLICIT_TARGET = {"type": "primitive", "isImplicit": True} modifiers = [ "", # before, end of - "", # token, line, file - "", # head, tail + "", # eg inside, bounds, just, head, tail, leading, trailing "", # funk, state, class "", # first past second word "", # matching/pair [curly, round] - "", # just - # "", # matching ] @@ -27,9 +26,18 @@ def cursorless_modifier(m) -> str: @mod.capture( rule="+ [] | " ) -def cursorless_primitive_target(m) -> str: +def cursorless_primitive_target(m) -> dict[str, Any]: """Supported extents for cursorless navigation""" result = BASE_TARGET.copy() - for capture in m: - result.update(capture) + + try: + result["mark"] = m.cursorless_mark + except AttributeError: + pass + + try: + result["modifiers"] = m.cursorless_modifier_list + except AttributeError: + pass + return result diff --git a/docs/contributing/CONTRIBUTING.md b/docs/contributing/CONTRIBUTING.md index 57be15a15c..ebeba5b069 100644 --- a/docs/contributing/CONTRIBUTING.md +++ b/docs/contributing/CONTRIBUTING.md @@ -34,6 +34,10 @@ locally, you need to run the extension in debug mode. To do so you need to run the `workbench.action.debug.selectandstart` command in VSCode and then select either "Run Extension" or "Extension Tests". +### Running a subset of tests + +The entire test suite takes a little while to run (1-2 mins), so if you'd like to run just a subset of the tests, you can edit the constant in [`runTestSubset`](../../src/test/suite/runTestSubset.ts) to a string supported by [mocha grep](https://mochajs.org/#-grep-regexp-g-regexp) and use the "Run Test Subset" launch config instead of the usual "Extension Tests". + ## Code formatting We use [`pre-commit`](https://pre-commit.com/) to automate autoformatting. diff --git a/docs/contributing/test-case-recorder.md b/docs/contributing/test-case-recorder.md index 95ce165b5c..287e822fe2 100644 --- a/docs/contributing/test-case-recorder.md +++ b/docs/contributing/test-case-recorder.md @@ -10,22 +10,47 @@ command run, and the final state, all in the form of a yaml document. See ## Initial setup -1. Add a voice command for recording to your personal talon files: - - `cursorless record: user.vscode("cursorless.recordTestCase")` - - We don't want to commit this so add it to your own repository. -1. If you'd like to be able to record tests which check the navigation map, please add the following to your personal talon files: +Add a voice command for recording to your personal talon files: - - https://github.com/pokey/pokey_talon/blob/9298c25dd6d28fd9fcf5ed39f305bc6b93e5f229/apps/vscode/vscode.talon#L468 - - https://github.com/pokey/pokey_talon/blob/49643bfa8f62cbec18b5ddad1658f5a28785eb01/apps/vscode/vscode.py#L203-L205 +```talon +cursorless record: user.vscode("cursorless.recordTestCase") +``` + +We don't want to commit this so please add it to your own Talon user file set. + +### Configuring the test case Recorder + +The test case recorder has several additional configuration options. The default configuration works for most tests, but you may find the following useful. + +#### Testing the hat map + +We have a way to test that the hats in the hat map update correctly during the course of a single phrase. These tests are also how we usually test our [range updating code](../api/modules/core_updateSelections_updateSelections). + +Please add the following to your personal talon files: + +- https://github.com/pokey/pokey_talon/blob/9298c25dd6d28fd9fcf5ed39f305bc6b93e5f229/apps/vscode/vscode.talon#L468 +- https://github.com/pokey/pokey_talon/blob/49643bfa8f62cbec18b5ddad1658f5a28785eb01/apps/vscode/vscode.py#L203-L205 - It is quite unlikely you'll need this second step. Most tests don't check the navigation map. +It is quite unlikely you'll need this second step. Most tests don't check the navigation map. -1. If you'd like to be able to record tests which assert on non-matches, please add another command to your personal talon files. See the two files links above for context. Add the command below to your to your `vscode.py` and ensure that there is a matching Talon command. +#### Capturing errors + +We support recording tests where the expected result is an error + +Please add a command to your personal talon files. See the two files links above for context. Add the command below to your to your `vscode.py` and ensure that there is a matching Talon command. ``` actions.user.vscode_with_plugin("cursorless.recordTestCase", {"recordErrors": True}) ``` +#### Testing decoration highlights + +We support testing our decoration highlights, eg the flash of red when something is deleted. If you'd like to be able to record tests which check our decoration highlights, please add another command to your personal talon files. See the two files links above for context. Add the command below to your to your `vscode.py` and ensure that there is a matching Talon command. + +``` + actions.user.vscode_with_plugin("cursorless.recordTestCase", {"isDecorationsTest": True}) +``` + ## Recording new tests 1. Start debugging (F5) diff --git a/package.json b/package.json index 328942cc54..cb24eeb58b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "color": "#00001A", "theme": "dark" }, - "version": "0.25.0", + "version": "0.26.0", "publisher": "pokey", "license": "MIT", "repository": { @@ -508,6 +508,7 @@ "pretest": "yarn run compile && yarn run lint && yarn run esbuild", "lint": "eslint src --ext ts", "test": "env CURSORLESS_TEST=true node ./out/test/runTest.js", + "unused-exports": "ts-unused-exports tsconfig.json --showLineNumber", "init-launch-sandbox": "node ./out/test/initLaunchSandbox.js", "prepare-for-extension-publish": "node ./out/scripts/prepareForExtensionPublish.js" }, @@ -533,7 +534,8 @@ "semver": "^7.3.5", "sinon": "^11.1.1", "typescript": "^4.5.5", - "vscode-test": "^1.4.1" + "vscode-test": "^1.4.1", + "ts-unused-exports": "8.0.0" }, "dependencies": { "@types/lodash": "^4.14.168", diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts new file mode 100644 index 0000000000..9e1f0e788a --- /dev/null +++ b/src/actions/Actions.ts @@ -0,0 +1,88 @@ +import { Graph } from "../typings/Types"; +import { ActionRecord } from "./actions.types"; +import { Bring, Move, Swap } from "./BringMoveSwap"; +import Call from "./Call"; +import Clear from "./Clear"; +import { CommentLines } from "./Comment"; +import { + CopyContentAfter as InsertCopyAfter, + CopyContentBefore as InsertCopyBefore, +} from "./InsertCopy"; +import { Copy, Cut, Paste } from "./CutCopyPaste"; +import Deselect from "./Deselect"; +import { EditNewBefore, EditNewAfter } from "./EditNew"; +import ExecuteCommand from "./ExecuteCommand"; +import ExtractVariable from "./ExtractVariable"; +import { FindInFiles } from "./Find"; +import { Fold, Unfold } from "./Fold"; +import FollowLink from "./FollowLink"; +import GetText from "./GetText"; +import Highlight from "./Highlight"; +import { IndentLines, OutdentLines } from "./Indent"; +import { + InsertEmptyLineAbove as InsertEmptyLineBefore, + InsertEmptyLineBelow as InsertEmptyLineAfter, + InsertEmptyLinesAround, +} from "./InsertEmptyLines"; +import Remove from "./Remove"; +import Replace from "./Replace"; +import Rewrap from "./Rewrap"; +import { ScrollToBottom, ScrollToCenter, ScrollToTop } from "./Scroll"; +import { + SetSelection, + SetSelectionAfter, + SetSelectionBefore, +} from "./SetSelection"; +import { Random, Reverse, Sort } from "./Sort"; +import ToggleBreakpoint from "./ToggleBreakpoint"; +import Wrap from "./Wrap"; +import WrapWithSnippet from "./WrapWithSnippet"; + +class Actions implements ActionRecord { + constructor(private graph: Graph) {} + + callAsFunction = new Call(this.graph); + clearAndSetSelection = new Clear(this.graph); + copyToClipboard = new Copy(this.graph); + cutToClipboard = new Cut(this.graph); + deselect = new Deselect(this.graph); + editNewLineAfter = new EditNewAfter(this.graph); + editNewLineBefore = new EditNewBefore(this.graph); + executeCommand = new ExecuteCommand(this.graph); + extractVariable = new ExtractVariable(this.graph); + findInWorkspace = new FindInFiles(this.graph); + foldRegion = new Fold(this.graph); + followLink = new FollowLink(this.graph); + getText = new GetText(this.graph); + highlight = new Highlight(this.graph); + indentLine = new IndentLines(this.graph); + insertCopyAfter = new InsertCopyAfter(this.graph); + insertCopyBefore = new InsertCopyBefore(this.graph); + insertEmptyLineAfter = new InsertEmptyLineAfter(this.graph); + insertEmptyLineBefore = new InsertEmptyLineBefore(this.graph); + insertEmptyLinesAround = new InsertEmptyLinesAround(this.graph); + moveToTarget = new Move(this.graph); + outdentLine = new OutdentLines(this.graph); + pasteFromClipboard = new Paste(this.graph); + randomizeTargets = new Random(this.graph); + remove = new Remove(this.graph); + replace = new Replace(this.graph); + replaceWithTarget = new Bring(this.graph); + reverseTargets = new Reverse(this.graph); + rewrapWithPairedDelimiter = new Rewrap(this.graph); + scrollToBottom = new ScrollToBottom(this.graph); + scrollToCenter = new ScrollToCenter(this.graph); + scrollToTop = new ScrollToTop(this.graph); + setSelection = new SetSelection(this.graph); + setSelectionAfter = new SetSelectionAfter(this.graph); + setSelectionBefore = new SetSelectionBefore(this.graph); + sortTargets = new Sort(this.graph); + swapTargets = new Swap(this.graph); + toggleLineBreakpoint = new ToggleBreakpoint(this.graph); + toggleLineComment = new CommentLines(this.graph); + unfoldRegion = new Unfold(this.graph); + wrapWithPairedDelimiter = new Wrap(this.graph); + wrapWithSnippet = new WrapWithSnippet(this.graph); +} + +export default Actions; diff --git a/src/actions/BringMoveSwap.ts b/src/actions/BringMoveSwap.ts index 72f853287a..462d8d5c10 100644 --- a/src/actions/BringMoveSwap.ts +++ b/src/actions/BringMoveSwap.ts @@ -1,57 +1,39 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, - Edit, -} from "../typings/Types"; -import { runForEachEditor } from "../util/targetUtils"; -import update from "immutability-helper"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { performOutsideAdjustment } from "../util/performInsideOutsideAdjustment"; import { flatten } from "lodash"; -import { Selection, TextEditor, DecorationRangeBehavior } from "vscode"; - -import { - getTextWithPossibleDelimiter, - maybeAddDelimiter, -} from "../util/getTextWithPossibleDelimiter"; +import { DecorationRangeBehavior, Selection, TextEditor } from "vscode"; import { getSelectionInfo, performEditsAndUpdateFullSelectionInfos, } from "../core/updateSelections/updateSelections"; -import { unifyTargets } from "../util/unifyRanges"; +import { Target } from "../typings/target.types"; +import { EditWithRangeUpdater, Graph } from "../typings/Types"; +import { selectionFromRange } from "../util/selectionUtils"; +import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; +import { getContentRange, runForEachEditor } from "../util/targetUtils"; +import { unifyRemovalTargets } from "../util/unifyRanges"; +import { Action, ActionReturnValue } from "./actions.types"; type ActionType = "bring" | "move" | "swap"; -interface ExtendedEdit extends Edit { +interface ExtendedEdit { + edit: EditWithRangeUpdater; editor: TextEditor; isSource: boolean; - originalSelection: TypedSelection; + originalTarget: Target; } interface MarkEntry { editor: TextEditor; selection: Selection; isSource: boolean; - typedSelection: TypedSelection; + target: Target; } class BringMoveSwap implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: null }, - { insideOutsideType: null }, - ]; - constructor(private graph: Graph, private type: ActionType) { this.run = this.run.bind(this); } - private broadcastSource( - sources: TypedSelection[], - destinations: TypedSelection[] - ) { + private broadcastSource(sources: Target[], destinations: Target[]) { if (sources.length === 1 && this.type !== "swap") { // If there is only one source target, expand it to same length as // destination target @@ -60,42 +42,45 @@ class BringMoveSwap implements Action { return sources; } - private getDecorationStyles() { + private getDecorationContext() { let sourceStyle; + let getSourceRangeCallback; if (this.type === "bring") { sourceStyle = this.graph.editStyles.referenced; + getSourceRangeCallback = getContentRange; } else if (this.type === "move") { sourceStyle = this.graph.editStyles.pendingDelete; + getSourceRangeCallback = getRemovalRange; } // NB this.type === "swap" else { sourceStyle = this.graph.editStyles.pendingModification1; + getSourceRangeCallback = getContentRange; } return { sourceStyle, destinationStyle: this.graph.editStyles.pendingModification0, + getSourceRangeCallback, }; } - private async decorateTargets( - sources: TypedSelection[], - destinations: TypedSelection[] - ) { - const decorationTypes = this.getDecorationStyles(); + private async decorateTargets(sources: Target[], destinations: Target[]) { + const decorationContext = this.getDecorationContext(); await Promise.all([ - displayPendingEditDecorations(sources, decorationTypes.sourceStyle), - displayPendingEditDecorations( + this.graph.editStyles.displayPendingEditDecorations( + sources, + decorationContext.sourceStyle, + decorationContext.getSourceRangeCallback + ), + this.graph.editStyles.displayPendingEditDecorations( destinations, - decorationTypes.destinationStyle + decorationContext.destinationStyle ), ]); } - private getEdits( - sources: TypedSelection[], - destinations: TypedSelection[] - ): ExtendedEdit[] { - const usedSources: TypedSelection[] = []; + private getEdits(sources: Target[], destinations: Target[]): ExtendedEdit[] { + const usedSources: Target[] = []; const results: ExtendedEdit[] = []; const zipSources = sources.length !== destinations.length && @@ -113,31 +98,22 @@ class BringMoveSwap implements Action { if (zipSources) { text = sources .map((source, i) => { - let text = source.selection.editor.document.getText( - source.selection.selection - ); - const selectionContext = destination.selectionContext - .isRawSelection - ? source.selectionContext - : destination.selectionContext; - return i > 0 && selectionContext.containingListDelimiter - ? selectionContext.containingListDelimiter + text - : text; + const text = source.contentText; + const delimiter = + (destination.isRaw ? null : destination.insertionDelimiter) ?? + (source.isRaw ? null : source.insertionDelimiter); + return i > 0 && delimiter != null ? delimiter + text : text; }) .join(""); - text = maybeAddDelimiter(text, destination); } else { - // Get text adjusting for destination position - text = getTextWithPossibleDelimiter(source, destination); + text = source.contentText; } // Add destination edit results.push({ - range: destination.selection.selection, - text, - editor: destination.selection.editor, - originalSelection: destination, + edit: destination.constructChangeEdit(text), + editor: destination.editor, + originalTarget: destination, isSource: false, - isReplace: destination.position === "after", }); } else { destination = destinations[0]; @@ -149,31 +125,23 @@ class BringMoveSwap implements Action { usedSources.push(source); if (this.type !== "move") { results.push({ - range: source.selection.selection, - text: destination.selection.editor.document.getText( - destination.selection.selection - ), - editor: source.selection.editor, - originalSelection: source, + edit: source.constructChangeEdit(destination.contentText), + editor: source.editor, + originalTarget: source, isSource: true, - isReplace: false, }); } } }); if (this.type === "move") { - let outsideSources = usedSources.map(performOutsideAdjustment); // Unify overlapping targets. - outsideSources = unifyTargets(outsideSources); - outsideSources.forEach((source) => { + unifyRemovalTargets(usedSources).forEach((source) => { results.push({ - range: source.selection.selection, - text: "", - editor: source.selection.editor, - originalSelection: source, + edit: source.constructRemovalEdit(), + editor: source.editor, + originalTarget: source, isSource: true, - isReplace: false, }); }); } @@ -195,10 +163,10 @@ class BringMoveSwap implements Action { ? edits : edits.filter(({ isSource }) => !isSource); - const editSelectionInfos = edits.map(({ originalSelection }) => + const editSelectionInfos = edits.map(({ originalTarget }) => getSelectionInfo( editor.document, - originalSelection.selection.selection, + originalTarget.contentSelection, DecorationRangeBehavior.OpenOpen ) ); @@ -215,23 +183,24 @@ class BringMoveSwap implements Action { await performEditsAndUpdateFullSelectionInfos( this.graph.rangeUpdater, editor, - filteredEdits, + filteredEdits.map(({ edit }) => edit), [editSelectionInfos, cursorSelectionInfos] ); - editor.selections = cursorSelections; + // NB: We set the selections here because we don't trust vscode to + // properly move the cursor on a bring. Sometimes it will smear an + // empty selection + setSelectionsWithoutFocusingEditor(editor, cursorSelections); - return edits.map((edit, index) => { + return edits.map((edit, index): MarkEntry => { const selection = updatedEditSelections[index]; + const range = edit.edit.updateRange(selection); + const target = edit.originalTarget; return { editor, - selection, - isSource: edit!.isSource, - typedSelection: update(edit!.originalSelection, { - selection: { - selection: { $set: selection! }, - }, - }), + selection: selectionFromRange(target.isReversed, range), + isSource: edit.isSource, + target, }; }); } @@ -240,19 +209,21 @@ class BringMoveSwap implements Action { } private async decorateThatMark(thatMark: MarkEntry[]) { - const decorationTypes = this.getDecorationStyles(); + const decorationContext = this.getDecorationContext(); + const getRange = (target: Target) => + thatMark.find((t) => t.target === target)!.selection; return Promise.all([ - displayPendingEditDecorations( - thatMark - .filter(({ isSource }) => isSource) - .map(({ typedSelection }) => typedSelection), - decorationTypes.sourceStyle + this.graph.editStyles.displayPendingEditDecorations( + thatMark.filter(({ isSource }) => isSource).map(({ target }) => target), + decorationContext.sourceStyle, + getRange ), - displayPendingEditDecorations( + this.graph.editStyles.displayPendingEditDecorations( thatMark .filter(({ isSource }) => !isSource) - .map(({ typedSelection }) => typedSelection), - decorationTypes.destinationStyle + .map(({ target }) => target), + decorationContext.destinationStyle, + getRange ), ]); } @@ -274,8 +245,8 @@ class BringMoveSwap implements Action { } async run([sources, destinations]: [ - TypedSelection[], - TypedSelection[] + Target[], + Target[] ]): Promise { sources = this.broadcastSource(sources, destinations); @@ -310,3 +281,7 @@ export class Swap extends BringMoveSwap { super(graph, "swap"); } } + +function getRemovalRange(target: Target) { + return target.getRemovalRange(); +} diff --git a/src/actions/Call.ts b/src/actions/Call.ts index fc5a72554c..11399a388d 100644 --- a/src/actions/Call.ts +++ b/src/actions/Call.ts @@ -1,25 +1,16 @@ -import { - Action, - ActionReturnValue, - ActionPreferences, - Graph, - TypedSelection, -} from "../typings/Types"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { ensureSingleTarget } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class Call implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } async run([sources, destinations]: [ - TypedSelection[], - TypedSelection[] + Target[], + Target[] ]): Promise { ensureSingleTarget(sources); @@ -30,10 +21,13 @@ export default class Call implements Action { } ); - return this.graph.actions.wrapWithPairedDelimiter.run( + // NB: We unwrap and then rewrap the return value here so that we don't include the source mark + const { thatMark } = await this.graph.actions.wrapWithPairedDelimiter.run( [destinations], texts[0] + "(", ")" ); + + return { thatMark }; } } diff --git a/src/actions/Clear.ts b/src/actions/Clear.ts index 805a365966..c51d64d404 100644 --- a/src/actions/Clear.ts +++ b/src/actions/Clear.ts @@ -1,26 +1,29 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { ensureSingleEditor } from "../util/targetUtils"; +import PlainTarget from "../processTargets/targets/PlainTarget"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { setSelectionsAndFocusEditor } from "../util/setSelectionsAndFocusEditor"; +import { ensureSingleEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class Clear implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { const editor = ensureSingleEditor(targets); + // Convert to plain targets so that the remove action just removes the + // content range instead of the removal range + const plainTargets = targets.map( + (target) => + new PlainTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange: target.contentRange, + }) + ); - const { thatMark } = await this.graph.actions.remove.run([targets]); + const { thatMark } = await this.graph.actions.remove.run([plainTargets]); if (thatMark != null) { await setSelectionsAndFocusEditor( diff --git a/src/actions/CommandAction.ts b/src/actions/CommandAction.ts index 815dbfaf15..1542682843 100644 --- a/src/actions/CommandAction.ts +++ b/src/actions/CommandAction.ts @@ -1,21 +1,18 @@ +import { flatten } from "lodash"; import { commands, window } from "vscode"; -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, - SelectionWithEditor, -} from "../typings/Types"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { runOnTargetsForEachEditor } from "../util/targetUtils"; +import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { focusEditor, setSelectionsAndFocusEditor, + setSelectionsWithoutFocusingEditor, } from "../util/setSelectionsAndFocusEditor"; -import { flatten } from "lodash"; -import { ensureSingleEditor } from "../util/targetUtils"; -import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { + ensureSingleEditor, + runOnTargetsForEachEditor, +} from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export interface CommandOptions { command?: string; @@ -33,59 +30,53 @@ const defaultOptions: CommandOptions = { }; export default class CommandAction implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph, private options: CommandOptions = {}) { this.run = this.run.bind(this); } private async runCommandAndUpdateSelections( - targets: TypedSelection[], + targets: Target[], options: Required ) { return flatten( - await runOnTargetsForEachEditor( - targets, - async (editor, targets) => { - const originalSelections = editor.selections; - - const targetSelections = targets.map( - (target) => target.selection.selection + await runOnTargetsForEachEditor(targets, async (editor, targets) => { + const originalSelections = editor.selections; + + const targetSelections = targets.map( + (target) => target.contentSelection + ); + + // For command to the work we have to have the correct editor focused + await setSelectionsAndFocusEditor(editor, targetSelections, false); + + const [updatedOriginalSelections, updatedTargetSelections] = + await callFunctionAndUpdateSelections( + this.graph.rangeUpdater, + () => + commands.executeCommand(options.command, ...options.commandArgs), + editor.document, + [originalSelections, targetSelections] ); - // For command to the work we have to have the correct editor focused - await setSelectionsAndFocusEditor(editor, targetSelections, false); - - const [updatedOriginalSelections, updatedTargetSelections] = - await callFunctionAndUpdateSelections( - this.graph.rangeUpdater, - () => - commands.executeCommand( - options.command, - ...options.commandArgs - ), - editor.document, - [originalSelections, targetSelections] - ); - - // Reset original selections - if (options.restoreSelection) { - editor.selections = updatedOriginalSelections; - } - - return updatedTargetSelections.map((selection) => ({ - editor, - selection, - })); + // Reset original selections + if (options.restoreSelection) { + // NB: We don't focus the editor here because we'll do that at the + // very end. This code can run on multiple editors in the course of + // one command, so we want to avoid focusing the editor multiple + // times. + setSelectionsWithoutFocusingEditor(editor, updatedOriginalSelections); } - ) + + return updatedTargetSelections.map((selection) => ({ + editor, + selection, + })); + }) ); } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], options: CommandOptions = {} ): Promise { const partialOptions = Object.assign( @@ -102,7 +93,7 @@ export default class CommandAction implements Action { const actualOptions = partialOptions as Required; if (actualOptions.showDecorations) { - await displayPendingEditDecorations( + await this.graph.editStyles.displayPendingEditDecorations( targets, this.graph.editStyles.referenced ); @@ -125,6 +116,9 @@ export default class CommandAction implements Action { originalEditor != null && originalEditor !== window.activeTextEditor ) { + // NB: We just do one editor focus at the end, instead of using + // setSelectionsAndFocusEditor because the command might operate on + // multiple editors, so we just do one focus at the end. await focusEditor(originalEditor); } diff --git a/src/actions/CopyLines.ts b/src/actions/CopyLines.ts deleted file mode 100644 index 0f9cd9144d..0000000000 --- a/src/actions/CopyLines.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { Range, Selection, TextEditor } from "vscode"; -import { displayPendingEditDecorationsForSelection } from "../util/editDisplayUtils"; -import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import { flatten } from "lodash"; -import unifyRanges from "../util/unifyRanges"; -import expandToContainingLine from "../util/expandToContainingLine"; -import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; - -class CopyLines implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - - constructor(private graph: Graph, private isUp: boolean) { - this.run = this.run.bind(this); - } - - private getRanges(editor: TextEditor, targets: TypedSelection[]) { - const paragraphTargets = targets.filter( - (target) => target.selectionType === "paragraph" - ); - const ranges = targets.map((target) => - expandToContainingLine(editor, target.selection.selection) - ); - const unifiedRanges = unifyRanges(ranges); - return unifiedRanges.map((range) => ({ - range, - isParagraph: - paragraphTargets.find((target) => - target.selection.selection.isEqual(range) - ) != null, - })); - } - - private getEdits( - editor: TextEditor, - ranges: { range: Range; isParagraph: boolean }[] - ) { - return ranges.map(({ range, isParagraph }) => { - const delimiter = isParagraph ? "\n\n" : "\n"; - let text = editor.document.getText(range); - const length = text.length; - text = this.isUp ? `${delimiter}${text}` : `${text}${delimiter}`; - const newRange = this.isUp - ? new Range(range.end, range.end) - : new Range(range.start, range.start); - return { - edit: { - editor, - range: newRange, - text, - isReplace: this.isUp, - }, - offset: delimiter.length, - length, - }; - }); - } - - async run([targets]: [TypedSelection[]]): Promise { - const results = flatten( - await runOnTargetsForEachEditor(targets, async (editor, targets) => { - const ranges = this.getRanges(editor, targets); - const editWrappers = this.getEdits(editor, ranges); - const rangeSelections = ranges.map( - ({ range }) => new Selection(range.start, range.end) - ); - - const [editorSelections, copySelections] = - await performEditsAndUpdateSelections( - this.graph.rangeUpdater, - editor, - editWrappers.map((wrapper) => wrapper.edit), - [editor.selections, rangeSelections] - ); - - editor.selections = editorSelections; - editor.revealRange(copySelections[0]); - - let sourceSelections; - if (this.isUp) { - sourceSelections = editWrappers.map((wrapper) => { - const startIndex = - editor.document.offsetAt(wrapper.edit.range.start) + - wrapper.offset; - const endIndex = startIndex + wrapper.length; - return new Selection( - editor.document.positionAt(startIndex), - editor.document.positionAt(endIndex) - ); - }); - } else { - sourceSelections = rangeSelections; - } - - return { - sourceMark: sourceSelections.map((selection) => ({ - editor, - selection, - })), - thatMark: copySelections.map((selection) => ({ - editor, - selection, - })), - }; - }) - ); - - await displayPendingEditDecorationsForSelection( - results.flatMap((result) => result.thatMark), - this.graph.editStyles.justAdded.token - ); - - const sourceMark = results.flatMap((result) => result.sourceMark); - const thatMark = results.flatMap((result) => result.thatMark); - - return { sourceMark, thatMark }; - } -} - -export class CopyLinesUp extends CopyLines { - constructor(graph: Graph) { - super(graph, true); - } -} - -export class CopyLinesDown extends CopyLines { - constructor(graph: Graph) { - super(graph, false); - } -} diff --git a/src/actions/CutCopyPaste.ts b/src/actions/CutCopyPaste.ts index deb1544570..3847b6de4b 100644 --- a/src/actions/CutCopyPaste.ts +++ b/src/actions/CutCopyPaste.ts @@ -1,52 +1,49 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { performInsideOutsideAdjustment } from "../util/performInsideOutsideAdjustment"; -import CommandAction from "./CommandAction"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; +import PlainTarget from "../processTargets/targets/PlainTarget"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { getOutsideOverflow } from "../util/targetUtils"; -import { zip } from "lodash"; +import { Action, ActionReturnValue } from "./actions.types"; +import CommandAction from "./CommandAction"; export class Cut implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: null }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { - const insideTargets = targets.map((target) => - performInsideOutsideAdjustment(target, "inside") - ); - const outsideTargets = targets.map((target) => - performInsideOutsideAdjustment(target, "outside") - ); - const outsideTargetDecorations = zip(insideTargets, outsideTargets).flatMap( - ([inside, outside]) => getOutsideOverflow(inside!, outside!) - ); - const options = { showDecorations: false }; + async run([targets]: [Target[]]): Promise { + const overflowTargets = targets.flatMap((target) => { + const range = target.getRemovalHighlightRange(); + if (range == null) { + return []; + } + return getOutsideOverflow(target.editor, target.contentRange, range).map( + (overflow): Target => + // TODO Instead of creating a new target display decorations by range + new PlainTarget({ + editor: target.editor, + contentRange: overflow, + isReversed: target.isReversed, + }) + ); + }); await Promise.all([ - displayPendingEditDecorations( - insideTargets, + this.graph.editStyles.displayPendingEditDecorations( + targets, this.graph.editStyles.referenced ), - displayPendingEditDecorations( - outsideTargetDecorations, + this.graph.editStyles.displayPendingEditDecorations( + overflowTargets, this.graph.editStyles.pendingDelete ), ]); - await this.graph.actions.copyToClipboard.run([insideTargets], options); + const options = { showDecorations: false }; + + await this.graph.actions.copyToClipboard.run([targets], options); const { thatMark } = await this.graph.actions.remove.run( - [outsideTargets], + [targets], options ); @@ -59,6 +56,7 @@ export class Copy extends CommandAction { super(graph, { command: "editor.action.clipboardCopyAction", ensureSingleEditor: true, + showDecorations: true, }); } } @@ -68,6 +66,7 @@ export class Paste extends CommandAction { super(graph, { command: "editor.action.clipboardPasteAction", ensureSingleEditor: true, + showDecorations: true, }); } } diff --git a/src/actions/Delete.ts b/src/actions/Delete.ts deleted file mode 100644 index 423d1b1339..0000000000 --- a/src/actions/Delete.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { flatten } from "lodash"; -import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; -import { unifyTargets } from "../util/unifyRanges"; - -export default class Delete implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "outside" }, - ]; - - constructor(private graph: Graph) { - this.run = this.run.bind(this); - } - - async run( - [targets]: [TypedSelection[]], - { showDecorations = true } = {} - ): Promise { - // Unify overlapping targets. - targets = unifyTargets(targets); - - if (showDecorations) { - await displayPendingEditDecorations( - targets, - this.graph.editStyles.pendingDelete - ); - } - - const thatMark = flatten( - await runOnTargetsForEachEditor(targets, async (editor, targets) => { - const edits = targets.map((target) => ({ - range: target.selection.selection, - text: "", - })); - - const [updatedSelections] = await performEditsAndUpdateSelections( - this.graph.rangeUpdater, - editor, - edits, - [targets.map((target) => target.selection.selection)] - ); - - return updatedSelections.map((selection) => ({ editor, selection })); - }) - ); - - return { thatMark }; - } -} diff --git a/src/actions/Deselect.ts b/src/actions/Deselect.ts index 656c59a40f..553f939cd5 100644 --- a/src/actions/Deselect.ts +++ b/src/actions/Deselect.ts @@ -1,41 +1,36 @@ import { Selection } from "vscode"; -import { - Action, - ActionPreferences, - ActionReturnValue, - TypedSelection, - Graph, -} from "../typings/Types"; -import { runOnTargetsForEachEditor } from "../util/targetUtils"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; +import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class Deselect implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { await runOnTargetsForEachEditor(targets, async (editor, targets) => { // Remove selections with a non-empty intersection const newSelections = editor.selections.filter( (selection) => !targets.some((target) => { - const intersection = - target.selection.selection.intersection(selection); + const intersection = target.contentRange.intersection(selection); return intersection && (!intersection.isEmpty || selection.isEmpty); }) ); // The editor requires at least one selection. Keep "primary" selection active - editor.selections = newSelections.length - ? newSelections - : [new Selection(editor.selection.active, editor.selection.active)]; + setSelectionsWithoutFocusingEditor( + editor, + newSelections.length > 0 + ? newSelections + : [new Selection(editor.selection.active, editor.selection.active)] + ); }); return { - thatMark: targets.map((target) => target.selection), + thatMark: createThatMark(targets), }; } } diff --git a/src/actions/EditNew.ts b/src/actions/EditNew.ts new file mode 100644 index 0000000000..608d1bb18d --- /dev/null +++ b/src/actions/EditNew.ts @@ -0,0 +1,237 @@ +import { zip } from "lodash"; +import { + commands, + DecorationRangeBehavior, + Range, + Selection, + TextEditor, +} from "vscode"; +import { + callFunctionAndUpdateRanges, + performEditsAndUpdateSelectionsWithBehavior, +} from "../core/updateSelections/updateSelections"; +import { weakContainingLineStage } from "../processTargets/modifiers/commonWeakContainingScopeStages"; +import { toPositionTarget } from "../processTargets/modifiers/PositionStage"; +import NotebookCellTarget from "../processTargets/targets/NotebookCellTarget"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { selectionFromRange } from "../util/selectionUtils"; +import { setSelectionsAndFocusEditor } from "../util/setSelectionsAndFocusEditor"; +import { + createThatMark, + ensureSingleEditor, + ensureSingleTarget, +} from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; + +class EditNew implements Action { + getFinalStages = () => [weakContainingLineStage]; + + constructor(private graph: Graph, private isBefore: boolean) { + this.run = this.run.bind(this); + } + + async run([targets]: [Target[]]): Promise { + if (targets.some((target) => target.isNotebookCell)) { + return this.handleNotebookCellTargets(targets); + } + + const editor = ensureSingleEditor(targets); + + const richTargets: RichTarget[] = targets.map((target) => { + const context = target.getEditNewContext(this.isBefore); + const common = { + target, + cursorRange: target.contentRange, + }; + switch (context.type) { + case "command": + return { + ...common, + type: "command", + command: context.command, + }; + case "delimiter": + return { + ...common, + type: "delimiter", + delimiter: context.delimiter, + }; + } + }); + + const commandTargets: CommandTarget[] = richTargets.filter( + (target): target is CommandTarget => target.type === "command" + ); + const delimiterTargets: DelimiterTarget[] = richTargets.filter( + (target): target is DelimiterTarget => target.type === "delimiter" + ); + + if (commandTargets.length > 0) { + await this.runCommandTargets(editor, richTargets, commandTargets); + } + if (delimiterTargets.length > 0) { + await this.runDelimiterTargets(editor, commandTargets, delimiterTargets); + } + + const newSelections = richTargets.map((target) => + selectionFromRange(target.target.isReversed, target.cursorRange) + ); + await setSelectionsAndFocusEditor(editor, newSelections); + + return { + thatMark: createThatMark(richTargets.map(({ target }) => target)), + }; + } + + async handleNotebookCellTargets( + targets: Target[] + ): Promise { + const target = ensureSingleTarget(targets) as NotebookCellTarget; + await this.setSelections([target]); + const command = target.getEditNewCommand(this.isBefore); + await commands.executeCommand(command); + const thatMark = createThatMark([target.thatTarget]); + + // Inserting a new jupyter cell above pushes the previous one down two lines + if (command === "jupyter.insertCellAbove") { + thatMark[0].selection = new Selection( + thatMark[0].selection.anchor.translate({ lineDelta: 2 }), + thatMark[0].selection.active.translate({ lineDelta: 2 }) + ); + } + + return { thatMark }; + } + + async runDelimiterTargets( + editor: TextEditor, + commandTargets: CommandTarget[], + delimiterTargets: DelimiterTarget[] + ) { + const position = this.isBefore ? "before" : "after"; + const edits = delimiterTargets.flatMap((target) => + toPositionTarget(target.target, position).constructChangeEdit("") + ); + + const cursorSelections = { selections: editor.selections }; + const contentSelections = { + selections: delimiterTargets.map( + ({ target }) => target.thatTarget.contentSelection + ), + }; + const editSelections = { + selections: edits.map( + ({ range }) => new Selection(range.start, range.end) + ), + rangeBehavior: DecorationRangeBehavior.OpenOpen, + }; + + const [ + updatedEditorSelections, + updatedContentSelections, + updatedEditSelections, + ]: Selection[][] = await performEditsAndUpdateSelectionsWithBehavior( + this.graph.rangeUpdater, + editor, + edits, + [cursorSelections, contentSelections, editSelections] + ); + + const insertionRanges = zip(edits, updatedEditSelections).map( + ([edit, selection]) => edit!.updateRange(selection!) + ); + + updateTargets(delimiterTargets, updatedContentSelections, insertionRanges); + updateCommandTargets(commandTargets, updatedEditorSelections); + } + + async runCommandTargets( + editor: TextEditor, + targets: RichTarget[], + commandTargets: CommandTarget[] + ) { + const command = ensureSingleCommand(commandTargets); + + await this.setSelections(commandTargets.map(({ target }) => target)); + + const [updatedTargetRanges, updatedCursorRanges] = + await callFunctionAndUpdateRanges( + this.graph.rangeUpdater, + () => commands.executeCommand(command), + editor.document, + [ + targets.map(({ target }) => target.thatTarget.contentRange), + targets.map(({ cursorRange }) => cursorRange), + ] + ); + + updateTargets(targets, updatedTargetRanges, updatedCursorRanges); + updateCommandTargets(commandTargets, editor.selections); + } + + private async setSelections(targets: Target[]) { + if (this.isBefore) { + await this.graph.actions.setSelectionBefore.run([targets]); + } else { + await this.graph.actions.setSelectionAfter.run([targets]); + } + } +} + +export class EditNewBefore extends EditNew { + constructor(graph: Graph) { + super(graph, true); + } +} + +export class EditNewAfter extends EditNew { + constructor(graph: Graph) { + super(graph, false); + } +} + +interface CommonTarget { + target: Target; + cursorRange: Range; +} + +interface CommandTarget extends CommonTarget { + type: "command"; + command: string; +} +interface DelimiterTarget extends CommonTarget { + type: "delimiter"; + delimiter: string; +} +type RichTarget = CommandTarget | DelimiterTarget; + +function ensureSingleCommand(targets: CommandTarget[]) { + const commands = targets.map((target) => target.command); + if (new Set(commands).size > 1) { + throw new Error("Can't run multiple different commands at once"); + } + return commands[0]; +} + +function updateCommandTargets( + targets: CommandTarget[], + cursorRanges: readonly Range[] +) { + targets.forEach((target, i) => { + target.cursorRange = cursorRanges[i]; + }); +} + +function updateTargets( + targets: RichTarget[], + updatedTargetRanges: Range[], + updatedCursorRanges: Range[] +) { + zip(targets, updatedTargetRanges, updatedCursorRanges).forEach( + ([target, updatedTargetRange, updatedCursorRange]) => { + target!.target = target!.target.withContentRange(updatedTargetRange!); + target!.cursorRange = updatedCursorRange!; + } + ); +} diff --git a/src/actions/EditNewLine.ts b/src/actions/EditNewLine.ts deleted file mode 100644 index 111b0fe917..0000000000 --- a/src/actions/EditNewLine.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { commands, Selection, TextEditor } from "vscode"; -import { getNotebookFromCellDocument } from "../util/notebook"; - -class EditNewLine implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - - constructor(private graph: Graph, private isAbove: boolean) { - this.run = this.run.bind(this); - } - - private correctForParagraph(targets: TypedSelection[]) { - targets.forEach((target) => { - let { start, end } = target.selection.selection; - if (target.selectionType === "paragraph") { - if ( - this.isAbove && - target.selectionContext.leadingDelimiterRange != null - ) { - start = start.translate({ lineDelta: -1 }); - } else if ( - !this.isAbove && - target.selectionContext.trailingDelimiterRange != null - ) { - end = end.translate({ lineDelta: 1 }); - } - target.selection.selection = new Selection(start, end); - } - }); - } - - private isNotebookEditor(editor: TextEditor) { - return getNotebookFromCellDocument(editor.document) != null; - } - - private getCommand(target: TypedSelection) { - if (target.selectionContext.isNotebookCell) { - if (this.isNotebookEditor(target.selection.editor)) { - return this.isAbove - ? "notebook.cell.insertCodeCellAbove" - : "notebook.cell.insertCodeCellBelow"; - } - return this.isAbove - ? "jupyter.insertCellAbove" - : "jupyter.insertCellBelow"; - } - return this.isAbove - ? "editor.action.insertLineBefore" - : "editor.action.insertLineAfter"; - } - - async run([targets]: [TypedSelection[]]): Promise { - this.correctForParagraph(targets); - - if (this.isAbove) { - await this.graph.actions.setSelectionBefore.run([targets]); - } else { - await this.graph.actions.setSelectionAfter.run([targets]); - } - - const command = this.getCommand(targets[0]); - await commands.executeCommand(command); - - return { - thatMark: targets.map((target) => ({ - selection: target.selection.editor.selection, - editor: target.selection.editor, - })), - }; - } -} - -export class EditNewLineAbove extends EditNewLine { - constructor(graph: Graph) { - super(graph, true); - } -} - -export class EditNewLineBelow extends EditNewLine { - constructor(graph: Graph) { - super(graph, false); - } -} diff --git a/src/actions/ExecuteCommand.ts b/src/actions/ExecuteCommand.ts index 6a27036034..3120b199bb 100644 --- a/src/actions/ExecuteCommand.ts +++ b/src/actions/ExecuteCommand.ts @@ -1,16 +1,9 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { Action, ActionReturnValue } from "./actions.types"; import CommandAction, { CommandOptions } from "./CommandAction"; export default class ExecuteCommand implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; private commandAction: CommandAction; constructor(graph: Graph) { @@ -19,7 +12,7 @@ export default class ExecuteCommand implements Action { } async run( - targets: [TypedSelection[]], + targets: [Target[]], command: string, args: CommandOptions = {} ): Promise { diff --git a/src/actions/ExtractVariable.ts b/src/actions/ExtractVariable.ts index ecf3daf2e2..c48e54b239 100644 --- a/src/actions/ExtractVariable.ts +++ b/src/actions/ExtractVariable.ts @@ -1,23 +1,15 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { ensureSingleTarget } from "../util/targetUtils"; import { commands } from "vscode"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { ensureSingleTarget } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class ExtractVariable implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { ensureSingleTarget(targets); await this.graph.actions.setSelection.run([targets]); diff --git a/src/actions/Find.ts b/src/actions/Find.ts index f69e0d58f3..f4c1e7c626 100644 --- a/src/actions/Find.ts +++ b/src/actions/Find.ts @@ -1,23 +1,15 @@ -import { - Action, - ActionReturnValue, - ActionPreferences, - Graph, - TypedSelection, -} from "../typings/Types"; import { commands } from "vscode"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { ensureSingleTarget } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export class FindInFiles implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { ensureSingleTarget(targets); const { diff --git a/src/actions/Fold.ts b/src/actions/Fold.ts index 62bcf074ec..05529ad784 100644 --- a/src/actions/Fold.ts +++ b/src/actions/Fold.ts @@ -1,27 +1,16 @@ import { commands, window } from "vscode"; -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { focusEditor } from "../util/setSelectionsAndFocusEditor"; -import { ensureSingleEditor } from "../util/targetUtils"; +import { createThatMark, ensureSingleEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; class FoldAction implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private command: string) { this.run = this.run.bind(this); } - async run([targets]: [ - TypedSelection[], - TypedSelection[] - ]): Promise { + async run([targets]: [Target[], Target[]]): Promise { const originalEditor = window.activeTextEditor; const editor = ensureSingleEditor(targets); @@ -30,10 +19,10 @@ class FoldAction implements Action { } const singleLineTargets = targets.filter( - (target) => target.selection.selection.isSingleLine + (target) => target.contentRange.isSingleLine ); const multiLineTargets = targets.filter( - (target) => !target.selection.selection.isSingleLine + (target) => !target.contentRange.isSingleLine ); // Don't mix multi and single line targets. // This is probably the result of an "every" command @@ -46,7 +35,7 @@ class FoldAction implements Action { levels: 1, direction: "down", selectionLines: selectedTargets.map( - (target) => target.selection.selection.start.line + (target) => target.contentRange.start.line ), }); @@ -56,7 +45,7 @@ class FoldAction implements Action { } return { - thatMark: targets.map((target) => target.selection), + thatMark: createThatMark(targets), }; } } diff --git a/src/actions/FollowLink.ts b/src/actions/FollowLink.ts index ee5e6af384..574d87ea44 100644 --- a/src/actions/FollowLink.ts +++ b/src/actions/FollowLink.ts @@ -1,28 +1,19 @@ import { env, Uri, window } from "vscode"; -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { getLinkForTarget } from "../util/getLinks"; -import { ensureSingleTarget } from "../util/targetUtils"; +import { createThatMark, ensureSingleTarget } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class FollowLink implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { const target = ensureSingleTarget(targets); - await displayPendingEditDecorations( + await this.graph.editStyles.displayPendingEditDecorations( targets, this.graph.editStyles.referenced ); @@ -39,7 +30,7 @@ export default class FollowLink implements Action { } return { - thatMark: targets.map((target) => target.selection), + thatMark: createThatMark(targets), }; } diff --git a/src/actions/GetText.ts b/src/actions/GetText.ts index 9fbb4db403..3b8594dfac 100644 --- a/src/actions/GetText.ts +++ b/src/actions/GetText.ts @@ -1,46 +1,34 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { ensureSingleTarget } from "../util/targetUtils"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { createThatMark, ensureSingleTarget } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class GetText implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], { showDecorations = true, ensureSingleTarget: doEnsureSingleTarget = false, } = {} ): Promise { if (showDecorations) { - await displayPendingEditDecorations( + await this.graph.editStyles.displayPendingEditDecorations( targets, this.graph.editStyles.referenced ); } + if (doEnsureSingleTarget) { ensureSingleTarget(targets); } - const returnValue = targets.map((target) => - target.selection.editor.document.getText(target.selection.selection) - ); - return { - returnValue, - thatMark: targets.map((target) => target.selection), + returnValue: targets.map((target) => target.contentText), + thatMark: createThatMark(targets), }; } } diff --git a/src/actions/Highlight.ts b/src/actions/Highlight.ts index 67753bf076..fc1657ea05 100644 --- a/src/actions/Highlight.ts +++ b/src/actions/Highlight.ts @@ -1,33 +1,25 @@ import { EditStyleName } from "../core/editStyles"; -import { - Action, - ActionPreferences, - ActionReturnValue, - TypedSelection, - Graph, -} from "../typings/Types"; -import { clearDecorations, setDecorations } from "../util/editDisplayUtils"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { createThatMark } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class Highlight implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], styleName: EditStyleName = "highlight0" ): Promise { const style = this.graph.editStyles[styleName]; - clearDecorations(style); - await setDecorations(targets, style); + this.graph.editStyles.clearDecorations(style); + await this.graph.editStyles.setDecorations(targets, style); return { - thatMark: targets.map((target) => target.selection), + thatMark: createThatMark(targets), }; } } diff --git a/src/actions/InsertCopy.ts b/src/actions/InsertCopy.ts new file mode 100644 index 0000000000..f8c42fe683 --- /dev/null +++ b/src/actions/InsertCopy.ts @@ -0,0 +1,95 @@ +import { flatten, zip } from "lodash"; +import { DecorationRangeBehavior, Selection, TextEditor } from "vscode"; +import { performEditsAndUpdateSelectionsWithBehavior } from "../core/updateSelections/updateSelections"; +import { weakContainingLineStage } from "../processTargets/modifiers/commonWeakContainingScopeStages"; +import { toPositionTarget } from "../processTargets/modifiers/PositionStage"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; +import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; + +class InsertCopy implements Action { + getFinalStages = () => [weakContainingLineStage]; + + constructor(private graph: Graph, private isBefore: boolean) { + this.run = this.run.bind(this); + this.runForEditor = this.runForEditor.bind(this); + } + + async run([targets]: [Target[]]): Promise { + const results = flatten( + await runOnTargetsForEachEditor(targets, this.runForEditor) + ); + + await this.graph.editStyles.displayPendingEditDecorationsForRanges( + results.flatMap((result) => + result.thatMark.map((that) => ({ + editor: that.editor, + range: that.selection, + })) + ), + this.graph.editStyles.justAdded, + true + ); + + return { + sourceMark: results.flatMap(({ sourceMark }) => sourceMark), + thatMark: results.flatMap(({ thatMark }) => thatMark), + }; + } + + private async runForEditor(editor: TextEditor, targets: Target[]) { + // isBefore is inverted because we want the selections to stay with what is to the user the "copy" + const position = this.isBefore ? "after" : "before"; + const edits = targets.flatMap((target) => + toPositionTarget(target, position).constructChangeEdit(target.contentText) + ); + + const cursorSelections = { selections: editor.selections }; + const contentSelections = { + selections: targets.map(({ contentSelection }) => contentSelection), + }; + const editSelections = { + selections: edits.map( + ({ range }) => new Selection(range.start, range.end) + ), + rangeBehavior: DecorationRangeBehavior.OpenOpen, + }; + + const [ + updatedEditorSelections, + updatedContentSelections, + updatedEditSelections, + ]: Selection[][] = await performEditsAndUpdateSelectionsWithBehavior( + this.graph.rangeUpdater, + editor, + edits, + [cursorSelections, contentSelections, editSelections] + ); + + const insertionRanges = zip(edits, updatedEditSelections).map( + ([edit, selection]) => edit!.updateRange(selection!) + ); + + setSelectionsWithoutFocusingEditor(editor, updatedEditorSelections); + editor.revealRange(editor.selection); + + return { + sourceMark: createThatMark(targets, insertionRanges), + thatMark: createThatMark(targets, updatedContentSelections), + }; + } +} + +export class CopyContentBefore extends InsertCopy { + constructor(graph: Graph) { + super(graph, true); + } +} + +export class CopyContentAfter extends InsertCopy { + constructor(graph: Graph) { + super(graph, false); + } +} diff --git a/src/actions/InsertEmptyLines.ts b/src/actions/InsertEmptyLines.ts index 415432f2fd..fefc942d62 100644 --- a/src/actions/InsertEmptyLines.ts +++ b/src/actions/InsertEmptyLines.ts @@ -1,21 +1,13 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { Selection, Range } from "vscode"; -import { displayPendingEditDecorationsForSelection } from "../util/editDisplayUtils"; -import { runOnTargetsForEachEditor } from "../util/targetUtils"; import { flatten } from "lodash"; +import { Range, Selection } from "vscode"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; +import { runOnTargetsForEachEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; class InsertEmptyLines implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor( private graph: Graph, private insertAbove: boolean, @@ -24,14 +16,14 @@ class InsertEmptyLines implements Action { this.run = this.run.bind(this); } - private getRanges(targets: TypedSelection[]) { + private getRanges(targets: Target[]) { let lines = targets.flatMap((target) => { - const lines = []; + const lines: number[] = []; if (this.insertAbove) { - lines.push(target.selection.selection.start.line); + lines.push(target.contentRange.start.line); } if (this.insertBelow) { - lines.push(target.selection.selection.end.line + 1); + lines.push(target.contentRange.end.line + 1); } return lines; }); @@ -48,36 +40,36 @@ class InsertEmptyLines implements Action { })); } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { const results = flatten( await runOnTargetsForEachEditor(targets, async (editor, targets) => { const ranges = this.getRanges(targets); const edits = this.getEdits(ranges); - const [updatedSelections, lineSelections, updatedOriginalSelections] = + const [updatedThatSelections, lineSelections, updatedCursorSelections] = await performEditsAndUpdateSelections( this.graph.rangeUpdater, editor, edits, [ - targets.map((target) => target.selection.selection), + targets.map((target) => target.thatTarget.contentSelection), ranges.map((range) => new Selection(range.start, range.end)), editor.selections, ] ); - editor.selections = updatedOriginalSelections; + setSelectionsWithoutFocusingEditor(editor, updatedCursorSelections); return { - thatMark: updatedSelections.map((selection) => ({ + thatMark: updatedThatSelections.map((selection) => ({ editor, selection, })), lineSelections: lineSelections.map((selection, index) => ({ editor, - selection: + range: ranges[index].start.line < editor.document.lineCount - 1 - ? new Selection( + ? new Range( selection.start.translate({ lineDelta: -1 }), selection.end.translate({ lineDelta: -1 }) ) @@ -87,9 +79,10 @@ class InsertEmptyLines implements Action { }) ); - await displayPendingEditDecorationsForSelection( + await this.graph.editStyles.displayPendingEditDecorationsForRanges( results.flatMap((result) => result.lineSelections), - this.graph.editStyles.justAdded.line + this.graph.editStyles.justAdded, + false ); const thatMark = results.flatMap((result) => result.thatMark); diff --git a/src/actions/Remove.ts b/src/actions/Remove.ts new file mode 100644 index 0000000000..a15f32103b --- /dev/null +++ b/src/actions/Remove.ts @@ -0,0 +1,47 @@ +import { flatten } from "lodash"; +import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; +import { unifyRemovalTargets } from "../util/unifyRanges"; +import { Action, ActionReturnValue } from "./actions.types"; + +export default class Delete implements Action { + constructor(private graph: Graph) { + this.run = this.run.bind(this); + } + + async run( + [targets]: [Target[]], + { showDecorations = true } = {} + ): Promise { + // Unify overlapping targets because of overlapping leading and trailing delimiters. + targets = unifyRemovalTargets(targets); + + if (showDecorations) { + await this.graph.editStyles.displayPendingEditDecorations( + targets, + this.graph.editStyles.pendingDelete, + (target) => target.getRemovalHighlightRange() + ); + } + + const thatMark = flatten( + await runOnTargetsForEachEditor(targets, async (editor, targets) => { + const edits = targets.map((target) => target.constructRemovalEdit()); + const ranges = edits.map((edit) => edit.range); + + const [updatedRanges] = await performEditsAndUpdateRanges( + this.graph.rangeUpdater, + editor, + edits, + [ranges] + ); + + return createThatMark(targets, updatedRanges); + }) + ); + + return { thatMark }; + } +} diff --git a/src/actions/Replace.ts b/src/actions/Replace.ts index 953ba4cd09..c96a306a77 100644 --- a/src/actions/Replace.ts +++ b/src/actions/Replace.ts @@ -1,29 +1,19 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { runForEachEditor } from "../util/targetUtils"; import { flatten, zip } from "lodash"; -import { maybeAddDelimiter } from "../util/getTextWithPossibleDelimiter"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { runForEachEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; type RangeGenerator = { start: number }; export default class implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: null }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } private getTexts( - targets: TypedSelection[], + targets: Target[], replaceWith: string[] | RangeGenerator ): string[] { if (Array.isArray(replaceWith)) { @@ -41,10 +31,10 @@ export default class implements Action { } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], replaceWith: string[] | RangeGenerator ): Promise { - await displayPendingEditDecorations( + await this.graph.editStyles.displayPendingEditDecorations( targets, this.graph.editStyles.pendingModification0 ); @@ -56,9 +46,8 @@ export default class implements Action { } const edits = zip(targets, texts).map(([target, text]) => ({ - editor: target!.selection.editor, - range: target!.selection.selection, - text: maybeAddDelimiter(text!, target!), + edit: target!.constructChangeEdit(text!), + editor: target!.editor, })); const thatMark = flatten( @@ -69,8 +58,8 @@ export default class implements Action { const [updatedSelections] = await performEditsAndUpdateSelections( this.graph.rangeUpdater, editor, - edits, - [targets.map((target) => target.selection.selection)] + edits.map(({ edit }) => edit), + [targets.map((target) => target.contentSelection)] ); return updatedSelections.map((selection) => ({ diff --git a/src/actions/Rewrap.ts b/src/actions/Rewrap.ts index cf738163c0..8146b04f88 100644 --- a/src/actions/Rewrap.ts +++ b/src/actions/Rewrap.ts @@ -1,104 +1,67 @@ -import { flatten, zip } from "lodash"; -import { TextEditor } from "vscode"; -import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - SelectionWithContext, - TypedSelection, -} from "../typings/Types"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { runForEachEditor } from "../util/targetUtils"; +import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; +import { weakContainingSurroundingPairStage } from "../processTargets/modifiers/commonWeakContainingScopeStages"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class Rewrap implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { - insideOutsideType: "inside", - modifier: { - type: "surroundingPair", - delimiter: "any", - delimiterInclusion: undefined, - }, - }, - ]; + getFinalStages = () => [weakContainingSurroundingPairStage]; constructor(private graph: Graph) { this.run = this.run.bind(this); } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], left: string, right: string ): Promise { - const targetInfos = targets.flatMap((target) => { - const boundary = target.selectionContext.boundary; + const boundaryTargets = targets.flatMap((target) => { + const boundary = target.getBoundaryStrict(); - if (boundary == null || boundary.length !== 2) { + if (boundary.length !== 2) { throw Error("Target must have an opening and closing delimiter"); } - return { - editor: target.selection.editor, - boundary: boundary.map((edge) => - constructSimpleTypedSelection(target.selection.editor, edge) - ), - targetSelection: target.selection.selection, - }; + return boundary; }); - await displayPendingEditDecorations( - targetInfos.flatMap(({ boundary }) => boundary), + await this.graph.editStyles.displayPendingEditDecorations( + boundaryTargets, this.graph.editStyles.pendingModification0 ); - const thatMark = flatten( - await runForEachEditor( - targetInfos, - (targetInfo) => targetInfo.editor, - async (editor, targetInfos) => { - const edits = targetInfos.flatMap((targetInfo) => - zip(targetInfo.boundary, [left, right]).map(([target, text]) => ({ - editor, - range: target!.selection.selection, - text: text!, - })) - ); - - const [updatedTargetSelections] = - await performEditsAndUpdateSelections( - this.graph.rangeUpdater, - editor, - edits, - [targetInfos.map((targetInfo) => targetInfo.targetSelection)] - ); + const results = await runOnTargetsForEachEditor( + boundaryTargets, + async (editor, boundaryTargets) => { + const edits = boundaryTargets.map((target, i) => ({ + editor, + range: target.contentRange, + text: i % 2 === 0 ? left : right, + })); - return updatedTargetSelections.map((selection) => ({ + const [updatedSourceRanges, updatedThatRanges] = + await performEditsAndUpdateRanges( + this.graph.rangeUpdater, editor, - selection, - })); - } - ) + edits, + [ + targets.map((target) => target.thatTarget.contentRange), + targets.map((target) => target.contentRange), + ] + ); + + return { + sourceMark: createThatMark(targets, updatedSourceRanges), + thatMark: createThatMark(targets, updatedThatRanges), + }; + } ); - return { thatMark }; + return { + sourceMark: results.flatMap(({ sourceMark }) => sourceMark), + thatMark: results.flatMap(({ thatMark }) => thatMark), + }; } } - -function constructSimpleTypedSelection( - editor: TextEditor, - selection: SelectionWithContext -): TypedSelection { - return { - selection: { - selection: selection.selection, - editor, - }, - selectionType: "token", - selectionContext: selection.context, - insideOutsideType: null, - position: "contents", - }; -} diff --git a/src/actions/Scroll.ts b/src/actions/Scroll.ts index 19ea4590bd..7ef90f4acc 100644 --- a/src/actions/Scroll.ts +++ b/src/actions/Scroll.ts @@ -1,29 +1,18 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { groupBy } from "../util/itertools"; import { commands, window } from "vscode"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { groupBy } from "../util/itertools"; import { focusEditor } from "../util/setSelectionsAndFocusEditor"; -import { displayPendingEditDecorationsForSelection } from "../util/editDisplayUtils"; +import { createThatMark } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; class Scroll implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph, private at: string) { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { - const selectionGroups = groupBy( - targets, - (t: TypedSelection) => t.selection.editor - ); + async run([targets]: [Target[]]): Promise { + const selectionGroups = groupBy(targets, (t: Target) => t.editor); const lines = Array.from(selectionGroups, ([editor, targets]) => { return { lineNumber: getLineNumber(targets, this.at), editor }; @@ -47,28 +36,27 @@ class Scroll implements Action { await focusEditor(originalEditor); } - const decorationSelections = targets - .map((target) => target.selection) - .filter((selection) => { - const visibleRanges = selection.editor.visibleRanges; - const startLine = visibleRanges[0].start.line; - const endLine = visibleRanges[visibleRanges.length - 1].end.line; - // Don't show decorations for selections that are larger than the visible range - return ( - selection.selection.start.line > startLine || - selection.selection.end.line < endLine || - (selection.selection.start.line === startLine && - selection.selection.end.line === endLine) - ); - }); + const decorationTargets = targets.filter((target) => { + const visibleRanges = target.editor.visibleRanges; + const startLine = visibleRanges[0].start.line; + const endLine = visibleRanges[visibleRanges.length - 1].end.line; + // Don't show decorations for selections that are larger than the visible range + return ( + target.contentRange.start.line > startLine || + target.contentRange.end.line < endLine || + (target.contentRange.start.line === startLine && + target.contentRange.end.line === endLine) + ); + }); - await displayPendingEditDecorationsForSelection( - decorationSelections, - this.graph.editStyles.referenced.line + await this.graph.editStyles.displayPendingEditDecorationsForTargets( + decorationTargets, + this.graph.editStyles.referenced, + false ); return { - thatMark: targets.map((target) => target.selection), + thatMark: createThatMark(targets), }; } } @@ -91,12 +79,12 @@ export class ScrollToBottom extends Scroll { } } -function getLineNumber(targets: TypedSelection[], at: string) { +function getLineNumber(targets: Target[], at: string) { let startLine = Number.MAX_SAFE_INTEGER; let endLine = 0; - targets.forEach((t: TypedSelection) => { - startLine = Math.min(startLine, t.selection.selection.start.line); - endLine = Math.max(endLine, t.selection.selection.end.line); + targets.forEach((target: Target) => { + startLine = Math.min(startLine, target.contentRange.start.line); + endLine = Math.max(endLine, target.contentRange.end.line); }); if (at === "top") { diff --git a/src/actions/SetSelection.ts b/src/actions/SetSelection.ts index 77db7cab02..ef796b8c85 100644 --- a/src/actions/SetSelection.ts +++ b/src/actions/SetSelection.ts @@ -1,52 +1,39 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - TypedSelection, - Graph, -} from "../typings/Types"; -import { ensureSingleEditor } from "../util/targetUtils"; import { Selection } from "vscode"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { setSelectionsAndFocusEditor } from "../util/setSelectionsAndFocusEditor"; +import { createThatMark, ensureSingleEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export class SetSelection implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } - protected getSelection(target: TypedSelection) { - return target.selection.selection; + protected getSelection(target: Target) { + return target.contentSelection; } - async run([targets]: [TypedSelection[]]): Promise { + async run([targets]: [Target[]]): Promise { const editor = ensureSingleEditor(targets); - await setSelectionsAndFocusEditor(editor, targets.map(this.getSelection)); + const selections = targets.map(this.getSelection); + await setSelectionsAndFocusEditor(editor, selections); return { - thatMark: targets.map((target) => target.selection), + thatMark: createThatMark(targets), }; } } export class SetSelectionBefore extends SetSelection { - protected getSelection(target: TypedSelection) { - return new Selection( - target.selection.selection.start, - target.selection.selection.start - ); + protected getSelection(target: Target) { + return new Selection(target.contentRange.start, target.contentRange.start); } } export class SetSelectionAfter extends SetSelection { - protected getSelection(target: TypedSelection) { - return new Selection( - target.selection.selection.end, - target.selection.selection.end - ); + protected getSelection(target: Target) { + return new Selection(target.contentRange.end, target.contentRange.end); } } diff --git a/src/actions/Sort.ts b/src/actions/Sort.ts index 1efd5a0cb0..0f0191194a 100644 --- a/src/actions/Sort.ts +++ b/src/actions/Sort.ts @@ -1,17 +1,9 @@ import { shuffle } from "lodash"; -import { - Action, - ActionReturnValue, - ActionPreferences, - Graph, - TypedSelection, -} from "../typings/Types"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { Action, ActionReturnValue } from "./actions.types"; export class Sort implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } @@ -20,7 +12,7 @@ export class Sort implements Action { return texts.sort(); } - async run(targets: TypedSelection[][]): Promise { + async run(targets: Target[][]): Promise { const { returnValue: unsortedTexts } = await this.graph.actions.getText.run( targets, { diff --git a/src/actions/SetBreakpoint.ts b/src/actions/ToggleBreakpoint.ts similarity index 56% rename from src/actions/SetBreakpoint.ts rename to src/actions/ToggleBreakpoint.ts index 0a224d1363..1261a82bac 100644 --- a/src/actions/SetBreakpoint.ts +++ b/src/actions/ToggleBreakpoint.ts @@ -1,20 +1,16 @@ import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import { - SourceBreakpoint, - Location, + Breakpoint, debug, - Uri, + Location, Range, - Breakpoint, + SourceBreakpoint, + Uri, } from "vscode"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; -import { isLineSelectionType } from "../util/selectionType"; +import { weakContainingLineStage } from "../processTargets/modifiers/commonWeakContainingScopeStages"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; +import { createThatMark } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; function getBreakpoints(uri: Uri, range: Range) { return debug.breakpoints.filter( @@ -25,21 +21,18 @@ function getBreakpoints(uri: Uri, range: Range) { ); } -export default class SetBreakpoint implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside", selectionType: "line" }, - ]; +export default class ToggleBreakpoint implements Action { + getFinalStages = () => [weakContainingLineStage]; constructor(private graph: Graph) { this.run = this.run.bind(this); } - async run([targets]: [ - TypedSelection[], - TypedSelection[] - ]): Promise { - await displayPendingEditDecorations( - targets, + async run([targets]: [Target[], Target[]]): Promise { + const thatTargets = targets.map(({ thatTarget }) => thatTarget); + + await this.graph.editStyles.displayPendingEditDecorations( + thatTargets, this.graph.editStyles.referenced ); @@ -47,17 +40,17 @@ export default class SetBreakpoint implements Action { const toRemove: Breakpoint[] = []; targets.forEach((target) => { - let range: Range = target.selection.selection; + let range = target.contentRange; // The action preference give us line content but line breakpoints are registered on character 0 - if (isLineSelectionType(target.selectionType)) { + if (target.isLine) { range = range.with(range.start.with(undefined, 0), undefined); } - const uri = target.selection.editor.document.uri; + const uri = target.editor.document.uri; const existing = getBreakpoints(uri, range); if (existing.length > 0) { toRemove.push(...existing); } else { - if (isLineSelectionType(target.selectionType)) { + if (target.isLine) { range = range.with(undefined, range.end.with(undefined, 0)); } toAdd.push(new SourceBreakpoint(new Location(uri, range))); @@ -67,8 +60,8 @@ export default class SetBreakpoint implements Action { debug.addBreakpoints(toAdd); debug.removeBreakpoints(toRemove); - const thatMark = targets.map((target) => target.selection); - - return { thatMark }; + return { + thatMark: createThatMark(thatTargets), + }; } } diff --git a/src/actions/Wrap.ts b/src/actions/Wrap.ts index 0cd11b086b..fa0173754d 100644 --- a/src/actions/Wrap.ts +++ b/src/actions/Wrap.ts @@ -1,126 +1,134 @@ import { DecorationRangeBehavior, Selection } from "vscode"; -import { flatten } from "lodash"; -import { - Action, - ActionPreferences, - ActionReturnValue, - Edit, - Graph, - SelectionWithEditor, - TypedSelection, -} from "../typings/Types"; -import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import { decorationSleep } from "../util/editDisplayUtils"; -import { FullSelectionInfo } from "../typings/updateSelections"; import { getSelectionInfo, performEditsAndUpdateFullSelectionInfos, } from "../core/updateSelections/updateSelections"; +import { Target } from "../typings/target.types"; +import { Edit, Graph } from "../typings/Types"; +import { FullSelectionInfo } from "../typings/updateSelections"; +import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; +import { runOnTargetsForEachEditor } from "../util/targetUtils"; +import { Action, ActionReturnValue } from "./actions.types"; export default class Wrap implements Action { - getTargetPreferences: () => ActionPreferences[] = () => [ - { insideOutsideType: "inside" }, - ]; - constructor(private graph: Graph) { this.run = this.run.bind(this); } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], left: string, right: string ): Promise { - const thatMark = flatten( - await runOnTargetsForEachEditor( - targets, - async (editor, targets) => { - const { document } = editor; - const boundaries = targets.map((target) => ({ - start: new Selection( - target.selection.selection.start, - target.selection.selection.start - ), - end: new Selection( - target.selection.selection.end, - target.selection.selection.end - ), - })); - - const edits: Edit[] = boundaries.flatMap(({ start, end }) => [ - { - text: left, - range: start, - }, - { - text: right, - range: end, - isReplace: true, - }, - ]); + const results = await runOnTargetsForEachEditor( + targets, + async (editor, targets) => { + const { document } = editor; + const boundaries = targets.map((target) => ({ + start: new Selection( + target.contentRange.start, + target.contentRange.start + ), + end: new Selection(target.contentRange.end, target.contentRange.end), + })); - const delimiterSelectionInfos: FullSelectionInfo[] = - boundaries.flatMap(({ start, end }) => { - return [ - getSelectionInfo( - document, - start, - DecorationRangeBehavior.OpenClosed - ), - getSelectionInfo( - document, - end, - DecorationRangeBehavior.ClosedOpen - ), - ]; - }); + const edits: Edit[] = boundaries.flatMap(({ start, end }) => [ + { + text: left, + range: start, + }, + { + text: right, + range: end, + isReplace: true, + }, + ]); - const cursorSelectionInfos = editor.selections.map((selection) => - getSelectionInfo( - document, - selection, - DecorationRangeBehavior.ClosedClosed - ) - ); - - const thatMarkSelectionInfos = targets.map( - ({ selection: { selection } }) => + const delimiterSelectionInfos: FullSelectionInfo[] = boundaries.flatMap( + ({ start, end }) => { + return [ getSelectionInfo( document, - selection, - DecorationRangeBehavior.OpenOpen - ) - ); + start, + DecorationRangeBehavior.OpenClosed + ), + getSelectionInfo( + document, + end, + DecorationRangeBehavior.ClosedOpen + ), + ]; + } + ); + + const cursorSelectionInfos = editor.selections.map((selection) => + getSelectionInfo( + document, + selection, + DecorationRangeBehavior.ClosedClosed + ) + ); + + const sourceMarkSelectionInfos = targets.map((target) => + getSelectionInfo( + document, + target.contentSelection, + DecorationRangeBehavior.ClosedClosed + ) + ); - const [delimiterSelections, cursorSelections, thatMarkSelections] = - await performEditsAndUpdateFullSelectionInfos( - this.graph.rangeUpdater, - editor, - edits, - [ - delimiterSelectionInfos, - cursorSelectionInfos, - thatMarkSelectionInfos, - ] - ); + const thatMarkSelectionInfos = targets.map((target) => + getSelectionInfo( + document, + target.contentSelection, + DecorationRangeBehavior.OpenOpen + ) + ); - editor.selections = cursorSelections; + const [ + delimiterSelections, + cursorSelections, + sourceMarkSelections, + thatMarkSelections, + ] = await performEditsAndUpdateFullSelectionInfos( + this.graph.rangeUpdater, + editor, + edits, + [ + delimiterSelectionInfos, + cursorSelectionInfos, + sourceMarkSelectionInfos, + thatMarkSelectionInfos, + ] + ); - editor.setDecorations( - this.graph.editStyles.justAdded.token, - delimiterSelections - ); - await decorationSleep(); - editor.setDecorations(this.graph.editStyles.justAdded.token, []); + setSelectionsWithoutFocusingEditor(editor, cursorSelections); - return thatMarkSelections.map((selection) => ({ + this.graph.editStyles.displayPendingEditDecorationsForRanges( + delimiterSelections.map((selection) => ({ + editor, + range: selection, + })), + this.graph.editStyles.justAdded, + true + ); + + return { + sourceMark: sourceMarkSelections.map((selection) => ({ + editor, + selection, + })), + thatMark: thatMarkSelections.map((selection) => ({ editor, selection, - })); - } - ) + })), + }; + } ); - return { thatMark }; + return { + sourceMark: results.flatMap(({ sourceMark }) => sourceMark), + thatMark: results.flatMap(({ thatMark }) => thatMark), + }; } } diff --git a/src/actions/WrapWithSnippet.ts b/src/actions/WrapWithSnippet.ts index 0fc160c4c6..6596fc04da 100644 --- a/src/actions/WrapWithSnippet.ts +++ b/src/actions/WrapWithSnippet.ts @@ -1,14 +1,9 @@ import { commands } from "vscode"; import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import ModifyIfWeakStage from "../processTargets/modifiers/ModifyIfWeakStage"; import { SnippetDefinition } from "../typings/snippet"; -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../typings/Types"; -import displayPendingEditDecorations from "../util/editDisplayUtils"; +import { Target } from "../typings/target.types"; +import { Graph } from "../typings/Types"; import { ensureSingleEditor } from "../util/targetUtils"; import { Placeholder, @@ -17,11 +12,12 @@ import { Variable, } from "../vendor/snippet/snippetParser"; import { KnownSnippetVariableNames } from "../vendor/snippet/snippetVariables"; +import { Action, ActionReturnValue } from "./actions.types"; export default class WrapWithSnippet implements Action { private snippetParser = new SnippetParser(); - getTargetPreferences(snippetLocation: string): ActionPreferences[] { + getFinalStages(snippetLocation: string) { const [snippetName, placeholderName] = parseSnippetLocation(snippetLocation); @@ -34,18 +30,17 @@ export default class WrapWithSnippet implements Action { const variables = snippet.variables ?? {}; const defaultScopeType = variables[placeholderName]?.wrapperScopeType; + if (defaultScopeType == null) { + return []; + } + return [ - { - insideOutsideType: "inside", - modifier: - defaultScopeType == null - ? undefined - : { - type: "containingScope", - scopeType: defaultScopeType, - includeSiblings: false, - }, - }, + new ModifyIfWeakStage({ + type: "containingScope", + scopeType: { + type: defaultScopeType, + }, + }), ]; } @@ -54,7 +49,7 @@ export default class WrapWithSnippet implements Action { } async run( - [targets]: [TypedSelection[]], + [targets]: [Target[]], snippetLocation: string ): Promise { const [snippetName, placeholderName] = @@ -84,14 +79,12 @@ export default class WrapWithSnippet implements Action { const snippetString = parsedSnippet.toTextmateString(); - await displayPendingEditDecorations( + await this.graph.editStyles.displayPendingEditDecorations( targets, this.graph.editStyles.pendingModification0 ); - const targetSelections = targets.map( - (target) => target.selection.selection - ); + const targetSelections = targets.map((target) => target.contentSelection); await this.graph.actions.setSelection.run([targets]); @@ -165,10 +158,10 @@ function parseSnippetLocation(snippetLocation: string): [string, string] { } function findMatchingSnippetDefinition( - typedSelection: TypedSelection, + target: Target, definitions: SnippetDefinition[] ) { - const languageId = typedSelection.selection.editor.document.languageId; + const languageId = target.editor.document.languageId; return definitions.find(({ scope }) => { if (scope == null) { diff --git a/src/actions/actions.types.ts b/src/actions/actions.types.ts new file mode 100644 index 0000000000..5c081147b4 --- /dev/null +++ b/src/actions/actions.types.ts @@ -0,0 +1,65 @@ +import { ModifierStage } from "../processTargets/PipelineStages.types"; +import { Target } from "../typings/target.types"; +import { SelectionWithEditor } from "../typings/Types"; + +export type ActionType = + | "callAsFunction" + | "clearAndSetSelection" + | "copyToClipboard" + | "cutToClipboard" + | "deselect" + | "editNewLineAfter" + | "editNewLineBefore" + | "executeCommand" + | "extractVariable" + | "findInWorkspace" + | "foldRegion" + | "followLink" + | "getText" + | "highlight" + | "indentLine" + | "insertCopyAfter" + | "insertCopyBefore" + | "insertEmptyLineAfter" + | "insertEmptyLineBefore" + | "insertEmptyLinesAround" + | "moveToTarget" + | "outdentLine" + | "pasteFromClipboard" + | "randomizeTargets" + | "remove" + | "replace" + | "replaceWithTarget" + | "reverseTargets" + | "rewrapWithPairedDelimiter" + | "scrollToBottom" + | "scrollToCenter" + | "scrollToTop" + | "setSelection" + | "setSelectionAfter" + | "setSelectionBefore" + | "sortTargets" + | "swapTargets" + | "toggleLineBreakpoint" + | "toggleLineComment" + | "unfoldRegion" + | "wrapWithPairedDelimiter" + | "wrapWithSnippet"; + +export interface ActionReturnValue { + returnValue?: any; + thatMark?: SelectionWithEditor[]; + sourceMark?: SelectionWithEditor[]; +} + +export interface Action { + run(targets: Target[][], ...args: any[]): Promise; + + /** + * Used to define final stages that should be run at the end of the pipeline before the action + * @param args Extra args to command + */ + getFinalStages?(...args: any[]): ModifierStage[]; +} + +export type ActionRecord = Record; diff --git a/src/actions/index.ts b/src/actions/index.ts index 143d2b4112..d3e3dc2ea6 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,84 +1,2 @@ -import { ActionRecord, Graph } from "../typings/Types"; -import Clear from "./Clear"; -import { Cut, Copy, Paste } from "./CutCopyPaste"; -import Delete from "./Delete"; -import ExtractVariable from "./ExtractVariable"; -import { Fold, Unfold } from "./Fold"; -import { EditNewLineAbove, EditNewLineBelow } from "./EditNewLine"; -import { - SetSelection, - SetSelectionBefore, - SetSelectionAfter, -} from "./SetSelection"; -import Wrap from "./Wrap"; -import { ScrollToTop, ScrollToCenter, ScrollToBottom } from "./Scroll"; -import { IndentLines, OutdentLines } from "./Indent"; -import { CommentLines } from "./Comment"; -import { Bring, Move, Swap } from "./BringMoveSwap"; -import { - InsertEmptyLineAbove, - InsertEmptyLineBelow, - InsertEmptyLinesAround, -} from "./InsertEmptyLines"; -import GetText from "./GetText"; -import { FindInFiles } from "./Find"; -import Replace from "./Replace"; -import { CopyLinesUp, CopyLinesDown } from "./CopyLines"; -import SetBreakpoint from "./SetBreakpoint"; -import { Sort, Reverse, Random } from "./Sort"; -import Call from "./Call"; -import WrapWithSnippet from "./WrapWithSnippet"; -import Deselect from "./Deselect"; -import Rewrap from "./Rewrap"; -import ExecuteCommand from "./ExecuteCommand"; -import FollowLink from "./FollowLink"; -import Highlight from "./Highlight"; - -class Actions implements ActionRecord { - constructor(private graph: Graph) {} - - callAsFunction = new Call(this.graph); - clearAndSetSelection = new Clear(this.graph); - copyToClipboard = new Copy(this.graph); - cutToClipboard = new Cut(this.graph); - editNewLineAfter = new EditNewLineBelow(this.graph); - editNewLineBefore = new EditNewLineAbove(this.graph); - executeCommand = new ExecuteCommand(this.graph); - extractVariable = new ExtractVariable(this.graph); - findInWorkspace = new FindInFiles(this.graph); - foldRegion = new Fold(this.graph); - followLink = new FollowLink(this.graph); - getText = new GetText(this.graph); - highlight = new Highlight(this.graph); - indentLine = new IndentLines(this.graph); - insertCopyAfter = new CopyLinesDown(this.graph); - insertCopyBefore = new CopyLinesUp(this.graph); - insertEmptyLineAfter = new InsertEmptyLineBelow(this.graph); - insertEmptyLineBefore = new InsertEmptyLineAbove(this.graph); - insertEmptyLinesAround = new InsertEmptyLinesAround(this.graph); - moveToTarget = new Move(this.graph); - outdentLine = new OutdentLines(this.graph); - pasteFromClipboard = new Paste(this.graph); - remove = new Delete(this.graph); - deselect = new Deselect(this.graph); - replace = new Replace(this.graph); - replaceWithTarget = new Bring(this.graph); - randomizeTargets = new Random(this.graph); - reverseTargets = new Reverse(this.graph); - rewrapWithPairedDelimiter = new Rewrap(this.graph); - scrollToBottom = new ScrollToBottom(this.graph); - scrollToCenter = new ScrollToCenter(this.graph); - scrollToTop = new ScrollToTop(this.graph); - setSelection = new SetSelection(this.graph); - setSelectionAfter = new SetSelectionAfter(this.graph); - setSelectionBefore = new SetSelectionBefore(this.graph); - sortTargets = new Sort(this.graph); - swapTargets = new Swap(this.graph); - toggleLineBreakpoint = new SetBreakpoint(this.graph); - toggleLineComment = new CommentLines(this.graph); - unfoldRegion = new Unfold(this.graph); - wrapWithPairedDelimiter = new Wrap(this.graph); - wrapWithSnippet = new WrapWithSnippet(this.graph); -} - +import Actions from "./Actions"; export default Actions; diff --git a/src/core/commandRunner/CommandRunner.ts b/src/core/commandRunner/CommandRunner.ts index 44b83d2964..84200426c6 100644 --- a/src/core/commandRunner/CommandRunner.ts +++ b/src/core/commandRunner/CommandRunner.ts @@ -1,17 +1,14 @@ import * as vscode from "vscode"; -import inferFullTargets from "../inferFullTargets"; +import { ActionType } from "../../actions/actions.types"; +import { ActionableError } from "../../errors"; import processTargets from "../../processTargets"; -import { - ActionType, - Graph, - PartialTarget, - ProcessedTargetsContext, -} from "../../typings/Types"; -import { ThatMark } from "../ThatMark"; -import { canonicalizeAndValidateCommand } from "../../util/canonicalizeAndValidateCommand"; -import { CommandArgument } from "./types"; +import { Graph, ProcessedTargetsContext } from "../../typings/Types"; import { isString } from "../../util/type"; -import { ActionableError } from "../../errors"; +import { canonicalizeAndValidateCommand } from "../commandVersionUpgrades/canonicalizeAndValidateCommand"; +import { PartialTargetV0V1 } from "../commandVersionUpgrades/upgradeV1ToV2/commandV1.types"; +import inferFullTargets from "../inferFullTargets"; +import { ThatMark } from "../ThatMark"; +import { Command } from "./command.types"; // TODO: Do this using the graph once we migrate its dependencies onto the graph export default class CommandRunner { @@ -42,14 +39,14 @@ export default class CommandRunner { * {@link canonicalizeAndValidateCommand}, primarily for the purpose of * backwards compatibility * 2. Perform inference on targets to fill in details left out using things - * like previous targets and action preferences. For example we would + * like previous targets. For example we would * automatically infer that `"take funk air and bat"` is equivalent to * `"take funk air and funk bat"`. See {@link inferFullTargets} for details * of how this is done. * 3. Construct a {@link ProcessedTargetsContext} object to capture the * environment needed by {@link processTargets}. * 4. Call {@link processTargets} to map each abstract {@link Target} object - * to a concrete list of {@link TypedSelection} objects. + * to a concrete list of {@link Target} objects. * 5. Run the requested action on the given selections. The mapping from * action id (eg `remove`) to implementation is defined in * {@link Actions}. To understand how actions work, see some examples, @@ -58,20 +55,20 @@ export default class CommandRunner { * action, and returns the desired return value indicated by the action, if * it has one. */ - async runCommand(commandArgument: CommandArgument) { + async runCommand(command: Command) { try { if (this.graph.debug.active) { - this.graph.debug.log(`commandArgument:`); - this.graph.debug.log(JSON.stringify(commandArgument, null, 3)); + this.graph.debug.log(`command:`); + this.graph.debug.log(JSON.stringify(command, null, 3)); } + const commandComplete = canonicalizeAndValidateCommand(command); const { spokenForm, - action: actionName, - targets: partialTargets, - extraArgs, + action: { name: actionName, args: actionArgs }, + targets: partialTargetDescriptors, usePrePhraseSnapshot, - } = canonicalizeAndValidateCommand(commandArgument); + } = commandComplete; const readableHatMap = await this.graph.hatTokenMap.getReadableMap( usePrePhraseSnapshot @@ -83,17 +80,20 @@ export default class CommandRunner { throw new Error(`Unknown action ${actionName}`); } - const targets = inferFullTargets( - partialTargets, - action.getTargetPreferences(...extraArgs) - ); + const targetDescriptors = inferFullTargets(partialTargetDescriptors); if (this.graph.debug.active) { this.graph.debug.log("Full targets:"); - this.graph.debug.log(JSON.stringify(targets, null, 3)); + this.graph.debug.log(JSON.stringify(targetDescriptors, null, 3)); } + const finalStages = + action.getFinalStages != null + ? action.getFinalStages(...actionArgs) + : []; + const processedTargetsContext: ProcessedTargetsContext = { + finalStages, currentSelections: vscode.window.activeTextEditor?.selections.map((selection) => ({ selection, @@ -107,26 +107,31 @@ export default class CommandRunner { }; if (this.graph.testCaseRecorder.isActive()) { + this.graph.editStyles.testDecorations = []; const context = { - targets, + targets: targetDescriptors, thatMark: this.thatMark, sourceMark: this.sourceMark, hatTokenMap: readableHatMap, spokenForm, + decorations: this.graph.editStyles.testDecorations, }; await this.graph.testCaseRecorder.preCommandHook( - commandArgument, + commandComplete, context ); } - const selections = processTargets(processedTargetsContext, targets); + const targets = processTargets( + processedTargetsContext, + targetDescriptors + ); const { returnValue, thatMark: newThatMark, sourceMark: newSourceMark, - } = await action.run(selections, ...extraArgs); + } = await action.run(targets, ...actionArgs); this.thatMark.set(newThatMark); this.sourceMark.set(newSourceMark); @@ -151,20 +156,20 @@ export default class CommandRunner { } private runCommandBackwardCompatible( - spokenFormOrCommandArgument: string | CommandArgument, + spokenFormOrCommand: string | Command, ...rest: unknown[] ) { - let commandArgument: CommandArgument; + let command: Command; - if (isString(spokenFormOrCommandArgument)) { - const spokenForm = spokenFormOrCommandArgument; + if (isString(spokenFormOrCommand)) { + const spokenForm = spokenFormOrCommand; const [action, targets, ...extraArgs] = rest as [ ActionType, - PartialTarget[], + PartialTargetV0V1[], ...unknown[] ]; - commandArgument = { + command = { version: 0, spokenForm, action, @@ -173,10 +178,10 @@ export default class CommandRunner { usePrePhraseSnapshot: false, }; } else { - commandArgument = spokenFormOrCommandArgument; + command = spokenFormOrCommand; } - return this.runCommand(commandArgument); + return this.runCommand(command); } dispose() { diff --git a/src/core/commandRunner/command.types.ts b/src/core/commandRunner/command.types.ts new file mode 100644 index 0000000000..4d22a22175 --- /dev/null +++ b/src/core/commandRunner/command.types.ts @@ -0,0 +1,57 @@ +import { PartialTargetDescriptor } from "../../typings/targetDescriptor.types"; +import { ActionType } from "../../actions/actions.types"; +import { + CommandV0, + CommandV1, +} from "../commandVersionUpgrades/upgradeV1ToV2/commandV1.types"; + +export type CommandComplete = Required> & + Pick & { action: Required }; + +export const LATEST_VERSION = 2 as const; + +export type CommandLatest = Command & { + version: typeof LATEST_VERSION; +}; + +export type Command = CommandV0 | CommandV1 | CommandV2; + +interface ActionCommand { + /** + * The action to run + */ + name: ActionType; + + /** + * A list of arguments expected by the given action. + */ + args?: unknown[]; +} + +export interface CommandV2 { + /** + * The version number of the command API + */ + version: 2; + + /** + * The spoken form of the command if issued from a voice command system + */ + spokenForm?: string; + + /** + * If the command is issued from a voice command system, this boolean indicates + * whether we should use the pre phrase snapshot. Only set this to true if the + * voice command system issues a pre phrase signal at the start of every + * phrase. + */ + usePrePhraseSnapshot: boolean; + + action: ActionCommand; + + /** + * A list of targets expected by the action. Inference will be run on the + * targets + */ + targets: PartialTargetDescriptor[]; +} diff --git a/src/core/commandRunner/types.ts b/src/core/commandRunner/types.ts deleted file mode 100644 index b70e169f78..0000000000 --- a/src/core/commandRunner/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ActionType, PartialTarget } from "../../typings/Types"; - -export type CommandArgumentComplete = Required< - Omit -> & - Pick; - -export const LATEST_VERSION = 1 as const; - -export type CommandArgumentLatest = CommandArgument & { - version: typeof LATEST_VERSION; -}; - -export type CommandArgument = CommandArgumentV0 | CommandArgumentV1; - -interface CommandArgumentV1 extends CommandArgumentV0V1 { - version: 1; -} - -interface CommandArgumentV0 extends CommandArgumentV0V1 { - version: 0; - usePrePhraseSnapshot?: false; -} - -interface CommandArgumentV0V1 { - /** - * The version number of the command API - */ - version: 0 | 1; - - /** - * The spoken form of the command if issued from a voice command system - */ - spokenForm?: string; - - /** - * If the command is issued from a voice command system, this boolean indicates - * whether we should use the pre phrase snapshot. Only set this to true if the - * voice command system issues a pre phrase signal at the start of every - * phrase. - */ - usePrePhraseSnapshot?: boolean; - - /** - * The action to run - */ - action: ActionType; - - /** - * A list of targets expected by the action. Inference will be run on the - * targets - */ - targets: PartialTarget[]; - - /** - * A list of extra arguments expected by the given action. - */ - extraArgs?: unknown[]; -} diff --git a/src/util/canonicalizeActionName.ts b/src/core/commandVersionUpgrades/canonicalizeActionName.ts similarity index 94% rename from src/util/canonicalizeActionName.ts rename to src/core/commandVersionUpgrades/canonicalizeActionName.ts index 983ce9c332..ee945da816 100644 --- a/src/util/canonicalizeActionName.ts +++ b/src/core/commandVersionUpgrades/canonicalizeActionName.ts @@ -1,4 +1,4 @@ -import { ActionType } from "../typings/Types"; +import { ActionType } from "../../actions/actions.types"; const actionAliasToCanonicalName: Record = { bring: "replaceWithTarget", diff --git a/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts b/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts new file mode 100644 index 0000000000..cf3b6cfb8d --- /dev/null +++ b/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts @@ -0,0 +1,120 @@ +import { commands } from "vscode"; +import { ActionableError } from "../../errors"; +import { + Modifier, + PartialTargetDescriptor, + SimpleScopeTypeType, +} from "../../typings/targetDescriptor.types"; +import { ActionType } from "../../actions/actions.types"; +import { getPartialPrimitiveTargets } from "../../util/getPrimitiveTargets"; +import { + Command, + CommandComplete, + CommandLatest, + LATEST_VERSION, +} from "../commandRunner/command.types"; +import canonicalizeActionName from "./canonicalizeActionName"; +import canonicalizeTargets from "./canonicalizeTargets"; +import { upgradeV0ToV1 } from "./upgradeV0ToV1"; +import { upgradeV1ToV2 } from "./upgradeV1ToV2"; + +/** + * Given a command argument which comes from the client, normalize it so that it + * conforms to the latest version of the expected cursorless command argument. + * + * @param command The command argument to normalize + * @returns The normalized command argument + */ +export function canonicalizeAndValidateCommand( + command: Command +): CommandComplete { + const commandUpgraded = upgradeCommand(command); + const { + action, + targets: inputPartialTargets, + usePrePhraseSnapshot = false, + version, + ...rest + } = commandUpgraded; + + const actionName = canonicalizeActionName(action.name); + const partialTargets = canonicalizeTargets(inputPartialTargets); + + validateCommand(actionName, partialTargets); + + return { + ...rest, + version: LATEST_VERSION, + action: { + name: actionName, + args: action.args ?? [], + }, + targets: partialTargets, + usePrePhraseSnapshot, + }; +} + +function upgradeCommand(command: Command): CommandLatest { + if (command.version > LATEST_VERSION) { + throw new ActionableError( + "Cursorless Talon version is ahead of Cursorless VSCode extension version. Please update Cursorless VSCode.", + [ + { + name: "Check for updates", + action: () => + commands.executeCommand( + "workbench.extensions.action.checkForUpdates" + ), + }, + ] + ); + } + + while (command.version < LATEST_VERSION) { + switch (command.version) { + case 0: + command = upgradeV0ToV1(command); + break; + case 1: + command = upgradeV1ToV2(command); + break; + default: + throw new Error( + `Can't upgrade from unknown version ${command.version}` + ); + } + } + + if (command.version !== LATEST_VERSION) { + throw new Error("Command is not latest version"); + } + + return command; +} + +export function validateCommand( + actionName: ActionType, + partialTargets: PartialTargetDescriptor[] +) { + if ( + usesScopeType("notebookCell", partialTargets) && + !["editNewLineBefore", "editNewLineAfter"].includes(actionName) + ) { + throw new Error( + "The notebookCell scope type is currently only supported with the actions editNewLineAbove and editNewLineBelow" + ); + } +} + +function usesScopeType( + scopeTypeType: SimpleScopeTypeType, + partialTargets: PartialTargetDescriptor[] +) { + return getPartialPrimitiveTargets(partialTargets).some((partialTarget) => + partialTarget.modifiers?.find( + (mod: Modifier) => + (mod.type === "containingScope" || mod.type === "everyScope") && + mod.scopeType.type === scopeTypeType + ) + ); +} diff --git a/src/core/commandVersionUpgrades/canonicalizeTargets.ts b/src/core/commandVersionUpgrades/canonicalizeTargets.ts new file mode 100644 index 0000000000..809368900a --- /dev/null +++ b/src/core/commandVersionUpgrades/canonicalizeTargets.ts @@ -0,0 +1,54 @@ +import update from "immutability-helper"; +import { flow } from "lodash"; +import { + PartialPrimitiveTargetDescriptor, + PartialTargetDescriptor, + SimpleScopeTypeType, +} from "../../typings/targetDescriptor.types"; +import { transformPartialPrimitiveTargets } from "../../util/getPrimitiveTargets"; +import { HatStyleName } from "../constants"; + +const SCOPE_TYPE_CANONICALIZATION_MAPPING: Record = + { + arrowFunction: "anonymousFunction", + dictionary: "map", + regex: "regularExpression", + }; + +const COLOR_CANONICALIZATION_MAPPING: Record = { + purple: "pink", +}; + +const canonicalizeScopeTypes = ( + target: PartialPrimitiveTargetDescriptor +): PartialPrimitiveTargetDescriptor => { + target.modifiers?.forEach((mod) => { + if (mod.type === "containingScope" || mod.type === "everyScope") { + mod.scopeType.type = + SCOPE_TYPE_CANONICALIZATION_MAPPING[mod.scopeType.type] ?? + mod.scopeType.type; + } + }); + return target; +}; + +const canonicalizeColors = ( + target: PartialPrimitiveTargetDescriptor +): PartialPrimitiveTargetDescriptor => + target.mark?.type === "decoratedSymbol" + ? update(target, { + mark: { + symbolColor: (symbolColor: string) => + COLOR_CANONICALIZATION_MAPPING[symbolColor] ?? symbolColor, + }, + }) + : target; + +export default function canonicalizeTargets( + partialTargets: PartialTargetDescriptor[] +) { + return transformPartialPrimitiveTargets( + partialTargets, + flow(canonicalizeScopeTypes, canonicalizeColors) + ); +} diff --git a/src/core/commandVersionUpgrades/upgradeV0ToV1/index.ts b/src/core/commandVersionUpgrades/upgradeV0ToV1/index.ts new file mode 100644 index 0000000000..2a30592649 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV0ToV1/index.ts @@ -0,0 +1 @@ +export * from "./upgradeV0ToV1"; diff --git a/src/core/commandVersionUpgrades/upgradeV0ToV1/upgradeV0ToV1.ts b/src/core/commandVersionUpgrades/upgradeV0ToV1/upgradeV0ToV1.ts new file mode 100644 index 0000000000..e46af93053 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV0ToV1/upgradeV0ToV1.ts @@ -0,0 +1,5 @@ +import { CommandV0, CommandV1 } from "../upgradeV1ToV2/commandV1.types"; + +export function upgradeV0ToV1(command: CommandV0): CommandV1 { + return { ...command, version: 1 }; +} diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/commandV1.types.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/commandV1.types.ts new file mode 100644 index 0000000000..fd7cb35cdf --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/commandV1.types.ts @@ -0,0 +1,257 @@ +// This file contains a snapshot of all the types used by the deprecated v0 / v1 target representation, +// to be used by on-the-fly target upgrading for backwards compatibility + +export interface CommandV1 extends CommandV0V1 { + version: 1; +} + +export interface CommandV0 extends CommandV0V1 { + version: 0; + usePrePhraseSnapshot?: false; +} + +interface CommandV0V1 { + /** + * The version number of the command API + */ + version: 0 | 1; + + /** + * The spoken form of the command if issued from a voice command system + */ + spokenForm?: string; + + /** + * If the command is issued from a voice command system, this boolean indicates + * whether we should use the pre phrase snapshot. Only set this to true if the + * voice command system issues a pre phrase signal at the start of every + * phrase. + */ + usePrePhraseSnapshot?: boolean; + + /** + * The action to run + */ + action: string; + + /** + * A list of targets expected by the action. Inference will be run on the + * targets + */ + targets: PartialTargetV0V1[]; + + /** + * A list of extra arguments expected by the given action. + */ + extraArgs?: unknown[]; +} + +export interface PartialPrimitiveTargetV0V1 { + type: "primitive"; + mark?: Mark; + modifier?: ModifierV0V1; + selectionType?: SelectionType; + position?: Position; + insideOutsideType?: InsideOutsideType; + isImplicit?: boolean; +} + +interface PartialRangeTarget { + type: "range"; + start: PartialPrimitiveTargetV0V1; + end: PartialPrimitiveTargetV0V1; + excludeStart?: boolean; + excludeEnd?: boolean; + rangeType?: RangeType; +} + +type RangeType = "continuous" | "vertical"; + +interface PartialListTarget { + type: "list"; + elements: (PartialPrimitiveTargetV0V1 | PartialRangeTarget)[]; +} + +export type PartialTargetV0V1 = + | PartialPrimitiveTargetV0V1 + | PartialRangeTarget + | PartialListTarget; + +type SelectionType = + | "token" + | "line" + | "notebookCell" + | "paragraph" + | "document" + | "nonWhitespaceSequence" + | "url"; + +interface CursorMark { + type: "cursor"; +} + +interface CursorMarkToken { + type: "cursorToken"; +} + +interface That { + type: "that"; +} + +interface Source { + type: "source"; +} + +interface Nothing { + type: "nothing"; +} + +interface DecoratedSymbol { + type: "decoratedSymbol"; + symbolColor: HatStyleName; + character: string; +} + +const HAT_COLORS = [ + "default", + "blue", + "green", + "red", + "pink", + "yellow", + "userColor1", + "userColor2", +] as const; + +const HAT_NON_DEFAULT_SHAPES = [ + "ex", + "fox", + "wing", + "hole", + "frame", + "curve", + "eye", + "play", + "bolt", + "crosshairs", +] as const; + +type HatColor = typeof HAT_COLORS[number]; +type HatNonDefaultShape = typeof HAT_NON_DEFAULT_SHAPES[number]; +type HatStyleName = HatColor | `${HatColor}-${HatNonDefaultShape}`; + +type LineNumberType = "absolute" | "relative" | "modulo100"; + +interface LineNumberPosition { + type: LineNumberType; + lineNumber: number; +} + +interface LineNumber { + type: "lineNumber"; + anchor: LineNumberPosition; + active: LineNumberPosition; +} + +type Mark = + | CursorMark + | CursorMarkToken + | That + | Source + | DecoratedSymbol + | Nothing + | LineNumber; + +type SimpleSurroundingPairName = + | "angleBrackets" + | "backtickQuotes" + | "curlyBrackets" + | "doubleQuotes" + | "escapedDoubleQuotes" + | "escapedParentheses" + | "escapedSquareBrackets" + | "escapedSingleQuotes" + | "parentheses" + | "singleQuotes" + | "squareBrackets"; + +type ComplexSurroundingPairName = "string" | "any"; + +type SurroundingPairName = + | SimpleSurroundingPairName + | ComplexSurroundingPairName; + +type ScopeType = string; + +type Position = "before" | "after" | "contents"; + +type InsideOutsideType = "inside" | "outside" | null; + +type SubTokenType = "word" | "character"; + +/** + * Indicates whether to include or exclude delimiters in a surrounding pair + * modifier. In the future, these will become proper modifiers that can be + * applied in many places, such as to restrict to the body of an if statement. + * By default, a surrounding pair modifier refers to the entire surrounding + * range, so if delimiter inclusion is undefined, it's equivalent to not having + * one of these modifiers; ie include the delimiters. + */ + +type DelimiterInclusion = "excludeInterior" | "interiorOnly" | undefined; + +type SurroundingPairDirection = "left" | "right"; + +interface SurroundingPairModifier { + type: "surroundingPair"; + delimiter: SurroundingPairName; + delimiterInclusion: DelimiterInclusion; + forceDirection?: SurroundingPairDirection; +} + +interface ContainingScopeModifier { + type: "containingScope"; + scopeType: ScopeType; + valueOnly?: boolean; + includeSiblings?: boolean; +} + +interface SubTokenModifier { + type: "subpiece"; + pieceType: SubTokenType; + anchor: number; + active: number; + excludeAnchor?: boolean; + excludeActive?: boolean; +} + +interface IdentityModifier { + type: "identity"; +} + +/** + * Converts its input to a raw selection with no type information so for + * example if it is the destination of a bring or move it should inherit the + * type information such as delimiters from its source. + */ + +interface RawSelectionModifier { + type: "toRawSelection"; +} + +interface HeadModifier { + type: "head"; +} + +interface TailModifier { + type: "tail"; +} + +export type ModifierV0V1 = + | IdentityModifier + | SurroundingPairModifier + | ContainingScopeModifier + | SubTokenModifier + | HeadModifier + | TailModifier + | RawSelectionModifier; diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/index.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/index.ts new file mode 100644 index 0000000000..d31a7b40a9 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/index.ts @@ -0,0 +1 @@ +export * from "./upgradeV1ToV2"; diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts new file mode 100644 index 0000000000..95f15c3b44 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts @@ -0,0 +1,19 @@ +import { isDeepStrictEqual } from "util"; +import { PartialPrimitiveTargetDescriptor } from "../../../typings/targetDescriptor.types"; + +const STRICT_HERE = { + type: "primitive", + mark: { type: "cursor" }, + selectionType: "token", + position: "contents", + modifier: { type: "identity" }, + insideOutsideType: "inside", +}; +const IMPLICIT_TARGET: PartialPrimitiveTargetDescriptor = { + type: "primitive", + isImplicit: true, +}; +export const upgradeStrictHere = ( + target: PartialPrimitiveTargetDescriptor +): PartialPrimitiveTargetDescriptor => + isDeepStrictEqual(target, STRICT_HERE) ? IMPLICIT_TARGET : target; diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts new file mode 100644 index 0000000000..69f0cae050 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts @@ -0,0 +1,201 @@ +import { flow } from "lodash"; +import { + Modifier, + PartialPrimitiveTargetDescriptor, + PartialRangeTargetDescriptor, + PartialTargetDescriptor, + SimpleScopeTypeType, +} from "../../../typings/targetDescriptor.types"; +import { ActionType } from "../../../actions/actions.types"; +import { transformPartialPrimitiveTargets } from "../../../util/getPrimitiveTargets"; +import { CommandV2 } from "../../commandRunner/command.types"; +import { + CommandV1, + ModifierV0V1, + PartialPrimitiveTargetV0V1, + PartialTargetV0V1, +} from "./commandV1.types"; +import { upgradeStrictHere } from "./upgradeStrictHere"; + +export function upgradeV1ToV2(command: CommandV1): CommandV2 { + const actionName = command.action as ActionType; + return { + spokenForm: command.spokenForm, + action: { + name: actionName, + args: command.extraArgs, + }, + targets: upgradeTargets(command.targets, actionName), + usePrePhraseSnapshot: command.usePrePhraseSnapshot ?? false, + version: 2, + }; +} + +function upgradeModifier(modifier: ModifierV0V1): Modifier[] { + switch (modifier.type) { + case "identity": + return []; + + case "containingScope": { + const { includeSiblings, scopeType, type, ...rest } = modifier; + + return [ + { + type: includeSiblings ? "everyScope" : "containingScope", + scopeType: { + type: scopeType as SimpleScopeTypeType, + }, + ...rest, + }, + ]; + } + + case "surroundingPair": { + const { delimiterInclusion, ...rest } = modifier; + const surroundingPairModifier = { + type: "containingScope", + scopeType: rest, + } as const; + + if (delimiterInclusion === "interiorOnly") { + return [{ type: "interiorOnly" }, surroundingPairModifier]; + } + + if (delimiterInclusion === "excludeInterior") { + return [{ type: "excludeInterior" }, surroundingPairModifier]; + } + + return [surroundingPairModifier]; + } + + case "subpiece": { + const { type, pieceType, ...rest } = modifier; + + return [ + { + type: "ordinalRange", + scopeType: { type: pieceType }, + ...rest, + }, + ]; + } + + case "head": + return [{ type: "extendThroughStartOf" }]; + case "tail": + return [{ type: "extendThroughEndOf" }]; + + default: + return [modifier]; + } +} + +function upgradePrimitiveTarget( + target: PartialPrimitiveTargetV0V1, + action: ActionType +): PartialPrimitiveTargetDescriptor { + const { + type, + isImplicit, + mark, + insideOutsideType, + modifier, + selectionType, + position, + } = target; + const modifiers: Modifier[] = []; + + if (position && position !== "contents") { + if (position === "before") { + if (insideOutsideType === "inside") { + modifiers.push({ type: "position", position: "start" }); + } else if (action === "remove") { + modifiers.push({ type: "leading" }); + } else { + modifiers.push({ type: "position", position: "before" }); + } + } else { + if (insideOutsideType === "inside") { + modifiers.push({ type: "position", position: "end" }); + } else if (action === "remove") { + modifiers.push({ type: "trailing" }); + } else { + modifiers.push({ type: "position", position: "after" }); + } + } + } + + if (selectionType) { + switch (selectionType) { + case "token": + if (modifier?.type === "subpiece") { + break; + } + case "line": + if (mark?.type === "lineNumber") { + break; + } + default: + modifiers.push({ + type: "containingScope", + scopeType: { type: selectionType }, + }); + } + } + + if (modifier) { + modifiers.push(...upgradeModifier(modifier)); + } + + return { + type, + isImplicit, + // Cursor token is just cursor position but treated as a token. This is done in the pipeline for normal cursor now + mark: mark?.type === "cursorToken" ? undefined : mark, + // Empty array of modifiers is not allowed + modifiers: modifiers.length > 0 ? modifiers : undefined, + }; +} + +function upgradeTarget( + target: PartialTargetV0V1, + action: ActionType +): PartialTargetDescriptor { + switch (target.type) { + case "list": + return { + ...target, + elements: target.elements.map( + (target) => + upgradeTarget(target, action) as + | PartialPrimitiveTargetDescriptor + | PartialRangeTargetDescriptor + ), + }; + case "range": + const { type, rangeType, start, end, excludeStart, excludeEnd } = target; + return { + type, + rangeType, + anchor: upgradePrimitiveTarget(start, action), + active: upgradePrimitiveTarget(end, action), + excludeAnchor: excludeStart ?? false, + excludeActive: excludeEnd ?? false, + }; + case "primitive": + return upgradePrimitiveTarget(target, action); + } +} + +function upgradeTargets( + partialTargets: PartialTargetV0V1[], + action: ActionType +) { + const partialTargetsV2: PartialTargetDescriptor[] = partialTargets.map( + (target) => upgradeTarget(target, action) + ); + return transformPartialPrimitiveTargets( + partialTargetsV2, + flow(upgradeStrictHere) + ); +} diff --git a/src/core/editStyles.ts b/src/core/editStyles.ts index 2c1c50c034..f281afa4c7 100644 --- a/src/core/editStyles.ts +++ b/src/core/editStyles.ts @@ -1,13 +1,26 @@ import { + DecorationRangeBehavior, + DecorationRenderOptions, + Position, + Range, + TextEditor, TextEditorDecorationType, ThemeColor, - DecorationRangeBehavior, window, - DecorationRenderOptions, + workspace, } from "vscode"; -import { Graph } from "../typings/Types"; +import isTesting from "../testUtil/isTesting"; +import { Target } from "../typings/target.types"; +import { Graph, RangeWithEditor } from "../typings/Types"; +import sleep from "../util/sleep"; +import { + getContentRange, + runForEachEditor, + runOnTargetsForEachEditor, +} from "../util/targetUtils"; export class EditStyle { + name: EditStyleThemeColorName; token: TextEditorDecorationType; line: TextEditorDecorationType; @@ -16,6 +29,7 @@ export class EditStyle { backgroundColor: new ThemeColor(`cursorless.${colorName}`), rangeBehavior: DecorationRangeBehavior.ClosedClosed, }; + this.name = colorName; this.token = window.createTextEditorDecorationType(options); this.line = window.createTextEditorDecorationType({ ...options, @@ -23,6 +37,10 @@ export class EditStyle { }); } + getDecoration(isToken: boolean) { + return isToken ? this.token : this.line; + } + dispose() { this.token.dispose(); this.line.dispose(); @@ -42,6 +60,13 @@ const EDIT_STYLE_NAMES = [ export type EditStyleName = typeof EDIT_STYLE_NAMES[number]; type EditStyleThemeColorName = `${EditStyleName}Background`; +export interface TestDecoration { + name: EditStyleThemeColorName; + type: "token" | "line"; + start: Position; + end: Position; +} + export class EditStyles implements Record { pendingDelete!: EditStyle; referenced!: EditStyle; @@ -50,8 +75,9 @@ export class EditStyles implements Record { justAdded!: EditStyle; highlight0!: EditStyle; highlight1!: EditStyle; + testDecorations: TestDecoration[] = []; - constructor(graph: Graph) { + constructor(private graph: Graph) { EDIT_STYLE_NAMES.forEach((editStyleName) => { this[editStyleName] = new EditStyle(`${editStyleName}Background`); }); @@ -59,9 +85,134 @@ export class EditStyles implements Record { graph.extensionContext.subscriptions.push(this); } + async displayPendingEditDecorations( + targets: Target[], + style: EditStyle, + getRange: (target: Target) => Range | undefined = getContentRange + ) { + await this.setDecorations(targets, style, getRange); + + await decorationSleep(); + + this.clearDecorations(style); + } + + displayPendingEditDecorationsForTargets( + targets: Target[], + style: EditStyle, + isToken: boolean + ) { + return this.displayPendingEditDecorationsForRanges( + targets.map(({ editor, contentRange }) => ({ + editor, + range: contentRange, + })), + style, + isToken + ); + } + + async displayPendingEditDecorationsForRanges( + ranges: RangeWithEditor[], + style: EditStyle, + isToken: boolean + ) { + await runForEachEditor( + ranges, + (range) => range.editor, + async (editor, ranges) => { + this.setEditorDecorations( + editor, + style, + isToken, + ranges.map((range) => range.range) + ); + } + ); + + await decorationSleep(); + + await runForEachEditor( + ranges, + (range) => range.editor, + async (editor) => { + editor.setDecorations(style.getDecoration(isToken), []); + } + ); + } + + setDecorations( + targets: Target[], + style: EditStyle, + getRange: (target: Target) => Range | undefined = getContentRange + ) { + return runOnTargetsForEachEditor(targets, async (editor, targets) => { + this.setEditorDecorations( + editor, + style, + true, + targets + .filter((target) => !target.isLine) + .map(getRange) + .filter((range): range is Range => !!range) + ); + this.setEditorDecorations( + editor, + style, + false, + targets + .filter((target) => target.isLine) + .map(getRange) + .filter((range): range is Range => !!range) + ); + }); + } + + clearDecorations(style: EditStyle) { + window.visibleTextEditors.map((editor) => { + editor.setDecorations(style.token, []); + editor.setDecorations(style.line, []); + }); + } + + private setEditorDecorations( + editor: TextEditor, + style: EditStyle, + isToken: boolean, + ranges: Range[] + ) { + if (this.graph.testCaseRecorder.isActive() || isTesting()) { + ranges.forEach((range) => { + this.testDecorations.push({ + name: style.name, + type: isToken ? "token" : "line", + start: range.start, + end: range.end, + }); + }); + if (isTesting()) { + return; + } + } + editor.setDecorations(style.getDecoration(isToken), ranges); + } + dispose() { EDIT_STYLE_NAMES.forEach((editStyleName) => { this[editStyleName].dispose(); }); } } + +function decorationSleep() { + if (isTesting()) { + return; + } + + return sleep(getPendingEditDecorationTime()); +} + +const getPendingEditDecorationTime = () => + workspace + .getConfiguration("cursorless") + .get("pendingEditDecorationTime")!; diff --git a/src/core/inferFullTargets.ts b/src/core/inferFullTargets.ts index 84c1eeac3f..270177095f 100644 --- a/src/core/inferFullTargets.ts +++ b/src/core/inferFullTargets.ts @@ -1,13 +1,12 @@ import { - ActionPreferences, - PartialPrimitiveTarget, - PartialRangeTarget, - PartialTarget, - PrimitiveTarget, - RangeTarget, - Target, - PartialListTarget, -} from "../typings/Types"; + PartialListTargetDescriptor, + PartialPrimitiveTargetDescriptor, + PartialRangeTargetDescriptor, + PartialTargetDescriptor, + PrimitiveTargetDescriptor, + RangeTargetDescriptor, + TargetDescriptor, +} from "../typings/targetDescriptor.types"; /** * Performs inference on the partial targets provided by the user, using @@ -16,173 +15,141 @@ import { * For example, we would automatically infer that `"take funk air and bat"` is * equivalent to `"take funk air and funk bat"`. * @param targets The partial targets which need to be completed by inference. - * @param actionPreferences The preferences provided by the action, so that different actions can provide their own defaults * @returns Target objects fully filled out and ready to be processed by {@link processTargets}. */ export default function inferFullTargets( - targets: PartialTarget[], - actionPreferences: ActionPreferences[] -): Target[] { - if (targets.length !== actionPreferences.length) { - throw new Error("Target length is not equal to action preference length"); - } - + targets: PartialTargetDescriptor[] +): TargetDescriptor[] { return targets.map((target, index) => - inferTarget(target, targets.slice(0, index), actionPreferences[index]) + inferTarget(target, targets.slice(0, index)) ); } function inferTarget( - target: PartialTarget, - previousTargets: PartialTarget[], - actionPreferences: ActionPreferences -): Target { + target: PartialTargetDescriptor, + previousTargets: PartialTargetDescriptor[] +): TargetDescriptor { switch (target.type) { case "list": - return inferListTarget(target, previousTargets, actionPreferences); + return inferListTarget(target, previousTargets); case "range": case "primitive": - return inferNonListTarget(target, previousTargets, actionPreferences); + return inferNonListTarget(target, previousTargets); } } function inferListTarget( - target: PartialListTarget, - previousTargets: PartialTarget[], - actionPreferences: ActionPreferences -): Target { + target: PartialListTargetDescriptor, + previousTargets: PartialTargetDescriptor[] +): TargetDescriptor { return { ...target, elements: target.elements.map((element, index) => inferNonListTarget( element, - previousTargets.concat(target.elements.slice(0, index)), - actionPreferences + previousTargets.concat(target.elements.slice(0, index)) ) ), }; } function inferNonListTarget( - target: PartialPrimitiveTarget | PartialRangeTarget, - previousTargets: PartialTarget[], - actionPreferences: ActionPreferences -): PrimitiveTarget | RangeTarget { + target: PartialPrimitiveTargetDescriptor | PartialRangeTargetDescriptor, + previousTargets: PartialTargetDescriptor[] +): PrimitiveTargetDescriptor | RangeTargetDescriptor { switch (target.type) { case "primitive": - return inferPrimitiveTarget(target, previousTargets, actionPreferences); + return inferPrimitiveTarget(target, previousTargets); case "range": - return inferRangeTarget(target, previousTargets, actionPreferences); + return inferRangeTarget(target, previousTargets); } } function inferRangeTarget( - target: PartialRangeTarget, - previousTargets: PartialTarget[], - actionPreferences: ActionPreferences -): RangeTarget { + target: PartialRangeTargetDescriptor, + previousTargets: PartialTargetDescriptor[] +): RangeTargetDescriptor { return { type: "range", - excludeAnchor: target.excludeStart ?? false, - excludeActive: target.excludeEnd ?? false, + excludeAnchor: target.excludeAnchor ?? false, + excludeActive: target.excludeActive ?? false, rangeType: target.rangeType ?? "continuous", - anchor: inferPrimitiveTarget( - target.start, - previousTargets, - actionPreferences - ), + anchor: inferPrimitiveTarget(target.anchor, previousTargets), active: inferPrimitiveTarget( - target.end, - previousTargets.concat(target.start), - actionPreferences + target.active, + previousTargets.concat(target.anchor) ), }; } function inferPrimitiveTarget( - target: PartialPrimitiveTarget, - previousTargets: PartialTarget[], - actionPreferences: ActionPreferences -): PrimitiveTarget { - const doAttributeInference = !hasContent(target) && !target.isImplicit; - - const previousTargetsForAttributes = doAttributeInference - ? previousTargets - : []; + target: PartialPrimitiveTargetDescriptor, + previousTargets: PartialTargetDescriptor[] +): PrimitiveTargetDescriptor { + if (target.isImplicit) { + return { + type: "primitive", + mark: { type: "cursor" }, + modifiers: [{ type: "toRawSelection" }], + }; + } - const maybeSelectionType = - target.selectionType ?? - getPreviousAttribute(previousTargetsForAttributes, "selectionType"); + const hasPosition = !!target.modifiers?.find( + (modifier) => modifier.type === "position" + ); + // Position without a mark can be something like "take air past end of line" const mark = target.mark ?? - (target.position === "before" || target.position === "after" - ? getPreviousMark(previousTargets) - : null) ?? { - type: maybeSelectionType === "token" ? "cursorToken" : "cursor", + (hasPosition ? getPreviousMark(previousTargets) : null) ?? { + type: "cursor", }; - const position = - target.position ?? - getPreviousPosition(previousTargets) ?? - actionPreferences.position ?? - "contents"; - - const selectionType = - maybeSelectionType ?? - (doAttributeInference ? actionPreferences.selectionType : null) ?? - "token"; - - const insideOutsideType = - target.insideOutsideType ?? - getPreviousAttribute(previousTargetsForAttributes, "insideOutsideType") ?? - actionPreferences.insideOutsideType; - - const modifier = target.modifier ?? - getPreviousAttribute(previousTargetsForAttributes, "modifier") ?? - (doAttributeInference ? actionPreferences.modifier : null) ?? { - type: "identity", - }; + const previousModifiers = getPreviousModifiers(previousTargets); + + const modifiers = target.modifiers ?? previousModifiers ?? []; + + // "bring line to after this" needs to infer line on second target + const modifierTypes = [ + ...new Set(modifiers.map((modifier) => modifier.type)), + ]; + if ( + previousModifiers != null && + modifierTypes.length === 1 && + modifierTypes[0] === "position" + ) { + const containingScopeModifier = previousModifiers.find( + (modifier) => modifier.type === "containingScope" + ); + if (containingScopeModifier != null) { + modifiers.push(containingScopeModifier); + } + } return { type: target.type, mark, - selectionType, - position, - insideOutsideType, - modifier, - isImplicit: target.isImplicit ?? false, + modifiers, }; } -function getPreviousMark(previousTargets: PartialTarget[]) { - return getPreviousAttribute( +function getPreviousMark(previousTargets: PartialTargetDescriptor[]) { + return getPreviousTarget( previousTargets, - "mark", - (target) => target["mark"] != null - ); + (target: PartialPrimitiveTargetDescriptor) => target.mark != null + )?.mark; } -function getPreviousPosition(previousTargets: PartialTarget[]) { - return getPreviousAttribute( +function getPreviousModifiers(previousTargets: PartialTargetDescriptor[]) { + return getPreviousTarget( previousTargets, - "position", - (target) => target["position"] != null - ); -} - -function getPreviousAttribute( - previousTargets: PartialTarget[], - attributeName: T, - useTarget: (target: PartialPrimitiveTarget) => boolean = hasContent -) { - const target = getPreviousTarget(previousTargets, useTarget); - return target != null ? target[attributeName] : null; + (target: PartialPrimitiveTargetDescriptor) => target.modifiers != null + )?.modifiers; } function getPreviousTarget( - previousTargets: PartialTarget[], - useTarget: (target: PartialPrimitiveTarget) => boolean -): PartialPrimitiveTarget | null { + previousTargets: PartialTargetDescriptor[], + useTarget: (target: PartialPrimitiveTargetDescriptor) => boolean +): PartialPrimitiveTargetDescriptor | null { // Search from back(last) to front(first) for (let i = previousTargets.length - 1; i > -1; --i) { const target = previousTargets[i]; @@ -193,8 +160,8 @@ function getPreviousTarget( } break; case "range": - if (useTarget(target.start)) { - return target.start; + if (useTarget(target.anchor)) { + return target.anchor; } break; case "list": @@ -207,16 +174,3 @@ function getPreviousTarget( } return null; } - -/** - * Determine whether the target has content, so that we shouldn't do inference - * @param target The target to inspect - * @returns A boolean indicating whether the target has content - */ -function hasContent(target: PartialPrimitiveTarget) { - return ( - target.selectionType != null || - target.modifier != null || - target.insideOutsideType != null - ); -} diff --git a/src/core/updateSelections/updateSelections.ts b/src/core/updateSelections/updateSelections.ts index db18afeac2..0e09437996 100644 --- a/src/core/updateSelections/updateSelections.ts +++ b/src/core/updateSelections/updateSelections.ts @@ -1,20 +1,25 @@ +import { flatten } from "lodash"; import { - Selection, - TextEditor, - TextDocument, DecorationRangeBehavior, Range, + Selection, + TextDocument, + TextEditor, } from "vscode"; -import { flatten } from "lodash"; +import { Edit } from "../../typings/Types"; import { FullSelectionInfo, SelectionInfo, } from "../../typings/updateSelections"; import { performDocumentEdits } from "../../util/performDocumentEdits"; import { isForward } from "../../util/selectionUtils"; -import { Edit } from "../../typings/Types"; import { RangeUpdater } from "./RangeUpdater"; +interface SelectionsWithBehavior { + selections: readonly Selection[]; + rangeBehavior?: DecorationRangeBehavior; +} + /** * Given a selection, this function creates a `SelectionInfo` object that can * be passed in to any of the commands that update selections. @@ -28,10 +33,24 @@ export function getSelectionInfo( document: TextDocument, selection: Selection, rangeBehavior: DecorationRangeBehavior +): FullSelectionInfo { + return getSelectionInfoInternal( + document, + selection, + isForward(selection), + rangeBehavior + ); +} + +function getSelectionInfoInternal( + document: TextDocument, + range: Range, + isForward: boolean, + rangeBehavior: DecorationRangeBehavior ): FullSelectionInfo { return { - range: new Range(selection.start, selection.end), - isForward: isForward(selection), + range, + isForward, expansionBehavior: { start: { type: @@ -49,10 +68,10 @@ export function getSelectionInfo( }, }, offsets: { - start: document.offsetAt(selection.start), - end: document.offsetAt(selection.end), + start: document.offsetAt(range.start), + end: document.offsetAt(range.end), }, - text: document.getText(selection), + text: document.getText(range), }; } @@ -64,7 +83,7 @@ export function getSelectionInfo( * @param rangeBehavior How selections should behave with respect to insertions on either end * @returns A list of lists of selection info objects */ -export function selectionsToSelectionInfos( +function selectionsToSelectionInfos( document: TextDocument, selectionMatrix: (readonly Selection[])[], rangeBehavior: DecorationRangeBehavior = DecorationRangeBehavior.ClosedClosed @@ -76,6 +95,18 @@ export function selectionsToSelectionInfos( ); } +function rangesToSelectionInfos( + document: TextDocument, + rangeMatrix: (readonly Range[])[], + rangeBehavior: DecorationRangeBehavior = DecorationRangeBehavior.ClosedClosed +): FullSelectionInfo[][] { + return rangeMatrix.map((ranges) => + ranges.map((range) => + getSelectionInfoInternal(document, range, false, rangeBehavior) + ) + ); +} + function fillOutSelectionInfos( document: TextDocument, selectionInfoMatrix: SelectionInfo[][] @@ -116,7 +147,7 @@ function selectionInfosToSelections( */ export async function callFunctionAndUpdateSelections( rangeUpdater: RangeUpdater, - func: () => Thenable, + func: () => Thenable, document: TextDocument, selectionMatrix: (readonly Selection[])[] ): Promise { @@ -133,6 +164,22 @@ export async function callFunctionAndUpdateSelections( ); } +export async function callFunctionAndUpdateRanges( + rangeUpdater: RangeUpdater, + func: () => Thenable, + document: TextDocument, + rangeMatrix: (readonly Range[])[] +): Promise { + const selectionInfoMatrix = rangesToSelectionInfos(document, rangeMatrix); + + return await callFunctionAndUpdateSelectionInfos( + rangeUpdater, + func, + document, + selectionInfoMatrix + ); +} + /** * Calls the given function and updates the given selections based on the * changes that occurred as a result of calling function. @@ -142,9 +189,9 @@ export async function callFunctionAndUpdateSelections( * @param selectionMatrix A matrix of selection info objects to update * @returns The initial selections updated based upon what happened in the function */ -export async function callFunctionAndUpdateSelectionInfos( +async function callFunctionAndUpdateSelectionInfos( rangeUpdater: RangeUpdater, - func: () => Thenable, + func: () => Thenable, document: TextDocument, selectionInfoMatrix: FullSelectionInfo[][] ) { @@ -180,14 +227,75 @@ export async function performEditsAndUpdateSelections( document, originalSelections ); + return performEditsAndUpdateInternal( + rangeUpdater, + editor, + edits, + selectionInfoMatrix + ); +} - await performEditsAndUpdateFullSelectionInfos( +/** + * Performs a list of edits and returns the given selections updated based on + * the applied edits + * @param rangeUpdater A RangeUpdate instance that will perform actual range updating + * @param editor The editor containing the selections + * @param edits A list of edits to apply + * @param originalSelections The selections to update + * @param rangeBehavior How selections should behave with respect to insertions on either end + * @returns The updated selections + */ +export function performEditsAndUpdateSelectionsWithBehavior( + rangeUpdater: RangeUpdater, + editor: TextEditor, + edits: Edit[], + originalSelections: SelectionsWithBehavior[] +) { + return performEditsAndUpdateFullSelectionInfos( + rangeUpdater, + editor, + edits, + originalSelections.map((selectionsWithBehavior) => + selectionsWithBehavior.selections.map((selection) => + getSelectionInfo( + editor.document, + selection, + selectionsWithBehavior.rangeBehavior ?? + DecorationRangeBehavior.ClosedClosed + ) + ) + ) + ); +} + +export async function performEditsAndUpdateRanges( + rangeUpdater: RangeUpdater, + editor: TextEditor, + edits: Edit[], + originalRanges: (readonly Range[])[] +): Promise { + const document = editor.document; + const selectionInfoMatrix = rangesToSelectionInfos(document, originalRanges); + return performEditsAndUpdateInternal( rangeUpdater, editor, edits, selectionInfoMatrix ); +} +async function performEditsAndUpdateInternal( + rangeUpdater: RangeUpdater, + editor: TextEditor, + edits: Edit[], + selectionInfoMatrix: FullSelectionInfo[][] +) { + await performEditsAndUpdateFullSelectionInfos( + rangeUpdater, + editor, + edits, + selectionInfoMatrix + ); return selectionInfosToSelections(selectionInfoMatrix); } @@ -197,7 +305,7 @@ export async function performEditsAndUpdateSelectionInfos( editor: TextEditor, edits: Edit[], originalSelectionInfos: SelectionInfo[][] -) { +): Promise { fillOutSelectionInfos(editor.document, originalSelectionInfos); return await performEditsAndUpdateFullSelectionInfos( @@ -222,7 +330,7 @@ export async function performEditsAndUpdateFullSelectionInfos( editor: TextEditor, edits: Edit[], originalSelectionInfos: FullSelectionInfo[][] -) { +): Promise { // NB: We do everything using VSCode listeners. We can associate changes // with our changes just by looking at their offets / text in order to // recover isReplace. We need to do this because VSCode does some fancy diff --git a/src/errors.ts b/src/errors.ts index 6cd1d5a8f3..b1681ba1cc 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -9,6 +9,13 @@ export class UnsupportedLanguageError extends Error { } } +export class UnsupportedError extends Error { + constructor(message: string) { + super(message); + this.name = "UnsupportedError"; + } +} + interface ErrorAction { /** * The name of the action to show to the user diff --git a/src/languages/clojure.ts b/src/languages/clojure.ts index 7089a01030..d79751a187 100644 --- a/src/languages/clojure.ts +++ b/src/languages/clojure.ts @@ -5,11 +5,8 @@ import { matcher, patternMatcher, } from "../util/nodeMatchers"; -import { - ScopeType, - NodeMatcherAlternative, - NodeFinder, -} from "../typings/Types"; +import { NodeMatcherAlternative, NodeFinder } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { SyntaxNode } from "web-tree-sitter"; import { delimitedSelector } from "../util/nodeSelectors"; import { identity } from "lodash"; @@ -133,7 +130,9 @@ const ifStatementFinder = functionNameBasedFinder( const ifStatementMatcher = matcher(ifStatementFinder); -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { comment: "comment", map: "map_lit", diff --git a/src/languages/cpp.ts b/src/languages/cpp.ts index 3ca9b1b7fa..1521c9177d 100644 --- a/src/languages/cpp.ts +++ b/src/languages/cpp.ts @@ -4,7 +4,8 @@ import { leadingMatcher, trailingMatcher, } from "../util/nodeMatchers"; -import { NodeMatcherAlternative, ScopeType } from "../typings/Types"; +import { NodeMatcherAlternative } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; // Generated by the following command: // > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-cpp/master/src/node-types.json | jq '[.[] | select(.type == "compound_statement") | .children.types[].type] + [.[] | select(.type == "_statement") | .subtypes[].type]' @@ -61,7 +62,9 @@ const TYPE_TYPES = [ "union_specifier", ]; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { statement: STATEMENT_TYPES, class: [ "class_specifier", diff --git a/src/languages/csharp.ts b/src/languages/csharp.ts index 7f9b4a06ed..f4ea4f35d6 100644 --- a/src/languages/csharp.ts +++ b/src/languages/csharp.ts @@ -10,7 +10,8 @@ import { conditionMatcher, patternMatcher, } from "../util/nodeMatchers"; -import { NodeMatcherAlternative, ScopeType } from "../typings/Types"; +import { NodeMatcherAlternative } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { nodeFinder, typedNodeFinder } from "../util/nodeFinders"; import { delimitedSelector } from "../util/nodeSelectors"; @@ -215,7 +216,9 @@ const getMapMatchers = { string: typeMatcher("string_literal"), }; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { ...getMapMatchers, ifStatement: "if_statement", class: "class_declaration", diff --git a/src/languages/getNodeMatcher.ts b/src/languages/getNodeMatcher.ts index eda070ca97..d368e46686 100644 --- a/src/languages/getNodeMatcher.ts +++ b/src/languages/getNodeMatcher.ts @@ -4,9 +4,9 @@ import { selectionWithEditorFromRange } from "../util/selectionUtils"; import { NodeMatcher, NodeMatcherValue, - ScopeType, SelectionWithEditor, } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import cpp from "./cpp"; import clojure from "./clojure"; import csharp from "./csharp"; @@ -26,7 +26,7 @@ import { SupportedLanguageId } from "./constants"; export function getNodeMatcher( languageId: string, - scopeType: ScopeType, + scopeTypeType: SimpleScopeTypeType, includeSiblings: boolean ): NodeMatcher { const matchers = languageMatchers[languageId as SupportedLanguageId]; @@ -35,7 +35,7 @@ export function getNodeMatcher( throw new UnsupportedLanguageError(languageId); } - const matcher = matchers[scopeType]; + const matcher = matchers[scopeTypeType]; if (matcher == null) { return notSupported; @@ -50,7 +50,7 @@ export function getNodeMatcher( const languageMatchers: Record< SupportedLanguageId, - Record + Record > = { c: cpp, cpp, diff --git a/src/languages/go.ts b/src/languages/go.ts index 8de97bd28c..66b265ee40 100644 --- a/src/languages/go.ts +++ b/src/languages/go.ts @@ -5,7 +5,8 @@ import { cascadingMatcher, patternMatcher, } from "../util/nodeMatchers"; -import { NodeMatcherAlternative, ScopeType } from "../typings/Types"; +import { NodeMatcherAlternative } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; // Generated by the following command: // `curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-go/master/src/node-types.json | jq '[.[] | select(.type == "_statement" or .type == "_simple_statement") | .subtypes[].type]'` @@ -36,7 +37,9 @@ const STATEMENT_TYPES = [ "var_declaration", ]; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { map: "composite_literal", list: ["composite_literal", "slice_type", "array_type"], statement: STATEMENT_TYPES, diff --git a/src/languages/html.ts b/src/languages/html.ts index 5089206109..73aec7a8b3 100644 --- a/src/languages/html.ts +++ b/src/languages/html.ts @@ -3,11 +3,8 @@ import { leadingMatcher, patternMatcher, } from "../util/nodeMatchers"; -import { - ScopeType, - NodeMatcherAlternative, - SelectionWithEditor, -} from "../typings/Types"; +import { NodeMatcherAlternative, SelectionWithEditor } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { SyntaxNode } from "web-tree-sitter"; import { getNodeRange } from "../util/nodeSelectors"; @@ -21,7 +18,9 @@ const getTags = (selection: SelectionWithEditor, node: SyntaxNode) => { const endTag = getEndTag(selection, node); return startTag != null && endTag != null ? startTag.concat(endTag) : null; }; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { xmlElement: ["element", "script_element", "style_element"], xmlBothTags: getTags, xmlStartTag: getStartTag, diff --git a/src/languages/java.ts b/src/languages/java.ts index 130aeffbd7..526e353a00 100644 --- a/src/languages/java.ts +++ b/src/languages/java.ts @@ -5,7 +5,8 @@ import { conditionMatcher, trailingMatcher, } from "../util/nodeMatchers"; -import { NodeMatcherAlternative, ScopeType } from "../typings/Types"; +import { NodeMatcherAlternative } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; // Generated by the following command: // > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-java/master/src/node-types.json | jq '[.[] | select(.type == "statement" or .type == "declaration") | .subtypes[].type]' @@ -43,7 +44,9 @@ const STATEMENT_TYPES = [ "switch_statement", ]; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { statement: STATEMENT_TYPES, class: "class_declaration", className: "class_declaration[name]", diff --git a/src/languages/json.ts b/src/languages/json.ts index 45405a8f2f..23c69c0718 100644 --- a/src/languages/json.ts +++ b/src/languages/json.ts @@ -4,15 +4,14 @@ import { leadingMatcher, trailingMatcher, } from "../util/nodeMatchers"; -import { - ScopeType, - NodeMatcherAlternative, - SelectionWithEditor, -} from "../typings/Types"; +import { NodeMatcherAlternative, SelectionWithEditor } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { SyntaxNode } from "web-tree-sitter"; import { getNodeRange } from "../util/nodeSelectors"; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { map: "object", list: "array", string: "string", diff --git a/src/languages/markdown.ts b/src/languages/markdown.ts index 3ce9f75071..7b4dbf32bd 100644 --- a/src/languages/markdown.ts +++ b/src/languages/markdown.ts @@ -3,9 +3,9 @@ import { SyntaxNode } from "web-tree-sitter"; import { NodeFinder, NodeMatcherAlternative, - ScopeType, SelectionWithContext, } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { leadingSiblingNodeFinder, patternFinder } from "../util/nodeFinders"; import { createPatternMatchers, @@ -34,12 +34,12 @@ function nameExtractor( const contentRange = range.isEmpty ? range : range.with(range.start.translate(0, 1)); - const outerRange = getNodeRange(node.parent!); + const removalRange = getNodeRange(node.parent!); return { selection: new Selection(contentRange.start, contentRange.end), context: { - outerSelection: new Selection(outerRange.start, outerRange.end), + removalRange, }, }; } @@ -89,7 +89,9 @@ function sectionMatcher(...patterns: string[]) { return matcher(leadingSiblingNodeFinder(finder), sectionExtractor); } -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { list: ["loose_list", "tight_list"], comment: "html_block", name: matcher( diff --git a/src/languages/php.ts b/src/languages/php.ts index f59d98ff13..f9e5b629fc 100644 --- a/src/languages/php.ts +++ b/src/languages/php.ts @@ -2,10 +2,10 @@ import { Selection, TextEditor } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; import { NodeMatcherAlternative, - ScopeType, SelectionWithContext, SelectionWithEditor, } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { patternFinder } from "../util/nodeFinders"; import { argumentMatcher, @@ -88,17 +88,19 @@ function castTypeExtractor( const contentRange = range; const leftParenRange = getNodeRange(node.previousSibling!); const rightParenRange = getNodeRange(node.nextSibling!.nextSibling!); - const outerRange = range.with(leftParenRange.start, rightParenRange.start); + const removalRange = range.with(leftParenRange.start, rightParenRange.start); return { selection: new Selection(contentRange.start, contentRange.end), context: { - outerSelection: new Selection(outerRange.start, outerRange.end), + removalRange, }, }; } -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { statement: STATEMENT_TYPES, ifStatement: "if_statement", class: "class_declaration", diff --git a/src/languages/python.ts b/src/languages/python.ts index 42ac0756db..e8ab89249b 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -10,7 +10,8 @@ import { matcher, } from "../util/nodeMatchers"; import { patternFinder } from "../util/nodeFinders"; -import { NodeMatcherAlternative, ScopeType } from "../typings/Types"; +import { NodeMatcherAlternative } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { childRangeSelector } from "../util/nodeSelectors"; // Generated by the following command: @@ -48,7 +49,9 @@ export const getTypeNode = (node: SyntaxNode) => const dictionaryTypes = ["dictionary", "dictionary_comprehension"]; const listTypes = ["list", "list_comprehension", "set"]; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { map: dictionaryTypes, list: listTypes, statement: STATEMENT_TYPES, diff --git a/src/languages/ruby.ts b/src/languages/ruby.ts index ec3846b9fb..ac7a9aa1ed 100644 --- a/src/languages/ruby.ts +++ b/src/languages/ruby.ts @@ -9,11 +9,8 @@ import { patternMatcher, trailingMatcher, } from "../util/nodeMatchers"; -import { - NodeMatcherAlternative, - ScopeType, - SelectionWithEditor, -} from "../typings/Types"; +import { NodeMatcherAlternative, SelectionWithEditor } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { SyntaxNode } from "web-tree-sitter"; import { getNodeRange } from "../util/nodeSelectors"; import { patternFinder } from "../util/nodeFinders"; @@ -151,7 +148,9 @@ function blockFinder(node: SyntaxNode) { return block; } -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { map: mapTypes, list: listTypes, statement: cascadingMatcher( diff --git a/src/languages/scala.ts b/src/languages/scala.ts index a0eddc8583..37f69ff80a 100644 --- a/src/languages/scala.ts +++ b/src/languages/scala.ts @@ -4,9 +4,12 @@ import { leadingMatcher, conditionMatcher, } from "../util/nodeMatchers"; -import { NodeMatcherAlternative, ScopeType } from "../typings/Types"; +import { NodeMatcherAlternative } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { // treating classes = classlike class: ["class_definition", "object_definition", "trait_definition"], className: [ diff --git a/src/languages/scss.ts b/src/languages/scss.ts index 1463d1707a..9e25a0933c 100644 --- a/src/languages/scss.ts +++ b/src/languages/scss.ts @@ -1,9 +1,6 @@ import { SyntaxNode } from "web-tree-sitter"; -import { - NodeMatcherAlternative, - ScopeType, - SelectionWithEditor, -} from "../typings/Types"; +import { NodeMatcherAlternative, SelectionWithEditor } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { patternFinder } from "../util/nodeFinders"; import { cascadingMatcher, @@ -90,7 +87,9 @@ function findAdjacentArgValues( }; } -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { ifStatement: "if_statement", condition: conditionMatcher("condition"), statement: cascadingMatcher( diff --git a/src/languages/typescript.ts b/src/languages/typescript.ts index 30744febc4..5a8350912c 100644 --- a/src/languages/typescript.ts +++ b/src/languages/typescript.ts @@ -11,9 +11,9 @@ import { import { NodeMatcher, NodeMatcherAlternative, - ScopeType, SelectionWithEditor, } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { getNodeInternalRange, getNodeRange, @@ -158,7 +158,9 @@ function valueMatcher() { const mapTypes = ["object", "object_pattern"]; const listTypes = ["array", "array_pattern"]; -const nodeMatchers: Partial> = { +const nodeMatchers: Partial< + Record +> = { map: mapTypes, list: listTypes, string: ["string", "template_string"], diff --git a/src/processTargets/PipelineStages.types.ts b/src/processTargets/PipelineStages.types.ts new file mode 100644 index 0000000000..eb1e1c10c9 --- /dev/null +++ b/src/processTargets/PipelineStages.types.ts @@ -0,0 +1,10 @@ +import { Target } from "../typings/target.types"; +import { ProcessedTargetsContext } from "../typings/Types"; + +export interface MarkStage { + run(context: ProcessedTargetsContext): Target[]; +} + +export interface ModifierStage { + run(context: ProcessedTargetsContext, target: Target): Target[]; +} diff --git a/src/processTargets/getMarkStage.ts b/src/processTargets/getMarkStage.ts new file mode 100644 index 0000000000..400cd5595c --- /dev/null +++ b/src/processTargets/getMarkStage.ts @@ -0,0 +1,25 @@ +import { Mark } from "../typings/targetDescriptor.types"; +import CursorStage from "./marks/CursorStage"; +import DecoratedSymbolStage from "./marks/DecoratedSymbolStage"; +import LineNumberStage from "./marks/LineNumberStage"; +import NothingStage from "./marks/NothingStage"; +import SourceStage from "./marks/SourceStage"; +import ThatStage from "./marks/ThatStage"; +import { MarkStage } from "./PipelineStages.types"; + +export default (mark: Mark): MarkStage => { + switch (mark.type) { + case "cursor": + return new CursorStage(mark); + case "that": + return new ThatStage(mark); + case "source": + return new SourceStage(mark); + case "decoratedSymbol": + return new DecoratedSymbolStage(mark); + case "lineNumber": + return new LineNumberStage(mark); + case "nothing": + return new NothingStage(mark); + } +}; diff --git a/src/processTargets/getModifierStage.ts b/src/processTargets/getModifierStage.ts new file mode 100644 index 0000000000..5d40653d5b --- /dev/null +++ b/src/processTargets/getModifierStage.ts @@ -0,0 +1,101 @@ +import { + ContainingScopeModifier, + ContainingSurroundingPairModifier, + EveryScopeModifier, + Modifier, +} from "../typings/targetDescriptor.types"; +import { HeadStage, TailStage } from "./modifiers/HeadTailStage"; +import { + ExcludeInteriorStage, + InteriorOnlyStage, +} from "./modifiers/InteriorStage"; +import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; +import OrdinalRangeSubTokenStage, { + OrdinalRangeSubTokenModifier, +} from "./modifiers/OrdinalRangeSubTokenStage"; +import PositionStage from "./modifiers/PositionStage"; +import RawSelectionStage from "./modifiers/RawSelectionStage"; +import ContainingSyntaxScopeStage, { + SimpleContainingScopeModifier, +} from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage"; +import DocumentStage from "./modifiers/scopeTypeStages/DocumentStage"; +import LineStage from "./modifiers/scopeTypeStages/LineStage"; +import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage"; +import ParagraphStage from "./modifiers/scopeTypeStages/ParagraphStage"; +import { + NonWhitespaceSequenceModifier, + NonWhitespaceSequenceStage, + UrlModifier, + UrlStage, +} from "./modifiers/scopeTypeStages/RegexStage"; +import TokenStage from "./modifiers/scopeTypeStages/TokenStage"; +import SurroundingPairStage from "./modifiers/SurroundingPairStage"; +import { ModifierStage } from "./PipelineStages.types"; + +export default (modifier: Modifier): ModifierStage => { + switch (modifier.type) { + case "position": + return new PositionStage(modifier); + case "extendThroughStartOf": + return new HeadStage(modifier); + case "extendThroughEndOf": + return new TailStage(modifier); + case "toRawSelection": + return new RawSelectionStage(modifier); + case "interiorOnly": + return new InteriorOnlyStage(modifier); + case "excludeInterior": + return new ExcludeInteriorStage(modifier); + case "leading": + return new LeadingStage(modifier); + case "trailing": + return new TrailingStage(modifier); + case "containingScope": + case "everyScope": + return getContainingScopeStage(modifier); + case "ordinalRange": + if (!["word", "character"].includes(modifier.scopeType.type)) { + throw Error( + `Unsupported ordinal scope type ${modifier.scopeType.type}` + ); + } + return new OrdinalRangeSubTokenStage( + modifier as OrdinalRangeSubTokenModifier + ); + } +}; + +const getContainingScopeStage = ( + modifier: ContainingScopeModifier | EveryScopeModifier +): ModifierStage => { + switch (modifier.scopeType.type) { + case "token": + return new TokenStage(modifier); + case "notebookCell": + return new NotebookCellStage(modifier); + case "document": + return new DocumentStage(modifier); + case "line": + return new LineStage(modifier); + case "paragraph": + return new ParagraphStage(modifier); + case "nonWhitespaceSequence": + return new NonWhitespaceSequenceStage( + modifier as NonWhitespaceSequenceModifier + ); + case "url": + return new UrlStage(modifier as UrlModifier); + case "surroundingPair": + return new SurroundingPairStage( + modifier as ContainingSurroundingPairModifier + ); + case "word": + case "character": + throw new Error(`Unsupported scope type ${modifier.scopeType.type}`); + default: + // Default to containing syntax scope using tree sitter + return new ContainingSyntaxScopeStage( + modifier as SimpleContainingScopeModifier + ); + } +}; diff --git a/src/processTargets/index.ts b/src/processTargets/index.ts index 0e9a8b1397..c3ac65a39c 100644 --- a/src/processTargets/index.ts +++ b/src/processTargets/index.ts @@ -1,308 +1,2 @@ -import { isEqual, zip } from "lodash"; -import { Selection } from "vscode"; -import { performInsideOutsideAdjustment } from "../util/performInsideOutsideAdjustment"; -import { - PrimitiveTarget, - ProcessedTargetsContext, - RangeTarget, - Target, - TypedSelection, -} from "../typings/Types"; -import processMark from "./processMark"; -import processModifier from "./modifiers/processModifier"; -import processPosition from "./processPosition"; -import processSelectionType from "./processSelectionType"; - -/** - * Converts the abstract target descriptions provided by the user to a concrete - * representation usable by actions. Conceptually, the input will be something - * like "the function call argument containing the cursor" and the output will be something - * like "line 3, characters 5 through 10". - * @param context Captures the environment needed to convert the abstract target - * description given by the user to a concrete representation usable by - * actions - * @param targets The abstract target representations provided by the user - * @returns A list of lists of typed selections, one list per input target. Each - * typed selection includes the selection, as well the uri of the document - * containing it, and potentially rich context information such as how to remove - * the target - */ -export default function ( - context: ProcessedTargetsContext, - targets: Target[] -): TypedSelection[][] { - return targets.map((target) => - filterDuplicateSelections(processTarget(context, target)) - ); -} - -function processTarget( - context: ProcessedTargetsContext, - target: Target -): TypedSelection[] { - switch (target.type) { - case "list": - return target.elements.flatMap((element) => - processNonListTarget(context, element) - ); - case "range": - case "primitive": - return processNonListTarget(context, target); - } -} - -function processNonListTarget( - context: ProcessedTargetsContext, - target: RangeTarget | PrimitiveTarget -): TypedSelection[] { - let selections; - switch (target.type) { - case "range": - selections = processRangeTarget(context, target); - break; - case "primitive": - selections = processPrimitiveTarget(context, target); - break; - } - return selections.map((selection) => - performInsideOutsideAdjustment(selection) - ); -} - -function processRangeTarget( - context: ProcessedTargetsContext, - target: RangeTarget -): TypedSelection[] { - const anchorTargets = processPrimitiveTarget(context, target.anchor); - const activeTargets = processPrimitiveTarget(context, target.active); - - return zip(anchorTargets, activeTargets).flatMap( - ([anchorTarget, activeTarget]) => { - if (anchorTarget == null || activeTarget == null) { - throw new Error("anchorTargets and activeTargets lengths don't match"); - } - - if (anchorTarget.selection.editor !== activeTarget.selection.editor) { - throw new Error( - "anchorTarget and activeTarget must be in same document" - ); - } - - const anchorSelection = anchorTarget.selection.selection; - const activeSelection = activeTarget.selection.selection; - - const isForward = anchorSelection.start.isBeforeOrEqual( - activeSelection.start - ); - - switch (target.rangeType) { - case "continuous": - return processContinuousRangeTarget( - target, - anchorTarget, - activeTarget, - isForward - ); - case "vertical": - return processVerticalRangeTarget( - target, - anchorTarget, - activeTarget, - isForward - ); - } - } - ); -} - -function processContinuousRangeTarget( - target: RangeTarget, - anchorTarget: TypedSelection, - activeTarget: TypedSelection, - isForward: boolean -): TypedSelection[] { - const anchor = targetToRangeLimitPosition( - anchorTarget, - isForward, - !target.excludeAnchor - ); - const active = targetToRangeLimitPosition( - activeTarget, - !isForward, - !target.excludeActive - ); - - const outerAnchor = target.excludeAnchor - ? null - : isForward - ? anchorTarget.selectionContext.outerSelection?.start - : anchorTarget.selectionContext.outerSelection?.end; - const outerActive = target.excludeActive - ? null - : isForward - ? activeTarget.selectionContext.outerSelection?.end - : activeTarget.selectionContext.outerSelection?.start; - const outerSelection = - outerAnchor != null || outerActive != null - ? new Selection(outerAnchor ?? anchor, outerActive ?? active) - : null; - - const startSelectionContext = target.excludeAnchor - ? null - : anchorTarget.selectionContext; - const endSelectionContext = target.excludeActive - ? null - : activeTarget.selectionContext; - const leadingDelimiterRange = isForward - ? startSelectionContext?.leadingDelimiterRange - : endSelectionContext?.leadingDelimiterRange; - const trailingDelimiterRange = isForward - ? endSelectionContext?.trailingDelimiterRange - : startSelectionContext?.trailingDelimiterRange; - - return [ - { - selection: { - selection: new Selection(anchor, active), - editor: anchorTarget.selection.editor, - }, - selectionType: anchorTarget.selectionType, - selectionContext: { - containingListDelimiter: - anchorTarget.selectionContext.containingListDelimiter, - isInDelimitedList: anchorTarget.selectionContext.isInDelimitedList, - leadingDelimiterRange, - trailingDelimiterRange, - outerSelection, - }, - insideOutsideType: anchorTarget.insideOutsideType, - position: "contents", - }, - ]; -} - -function processVerticalRangeTarget( - target: RangeTarget, - anchorTarget: TypedSelection, - activeTarget: TypedSelection, - isForward: boolean -): TypedSelection[] { - const anchorLine = targetToLineLimitPosition( - anchorTarget, - isForward, - !target.excludeAnchor - ); - const activeLine = targetToLineLimitPosition( - activeTarget, - !isForward, - !target.excludeActive - ); - const anchorSelection = anchorTarget.selection.selection; - const delta = isForward ? 1 : -1; - const results: TypedSelection[] = []; - - for (let i = anchorLine; true; i += delta) { - results.push({ - selection: { - selection: new Selection( - i, - anchorSelection.anchor.character, - i, - anchorSelection.active.character - ), - editor: anchorTarget.selection.editor, - }, - selectionType: anchorTarget.selectionType, - selectionContext: { - containingListDelimiter: - anchorTarget.selectionContext.containingListDelimiter, - }, - insideOutsideType: anchorTarget.insideOutsideType, - position: anchorTarget.position, - }); - if (i === activeLine) { - return results; - } - } -} - -/** - * Given a target which forms one end of a range target, do necessary - * adjustments to get the proper position for the output range - * @param target The target to get position from - * @param isStartOfRange If true this position is the start of the range - * @param exclude If true the content of this position should be excluded - */ -function targetToRangeLimitPosition( - target: TypedSelection, - isStartOfRange: boolean, - includeTarget: boolean -) { - const selection = target.selection.selection; - - if (includeTarget) { - return isStartOfRange ? selection.start : selection.end; - } - - const outerSelection = target.selectionContext.outerSelection; - - if (outerSelection != null) { - const delimiterPosition = isStartOfRange - ? target.selectionContext.trailingDelimiterRange?.end - : target.selectionContext.leadingDelimiterRange?.start; - if (delimiterPosition != null) { - return delimiterPosition; - } - return isStartOfRange ? outerSelection.end : outerSelection.start; - } - - return isStartOfRange ? selection.end : selection.start; -} - -// Same as targetToRangeLimitPosition but only operates on and returns line number -function targetToLineLimitPosition( - target: TypedSelection, - isStartOfRange: boolean, - includeTarget: boolean -) { - const position = targetToRangeLimitPosition( - target, - isStartOfRange, - includeTarget - ); - if (includeTarget) { - return position.line; - } - return position.line + (isStartOfRange ? 1 : -1); -} - -function processPrimitiveTarget( - context: ProcessedTargetsContext, - target: PrimitiveTarget -): TypedSelection[] { - const markSelections = processMark(context, target.mark); - const modifiedSelections = markSelections.flatMap((markSelection) => - processModifier(context, target, markSelection) - ); - if (target.isImplicit) { - modifiedSelections.forEach((typedSelection) => { - typedSelection.context.isRawSelection = true; - }); - } - - const typedSelections = modifiedSelections.map( - ({ selection, context: selectionContext }) => - processSelectionType(context, target, selection, selectionContext) - ); - - return typedSelections.map((selection) => - processPosition(context, target, selection) - ); -} - -function filterDuplicateSelections(selections: TypedSelection[]) { - return selections.filter( - (selection, index, selections) => - selections.findIndex((s) => isEqual(s, selection)) === index - ); -} +import processTargets from "./processTargets"; +export default processTargets; diff --git a/src/processTargets/marks/CursorStage.ts b/src/processTargets/marks/CursorStage.ts new file mode 100644 index 0000000000..71a4727fb8 --- /dev/null +++ b/src/processTargets/marks/CursorStage.ts @@ -0,0 +1,20 @@ +import { Target } from "../../typings/target.types"; +import { CursorMark } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { isReversed } from "../../util/selectionUtils"; +import { MarkStage } from "../PipelineStages.types"; +import WeakTarget from "../targets/WeakTarget"; + +export default class CursorStage implements MarkStage { + constructor(_modifier: CursorMark) {} + + run(context: ProcessedTargetsContext): Target[] { + return context.currentSelections.map((selection) => { + return new WeakTarget({ + editor: selection.editor, + isReversed: isReversed(selection.selection), + contentRange: selection.selection, + }); + }); + } +} diff --git a/src/processTargets/marks/DecoratedSymbolStage.ts b/src/processTargets/marks/DecoratedSymbolStage.ts new file mode 100644 index 0000000000..76050d95a3 --- /dev/null +++ b/src/processTargets/marks/DecoratedSymbolStage.ts @@ -0,0 +1,30 @@ +import { Target } from "../../typings/target.types"; +import { DecoratedSymbolMark } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { MarkStage } from "../PipelineStages.types"; +import WeakTarget from "../targets/WeakTarget"; + +export default class implements MarkStage { + constructor(private modifier: DecoratedSymbolMark) {} + + run(context: ProcessedTargetsContext): Target[] { + const token = context.hatTokenMap.getToken( + this.modifier.symbolColor, + this.modifier.character + ); + + if (token == null) { + throw new Error( + `Couldn't find mark ${this.modifier.symbolColor} '${this.modifier.character}'` + ); + } + + return [ + new WeakTarget({ + editor: token.editor, + contentRange: token.range, + isReversed: false, + }), + ]; + } +} diff --git a/src/processTargets/marks/LineNumberStage.ts b/src/processTargets/marks/LineNumberStage.ts new file mode 100644 index 0000000000..955264936f --- /dev/null +++ b/src/processTargets/marks/LineNumberStage.ts @@ -0,0 +1,68 @@ +import { TextEditor } from "vscode"; +import { + LineNumberMark, + LineNumberPosition, +} from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { createLineTarget } from "../modifiers/scopeTypeStages/LineStage"; +import { MarkStage } from "../PipelineStages.types"; +import LineTarget from "../targets/LineTarget"; + +export default class implements MarkStage { + constructor(private modifier: LineNumberMark) {} + + run(context: ProcessedTargetsContext): LineTarget[] { + if (context.currentEditor == null) { + return []; + } + const editor = context.currentEditor; + const anchorLine = getLine(editor, this.modifier.anchor); + const activeLine = getLine(editor, this.modifier.active); + const anchorRange = editor.document.lineAt(anchorLine).range; + const activeRange = editor.document.lineAt(activeLine).range; + const contentRange = anchorRange.union(activeRange); + const isReversed = this.modifier.anchor < this.modifier.active; + return [createLineTarget(editor, contentRange, isReversed)]; + } +} + +const getLine = (editor: TextEditor, linePosition: LineNumberPosition) => { + switch (linePosition.type) { + case "absolute": + return linePosition.lineNumber; + case "relative": + return editor.selection.active.line + linePosition.lineNumber; + case "modulo100": + const stepSize = 100; + const startLine = editor.visibleRanges[0].start.line; + const endLine = + editor.visibleRanges[editor.visibleRanges.length - 1].end.line; + const base = Math.floor(startLine / stepSize) * stepSize; + const visibleLines = []; + const invisibleLines = []; + let lineNumber = base + linePosition.lineNumber; + while (lineNumber <= endLine) { + if (lineNumber >= startLine) { + const visible = editor.visibleRanges.find( + (r) => lineNumber >= r.start.line && lineNumber <= r.end.line + ); + if (visible) { + visibleLines.push(lineNumber); + } else { + invisibleLines.push(lineNumber); + } + } + lineNumber += stepSize; + } + if (visibleLines.length === 1) { + return visibleLines[0]; + } + if (visibleLines.length + invisibleLines.length > 1) { + throw new Error("Multiple lines matching"); + } + if (invisibleLines.length === 1) { + return invisibleLines[0]; + } + throw new Error("Line is not in viewport"); + } +}; diff --git a/src/processTargets/marks/NothingStage.ts b/src/processTargets/marks/NothingStage.ts new file mode 100644 index 0000000000..818b242173 --- /dev/null +++ b/src/processTargets/marks/NothingStage.ts @@ -0,0 +1,11 @@ +import { Target } from "../../typings/target.types"; +import { NothingMark } from "../../typings/targetDescriptor.types"; +import { MarkStage } from "../PipelineStages.types"; + +export default class implements MarkStage { + constructor(private modifier: NothingMark) {} + + run(): Target[] { + return []; + } +} diff --git a/src/processTargets/marks/SourceStage.ts b/src/processTargets/marks/SourceStage.ts new file mode 100644 index 0000000000..c57e4f0136 --- /dev/null +++ b/src/processTargets/marks/SourceStage.ts @@ -0,0 +1,23 @@ +import { Target } from "../../typings/target.types"; +import { SourceMark } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { isReversed } from "../../util/selectionUtils"; +import { MarkStage } from "../PipelineStages.types"; +import WeakTarget from "../targets/WeakTarget"; + +export default class implements MarkStage { + constructor(private modifier: SourceMark) {} + + run(context: ProcessedTargetsContext): Target[] { + if (context.sourceMark.length === 0) { + throw Error("No available source marks"); + } + return context.sourceMark.map((selection) => { + return new WeakTarget({ + editor: selection.editor, + isReversed: isReversed(selection.selection), + contentRange: selection.selection, + }); + }); + } +} diff --git a/src/processTargets/marks/ThatStage.ts b/src/processTargets/marks/ThatStage.ts new file mode 100644 index 0000000000..0dbd05518f --- /dev/null +++ b/src/processTargets/marks/ThatStage.ts @@ -0,0 +1,23 @@ +import { Target } from "../../typings/target.types"; +import { ThatMark } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { isReversed } from "../../util/selectionUtils"; +import { MarkStage } from "../PipelineStages.types"; +import WeakTarget from "../targets/WeakTarget"; + +export default class implements MarkStage { + constructor(private modifier: ThatMark) {} + + run(context: ProcessedTargetsContext): Target[] { + if (context.thatMark.length === 0) { + throw Error("No available that marks"); + } + return context.thatMark.map((selection) => { + return new WeakTarget({ + editor: selection.editor, + isReversed: isReversed(selection.selection), + contentRange: selection.selection, + }); + }); + } +} diff --git a/src/processTargets/modifiers/HeadTailStage.ts b/src/processTargets/modifiers/HeadTailStage.ts new file mode 100644 index 0000000000..175eaceb64 --- /dev/null +++ b/src/processTargets/modifiers/HeadTailStage.ts @@ -0,0 +1,46 @@ +import { Position, Range, TextEditor } from "vscode"; +import { Target } from "../../typings/target.types"; +import { + HeadModifier, + TailModifier, +} from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import TokenTarget from "../targets/TokenTarget"; + +abstract class HeadTailStage implements ModifierStage { + abstract update(editor: TextEditor, range: Range): Range; + + constructor(private isReversed: boolean) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const contentRange = this.update(target.editor, target.contentRange); + return [ + new TokenTarget({ + editor: target.editor, + isReversed: this.isReversed, + contentRange, + }), + ]; + } +} + +export class HeadStage extends HeadTailStage { + constructor(private modifier: HeadModifier) { + super(true); + } + + update(editor: TextEditor, range: Range) { + return new Range(new Position(range.start.line, 0), range.end); + } +} + +export class TailStage extends HeadTailStage { + constructor(private modifier: TailModifier) { + super(false); + } + + update(editor: TextEditor, range: Range) { + return new Range(range.start, editor.document.lineAt(range.end).range.end); + } +} diff --git a/src/processTargets/modifiers/InteriorStage.ts b/src/processTargets/modifiers/InteriorStage.ts new file mode 100644 index 0000000000..2905b7fec3 --- /dev/null +++ b/src/processTargets/modifiers/InteriorStage.ts @@ -0,0 +1,28 @@ +import { Target } from "../../typings/target.types"; +import { + ExcludeInteriorModifier, + InteriorOnlyModifier, +} from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import { weakContainingSurroundingPairStage } from "./commonWeakContainingScopeStages"; + +export class InteriorOnlyStage implements ModifierStage { + constructor(private modifier: InteriorOnlyModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + return weakContainingSurroundingPairStage + .run(context, target) + .flatMap((target) => target.getInteriorStrict()); + } +} + +export class ExcludeInteriorStage implements ModifierStage { + constructor(private modifier: ExcludeInteriorModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + return weakContainingSurroundingPairStage + .run(context, target) + .flatMap((target) => target.getBoundaryStrict()); + } +} diff --git a/src/processTargets/modifiers/LeadingTrailingStages.ts b/src/processTargets/modifiers/LeadingTrailingStages.ts new file mode 100644 index 0000000000..0494e08290 --- /dev/null +++ b/src/processTargets/modifiers/LeadingTrailingStages.ts @@ -0,0 +1,31 @@ +import { Target } from "../../typings/target.types"; +import { + LeadingModifier, + TrailingModifier, +} from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; + +export class LeadingStage implements ModifierStage { + constructor(private modifier: LeadingModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const leading = target.getLeadingDelimiterTarget(); + if (leading == null) { + throw Error("No available leading range"); + } + return [leading]; + } +} + +export class TrailingStage implements ModifierStage { + constructor(private modifier: TrailingModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const trailing = target.getTrailingDelimiterTarget(); + if (trailing == null) { + throw Error("No available trailing range"); + } + return [trailing]; + } +} diff --git a/src/processTargets/modifiers/ModifyIfWeakStage.ts b/src/processTargets/modifiers/ModifyIfWeakStage.ts new file mode 100644 index 0000000000..6ac3289956 --- /dev/null +++ b/src/processTargets/modifiers/ModifyIfWeakStage.ts @@ -0,0 +1,29 @@ +import { Target } from "../../typings/target.types"; +import { Modifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import getModifierStage from "../getModifierStage"; +import { ModifierStage } from "../PipelineStages.types"; + +export default class ModifyIfWeakStage implements ModifierStage { + private nestedStage_?: ModifierStage; + + constructor(private nestedModifier: Modifier) {} + + private get nestedStage() { + if (this.nestedStage_ == null) { + this.nestedStage_ = getModifierStage(this.nestedModifier); + } + + return this.nestedStage_; + } + + run(context: ProcessedTargetsContext, target: Target): Target[] { + /** If true this target is of weak type and should use inference/upgrade when needed. See {@link WeakTarget} for more info */ + if (target.isWeak) { + return this.nestedStage + .run(context, target) + .map((newTarget) => newTarget.withThatTarget(target)); + } + return [target]; + } +} diff --git a/src/processTargets/modifiers/OrdinalRangeSubTokenStage.ts b/src/processTargets/modifiers/OrdinalRangeSubTokenStage.ts new file mode 100644 index 0000000000..ac1d14855b --- /dev/null +++ b/src/processTargets/modifiers/OrdinalRangeSubTokenStage.ts @@ -0,0 +1,136 @@ +import { range } from "lodash"; +import { Range } from "vscode"; +import { SUBWORD_MATCHER } from "../../core/constants"; +import { Target } from "../../typings/target.types"; +import { + OrdinalRangeModifier, + SimpleScopeType, +} from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import PlainTarget from "../targets/PlainTarget"; +import SubTokenWordTarget from "../targets/SubTokenWordTarget"; +import { getTokenRangeForSelection } from "./scopeTypeStages/TokenStage"; + +interface OrdinalScopeType extends SimpleScopeType { + type: "character" | "word"; +} + +export interface OrdinalRangeSubTokenModifier extends OrdinalRangeModifier { + scopeType: OrdinalScopeType; +} + +export default class OrdinalRangeSubTokenStage implements ModifierStage { + constructor(private modifier: OrdinalRangeSubTokenModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const { editor } = target; + const tokenContentRange = target.contentRange.isEmpty + ? getTokenRangeForSelection(target.editor, target.contentRange) + : target.contentRange; + + const tokenText = editor.document.getText(tokenContentRange); + let pieces: { start: number; end: number }[] = []; + + if (this.modifier.excludeActive || this.modifier.excludeAnchor) { + throw new Error("Subtoken exclusions unsupported"); + } + + if (this.modifier.scopeType.type === "word") { + pieces = [...tokenText.matchAll(SUBWORD_MATCHER)].map((match) => ({ + start: match.index!, + end: match.index! + match[0].length, + })); + } else if (this.modifier.scopeType.type === "character") { + pieces = range(tokenText.length).map((index) => ({ + start: index, + end: index + 1, + })); + } + + const anchorIndex = + this.modifier.anchor < 0 + ? this.modifier.anchor + pieces.length + : this.modifier.anchor; + const activeIndex = + this.modifier.active < 0 + ? this.modifier.active + pieces.length + : this.modifier.active; + + if ( + anchorIndex < 0 || + activeIndex < 0 || + anchorIndex >= pieces.length || + activeIndex >= pieces.length + ) { + throw new Error("Subtoken index out of range"); + } + + const isReversed = activeIndex < anchorIndex; + + const anchor = tokenContentRange.start.translate( + undefined, + isReversed ? pieces[anchorIndex].end : pieces[anchorIndex].start + ); + const active = tokenContentRange.start.translate( + undefined, + isReversed ? pieces[activeIndex].start : pieces[activeIndex].end + ); + + const contentRange = new Range(anchor, active); + + if (this.modifier.scopeType.type === "character") { + return [ + new PlainTarget({ + editor, + isReversed, + contentRange, + }), + ]; + } + + const startIndex = Math.min(anchorIndex, activeIndex); + const endIndex = Math.max(anchorIndex, activeIndex); + const leadingDelimiterRange = + startIndex > 0 && pieces[startIndex - 1].end < pieces[startIndex].start + ? new Range( + tokenContentRange.start.translate({ + characterDelta: pieces[startIndex - 1].end, + }), + tokenContentRange.start.translate({ + characterDelta: pieces[startIndex].start, + }) + ) + : undefined; + const trailingDelimiterRange = + endIndex + 1 < pieces.length && + pieces[endIndex].end < pieces[endIndex + 1].start + ? new Range( + tokenContentRange.start.translate({ + characterDelta: pieces[endIndex].end, + }), + tokenContentRange.start.translate({ + characterDelta: pieces[endIndex + 1].start, + }) + ) + : undefined; + const isInDelimitedList = + leadingDelimiterRange != null || trailingDelimiterRange != null; + const insertionDelimiter = isInDelimitedList + ? editor.document.getText( + (leadingDelimiterRange ?? trailingDelimiterRange)! + ) + : ""; + + return [ + new SubTokenWordTarget({ + editor, + isReversed, + contentRange, + insertionDelimiter, + leadingDelimiterRange, + trailingDelimiterRange, + }), + ]; + } +} diff --git a/src/processTargets/modifiers/PositionStage.ts b/src/processTargets/modifiers/PositionStage.ts new file mode 100644 index 0000000000..5b9847e608 --- /dev/null +++ b/src/processTargets/modifiers/PositionStage.ts @@ -0,0 +1,55 @@ +import { Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import { + Position, + PositionModifier, +} from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import PositionTarget from "../targets/PositionTarget"; + +export default class PositionStage implements ModifierStage { + constructor(private modifier: PositionModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + return [toPositionTarget(target, this.modifier.position)]; + } +} + +export function toPositionTarget(target: Target, position: Position): Target { + const { start, end } = target.contentRange; + let contentRange: Range; + let insertionDelimiter: string; + + switch (position) { + case "before": + contentRange = new Range(start, start); + insertionDelimiter = target.insertionDelimiter; + break; + + case "after": + contentRange = new Range(end, end); + insertionDelimiter = target.insertionDelimiter; + break; + + case "start": + contentRange = new Range(start, start); + // This it NOT a raw target. Joining with this should be done on empty delimiter. + insertionDelimiter = ""; + break; + + case "end": + contentRange = new Range(end, end); + insertionDelimiter = ""; + break; + } + + return new PositionTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange, + position, + insertionDelimiter, + isRaw: target.isRaw, + }); +} diff --git a/src/processTargets/modifiers/RawSelectionStage.ts b/src/processTargets/modifiers/RawSelectionStage.ts new file mode 100644 index 0000000000..2b8da3ccb9 --- /dev/null +++ b/src/processTargets/modifiers/RawSelectionStage.ts @@ -0,0 +1,19 @@ +import { Target } from "../../typings/target.types"; +import { RawSelectionModifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import RawSelectionTarget from "../targets/RawSelectionTarget"; + +export default class RawSelectionStage implements ModifierStage { + constructor(private modifier: RawSelectionModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + return [ + new RawSelectionTarget({ + editor: target.editor, + contentRange: target.contentRange, + isReversed: target.isReversed, + }), + ]; + } +} diff --git a/src/processTargets/modifiers/SurroundingPairStage.ts b/src/processTargets/modifiers/SurroundingPairStage.ts new file mode 100644 index 0000000000..bb92ef29b1 --- /dev/null +++ b/src/processTargets/modifiers/SurroundingPairStage.ts @@ -0,0 +1,55 @@ +import { Target } from "../../typings/target.types"; +import { ContainingSurroundingPairModifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import SurroundingPairTarget from "../targets/SurroundingPairTarget"; +import { processSurroundingPair } from "./surroundingPair"; + +/** + * Applies the surrounding pair modifier to the given selection. First looks to + * see if the target is itself adjacent to or contained by a modifier token. If + * so it will expand the selection to the opposite delimiter token. Otherwise, + * or if the opposite token wasn't found, it will proceed by finding the + * smallest pair of delimiters which contains the selection. + * + * @param context Context to be leveraged by modifier + * @param selection The selection to process + * @param modifier The surrounding pair modifier information + * @returns The new selection expanded to the containing surrounding pair or + * `null` if none was found + */ +export default class SurroundingPairStage implements ModifierStage { + constructor(private modifier: ContainingSurroundingPairModifier) {} + + run( + context: ProcessedTargetsContext, + target: Target + ): SurroundingPairTarget[] { + return processedSurroundingPairTarget(this.modifier, context, target); + } +} + +function processedSurroundingPairTarget( + modifier: ContainingSurroundingPairModifier, + context: ProcessedTargetsContext, + target: Target +): SurroundingPairTarget[] { + const pairInfo = processSurroundingPair( + context, + target.editor, + target.contentRange, + modifier.scopeType + ); + + if (pairInfo == null) { + throw new Error("Couldn't find containing pair"); + } + + return [ + new SurroundingPairTarget({ + ...pairInfo, + editor: target.editor, + isReversed: target.isReversed, + }), + ]; +} diff --git a/src/processTargets/modifiers/commonWeakContainingScopeStages.ts b/src/processTargets/modifiers/commonWeakContainingScopeStages.ts new file mode 100644 index 0000000000..22ed334c4e --- /dev/null +++ b/src/processTargets/modifiers/commonWeakContainingScopeStages.ts @@ -0,0 +1,11 @@ +import ModifyIfWeakStage from "./ModifyIfWeakStage"; + +export const weakContainingSurroundingPairStage = new ModifyIfWeakStage({ + type: "containingScope", + scopeType: { type: "surroundingPair", delimiter: "any" }, +}); + +export const weakContainingLineStage = new ModifyIfWeakStage({ + type: "containingScope", + scopeType: { type: "line" }, +}); diff --git a/src/processTargets/modifiers/processModifier.ts b/src/processTargets/modifiers/processModifier.ts deleted file mode 100644 index 4292290a38..0000000000 --- a/src/processTargets/modifiers/processModifier.ts +++ /dev/null @@ -1,265 +0,0 @@ -import update from "immutability-helper"; -import { range } from "lodash"; -import { Location, Position, Range, Selection } from "vscode"; -import { SyntaxNode } from "web-tree-sitter"; -import { SUBWORD_MATCHER } from "../../core/constants"; -import { selectionWithEditorFromRange } from "../../util/selectionUtils"; -import { - ContainingScopeModifier, - HeadModifier, - NodeMatcher, - PrimitiveTarget, - ProcessedTargetsContext, - SelectionContext, - SelectionWithEditor, - SubTokenModifier, - TailModifier, -} from "../../typings/Types"; -import { processSurroundingPair } from "./surroundingPair"; -import { getNodeMatcher } from "../../languages/getNodeMatcher"; -import { NoContainingScopeError } from "../../errors"; - -export type SelectionWithEditorWithContext = { - selection: SelectionWithEditor; - context: SelectionContext; -}; - -/** - * Processes a single modifier in the {@link processTargets} pipeline. - * @param context Context created in {@link CommandRunner#run} - * @param target Target description, including information about the modifier - * to apply - * @param selection Selection to apply the modifier to, as output by previous - * stages of pipeline, such as the mark. - * @returns Modified selection including additional rich context information - */ -export default function ( - context: ProcessedTargetsContext, - target: PrimitiveTarget, - selection: SelectionWithEditor -): SelectionWithEditorWithContext[] { - const { modifier } = target; - let result; - - switch (modifier.type) { - case "identity": - result = [{ selection, context: {} }]; - break; - - case "containingScope": - result = processScopeType(context, selection, modifier); - break; - - case "subpiece": - result = processSubToken(context, selection, modifier); - break; - - case "head": - case "tail": - result = processHeadTail(context, selection, modifier); - break; - - case "surroundingPair": - result = processSurroundingPair(context, selection, modifier); - break; - - case "toRawSelection": - result = processRawSelectionModifier(context, selection); - break; - - default: - // Make sure we haven't missed any cases - const _neverCheck: never = modifier; - } - - if (result == null) { - throw new Error(`Couldn't find containing`); - } - - return result; -} - -function processScopeType( - context: ProcessedTargetsContext, - selection: SelectionWithEditor, - modifier: ContainingScopeModifier -): SelectionWithEditorWithContext[] | null { - const nodeMatcher = getNodeMatcher( - selection.editor.document.languageId, - modifier.scopeType, - modifier.includeSiblings ?? false - ); - const node: SyntaxNode | null = context.getNodeAtLocation( - new Location(selection.editor.document.uri, selection.selection) - ); - - const result = findNearestContainingAncestorNode( - node, - nodeMatcher, - selection - ); - - if (result == null) { - throw new NoContainingScopeError(modifier.scopeType); - } - - return result; -} - -function processSubToken( - context: ProcessedTargetsContext, - selection: SelectionWithEditor, - modifier: SubTokenModifier -): SelectionWithEditorWithContext[] | null { - const token = selection.editor.document.getText(selection.selection); - let pieces: { start: number; end: number }[] = []; - - if (modifier.excludeActive || modifier.excludeAnchor) { - throw new Error("Subtoken exclusions unsupported"); - } - - if (modifier.pieceType === "word") { - pieces = [...token.matchAll(SUBWORD_MATCHER)].map((match) => ({ - start: match.index!, - end: match.index! + match[0].length, - })); - } else if (modifier.pieceType === "character") { - pieces = range(token.length).map((index) => ({ - start: index, - end: index + 1, - })); - } - - const anchorIndex = - modifier.anchor < 0 ? modifier.anchor + pieces.length : modifier.anchor; - const activeIndex = - modifier.active < 0 ? modifier.active + pieces.length : modifier.active; - - if ( - anchorIndex < 0 || - activeIndex < 0 || - anchorIndex >= pieces.length || - activeIndex >= pieces.length - ) { - throw new Error("Subtoken index out of range"); - } - - const isReversed = activeIndex < anchorIndex; - - const anchor = selection.selection.start.translate( - undefined, - isReversed ? pieces[anchorIndex].end : pieces[anchorIndex].start - ); - const active = selection.selection.start.translate( - undefined, - isReversed ? pieces[activeIndex].start : pieces[activeIndex].end - ); - - const startIndex = Math.min(anchorIndex, activeIndex); - const endIndex = Math.max(anchorIndex, activeIndex); - const leadingDelimiterRange = - startIndex > 0 && pieces[startIndex - 1].end < pieces[startIndex].start - ? new Range( - selection.selection.start.translate({ - characterDelta: pieces[startIndex - 1].end, - }), - selection.selection.start.translate({ - characterDelta: pieces[startIndex].start, - }) - ) - : null; - const trailingDelimiterRange = - endIndex + 1 < pieces.length && - pieces[endIndex].end < pieces[endIndex + 1].start - ? new Range( - selection.selection.start.translate({ - characterDelta: pieces[endIndex].end, - }), - selection.selection.start.translate({ - characterDelta: pieces[endIndex + 1].start, - }) - ) - : null; - const isInDelimitedList = - leadingDelimiterRange != null || trailingDelimiterRange != null; - const containingListDelimiter = isInDelimitedList - ? selection.editor.document.getText( - (leadingDelimiterRange ?? trailingDelimiterRange)! - ) - : null; - - return [ - { - selection: update(selection, { - selection: () => new Selection(anchor, active), - }), - context: { - isInDelimitedList, - containingListDelimiter: containingListDelimiter ?? undefined, - leadingDelimiterRange, - trailingDelimiterRange, - }, - }, - ]; -} - -function processHeadTail( - context: ProcessedTargetsContext, - selection: SelectionWithEditor, - modifier: HeadModifier | TailModifier -): SelectionWithEditorWithContext[] | null { - let anchor: Position, active: Position; - if (modifier.type === "head") { - anchor = selection.selection.end; - active = new Position(selection.selection.start.line, 0); - } else { - anchor = selection.selection.start; - active = selection.editor.document.lineAt(selection.selection.end).range - .end; - } - return [ - { - selection: update(selection, { - selection: () => new Selection(anchor, active), - }), - context: {}, - }, - ]; -} - -export function findNearestContainingAncestorNode( - startNode: SyntaxNode, - nodeMatcher: NodeMatcher, - selection: SelectionWithEditor -) { - let node: SyntaxNode | null = startNode; - while (node != null) { - const matches = nodeMatcher(selection, node); - if (matches != null) { - return matches - .map((match) => match.selection) - .map((matchedSelection) => ({ - selection: selectionWithEditorFromRange( - selection, - matchedSelection.selection - ), - context: matchedSelection.context, - })); - } - node = node.parent; - } - - return null; -} - -function processRawSelectionModifier( - context: ProcessedTargetsContext, - selection: SelectionWithEditor -): SelectionWithEditorWithContext[] | null { - return [ - { - selection, - context: { isRawSelection: true }, - }, - ]; -} diff --git a/src/processTargets/modifiers/scopeTypeStages/ContainingSyntaxScopeStage.ts b/src/processTargets/modifiers/scopeTypeStages/ContainingSyntaxScopeStage.ts new file mode 100644 index 0000000000..5844e86b7d --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/ContainingSyntaxScopeStage.ts @@ -0,0 +1,113 @@ +import { Location, Selection } from "vscode"; +import { SyntaxNode } from "web-tree-sitter"; +import { NoContainingScopeError } from "../../../errors"; +import { getNodeMatcher } from "../../../languages/getNodeMatcher"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, + SimpleScopeType, +} from "../../../typings/targetDescriptor.types"; +import { + NodeMatcher, + ProcessedTargetsContext, + SelectionWithEditor, + SelectionWithEditorWithContext, +} from "../../../typings/Types"; +import { selectionWithEditorFromRange } from "../../../util/selectionUtils"; +import { ModifierStage } from "../../PipelineStages.types"; +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; + +export interface SimpleContainingScopeModifier extends ContainingScopeModifier { + scopeType: SimpleScopeType; +} + +export interface SimpleEveryScopeModifier extends EveryScopeModifier { + scopeType: SimpleScopeType; +} + +export default class implements ModifierStage { + constructor( + private modifier: SimpleContainingScopeModifier | SimpleEveryScopeModifier + ) {} + + run(context: ProcessedTargetsContext, target: Target): ScopeTypeTarget[] { + const nodeMatcher = getNodeMatcher( + target.editor.document.languageId, + this.modifier.scopeType.type, + this.modifier.type === "everyScope" + ); + + const node: SyntaxNode | null = context.getNodeAtLocation( + new Location(target.editor.document.uri, target.contentRange) + ); + + const scopeNodes = findNearestContainingAncestorNode(node, nodeMatcher, { + editor: target.editor, + selection: new Selection( + target.contentRange.start, + target.contentRange.end + ), + }); + + if (scopeNodes == null) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + return scopeNodes.map((scope) => { + const { + containingListDelimiter, + leadingDelimiterRange, + trailingDelimiterRange, + removalRange, + } = scope.context; + + if ( + removalRange != null && + (leadingDelimiterRange != null || trailingDelimiterRange != null) + ) { + throw Error( + "Removal range is mutually exclusive with leading or trailing delimiter range" + ); + } + + const { editor, selection: contentSelection } = scope.selection; + + return new ScopeTypeTarget({ + scopeTypeType: this.modifier.scopeType.type, + editor, + isReversed: target.isReversed, + contentRange: contentSelection, + removalRange: removalRange, + delimiter: containingListDelimiter, + leadingDelimiterRange, + trailingDelimiterRange, + }); + }); + } +} + +function findNearestContainingAncestorNode( + startNode: SyntaxNode, + nodeMatcher: NodeMatcher, + selection: SelectionWithEditor +): SelectionWithEditorWithContext[] | null { + let node: SyntaxNode | null = startNode; + while (node != null) { + const matches = nodeMatcher(selection, node); + if (matches != null) { + return matches + .map((match) => match.selection) + .map((matchedSelection) => ({ + selection: selectionWithEditorFromRange( + selection, + matchedSelection.selection + ), + context: matchedSelection.context, + })); + } + node = node.parent; + } + + return null; +} diff --git a/src/processTargets/modifiers/scopeTypeStages/DocumentStage.ts b/src/processTargets/modifiers/scopeTypeStages/DocumentStage.ts new file mode 100644 index 0000000000..ebd7dcd39c --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/DocumentStage.ts @@ -0,0 +1,30 @@ +import { Position, Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import DocumentTarget from "../../targets/DocumentTarget"; + +export default class implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): DocumentTarget[] { + return [ + new DocumentTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange: getDocumentRange(target.editor), + }), + ]; + } +} + +function getDocumentRange(editor: TextEditor) { + return new Range( + new Position(0, 0), + editor.document.lineAt(editor.document.lineCount - 1).range.end + ); +} diff --git a/src/processTargets/modifiers/scopeTypeStages/LineStage.ts b/src/processTargets/modifiers/scopeTypeStages/LineStage.ts new file mode 100644 index 0000000000..2e8afefcfa --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/LineStage.ts @@ -0,0 +1,82 @@ +import { Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import LineTarget from "../../targets/LineTarget"; + +export default class implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): LineTarget[] { + if (this.modifier.type === "everyScope") { + return this.getEveryTarget(target); + } + return [toLineTarget(target)]; + } + + getEveryTarget(target: Target): LineTarget[] { + const { contentRange, editor } = target; + const { isEmpty } = contentRange; + const startLine = isEmpty ? 0 : contentRange.start.line; + const endLine = isEmpty + ? editor.document.lineCount - 1 + : contentRange.end.line; + const targets: LineTarget[] = []; + + for (let i = startLine; i <= endLine; ++i) { + const line = editor.document.lineAt(i); + if (!line.isEmptyOrWhitespace) { + targets.push( + createLineTarget(target.editor, line.range, target.isReversed) + ); + } + } + + if (targets.length === 0) { + throw new Error( + `Couldn't find containing ${this.modifier.scopeType.type}` + ); + } + + return targets; + } +} + +export function toLineTarget(target: Target): LineTarget { + return createLineTarget( + target.editor, + target.contentRange, + target.isReversed + ); +} + +export function createLineTarget( + editor: TextEditor, + range: Range, + isReversed: boolean +) { + return new LineTarget({ + editor, + isReversed, + contentRange: fitRangeToLineContent(editor, range), + }); +} + +export function fitRangeToLineContent(editor: TextEditor, range: Range) { + const startLine = editor.document.lineAt(range.start); + const endLine = editor.document.lineAt(range.end); + const endCharacterIndex = + endLine.range.end.character - + (endLine.text.length - endLine.text.trimEnd().length); + + return new Range( + startLine.lineNumber, + startLine.firstNonWhitespaceCharacterIndex, + endLine.lineNumber, + endCharacterIndex + ); +} diff --git a/src/processTargets/modifiers/scopeTypeStages/NotebookCellStage.ts b/src/processTargets/modifiers/scopeTypeStages/NotebookCellStage.ts new file mode 100644 index 0000000000..0ff8c9b5e8 --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/NotebookCellStage.ts @@ -0,0 +1,26 @@ +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import NotebookCellTarget from "../../targets/NotebookCellTarget"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; + +export default class implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): NotebookCellTarget[] { + if (this.modifier.type === "everyScope") { + throw new Error(`Every ${this.modifier.type} not yet implemented`); + } + + return [ + new NotebookCellTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange: target.contentRange, + }), + ]; + } +} diff --git a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts new file mode 100644 index 0000000000..cac90e429a --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts @@ -0,0 +1,114 @@ +import { Range } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import ParagraphTarget from "../../targets/ParagraphTarget"; +import { fitRangeToLineContent } from "./LineStage"; + +export default class implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): ParagraphTarget[] { + if (this.modifier.type === "everyScope") { + return this.getEveryTarget(target); + } + return [this.getSingleTarget(target)]; + } + + getEveryTarget(target: Target): ParagraphTarget[] { + const { contentRange, editor } = target; + const { isEmpty } = contentRange; + const { lineCount } = editor.document; + const startLine = isEmpty ? 0 : contentRange.start.line; + const endLine = isEmpty ? lineCount - 1 : contentRange.end.line; + const targets: ParagraphTarget[] = []; + let paragraphStart = -1; + + const possiblyAddParagraph = ( + paragraphStart: number, + paragraphEnd: number + ) => { + // Paragraph and selection intersects + if (paragraphEnd >= startLine && paragraphStart <= endLine) { + targets.push( + this.getTargetFromRange( + target, + new Range(paragraphStart, 0, paragraphEnd, 0) + ) + ); + } + }; + + for (let i = 0; i < lineCount; ++i) { + const line = editor.document.lineAt(i); + if (line.isEmptyOrWhitespace) { + // End of paragraph + if (paragraphStart > -1) { + possiblyAddParagraph(paragraphStart, i - 1); + paragraphStart = -1; + } + } + // Start of paragraph + else if (paragraphStart < 0) { + paragraphStart = i; + } + // Last line is non empty. End of paragraph + if (i === lineCount - 1 && !line.isEmptyOrWhitespace) { + possiblyAddParagraph(paragraphStart, i); + } + } + + if (targets.length === 0) { + throw new Error( + `Couldn't find containing ${this.modifier.scopeType.type}` + ); + } + + return targets; + } + + getSingleTarget(target: Target): ParagraphTarget { + return this.getTargetFromRange(target); + } + + getTargetFromRange(target: Target, range?: Range): ParagraphTarget { + if (range == null) { + range = calculateRange(target); + } + + return new ParagraphTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange: fitRangeToLineContent(target.editor, range), + }); + } +} + +function calculateRange(target: Target) { + const { document } = target.editor; + let startLine = document.lineAt(target.contentRange.start); + if (!startLine.isEmptyOrWhitespace) { + while (startLine.lineNumber > 0) { + const line = document.lineAt(startLine.lineNumber - 1); + if (line.isEmptyOrWhitespace) { + break; + } + startLine = line; + } + } + let endLine = document.lineAt(target.contentRange.end); + if (!endLine.isEmptyOrWhitespace) { + while (endLine.lineNumber + 1 < document.lineCount) { + const line = document.lineAt(endLine.lineNumber + 1); + if (line.isEmptyOrWhitespace) { + break; + } + endLine = line; + } + } + return new Range(startLine.range.start, endLine.range.end); +} diff --git a/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts b/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts new file mode 100644 index 0000000000..a0b7bf86cd --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts @@ -0,0 +1,136 @@ +import { Position, Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; + +type RegexModifier = NonWhitespaceSequenceModifier | UrlModifier; + +class RegexStage implements ModifierStage { + constructor( + private modifier: RegexModifier, + private regex: RegExp, + private name?: string + ) {} + + run(context: ProcessedTargetsContext, target: Target): ScopeTypeTarget[] { + if (this.modifier.type === "everyScope") { + return this.getEveryTarget(target); + } + return [this.getSingleTarget(target)]; + } + + getEveryTarget(target: Target): ScopeTypeTarget[] { + const { contentRange, editor } = target; + const { isEmpty } = contentRange; + const start = isEmpty + ? editor.document.lineAt(contentRange.start).range.start + : contentRange.start; + const end = isEmpty + ? editor.document.lineAt(contentRange.end).range.end + : contentRange.end; + const targets: ScopeTypeTarget[] = []; + + for (let i = start.line; i <= end.line; ++i) { + this.getMatchesForLine(editor, i).forEach((range) => { + // Regex match and selection intersects + if (range.end.isAfterOrEqual(start) && range.end.isBeforeOrEqual(end)) { + targets.push(this.getTargetFromRange(target, range)); + } + }); + } + + if (targets.length === 0) { + if (targets.length === 0) { + throw new Error( + `Couldn't find containing ${this.modifier.scopeType.type}` + ); + } + } + + return targets; + } + + getSingleTarget(target: Target): ScopeTypeTarget { + const { editor } = target; + const start = this.getMatchForPos(editor, target.contentRange.start).start; + const end = this.getMatchForPos(editor, target.contentRange.end).end; + const contentRange = new Range(start, end); + return this.getTargetFromRange(target, contentRange); + } + + getTargetFromRange(target: Target, range: Range): ScopeTypeTarget { + return new ScopeTypeTarget({ + scopeTypeType: this.modifier.scopeType.type, + editor: target.editor, + isReversed: target.isReversed, + contentRange: range, + }); + } + + getMatchForPos(editor: TextEditor, position: Position) { + const match = this.getMatchesForLine(editor, position.line).find((range) => + range.contains(position) + ); + if (match == null) { + if (this.name) { + throw new Error(`Couldn't find containing ${this.name}`); + } else { + throw new Error(`Cannot find sequence defined by regex: ${this.regex}`); + } + } + return match; + } + + getMatchesForLine(editor: TextEditor, lineNum: number) { + const line = editor.document.lineAt(lineNum); + const result = [...line.text.matchAll(this.regex)].map( + (match) => + new Range( + lineNum, + match.index!, + lineNum, + match.index! + match[0].length + ) + ); + if (result == null) { + if (this.name) { + throw new Error(`Couldn't find containing ${this.name}`); + } else { + throw new Error(`Cannot find sequence defined by regex: ${this.regex}`); + } + } + return result; + } +} + +export type NonWhitespaceSequenceModifier = ( + | ContainingScopeModifier + | EveryScopeModifier +) & { + scopeType: { type: "nonWhitespaceSequence" }; +}; + +export class NonWhitespaceSequenceStage extends RegexStage { + constructor(modifier: NonWhitespaceSequenceModifier) { + super(modifier, /\S+/g, "Non whitespace sequence"); + } +} + +// taken from https://regexr.com/3e6m0 +const URL_REGEX = + /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g; + +export type UrlModifier = (ContainingScopeModifier | EveryScopeModifier) & { + scopeType: { type: "url" }; +}; + +export class UrlStage extends RegexStage { + constructor(modifier: UrlModifier) { + super(modifier, URL_REGEX, "URL"); + } +} diff --git a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts b/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts new file mode 100644 index 0000000000..be7f943683 --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts @@ -0,0 +1,162 @@ +import { Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { getTokensInRange, PartialToken } from "../../../util/getTokensInRange"; +import { ModifierStage } from "../../PipelineStages.types"; +import TokenTarget from "../../targets/TokenTarget"; + +export default class implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): TokenTarget[] { + if (this.modifier.type === "everyScope") { + return this.getEveryTarget(context, target); + } + return [this.getSingleTarget(target)]; + } + + getEveryTarget( + context: ProcessedTargetsContext, + target: Target + ): TokenTarget[] { + const { contentRange, editor } = target; + const { isEmpty } = contentRange; + const start = isEmpty + ? editor.document.lineAt(contentRange.start).range.start + : contentRange.start; + const end = isEmpty + ? editor.document.lineAt(contentRange.end).range.end + : contentRange.end; + const range = new Range(start, end); + + const targets = getTokensInRange(editor, range).map(({ range }) => + this.getTargetFromRange(target, range) + ); + + if (targets.length === 0) { + throw new Error( + `Couldn't find containing ${this.modifier.scopeType.type}` + ); + } + + return targets; + } + + getSingleTarget(target: Target): TokenTarget { + return this.getTargetFromRange(target, target.contentRange); + } + + getTargetFromRange(target: Target, range: Range): TokenTarget { + const contentRange = getTokenRangeForSelection(target.editor, range); + return new TokenTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange, + }); + } +} + +/** + * Given a selection returns a new range which contains the tokens + * intersecting the given selection. Uses heuristics to tie break when the + * given selection is empty and abuts 2 adjacent tokens + * @param selection Selection to operate on + * @returns Modified range + */ +export function getTokenRangeForSelection( + editor: TextEditor, + range: Range +): Range { + let tokens = getTokenIntersectionsForSelection(editor, range); + // Use single token for overlapping or adjacent range + if (range.isEmpty) { + // If multiple matches sort and take the first + tokens.sort(({ token: a }, { token: b }) => { + // First sort on alphanumeric + const aIsAlphaNum = isAlphaNum(a.text); + const bIsAlphaNum = isAlphaNum(b.text); + if (aIsAlphaNum && !bIsAlphaNum) { + return -1; + } + if (bIsAlphaNum && !aIsAlphaNum) { + return 1; + } + // Second sort on length + const lengthDiff = b.text.length - a.text.length; + if (lengthDiff !== 0) { + return lengthDiff; + } + // Lastly sort on start position. ie leftmost + return a.offsets.start - b.offsets.start; + }); + tokens = tokens.slice(0, 1); + } + // Use tokens for overlapping ranges + else { + tokens = tokens.filter((token) => !token.intersection.isEmpty); + tokens.sort((a, b) => a.token.offsets.start - b.token.offsets.start); + } + if (tokens.length < 1) { + throw new Error("Couldn't find token in selection"); + } + const start = tokens[0].token.range.start; + const end = tokens[tokens.length - 1].token.range.end; + return new Range(start, end); +} + +/** + * Returns tokens that intersect with the selection that may be relevant for + * expanding the selection to its containing token. + * @param selection The selection + * @returns All tokens that intersect with the selection and are on the same line as the start or endpoint of the selection + */ +export function getTokenIntersectionsForSelection( + editor: TextEditor, + range: Range +) { + const tokens = getRelevantTokens(editor, range); + + const tokenIntersections: { token: PartialToken; intersection: Range }[] = []; + + tokens.forEach((token) => { + const intersection = token.range.intersection(range); + if (intersection != null) { + tokenIntersections.push({ token, intersection }); + } + }); + + return tokenIntersections; +} + +/** + * Given a selection, finds all tokens that we might use to expand the + * selection. Just looks at tokens on the same line as the start and end of the + * selection, because we assume that a token cannot span multiple lines. + * @param selection The selection we care about + * @returns A list of tokens that we might expand to + */ +function getRelevantTokens(editor: TextEditor, range: Range) { + const startLine = range.start.line; + const endLine = range.end.line; + + let tokens = getTokensInRange( + editor, + editor.document.lineAt(startLine).range + ); + + if (endLine !== startLine) { + tokens.push( + ...getTokensInRange(editor, editor.document.lineAt(endLine).range) + ); + } + + return tokens; +} + +function isAlphaNum(text: string) { + return /^\w+$/.test(text); +} diff --git a/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts b/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts index 5204d2c3fb..b0d69574c9 100644 --- a/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts +++ b/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts @@ -1,7 +1,7 @@ import { ComplexSurroundingPairName, SimpleSurroundingPairName, -} from "../../../typings/Types"; +} from "../../../typings/targetDescriptor.types"; type IndividualDelimiterText = string | string[]; diff --git a/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts b/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts index 236e0d8c96..a3e51fc510 100644 --- a/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts +++ b/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts @@ -1,10 +1,12 @@ -import { Selection, TextDocument } from "vscode"; -import { - DelimiterInclusion, - SelectionWithContext, -} from "../../../typings/Types"; +import { Range, Selection, TextDocument } from "vscode"; import { SurroundingPairOffsets } from "./types"; +export interface SurroundingPairInfo { + contentRange: Selection; + boundary: [Range, Range]; + interiorRange: Range; +} + /** * Given offsets describing a surrounding pair, returns a selection * @@ -17,73 +19,41 @@ import { SurroundingPairOffsets } from "./types"; export function extractSelectionFromSurroundingPairOffsets( document: TextDocument, baseOffset: number, - surroundingPairOffsets: SurroundingPairOffsets, - delimiterInclusion: DelimiterInclusion -): SelectionWithContext[] { - const interior = [ - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.end - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.start - ) + surroundingPairOffsets: SurroundingPairOffsets +): SurroundingPairInfo { + const interior = new Range( + document.positionAt(baseOffset + surroundingPairOffsets.leftDelimiter.end), + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.start + ) + ); + const boundary: [Range, Range] = [ + new Range( + document.positionAt( + baseOffset + surroundingPairOffsets.leftDelimiter.start ), - context: {}, - }, - ]; - - const boundary = [ - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.start - ), - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.end - ) + document.positionAt(baseOffset + surroundingPairOffsets.leftDelimiter.end) + ), + new Range( + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.start ), - context: {}, - }, - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.start - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.end - ) - ), - context: {}, - }, + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.end + ) + ), ]; - // If delimiter inclusion is null, do default behavior and include the - // delimiters - if (delimiterInclusion == null) { - return [ - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.start - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.end - ) - ), - context: { - boundary, - interior, - }, - }, - ]; - } - - switch (delimiterInclusion) { - case "interiorOnly": - return interior; - case "excludeInterior": - return boundary; - } + return { + contentRange: new Selection( + document.positionAt( + baseOffset + surroundingPairOffsets.leftDelimiter.start + ), + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.end + ) + ), + boundary, + interiorRange: interior, + }; } diff --git a/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts b/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts index 6d10a5ad3a..692ed3c1c5 100644 --- a/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts +++ b/src/processTargets/modifiers/surroundingPair/findDelimiterPairContainingSelection.ts @@ -5,7 +5,7 @@ import { Offsets, } from "./types"; import { generateUnmatchedDelimiters } from "./generateUnmatchedDelimiters"; -import { SimpleSurroundingPairName } from "../../../typings/Types"; +import { SimpleSurroundingPairName } from "../../../typings/targetDescriptor.types"; /** * Looks for a surrounding pair that contains the selection, returning null if none is found. diff --git a/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts b/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts index c2e6342725..3f61c8e127 100644 --- a/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts +++ b/src/processTargets/modifiers/surroundingPair/findOppositeDelimiter.ts @@ -1,4 +1,4 @@ -import { SurroundingPairDirection } from "../../../typings/Types"; +import { SurroundingPairDirection } from "../../../typings/targetDescriptor.types"; import { findUnmatchedDelimiter } from "./generateUnmatchedDelimiters"; import { DelimiterOccurrence, diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts index 9cf1d883d3..d0485053b6 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts @@ -1,5 +1,5 @@ import { sortedIndexBy } from "lodash"; -import { SimpleSurroundingPairName } from "../../../typings/Types"; +import { SimpleSurroundingPairName } from "../../../typings/targetDescriptor.types"; import { findDelimiterPairAdjacentToSelection } from "./findDelimiterPairAdjacentToSelection"; import { findDelimiterPairContainingSelection } from "./findDelimiterPairContainingSelection"; import { diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts index 8e9ee00691..374f1772e2 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts @@ -1,10 +1,9 @@ -import { Selection, TextDocument, TextEditor } from "vscode"; +import { Range, TextDocument, TextEditor } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; import { SimpleSurroundingPairName, - DelimiterInclusion, SurroundingPairDirection, -} from "../../../typings/Types"; +} from "../../../typings/targetDescriptor.types"; import { getNodeRange } from "../../../util/nodeSelectors"; import { isContainedInErrorNode } from "../../../util/treeSitterUtils"; import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; @@ -56,15 +55,13 @@ import { * @param selection The selection to find surrounding pair around * @param node A parse tree node overlapping with the selection * @param delimiters The acceptable surrounding pair names - * @param delimiterInclusion Whether to include / exclude the delimiters themselves * @returns The newly expanded selection, including editor info */ export function findSurroundingPairParseTreeBased( editor: TextEditor, - selection: Selection, + selection: Range, node: SyntaxNode, delimiters: SimpleSurroundingPairName[], - delimiterInclusion: DelimiterInclusion, forceDirection: "left" | "right" | undefined ) { const document: TextDocument = editor.document; @@ -118,12 +115,8 @@ export function findSurroundingPairParseTreeBased( return extractSelectionFromSurroundingPairOffsets( document, 0, - pairOffsets, - delimiterInclusion - ).map(({ selection, context }) => ({ - selection: { selection, editor }, - context, - })); + pairOffsets + ); } } @@ -182,11 +175,12 @@ function findSurroundingPairContainedInNode( /** * A list of all delimiter nodes descending from `node`, as determined by - * their type + * their type. + * Handles the case of error nodes with no text. https://github.com/cursorless-dev/cursorless/issues/688 */ - const possibleDelimiterNodes = node.descendantsOfType( - individualDelimiters.map(({ text }) => text) - ); + const possibleDelimiterNodes = node + .descendantsOfType(individualDelimiters.map(({ text }) => text)) + .filter((node) => !(node.text === "" && node.hasError())); /** * A list of all delimiter occurrences, generated from the delimiter nodes. diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts index 79abe8962b..7caf64353c 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts @@ -1,21 +1,20 @@ import { escapeRegExp, findLast, uniq } from "lodash"; -import { Range, Selection, TextDocument, TextEditor } from "vscode"; +import { Range, TextDocument, TextEditor } from "vscode"; import { SimpleSurroundingPairName, - DelimiterInclusion, - SurroundingPairName, SurroundingPairDirection, -} from "../../../typings/Types"; + SurroundingPairName, +} from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { matchAll } from "../../../util/regex"; import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; import { findSurroundingPairCore } from "./findSurroundingPairCore"; import { getIndividualDelimiters } from "./getIndividualDelimiters"; import { + IndividualDelimiter, Offsets, - SurroundingPairOffsets, PossibleDelimiterOccurrence, - IndividualDelimiter, + SurroundingPairOffsets, } from "./types"; /** @@ -60,19 +59,17 @@ const SCAN_EXPANSION_FACTOR = 3; * form of opening quote in Python. * * @param editor The text editor containing the selection - * @param selection The selection to find surrounding pair around + * @param range The selection to find surrounding pair around * @param allowableRange The range in which to look for delimiters, or the * entire document if `null` * @param delimiters The acceptable surrounding pair names - * @param delimiterInclusion Whether to include / exclude the delimiters themselves * @returns The newly expanded selection, including editor info */ export function findSurroundingPairTextBased( editor: TextEditor, - selection: Selection, + range: Range, allowableRange: Range | null, delimiters: SimpleSurroundingPairName[], - delimiterInclusion: DelimiterInclusion, forceDirection: "left" | "right" | undefined ) { const document: TextDocument = editor.document; @@ -101,8 +98,8 @@ export function findSurroundingPairTextBased( end: document.offsetAt(fullRange.end), }; const selectionOffsets = { - start: document.offsetAt(selection.start), - end: document.offsetAt(selection.end), + start: document.offsetAt(range.start), + end: document.offsetAt(range.end), }; /** @@ -142,7 +139,7 @@ export function findSurroundingPairTextBased( // Just bail early if the range doesn't completely contain our selection as // it is a lost cause. - if (!currentRange.contains(selection)) { + if (!currentRange.contains(range)) { continue; } @@ -168,12 +165,8 @@ export function findSurroundingPairTextBased( return extractSelectionFromSurroundingPairOffsets( document, currentRangeOffsets.start, - pairOffsets, - delimiterInclusion - ).map(({ selection, context }) => ({ - selection: { selection, editor }, - context, - })); + pairOffsets + ); } // If the current range is greater than are equal to the full range then we diff --git a/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts b/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts index d733157d40..12c8e9cd10 100644 --- a/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts +++ b/src/processTargets/modifiers/surroundingPair/generateUnmatchedDelimiters.ts @@ -1,5 +1,5 @@ import { range } from "lodash"; -import { SimpleSurroundingPairName } from "../../../typings/Types"; +import { SimpleSurroundingPairName } from "../../../typings/targetDescriptor.types"; import { DelimiterOccurrence, DelimiterSide, diff --git a/src/processTargets/modifiers/surroundingPair/getIndividualDelimiters.ts b/src/processTargets/modifiers/surroundingPair/getIndividualDelimiters.ts index 75f78419c6..35dd7b0de4 100644 --- a/src/processTargets/modifiers/surroundingPair/getIndividualDelimiters.ts +++ b/src/processTargets/modifiers/surroundingPair/getIndividualDelimiters.ts @@ -1,4 +1,4 @@ -import { SimpleSurroundingPairName } from "../../../typings/Types"; +import { SimpleSurroundingPairName } from "../../../typings/targetDescriptor.types"; import { IndividualDelimiter } from "./types"; import { delimiterToText } from "./delimiterMaps"; import { concat, uniq } from "lodash"; diff --git a/src/processTargets/modifiers/surroundingPair/index.ts b/src/processTargets/modifiers/surroundingPair/index.ts index 97d74c5f2f..b86b154785 100644 --- a/src/processTargets/modifiers/surroundingPair/index.ts +++ b/src/processTargets/modifiers/surroundingPair/index.ts @@ -1,18 +1,17 @@ -import { Location } from "vscode"; +import { Location, Range, Selection, TextEditor } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; -import { findSurroundingPairParseTreeBased } from "./findSurroundingPairParseTreeBased"; -import { findSurroundingPairTextBased } from "./findSurroundingPairTextBased"; -import { - ComplexSurroundingPairName, - ProcessedTargetsContext, - SelectionWithEditor, - SurroundingPairModifier, -} from "../../../typings/Types"; -import { SelectionWithEditorWithContext } from "../processModifier"; -import { complexDelimiterMap } from "./delimiterMaps"; import getTextFragmentExtractor, { TextFragmentExtractor, } from "../../../languages/getTextFragmentExtractor"; +import { + ComplexSurroundingPairName, + SurroundingPairScopeType, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { complexDelimiterMap } from "./delimiterMaps"; +import { SurroundingPairInfo } from "./extractSelectionFromSurroundingPairOffsets"; +import { findSurroundingPairParseTreeBased } from "./findSurroundingPairParseTreeBased"; +import { findSurroundingPairTextBased } from "./findSurroundingPairTextBased"; /** * Applies the surrounding pair modifier to the given selection. First looks to @@ -29,10 +28,11 @@ import getTextFragmentExtractor, { */ export function processSurroundingPair( context: ProcessedTargetsContext, - selection: SelectionWithEditor, - modifier: SurroundingPairModifier -): SelectionWithEditorWithContext[] | null { - const document = selection.editor.document; + editor: TextEditor, + range: Range, + modifier: SurroundingPairScopeType +): SurroundingPairInfo | null { + const document = editor.document; const delimiters = complexDelimiterMap[ modifier.delimiter as ComplexSurroundingPairName ] ?? [modifier.delimiter]; @@ -41,9 +41,7 @@ export function processSurroundingPair( let textFragmentExtractor: TextFragmentExtractor; try { - node = context.getNodeAtLocation( - new Location(document.uri, selection.selection) - ); + node = context.getNodeAtLocation(new Location(document.uri, range)); textFragmentExtractor = getTextFragmentExtractor(document.languageId); } catch (err) { @@ -51,11 +49,10 @@ export function processSurroundingPair( // If we're in a language where we don't have a parse tree we use the text // based algorithm return findSurroundingPairTextBased( - selection.editor, - selection.selection, + editor, + range, null, delimiters, - modifier.delimiterInclusion, modifier.forceDirection ); } else { @@ -65,14 +62,17 @@ export function processSurroundingPair( // If we have a parse tree but we are in a string node or in a comment node, // then we use the text-based algorithm - const textFragmentRange = textFragmentExtractor(node, selection); + const selectionWithEditor = { + editor, + selection: new Selection(range.start, range.end), + }; + const textFragmentRange = textFragmentExtractor(node, selectionWithEditor); if (textFragmentRange != null) { const surroundingRange = findSurroundingPairTextBased( - selection.editor, - selection.selection, + editor, + range, textFragmentRange, delimiters, - modifier.delimiterInclusion, modifier.forceDirection ); @@ -85,11 +85,10 @@ export function processSurroundingPair( // couldn't find a surrounding pair within a string or comment, we use the // parse tree-based algorithm return findSurroundingPairParseTreeBased( - selection.editor, - selection.selection, + editor, + range, node, delimiters, - modifier.delimiterInclusion, modifier.forceDirection ); } diff --git a/src/processTargets/modifiers/surroundingPair/types.ts b/src/processTargets/modifiers/surroundingPair/types.ts index f8be89b5d6..428fe852b8 100644 --- a/src/processTargets/modifiers/surroundingPair/types.ts +++ b/src/processTargets/modifiers/surroundingPair/types.ts @@ -1,4 +1,4 @@ -import { SimpleSurroundingPairName } from "../../../typings/Types"; +import { SimpleSurroundingPairName } from "../../../typings/targetDescriptor.types"; /** * Used to indicate whether a particular side of the delimiter is left or right diff --git a/src/processTargets/processMark.ts b/src/processTargets/processMark.ts deleted file mode 100644 index 6e4c7c56cb..0000000000 --- a/src/processTargets/processMark.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Range, Selection } from "vscode"; -import { - DecoratedSymbol, - LineNumber, - LineNumberPosition, - Mark, - ProcessedTargetsContext, - SelectionWithEditor, -} from "../typings/Types"; -import { getTokensInRange, PartialToken } from "../util/getTokensInRange"; -import { selectionWithEditorFromPositions } from "../util/selectionUtils"; - -export default function ( - context: ProcessedTargetsContext, - mark: Mark -): SelectionWithEditor[] { - switch (mark.type) { - case "cursor": - return context.currentSelections; - case "that": - return context.thatMark; - case "source": - return context.sourceMark; - case "cursorToken": - return context.currentSelections.map((selection) => - getTokenSelectionForSelection(context, selection) - ); - case "decoratedSymbol": - return processDecoratedSymbol(context, mark); - case "lineNumber": - return processLineNumber(context, mark); - case "nothing": - return []; - } -} - -/** - * Returns tokens that intersect with the selection that may be relevant for - * expanding the selection to its containing token. - * @param selection The selection - * @returns All tokens that intersect with the selection and are on the same line as the start or endpoint of the selection - */ -function getTokenIntersectionsForSelection(selection: SelectionWithEditor) { - let tokens = getRelevantTokens(selection); - - const tokenIntersections: { token: PartialToken; intersection: Range }[] = []; - - tokens.forEach((token) => { - const intersection = token.range.intersection(selection.selection); - if (intersection != null) { - tokenIntersections.push({ token, intersection }); - } - }); - - return tokenIntersections; -} - -/** - * Given a selection, finds all tokens that we might use to expand the - * selection. Just looks at tokens on the same line as the start and end of the - * selection, because we assume that a token cannot span multiple lines. - * @param selection The selection we care about - * @returns A list of tokens that we might expand to - */ -function getRelevantTokens(selection: SelectionWithEditor) { - const startLine = selection.selection.start.line; - const endLine = selection.selection.end.line; - - let tokens = getTokensInRange( - selection.editor, - selection.editor.document.lineAt(startLine).range - ); - - if (endLine !== startLine) { - tokens.push( - ...getTokensInRange( - selection.editor, - selection.editor.document.lineAt(endLine).range - ) - ); - } - - return tokens; -} - -/** - * Given a selection returns a new selection which contains the tokens - * intersecting the given selection. Uses heuristics to tie break when the - * given selection is empty and abuts 2 adjacent tokens - * @param selection Selection to operate on - * @returns Modified selection - */ -function getTokenSelectionForSelection( - context: ProcessedTargetsContext, - selection: SelectionWithEditor -): SelectionWithEditor { - let tokens = getTokenIntersectionsForSelection(selection); - // Use single token for overlapping or adjacent range - if (selection.selection.isEmpty) { - // If multiple matches sort and take the first - tokens.sort(({ token: a }, { token: b }) => { - // First sort on alphanumeric - const aIsAlphaNum = isAlphaNum(a.text); - const bIsAlphaNum = isAlphaNum(b.text); - if (aIsAlphaNum && !bIsAlphaNum) { - return -1; - } - if (bIsAlphaNum && !aIsAlphaNum) { - return 1; - } - // Second sort on length - const lengthDiff = b.text.length - a.text.length; - if (lengthDiff !== 0) { - return lengthDiff; - } - // Lastly sort on start position. ie leftmost - return a.offsets.start - b.offsets.start; - }); - tokens = tokens.slice(0, 1); - } - // Use tokens for overlapping ranges - else { - tokens = tokens.filter((token) => !token.intersection.isEmpty); - tokens.sort((a, b) => a.token.offsets.start - b.token.offsets.start); - } - if (tokens.length < 1) { - throw new Error("Couldn't find token in selection"); - } - const start = tokens[0].token.range.start; - const end = tokens[tokens.length - 1].token.range.end; - return selectionWithEditorFromPositions(selection, start, end); -} - -function processDecoratedSymbol( - context: ProcessedTargetsContext, - mark: DecoratedSymbol -) { - const token = context.hatTokenMap.getToken(mark.symbolColor, mark.character); - if (token == null) { - throw new Error( - `Couldn't find mark ${mark.symbolColor} '${mark.character}'` - ); - } - return [ - { - selection: new Selection(token.range.start, token.range.end), - editor: token.editor, - }, - ]; -} - -function processLineNumber(context: ProcessedTargetsContext, mark: LineNumber) { - const editor = context.currentEditor!; - const getLine = (linePosition: LineNumberPosition) => { - switch (linePosition.type) { - case "absolute": - return linePosition.lineNumber; - case "relative": - return editor.selection.active.line + linePosition.lineNumber; - case "modulo100": - const stepSize = 100; - const startLine = editor.visibleRanges[0].start.line; - const endLine = - editor.visibleRanges[editor.visibleRanges.length - 1].end.line; - const base = Math.floor(startLine / stepSize) * stepSize; - const visibleLines = []; - const invisibleLines = []; - let lineNumber = base + linePosition.lineNumber; - while (lineNumber <= endLine) { - if (lineNumber >= startLine) { - const visible = editor.visibleRanges.find( - (r) => lineNumber >= r.start.line && lineNumber <= r.end.line - ); - if (visible) { - visibleLines.push(lineNumber); - } else { - invisibleLines.push(lineNumber); - } - } - lineNumber += stepSize; - } - if (visibleLines.length === 1) { - return visibleLines[0]; - } - if (visibleLines.length + invisibleLines.length > 1) { - throw new Error("Multiple lines matching"); - } - if (invisibleLines.length === 1) { - return invisibleLines[0]; - } - throw new Error("Line is not in viewport"); - } - }; - return [ - { - selection: new Selection( - getLine(mark.anchor), - 0, - getLine(mark.active), - 0 - ), - editor: context.currentEditor!, - }, - ]; -} - -function isAlphaNum(text: string) { - return /^\w+$/.test(text); -} diff --git a/src/processTargets/processPosition.ts b/src/processTargets/processPosition.ts deleted file mode 100644 index d45c874430..0000000000 --- a/src/processTargets/processPosition.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Selection } from "vscode"; -import { - PrimitiveTarget, - ProcessedTargetsContext, - TypedSelection, -} from "../typings/Types"; - -export default function ( - context: ProcessedTargetsContext, - target: PrimitiveTarget, - selection: TypedSelection -): TypedSelection { - const { position } = target; - const originalSelection = selection.selection.selection; - let newSelection; - - switch (position) { - case "contents": - newSelection = originalSelection; - break; - - case "before": - newSelection = new Selection( - originalSelection.start, - originalSelection.start - ); - break; - - case "after": - newSelection = new Selection( - originalSelection.end, - originalSelection.end - ); - break; - } - - return { - selection: { - selection: newSelection, - editor: selection.selection.editor, - }, - selectionType: selection.selectionType, - selectionContext: selection.selectionContext, - insideOutsideType: target.insideOutsideType ?? null, - position, - }; -} diff --git a/src/processTargets/processSelectionType.ts b/src/processTargets/processSelectionType.ts deleted file mode 100644 index 2848cd8957..0000000000 --- a/src/processTargets/processSelectionType.ts +++ /dev/null @@ -1,429 +0,0 @@ -import { Position, Range, TextDocument } from "vscode"; -import { - selectionFromPositions, - selectionWithEditorFromPositions, - selectionWithEditorFromRange, -} from "../util/selectionUtils"; -import { - InsideOutsideType, - Modifier, - PrimitiveTarget, - ProcessedTargetsContext, - SelectionContext, - SelectionWithEditor, - TypedSelection, - Position as TargetPosition, -} from "../typings/Types"; -import { getDocumentRange } from "../util/range"; - -// taken from https://regexr.com/3e6m0 -const URL_REGEX = - /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g; - -export default function ( - context: ProcessedTargetsContext, - target: PrimitiveTarget, - selection: SelectionWithEditor, - selectionContext: SelectionContext -): TypedSelection { - switch (target.selectionType) { - case "token": - return processToken(target, selection, selectionContext); - case "notebookCell": - return processNotebookCell(target, selection, selectionContext); - case "document": - return processDocument(target, selection, selectionContext); - case "line": - return processLine(target, selection, selectionContext); - case "paragraph": - return processParagraph(target, selection, selectionContext); - case "nonWhitespaceSequence": - return processRegexDefinedScope( - /\S+/g, - target, - selection, - selectionContext - ); - case "url": - return processRegexDefinedScope( - URL_REGEX, - target, - selection, - selectionContext - ); - } -} - -function processNotebookCell( - target: PrimitiveTarget, - selection: SelectionWithEditor, - selectionContext: SelectionContext -): TypedSelection { - const { selectionType, insideOutsideType, position } = target; - return { - selection, - selectionType, - position, - insideOutsideType, - selectionContext: { ...selectionContext, isNotebookCell: true }, - }; -} - -function processToken( - target: PrimitiveTarget, - selection: SelectionWithEditor, - selectionContext: SelectionContext -) { - const { selectionType, insideOutsideType, position, modifier } = target; - return { - selection, - selectionType, - position, - insideOutsideType, - // NB: This is a hack to work around the fact that it's not currently - // possible to apply a modifier after processing the selection type. We - // would really prefer that the user be able to say "just" and have that be - // processed after we've processed the selection type, which would strip - // away the type information and turn it into a raw target. Until that's - // possible using the new pipelines, we instead just check for it here when - // we're doing the selection type and bail out if it is a raw target. - selectionContext: selectionContext.isRawSelection - ? selectionContext - : getTokenSelectionContext( - selection, - modifier, - position, - insideOutsideType, - selectionContext - ), - }; -} - -function processDocument( - target: PrimitiveTarget, - selection: SelectionWithEditor, - selectionContext: SelectionContext -) { - const { selectionType, insideOutsideType, position } = target; - const newSelection = selectionWithEditorFromRange( - selection, - getDocumentRange(selection.editor.document) - ); - - return { - selection: newSelection, - selectionType, - position, - insideOutsideType, - selectionContext, - }; -} - -function processLine( - target: PrimitiveTarget, - selection: SelectionWithEditor, - _selectionContext: SelectionContext -) { - const { selectionType, insideOutsideType, position } = target; - const { document } = selection.editor; - const startLine = document.lineAt(selection.selection.start); - const endLine = document.lineAt(selection.selection.end); - const start = new Position( - startLine.lineNumber, - startLine.firstNonWhitespaceCharacterIndex - ); - const end = endLine.range.end; - - const newSelection = selectionWithEditorFromPositions(selection, start, end); - - return { - selection: newSelection, - selectionType, - position, - insideOutsideType, - selectionContext: getLineSelectionContext(newSelection), - }; -} - -function processParagraph( - target: PrimitiveTarget, - selection: SelectionWithEditor, - _selectionContext: SelectionContext -) { - const { selectionType, insideOutsideType, position } = target; - const { document } = selection.editor; - let startLine = document.lineAt(selection.selection.start); - if (!startLine.isEmptyOrWhitespace) { - while (startLine.lineNumber > 0) { - const line = document.lineAt(startLine.lineNumber - 1); - if (line.isEmptyOrWhitespace) { - break; - } - startLine = line; - } - } - const lineCount = document.lineCount; - let endLine = document.lineAt(selection.selection.end); - if (!endLine.isEmptyOrWhitespace) { - while (endLine.lineNumber + 1 < lineCount) { - const line = document.lineAt(endLine.lineNumber + 1); - if (line.isEmptyOrWhitespace) { - break; - } - endLine = line; - } - } - - const start = new Position( - startLine.lineNumber, - startLine.firstNonWhitespaceCharacterIndex - ); - const end = endLine.range.end; - - const newSelection = selectionWithEditorFromPositions(selection, start, end); - - return { - selection: newSelection, - position, - selectionType, - insideOutsideType, - selectionContext: getParagraphSelectionContext(newSelection), - }; -} - -function processRegexDefinedScope( - regex: RegExp, - target: PrimitiveTarget, - selection: SelectionWithEditor, - selectionContext: SelectionContext -) { - const { selectionType, insideOutsideType, position, modifier } = target; - - const getMatch = (position: Position) => { - const line = selection.editor.document.lineAt(position); - const result = [...line.text.matchAll(regex)] - .map( - (match) => - new Range( - position.line, - match.index!, - position.line, - match.index! + match[0].length - ) - ) - .find((range) => range.contains(position)); - if (result == null) { - throw new Error(`Cannot find sequence defined by regex: ${regex}`); - } - return result; - }; - - const start = getMatch(selection.selection.start).start; - const end = getMatch(selection.selection.end).end; - const newSelection = selectionWithEditorFromPositions(selection, start, end); - - return { - selection: newSelection, - position, - selectionType, - insideOutsideType, - selectionContext: getTokenSelectionContext( - newSelection, - modifier, - position, - insideOutsideType, - selectionContext - ), - }; -} - -function getTokenSelectionContext( - selection: SelectionWithEditor, - modifier: Modifier, - position: TargetPosition, - insideOutsideType: InsideOutsideType, - selectionContext: SelectionContext -): SelectionContext { - if (!isSelectionContextEmpty(selectionContext)) { - return selectionContext; - } - if (modifier.type === "subpiece") { - return selectionContext; - } - - const document = selection.editor.document; - const { start, end } = selection.selection; - const endLine = document.lineAt(end); - let leadingDelimiterRange, trailingDelimiterRange; - - // Positions start/end of has no delimiters - if (position !== "before" || insideOutsideType !== "inside") { - const startLine = document.lineAt(start); - const leadingText = startLine.text.slice(0, start.character); - const leadingDelimiters = leadingText.match(/\s+$/); - leadingDelimiterRange = - leadingDelimiters != null - ? new Range( - start.line, - start.character - leadingDelimiters[0].length, - start.line, - start.character - ) - : null; - } - - if (position !== "after" || insideOutsideType !== "inside") { - const trailingText = endLine.text.slice(end.character); - const trailingDelimiters = trailingText.match(/^\s+/); - trailingDelimiterRange = - trailingDelimiters != null - ? new Range( - end.line, - end.character, - end.line, - end.character + trailingDelimiters[0].length - ) - : null; - } - - let isInDelimitedList; - if (position === "contents") { - isInDelimitedList = - (leadingDelimiterRange != null || trailingDelimiterRange != null) && - (leadingDelimiterRange != null || start.character === 0) && - (trailingDelimiterRange != null || end.isEqual(endLine.range.end)); - } else { - isInDelimitedList = - leadingDelimiterRange != null || trailingDelimiterRange != null; - } - - return { - ...selectionContext, - isInDelimitedList, - containingListDelimiter: " ", - leadingDelimiterRange: isInDelimitedList ? leadingDelimiterRange : null, - trailingDelimiterRange: isInDelimitedList ? trailingDelimiterRange : null, - }; -} - -// TODO Clean this up once we have rich targets and better polymorphic -// selection contexts that indicate their type -function isSelectionContextEmpty(selectionContext: SelectionContext) { - return ( - selectionContext.isInDelimitedList == null && - selectionContext.containingListDelimiter == null && - selectionContext.leadingDelimiterRange == null && - selectionContext.trailingDelimiterRange == null - ); -} - -function getLineSelectionContext( - selection: SelectionWithEditor -): SelectionContext { - const { document } = selection.editor; - const { start, end } = selection.selection; - const outerSelection = getOuterSelection(selection, start, end); - - const leadingDelimiterRange = - start.line > 0 - ? new Range( - document.lineAt(start.line - 1).range.end, - outerSelection.start - ) - : null; - const trailingDelimiterRange = - end.line + 1 < document.lineCount - ? new Range(outerSelection.end, new Position(end.line + 1, 0)) - : null; - const isInDelimitedList = - leadingDelimiterRange != null || trailingDelimiterRange != null; - - return { - isInDelimitedList, - containingListDelimiter: "\n", - leadingDelimiterRange, - trailingDelimiterRange, - outerSelection, - }; -} - -function getParagraphSelectionContext( - selection: SelectionWithEditor -): SelectionContext { - const { document } = selection.editor; - const { start, end } = selection.selection; - const outerSelection = getOuterSelection(selection, start, end); - const leadingLine = getPreviousNonEmptyLine(document, start.line); - const trailingLine = getNextNonEmptyLine(document, end.line); - - const leadingDelimiterStart = - leadingLine != null - ? leadingLine.range.end - : start.line > 0 - ? new Position(0, 0) - : null; - const trailingDelimiterEnd = - trailingLine != null - ? trailingLine.range.start - : end.line < document.lineCount - 1 - ? document.lineAt(document.lineCount - 1).range.end - : null; - const leadingDelimiterRange = - leadingDelimiterStart != null - ? new Range(leadingDelimiterStart, outerSelection.start) - : null; - const trailingDelimiterRange = - trailingDelimiterEnd != null - ? new Range(outerSelection.end, trailingDelimiterEnd) - : null; - const isInDelimitedList = - leadingDelimiterRange != null || trailingDelimiterRange != null; - - return { - isInDelimitedList, - containingListDelimiter: "\n\n", - leadingDelimiterRange, - trailingDelimiterRange, - outerSelection, - }; -} - -function getOuterSelection( - selection: SelectionWithEditor, - start: Position, - end: Position -) { - // Outer selection contains the entire lines - return selectionFromPositions( - selection.selection, - new Position(start.line, 0), - selection.editor.document.lineAt(end).range.end - ); -} - -function getPreviousNonEmptyLine( - document: TextDocument, - startLineNumber: number -) { - let line = document.lineAt(startLineNumber); - while (line.lineNumber > 0) { - const previousLine = document.lineAt(line.lineNumber - 1); - if (!previousLine.isEmptyOrWhitespace) { - return previousLine; - } - line = previousLine; - } - return null; -} - -function getNextNonEmptyLine(document: TextDocument, startLineNumber: number) { - let line = document.lineAt(startLineNumber); - while (line.lineNumber + 1 < document.lineCount) { - const nextLine = document.lineAt(line.lineNumber + 1); - if (!nextLine.isEmptyOrWhitespace) { - return nextLine; - } - line = nextLine; - } - return null; -} diff --git a/src/processTargets/processTargets.ts b/src/processTargets/processTargets.ts new file mode 100644 index 0000000000..922bd7371d --- /dev/null +++ b/src/processTargets/processTargets.ts @@ -0,0 +1,231 @@ +import { uniqWith, zip } from "lodash"; +import { Range } from "vscode"; +import { Target } from "../typings/target.types"; +import { + PrimitiveTargetDescriptor, + RangeTargetDescriptor, + TargetDescriptor, +} from "../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../typings/Types"; +import { ensureSingleEditor } from "../util/targetUtils"; +import getMarkStage from "./getMarkStage"; +import getModifierStage from "./getModifierStage"; +import PlainTarget from "./targets/PlainTarget"; +import PositionTarget from "./targets/PositionTarget"; + +/** + * Converts the abstract target descriptions provided by the user to a concrete + * representation usable by actions. Conceptually, the input will be something + * like "the function call argument containing the cursor" and the output will be something + * like "line 3, characters 5 through 10". + * @param context Captures the environment needed to convert the abstract target + * description given by the user to a concrete representation usable by + * actions + * @param targets The abstract target representations provided by the user + * @returns A list of lists of typed selections, one list per input target. Each + * typed selection includes the selection, as well the uri of the document + * containing it, and potentially rich context information such as how to remove + * the target + */ +export default function ( + context: ProcessedTargetsContext, + targets: TargetDescriptor[] +): Target[][] { + return targets.map((target) => uniqTargets(processTarget(context, target))); +} + +function processTarget( + context: ProcessedTargetsContext, + target: TargetDescriptor +): Target[] { + switch (target.type) { + case "list": + return target.elements.flatMap((element) => + processTarget(context, element) + ); + case "range": + return processRangeTarget(context, target); + case "primitive": + return processPrimitiveTarget(context, target); + } +} + +function processRangeTarget( + context: ProcessedTargetsContext, + targetDesc: RangeTargetDescriptor +): Target[] { + const anchorTargets = processPrimitiveTarget(context, targetDesc.anchor); + const activeTargets = processPrimitiveTarget(context, targetDesc.active); + + return zip(anchorTargets, activeTargets).flatMap( + ([anchorTarget, activeTarget]) => { + if (anchorTarget == null || activeTarget == null) { + throw new Error("anchorTargets and activeTargets lengths don't match"); + } + + if (anchorTarget.editor !== activeTarget.editor) { + throw new Error( + "anchorTarget and activeTarget must be in same document" + ); + } + + switch (targetDesc.rangeType) { + case "continuous": + return [ + processContinuousRangeTarget( + anchorTarget, + activeTarget, + targetDesc.excludeAnchor, + targetDesc.excludeActive + ), + ]; + case "vertical": + return processVerticalRangeTarget( + anchorTarget, + activeTarget, + targetDesc.excludeAnchor, + targetDesc.excludeActive + ); + } + } + ); +} + +function processContinuousRangeTarget( + anchorTarget: Target, + activeTarget: Target, + excludeAnchor: boolean, + excludeActive: boolean +): Target { + ensureSingleEditor([anchorTarget, activeTarget]); + const isReversed = calcIsReversed(anchorTarget, activeTarget); + const startTarget = isReversed ? activeTarget : anchorTarget; + const endTarget = isReversed ? anchorTarget : activeTarget; + const excludeStart = isReversed ? excludeActive : excludeAnchor; + const excludeEnd = isReversed ? excludeAnchor : excludeActive; + + return startTarget.createContinuousRangeTarget( + isReversed, + endTarget, + !excludeStart, + !excludeEnd + ); +} + +export function targetsToContinuousTarget( + anchorTarget: Target, + activeTarget: Target +): Target { + return processContinuousRangeTarget(anchorTarget, activeTarget, false, false); +} + +function processVerticalRangeTarget( + anchorTarget: Target, + activeTarget: Target, + excludeAnchor: boolean, + excludeActive: boolean +): Target[] { + const isReversed = calcIsReversed(anchorTarget, activeTarget); + const delta = isReversed ? -1 : 1; + + const anchorPosition = isReversed + ? anchorTarget.contentRange.start + : anchorTarget.contentRange.end; + const anchorLine = anchorPosition.line + (excludeAnchor ? delta : 0); + const activePosition = isReversed + ? activeTarget.contentRange.start + : activeTarget.contentRange.end; + const activeLine = activePosition.line - (excludeActive ? delta : 0); + + const results: Target[] = []; + for (let i = anchorLine; true; i += delta) { + const contentRange = new Range( + i, + anchorTarget.contentRange.start.character, + i, + anchorTarget.contentRange.end.character + ); + + if (anchorTarget instanceof PositionTarget) { + results.push(anchorTarget.withContentRange(contentRange)); + } else { + results.push( + new PlainTarget({ + editor: anchorTarget.editor, + isReversed: anchorTarget.isReversed, + contentRange, + }) + ); + } + + if (i === activeLine) { + return results; + } + } +} + +/** + * This function implements the modifier pipeline that is at the core of Cursorless target processing. + * It proceeds as follows: + * + * 1. It begins by getting the output from the {@link markStage} (eg "air", "this", etc). + * This output is a list of zero or more targets. + * 2. It then constructs a pipeline from the modifiers on the {@link targetDescriptor} + * 3. It then runs each pipeline stage in turn, feeding the first stage with + * the list of targets output from the {@link markStage}. For each pipeline + * stage, it passes the targets from the previous stage to the pipeline stage + * one by one. For each target, the stage will output a list of zero or more output + * targets. It then concatenates all of these lists into the list of targets + * that will be passed to the next pipeline stage. This process is similar to + * the way that [jq](https://stedolan.github.io/jq/) processes its inputs. + * + * @param context The context that captures the state of the environment used + * by each stage to process its input targets + * @param targetDescriptor The description of the target, consisting of a mark + * and zero or more modifiers + * @returns The output of running the modifier pipeline on the output from the mark + */ +function processPrimitiveTarget( + context: ProcessedTargetsContext, + targetDescriptor: PrimitiveTargetDescriptor +): Target[] { + // First, get the targets output by the mark + const markStage = getMarkStage(targetDescriptor.mark); + const markOutputTargets = markStage.run(context); + + /** + * The modifier pipeline that will be applied to construct our final targets + */ + const modifierStages = [ + // Reverse target modifiers because they are returned in reverse order from + // the api, to match the order in which they are spoken. + ...targetDescriptor.modifiers.map(getModifierStage).reverse(), + ...context.finalStages, + ]; + + /** + * Intermediate variable to store the output of the current pipeline stage. + * We initialise it to start with the outputs from the mark. + */ + let currentTargets = markOutputTargets; + + // Then we apply each stage in sequence, letting each stage see the targets + // one-by-one and concatenating the results before passing them on to the + // next stage. + modifierStages.forEach((stage) => { + currentTargets = currentTargets.flatMap((target) => + stage.run(context, target) + ); + }); + + // Then return the output from the final stage + return currentTargets; +} + +function calcIsReversed(anchor: Target, active: Target) { + return anchor.contentRange.start.isAfter(active.contentRange.start); +} + +function uniqTargets(array: Target[]): Target[] { + return uniqWith(array, (a, b) => a.isEqual(b)); +} diff --git a/src/processTargets/targetUtil/createContinuousRange.ts b/src/processTargets/targetUtil/createContinuousRange.ts new file mode 100644 index 0000000000..001282db93 --- /dev/null +++ b/src/processTargets/targetUtil/createContinuousRange.ts @@ -0,0 +1,87 @@ +import { Position, Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import WeakTarget from "../targets/WeakTarget"; + +export function createContinuousRange( + startTarget: Target, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean +) { + return createContinuousRangeFromRanges( + startTarget.contentRange, + endTarget.contentRange, + includeStart, + includeEnd + ); +} + +export function createContinuousRangeFromRanges( + startRange: Range, + endRange: Range, + includeStart: boolean, + includeEnd: boolean +) { + return new Range( + includeStart ? startRange.start : startRange.end, + includeEnd ? endRange.end : endRange.start + ); +} + +export function createContinuousLineRange( + startTarget: Target, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean +) { + const start = includeStart + ? startTarget.contentRange.start + : new Position(startTarget.contentRange.end.line + 1, 0); + + const end = includeEnd + ? endTarget.contentRange.end + : endTarget.editor.document.lineAt(endTarget.contentRange.start.line - 1) + .range.end; + + return new Range(start, end); +} + +export function createSimpleContinuousRangeTarget( + target1: Target, + target2: Target, + isReversed: boolean, + includeStart: boolean = true, + includeEnd: boolean = true +) { + const isForward = target1.contentRange.start.isBefore( + target2.contentRange.start + ); + const anchorTarget = isForward ? target1 : target2; + const activeTarget = isForward ? target2 : target1; + + return anchorTarget.createContinuousRangeTarget( + isReversed, + activeTarget, + includeStart, + includeEnd + ); +} + +export function createContinuousRangeWeakTarget( + isReversed: boolean, + startTarget: Target, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean +): WeakTarget { + return new WeakTarget({ + editor: startTarget.editor, + isReversed, + contentRange: createContinuousRange( + startTarget, + endTarget, + includeStart, + includeEnd + ), + }); +} diff --git a/src/processTargets/targetUtil/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts b/src/processTargets/targetUtil/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts new file mode 100644 index 0000000000..b26b7d2c4f --- /dev/null +++ b/src/processTargets/targetUtil/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts @@ -0,0 +1,24 @@ +import { Range } from "vscode"; +import { Target } from "../../../typings/target.types"; + +/** + * Constructs a removal range for the given target that includes either the + * trailing or leading delimiter + * @param target The target to get the removal range for + * @param contentRange Can be used to override the content range instead of + * using the one on the target + * @returns The removal range for the given target + */ +export function getDelimitedSequenceRemovalRange( + target: Target, + contentRange?: Range +): Range { + contentRange = contentRange ?? target.contentRange; + + const delimiterTarget = + target.getTrailingDelimiterTarget() ?? target.getLeadingDelimiterTarget(); + + return delimiterTarget != null + ? contentRange.union(delimiterTarget.contentRange) + : contentRange; +} diff --git a/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts b/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts new file mode 100644 index 0000000000..253aaf566a --- /dev/null +++ b/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts @@ -0,0 +1,89 @@ +import { Range } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { isAtEndOfLine, isAtStartOfLine } from "../../../util/rangeUtils"; +import PlainTarget from "../../targets/PlainTarget"; +import { getDelimitedSequenceRemovalRange } from "./DelimitedSequenceInsertionRemovalBehavior"; + +export function getTokenLeadingDelimiterTarget( + target: Target +): Target | undefined { + const { editor } = target; + const { start } = target.contentRange; + + const startLine = editor.document.lineAt(start); + const leadingText = startLine.text.slice(0, start.character); + const leadingDelimiters = leadingText.match(/\s+$/); + + return leadingDelimiters == null + ? undefined + : new PlainTarget({ + contentRange: new Range( + start.line, + start.character - leadingDelimiters[0].length, + start.line, + start.character + ), + editor, + isReversed: target.isReversed, + }); +} + +export function getTokenTrailingDelimiterTarget( + target: Target +): Target | undefined { + const { editor } = target; + const { end } = target.contentRange; + + const endLine = editor.document.lineAt(end); + const trailingText = endLine.text.slice(end.character); + const trailingDelimiters = trailingText.match(/^\s+/); + + return trailingDelimiters == null + ? undefined + : new PlainTarget({ + contentRange: new Range( + end.line, + end.character, + end.line, + end.character + trailingDelimiters[0].length + ), + editor, + isReversed: target.isReversed, + }); +} + +/** + * Constructs a removal range for the given target that will clean up a json + * whitespace on one side unless it will cause two tokens to be merged. This + * removal range is designed to be used with things that should clean themselves + * up as if they're a range of tokens. + * @param target The target to get the token removal range for + * @param contentRange Can be used to override the content range instead of + * using the one on the target + * @returns The removal range for the given target + */ +export function getTokenRemovalRange( + target: Target, + contentRange?: Range +): Range { + const { start, end } = contentRange ?? target.contentRange; + + const leadingDelimiterTarget = getTokenLeadingDelimiterTarget(target); + const trailingDelimiterTarget = getTokenTrailingDelimiterTarget(target); + + // If there is a token directly to the left or right of us with no + // separating white space, then we might join two tokens if we try to clean + // up whitespace space. In this case we just remove the content range + // without attempting to clean up white space. + // + // In the future, we might get more sophisticated and to clean up white space if we can detect that it won't cause two tokens be merged + if ( + (leadingDelimiterTarget == null && !isAtStartOfLine(start)) || + (trailingDelimiterTarget == null && !isAtEndOfLine(target.editor, end)) + ) { + return contentRange ?? target.contentRange; + } + + // Otherwise, behave like a whitespace delimited sequence + return getDelimitedSequenceRemovalRange(target, contentRange); +} diff --git a/src/processTargets/targetUtil/insertionRemovalBehaviors/insertionRemovalBehavior.types.ts b/src/processTargets/targetUtil/insertionRemovalBehaviors/insertionRemovalBehavior.types.ts new file mode 100644 index 0000000000..5a4c89d1eb --- /dev/null +++ b/src/processTargets/targetUtil/insertionRemovalBehaviors/insertionRemovalBehavior.types.ts @@ -0,0 +1,9 @@ +import { Range } from "vscode"; +import { Target } from "../../../typings/target.types"; + +export default interface InsertionRemovalBehavior { + getLeadingDelimiterTarget(): Target | undefined; + getTrailingDelimiterTarget(): Target | undefined; + getRemovalRange(): Range; + insertionDelimiter: string | undefined; +} diff --git a/src/processTargets/targets/BaseTarget.ts b/src/processTargets/targets/BaseTarget.ts new file mode 100644 index 0000000000..3c1a7a1fcd --- /dev/null +++ b/src/processTargets/targets/BaseTarget.ts @@ -0,0 +1,164 @@ +import { isEqual } from "lodash"; +import { Range, Selection, TextEditor } from "vscode"; +import { EditNewContext, Target } from "../../typings/target.types"; +import { EditWithRangeUpdater } from "../../typings/Types"; +import { selectionFromRange } from "../../util/selectionUtils"; +import { isSameType } from "../../util/typeUtils"; +import { + createContinuousRange, + createContinuousRangeWeakTarget, +} from "../targetUtil/createContinuousRange"; + +/** Parameters supported by all target classes */ +export interface CommonTargetParameters { + readonly editor: TextEditor; + readonly isReversed: boolean; + readonly contentRange: Range; + readonly thatTarget?: Target; +} + +export interface CloneWithParameters { + readonly thatTarget?: Target; + readonly contentRange?: Range; +} + +export default abstract class BaseTarget implements Target { + protected readonly state: CommonTargetParameters; + isLine = false; + isWeak = false; + isRaw = false; + isNotebookCell = false; + + constructor(parameters: CommonTargetParameters) { + this.state = { + editor: parameters.editor, + isReversed: parameters.isReversed, + contentRange: parameters.contentRange, + thatTarget: parameters.thatTarget, + }; + } + + get editor() { + return this.state.editor; + } + get isReversed() { + return this.state.isReversed; + } + + get thatTarget(): Target { + return this.state.thatTarget != null + ? this.state.thatTarget.thatTarget + : this; + } + + get contentText(): string { + return this.editor.document.getText(this.contentRange); + } + + get contentSelection(): Selection { + return selectionFromRange(this.isReversed, this.contentRange); + } + + get contentRange(): Range { + return this.state.contentRange; + } + + constructChangeEdit(text: string): EditWithRangeUpdater { + return { + range: this.contentRange, + text, + updateRange: (range) => range, + }; + } + + constructRemovalEdit(): EditWithRangeUpdater { + return { + range: this.getRemovalRange(), + text: "", + updateRange: (range) => range, + }; + } + + getEditNewContext(isBefore: boolean): EditNewContext { + const delimiter = this.insertionDelimiter ?? ""; + if (delimiter === "\n" && !isBefore) { + return { type: "command", command: "editor.action.insertLineAfter" }; + } + return { + type: "delimiter", + delimiter, + }; + } + + getRemovalHighlightRange(): Range | undefined { + return this.getRemovalRange(); + } + + withThatTarget(thatTarget: Target): Target { + return this.cloneWith({ thatTarget }); + } + + withContentRange(contentRange: Range): Target { + return this.cloneWith({ contentRange }); + } + + getInteriorStrict(): Target[] { + throw Error("No available interior"); + } + getBoundaryStrict(): Target[] { + throw Error("No available boundaries"); + } + + readonly cloneWith = (parameters: CloneWithParameters) => { + const constructor = Object.getPrototypeOf(this).constructor; + + return new constructor({ + ...this.getCloneParameters(), + ...parameters, + }); + }; + + protected abstract getCloneParameters(): object; + + createContinuousRangeTarget( + isReversed: boolean, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean + ): Target { + if (isSameType(this, endTarget)) { + const constructor = Object.getPrototypeOf(this).constructor; + + return new constructor({ + ...this.getCloneParameters(), + isReversed, + contentRange: createContinuousRange( + this, + endTarget, + includeStart, + includeEnd + ), + }); + } + + return createContinuousRangeWeakTarget( + isReversed, + this, + endTarget, + includeStart, + includeEnd + ); + } + + isEqual(target: Target): boolean { + return ( + target instanceof BaseTarget && + isEqual(this.getCloneParameters(), target.getCloneParameters()) + ); + } + + abstract get insertionDelimiter(): string; + abstract getLeadingDelimiterTarget(): Target | undefined; + abstract getTrailingDelimiterTarget(): Target | undefined; + abstract getRemovalRange(): Range; +} diff --git a/src/processTargets/targets/DocumentTarget.ts b/src/processTargets/targets/DocumentTarget.ts new file mode 100644 index 0000000000..7df946566c --- /dev/null +++ b/src/processTargets/targets/DocumentTarget.ts @@ -0,0 +1,65 @@ +import { Range, TextEditor } from "vscode"; +import { fitRangeToLineContent } from "../modifiers/scopeTypeStages/LineStage"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; +import WeakTarget from "./WeakTarget"; + +export default class DocumentTarget extends BaseTarget { + insertionDelimiter = "\n"; + isLine = true; + + constructor(parameters: CommonTargetParameters) { + super(parameters); + } + + getLeadingDelimiterTarget() { + return undefined; + } + getTrailingDelimiterTarget() { + return undefined; + } + getRemovalRange(): Range { + return this.contentRange; + } + + getInteriorStrict() { + return [ + new WeakTarget({ + editor: this.editor, + isReversed: this.isReversed, + contentRange: getDocumentContentRange(this.editor), + }), + ]; + } + + protected getCloneParameters() { + return this.state; + } +} + +function getDocumentContentRange(editor: TextEditor) { + const { document } = editor; + let firstLineNum = 0; + let lastLineNum = document.lineCount - 1; + + for (let i = firstLineNum; i < document.lineCount; ++i) { + if (!document.lineAt(i).isEmptyOrWhitespace) { + firstLineNum = i; + break; + } + } + + for (let i = lastLineNum; i > -1; --i) { + if (!document.lineAt(i).isEmptyOrWhitespace) { + lastLineNum = i; + break; + } + } + + const firstLine = document.lineAt(firstLineNum); + const lastLine = document.lineAt(lastLineNum); + + return fitRangeToLineContent( + editor, + new Range(firstLine.range.start, lastLine.range.end) + ); +} diff --git a/src/processTargets/targets/LineTarget.ts b/src/processTargets/targets/LineTarget.ts new file mode 100644 index 0000000000..4b074cf0ec --- /dev/null +++ b/src/processTargets/targets/LineTarget.ts @@ -0,0 +1,88 @@ +import { Position, Range, TextEditor } from "vscode"; +import { Target } from "../../typings/target.types"; +import { expandToFullLine } from "../../util/rangeUtils"; +import { tryConstructPlainTarget } from "../../util/tryConstructTarget"; +import { createContinuousLineRange } from "../targetUtil/createContinuousRange"; +import BaseTarget from "./BaseTarget"; + +export default class LineTarget extends BaseTarget { + insertionDelimiter = "\n"; + isLine = true; + + private get fullLineContentRange() { + return expandToFullLine(this.editor, this.contentRange); + } + + getLeadingDelimiterTarget() { + return tryConstructPlainTarget( + this.editor, + getLeadingDelimiterRange(this.editor, this.fullLineContentRange), + this.isReversed + ); + } + + getTrailingDelimiterTarget() { + return tryConstructPlainTarget( + this.editor, + getTrailingDelimiterRange(this.editor, this.fullLineContentRange), + this.isReversed + ); + } + + getRemovalRange() { + const contentRemovalRange = this.fullLineContentRange; + const delimiterTarget = + this.getTrailingDelimiterTarget() ?? this.getLeadingDelimiterTarget(); + + return delimiterTarget == null + ? contentRemovalRange + : contentRemovalRange.union(delimiterTarget.contentRange); + } + + getRemovalHighlightRange = () => this.fullLineContentRange; + + createContinuousRangeTarget( + isReversed: boolean, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean + ): Target { + if (endTarget.isLine) { + return new LineTarget({ + editor: this.editor, + isReversed, + contentRange: createContinuousLineRange( + this, + endTarget, + includeStart, + includeEnd + ), + }); + } + + return super.createContinuousRangeTarget( + isReversed, + endTarget, + includeStart, + includeEnd + ); + } + + protected getCloneParameters() { + return this.state; + } +} + +function getLeadingDelimiterRange(editor: TextEditor, range: Range) { + const { start } = range; + return start.line > 0 + ? new Range(editor.document.lineAt(start.line - 1).range.end, range.start) + : undefined; +} + +function getTrailingDelimiterRange(editor: TextEditor, range: Range) { + const { end } = range; + return end.line + 1 < editor.document.lineCount + ? new Range(range.end, new Position(end.line + 1, 0)) + : undefined; +} diff --git a/src/processTargets/targets/NotebookCellTarget.ts b/src/processTargets/targets/NotebookCellTarget.ts new file mode 100644 index 0000000000..8f32615c07 --- /dev/null +++ b/src/processTargets/targets/NotebookCellTarget.ts @@ -0,0 +1,33 @@ +import { TextEditor } from "vscode"; +import { getNotebookFromCellDocument } from "../../util/notebook"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; + +export default class NotebookCellTarget extends BaseTarget { + insertionDelimiter = "\n"; + isNotebookCell = true; + + constructor(parameters: CommonTargetParameters) { + super(parameters); + } + + getLeadingDelimiterTarget = () => undefined; + getTrailingDelimiterTarget = () => undefined; + getRemovalRange = () => this.contentRange; + + getEditNewCommand(isBefore: boolean): string { + if (this.isNotebookEditor(this.editor)) { + return isBefore + ? "notebook.cell.insertCodeCellAbove" + : "notebook.cell.insertCodeCellBelow"; + } + return isBefore ? "jupyter.insertCellAbove" : "jupyter.insertCellBelow"; + } + + protected getCloneParameters() { + return this.state; + } + + private isNotebookEditor(editor: TextEditor) { + return getNotebookFromCellDocument(editor.document) != null; + } +} diff --git a/src/processTargets/targets/ParagraphTarget.ts b/src/processTargets/targets/ParagraphTarget.ts new file mode 100644 index 0000000000..89e098f2c4 --- /dev/null +++ b/src/processTargets/targets/ParagraphTarget.ts @@ -0,0 +1,184 @@ +import { Position, Range, TextDocument, TextEditor, TextLine } from "vscode"; +import { Target } from "../../typings/target.types"; +import { expandToFullLine } from "../../util/rangeUtils"; +import { isSameType } from "../../util/typeUtils"; +import { constructLineTarget } from "../../util/tryConstructTarget"; +import { createContinuousLineRange } from "../targetUtil/createContinuousRange"; +import BaseTarget from "./BaseTarget"; +import LineTarget from "./LineTarget"; + +export default class ParagraphTarget extends BaseTarget { + insertionDelimiter = "\n\n"; + isLine = true; + + getLeadingDelimiterTarget() { + return constructLineTarget( + this.editor, + getLeadingDelimiterRange(this.editor, this.fullLineContentRange), + this.isReversed + ); + } + + getTrailingDelimiterTarget() { + return constructLineTarget( + this.editor, + getTrailingDelimiterRange(this.editor, this.fullLineContentRange), + this.isReversed + ); + } + + getRemovalRange(): Range { + // TODO: In the future we could get rid of this function if {@link + // getDelimitedSequenceRemovalRange} made a continuous range from the target + // past its delimiter target and then used the removal range of that. + const delimiterTarget = + this.getTrailingDelimiterTarget() ?? this.getLeadingDelimiterTarget(); + + const removalContentRange = + delimiterTarget != null + ? this.contentRange.union(delimiterTarget.contentRange) + : this.contentRange; + + // If there is a delimiter, it will be a line target, so we join it with + // ourself to create a line target containing ourself and the delimiter + // line. We then allow the line target removal range code to cleanup any + // extra leading or trailing newline + // + // If there is no delimiter, we just use the line content range, + // converting it to a line target so that it cleans up leading or trailing + // newline as necessary + return new LineTarget({ + contentRange: removalContentRange, + editor: this.editor, + isReversed: this.isReversed, + }).getRemovalRange(); + } + + private get fullLineContentRange() { + return expandToFullLine(this.editor, this.contentRange); + } + + getRemovalHighlightRange() { + const delimiterTarget = + this.getTrailingDelimiterTarget() ?? this.getLeadingDelimiterTarget(); + + return delimiterTarget != null + ? this.fullLineContentRange.union(delimiterTarget.contentRange) + : this.fullLineContentRange; + } + + createContinuousRangeTarget( + isReversed: boolean, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean + ): Target { + if (isSameType(this, endTarget)) { + return new ParagraphTarget({ + ...this.getCloneParameters(), + isReversed, + contentRange: createContinuousLineRange( + this, + endTarget, + includeStart, + includeEnd + ), + }); + } + + if (endTarget.isLine) { + return new LineTarget({ + editor: this.editor, + isReversed, + contentRange: createContinuousLineRange( + this, + endTarget, + includeStart, + includeEnd + ), + }); + } + + return super.createContinuousRangeTarget( + isReversed, + endTarget, + includeStart, + includeEnd + ); + } + + protected getCloneParameters() { + return this.state; + } +} + +function getLeadingDelimiterRange(editor: TextEditor, contentRange: Range) { + const { document } = editor; + const { lineAt } = document; + const startLine = lineAt(contentRange.start); + const leadingLine = getPreviousNonEmptyLine(document, startLine); + // Lines are next to each other so they can be no delimiter range + if (leadingLine != null) { + if (leadingLine.lineNumber + 1 === startLine.lineNumber) { + return undefined; + } + return new Range( + new Position(leadingLine.lineNumber + 1, 0), + lineAt(startLine.lineNumber - 1).range.end + ); + } + // Leading delimiter to start of file + if (startLine.lineNumber > 0) { + return new Range( + new Position(0, 0), + lineAt(startLine.lineNumber - 1).range.end + ); + } + return undefined; +} + +function getTrailingDelimiterRange(editor: TextEditor, contentRange: Range) { + const { document } = editor; + const { lineAt } = document; + const endLine = lineAt(contentRange.end); + const trailingLine = getNextNonEmptyLine(document, endLine); + if (trailingLine != null) { + if (trailingLine.lineNumber - 1 === endLine.lineNumber) { + return undefined; + } + return new Range( + new Position(endLine.lineNumber + 1, 0), + lineAt(trailingLine.lineNumber - 1).range.end + ); + } + // Trailing delimiter to end of file + if (endLine.lineNumber < document.lineCount - 1) { + return new Range( + new Position(endLine.lineNumber + 1, 0), + lineAt(document.lineCount - 1).range.end + ); + } + return undefined; +} + +function getPreviousNonEmptyLine(document: TextDocument, line: TextLine) { + while (line.lineNumber > 0) { + const previousLine = document.lineAt(line.lineNumber - 1); + if (!previousLine.isEmptyOrWhitespace) { + return previousLine; + } + line = previousLine; + } + return null; +} + +function getNextNonEmptyLine(document: TextDocument, line: TextLine) { + while (line.lineNumber + 1 < document.lineCount) { + const nextLine = document.lineAt(line.lineNumber + 1); + if (!nextLine.isEmptyOrWhitespace) { + return nextLine; + } + line = nextLine; + } + return null; +} diff --git a/src/processTargets/targets/PlainTarget.ts b/src/processTargets/targets/PlainTarget.ts new file mode 100644 index 0000000000..ffb542846b --- /dev/null +++ b/src/processTargets/targets/PlainTarget.ts @@ -0,0 +1,15 @@ +import BaseTarget from "./BaseTarget"; + +/** + * A target that has no leading or trailing delimiters so it's removal range + * just consists of the content itself. Its insertion delimiter is empty string. + */ +export default class PlainTarget extends BaseTarget { + insertionDelimiter = ""; + + getLeadingDelimiterTarget = () => undefined; + getTrailingDelimiterTarget = () => undefined; + getRemovalRange = () => this.contentRange; + + protected getCloneParameters = () => this.state; +} diff --git a/src/processTargets/targets/PositionTarget.ts b/src/processTargets/targets/PositionTarget.ts new file mode 100644 index 0000000000..826f939cee --- /dev/null +++ b/src/processTargets/targets/PositionTarget.ts @@ -0,0 +1,142 @@ +import * as vscode from "vscode"; +import { Range, TextEditor } from "vscode"; +import { UnsupportedError } from "../../errors"; +import { Position } from "../../typings/targetDescriptor.types"; +import { EditWithRangeUpdater } from "../../typings/Types"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; + +interface PositionTargetParameters extends CommonTargetParameters { + readonly position: Position; + readonly insertionDelimiter: string; + readonly isRaw: boolean; +} + +export default class PositionTarget extends BaseTarget { + insertionDelimiter: string; + isRaw: boolean; + private position: Position; + + constructor(parameters: PositionTargetParameters) { + super(parameters); + this.position = parameters.position; + this.insertionDelimiter = parameters.insertionDelimiter; + this.isRaw = parameters.isRaw; + } + + getLeadingDelimiterTarget = () => undefined; + getTrailingDelimiterTarget = () => undefined; + + getRemovalRange(): Range { + const preferredModifier = + this.position === "after" || this.position === "end" + ? "trailing" + : "leading"; + + throw new UnsupportedError( + `Please use "${preferredModifier}" modifier; removal is not supported for "${this.position}"` + ); + } + + protected getCloneParameters(): PositionTargetParameters { + return { + ...this.state, + position: this.position, + insertionDelimiter: this.insertionDelimiter, + isRaw: this.isRaw, + }; + } + + constructChangeEdit(text: string): EditWithRangeUpdater { + return this.position === "before" || this.position === "after" + ? this.constructEditWithDelimiters(text, true) + : this.constructEditWithoutDelimiters(text); + } + + private constructEditWithDelimiters( + text: string, + useLinePadding: boolean + ): EditWithRangeUpdater { + const delimiter = this.insertionDelimiter; + const isLine = delimiter.includes("\n"); + const isBefore = this.position === "before"; + + const range = getEditRange( + this.editor, + this.contentRange, + isLine, + useLinePadding, + isBefore + ); + const padding = + isLine && useLinePadding + ? getLinePadding(this.editor, range, isBefore) + : ""; + + const editText = isBefore + ? text + delimiter + padding + : delimiter + padding + text; + + const updateRange = (range: Range) => { + const startOffset = this.editor.document.offsetAt(range.start); + const startIndex = isBefore + ? startOffset + : startOffset + delimiter.length + padding.length; + const endIndex = startIndex + text.length; + return new Range( + this.editor.document.positionAt(startIndex), + this.editor.document.positionAt(endIndex) + ); + }; + + return { + range: range, + text: editText, + isReplace: this.position === "after", + updateRange, + }; + } + + private constructEditWithoutDelimiters(text: string): EditWithRangeUpdater { + return { + range: this.contentRange, + text, + updateRange: (range) => range, + }; + } +} + +function getLinePadding(editor: TextEditor, range: Range, isBefore: boolean) { + const line = editor.document.lineAt(isBefore ? range.start : range.end); + const characterIndex = line.isEmptyOrWhitespace + ? range.start.character + : line.firstNonWhitespaceCharacterIndex; + return line.text.slice(0, characterIndex); +} + +function getEditRange( + editor: TextEditor, + range: Range, + isLine: boolean, + useLinePadding: boolean, + isBefore: boolean +) { + let position: vscode.Position; + if (isLine) { + const line = editor.document.lineAt(isBefore ? range.start : range.end); + if (isBefore) { + position = useLinePadding + ? line.isEmptyOrWhitespace + ? range.start + : new vscode.Position( + line.lineNumber, + line.firstNonWhitespaceCharacterIndex + ) + : line.range.start; + } else { + position = line.range.end; + } + } else { + position = isBefore ? range.start : range.end; + } + return new Range(position, position); +} diff --git a/src/processTargets/targets/RawSelectionTarget.ts b/src/processTargets/targets/RawSelectionTarget.ts new file mode 100644 index 0000000000..8a2c2cfd9d --- /dev/null +++ b/src/processTargets/targets/RawSelectionTarget.ts @@ -0,0 +1,17 @@ +import BaseTarget from "./BaseTarget"; + +/** + * A target that has no leading or trailing delimiters so it's removal range + * just consists of the content itself. Its insertion delimiter will be + * inherited from the source in the case of a bring after a bring before + */ +export default class RawSelectionTarget extends BaseTarget { + insertionDelimiter = ""; + isRaw = true; + + getLeadingDelimiterTarget = () => undefined; + getTrailingDelimiterTarget = () => undefined; + getRemovalRange = () => this.contentRange; + + protected getCloneParameters = () => this.state; +} diff --git a/src/processTargets/targets/ScopeTypeTarget.ts b/src/processTargets/targets/ScopeTypeTarget.ts new file mode 100644 index 0000000000..c41b976513 --- /dev/null +++ b/src/processTargets/targets/ScopeTypeTarget.ts @@ -0,0 +1,148 @@ +import { Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import { SimpleScopeTypeType } from "../../typings/targetDescriptor.types"; +import { isSameType } from "../../util/typeUtils"; +import { + createContinuousRange, + createContinuousRangeFromRanges, +} from "../targetUtil/createContinuousRange"; +import { getDelimitedSequenceRemovalRange } from "../targetUtil/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior"; +import { + getTokenLeadingDelimiterTarget, + getTokenRemovalRange, + getTokenTrailingDelimiterTarget, +} from "../targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; +import PlainTarget from "./PlainTarget"; + +export interface ScopeTypeTargetParameters extends CommonTargetParameters { + readonly scopeTypeType: SimpleScopeTypeType; + readonly delimiter?: string; + readonly removalRange?: Range; + readonly leadingDelimiterRange?: Range; + readonly trailingDelimiterRange?: Range; +} + +export default class ScopeTypeTarget extends BaseTarget { + private scopeTypeType_: SimpleScopeTypeType; + private removalRange_?: Range; + private leadingDelimiterRange_?: Range; + private trailingDelimiterRange_?: Range; + private hasDelimiterRange_: boolean; + insertionDelimiter: string; + + constructor(parameters: ScopeTypeTargetParameters) { + super(parameters); + this.scopeTypeType_ = parameters.scopeTypeType; + this.removalRange_ = parameters.removalRange; + this.leadingDelimiterRange_ = parameters.leadingDelimiterRange; + this.trailingDelimiterRange_ = parameters.trailingDelimiterRange; + this.insertionDelimiter = + parameters.delimiter ?? getDelimiter(parameters.scopeTypeType); + this.hasDelimiterRange_ = + !!this.leadingDelimiterRange_ || !!this.trailingDelimiterRange_; + } + + getLeadingDelimiterTarget(): Target | undefined { + if (this.leadingDelimiterRange_ != null) { + return new PlainTarget({ + editor: this.editor, + isReversed: this.isReversed, + contentRange: this.leadingDelimiterRange_, + }); + } + if (!this.hasDelimiterRange_) { + return getTokenLeadingDelimiterTarget(this); + } + return undefined; + } + + getTrailingDelimiterTarget(): Target | undefined { + if (this.trailingDelimiterRange_ != null) { + return new PlainTarget({ + editor: this.editor, + isReversed: this.isReversed, + contentRange: this.trailingDelimiterRange_, + }); + } + if (!this.hasDelimiterRange_) { + return getTokenTrailingDelimiterTarget(this); + } + return undefined; + } + + getRemovalRange(): Range { + return this.removalRange_ != null + ? getTokenRemovalRange(this, this.removalRange_) + : this.hasDelimiterRange_ + ? getDelimitedSequenceRemovalRange(this) + : getTokenRemovalRange(this); + } + + createContinuousRangeTarget( + isReversed: boolean, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean + ): Target { + if (isSameType(this, endTarget)) { + const scopeTarget = endTarget; + if (this.scopeTypeType_ === scopeTarget.scopeTypeType_) { + const contentRemovalRange = + this.removalRange_ != null || scopeTarget.removalRange_ != null + ? createContinuousRangeFromRanges( + this.removalRange_ ?? this.contentRange, + scopeTarget.removalRange_ ?? scopeTarget.contentRange, + includeStart, + includeEnd + ) + : undefined; + + return new ScopeTypeTarget({ + ...this.getCloneParameters(), + isReversed, + leadingDelimiterRange: this.leadingDelimiterRange_, + trailingDelimiterRange: scopeTarget.trailingDelimiterRange_, + removalRange: contentRemovalRange, + contentRange: createContinuousRange( + this, + endTarget, + includeStart, + includeEnd + ), + }); + } + } + + return super.createContinuousRangeTarget( + isReversed, + endTarget, + includeStart, + includeEnd + ); + } + + protected getCloneParameters() { + return { + ...this.state, + scopeTypeType: this.scopeTypeType_, + contentRemovalRange: this.removalRange_, + leadingDelimiterRange: this.leadingDelimiterRange_, + trailingDelimiterRange: this.trailingDelimiterRange_, + }; + } +} + +function getDelimiter(scopeType: SimpleScopeTypeType): string { + switch (scopeType) { + case "anonymousFunction": + case "statement": + case "ifStatement": + return "\n"; + case "class": + case "namedFunction": + return "\n\n"; + default: + return " "; + } +} diff --git a/src/processTargets/targets/SubTokenWordTarget.ts b/src/processTargets/targets/SubTokenWordTarget.ts new file mode 100644 index 0000000000..9fa37354b2 --- /dev/null +++ b/src/processTargets/targets/SubTokenWordTarget.ts @@ -0,0 +1,51 @@ +import { Range } from "vscode"; +import { tryConstructPlainTarget as tryConstructPlainTarget } from "../../util/tryConstructTarget"; +import { getDelimitedSequenceRemovalRange } from "../targetUtil/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; + +export interface SubTokenTargetParameters extends CommonTargetParameters { + readonly insertionDelimiter: string; + readonly leadingDelimiterRange?: Range; + readonly trailingDelimiterRange?: Range; +} + +export default class SubTokenWordTarget extends BaseTarget { + private leadingDelimiterRange_?: Range; + private trailingDelimiterRange_?: Range; + insertionDelimiter: string; + + constructor(parameters: SubTokenTargetParameters) { + super(parameters); + this.leadingDelimiterRange_ = parameters.leadingDelimiterRange; + this.trailingDelimiterRange_ = parameters.trailingDelimiterRange; + this.insertionDelimiter = parameters.insertionDelimiter; + } + + getLeadingDelimiterTarget() { + return tryConstructPlainTarget( + this.editor, + this.leadingDelimiterRange_, + this.isReversed + ); + } + + getTrailingDelimiterTarget() { + return tryConstructPlainTarget( + this.editor, + this.trailingDelimiterRange_, + this.isReversed + ); + } + + getRemovalRange(): Range { + return getDelimitedSequenceRemovalRange(this); + } + + protected getCloneParameters() { + return { + ...this.state, + leadingDelimiterRange: this.leadingDelimiterRange_, + trailingDelimiterRange: this.trailingDelimiterRange_, + }; + } +} diff --git a/src/processTargets/targets/SurroundingPairTarget.ts b/src/processTargets/targets/SurroundingPairTarget.ts new file mode 100644 index 0000000000..c3bc151d74 --- /dev/null +++ b/src/processTargets/targets/SurroundingPairTarget.ts @@ -0,0 +1,77 @@ +import { Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import { + getTokenLeadingDelimiterTarget, + getTokenRemovalRange, + getTokenTrailingDelimiterTarget, +} from "../targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; +import TokenTarget from "./TokenTarget"; +import WeakTarget from "./WeakTarget"; + +interface SurroundingPairTargetParameters extends CommonTargetParameters { + /** + * Represents the interior range of this selection. For example, for a + * surrounding pair this would exclude the opening and closing delimiter. For an if + * statement this would be the statements in the body. + */ + readonly interiorRange: Range; + + /** + * Represents the boundary ranges of this selection. For example, for a + * surrounding pair this would be the opening and closing delimiter. For an if + * statement this would be the line of the guard as well as the closing brace. + */ + readonly boundary: [Range, Range]; +} + +export default class SurroundingPairTarget extends BaseTarget { + insertionDelimiter = " "; + private interiorRange_: Range; + private boundary_: [Range, Range]; + + constructor(parameters: SurroundingPairTargetParameters) { + super(parameters); + this.boundary_ = parameters.boundary; + this.interiorRange_ = parameters.interiorRange; + } + + getLeadingDelimiterTarget(): Target | undefined { + return getTokenLeadingDelimiterTarget(this); + } + getTrailingDelimiterTarget(): Target | undefined { + return getTokenTrailingDelimiterTarget(this); + } + getRemovalRange(): Range { + return getTokenRemovalRange(this); + } + + getInteriorStrict() { + return [ + new WeakTarget({ + editor: this.editor, + isReversed: this.isReversed, + contentRange: this.interiorRange_, + }), + ]; + } + + getBoundaryStrict() { + return this.boundary_.map( + (contentRange) => + new TokenTarget({ + editor: this.editor, + isReversed: this.isReversed, + contentRange, + }) + ); + } + + protected getCloneParameters() { + return { + ...this.state, + interiorRange: this.interiorRange_, + boundary: this.boundary_, + }; + } +} diff --git a/src/processTargets/targets/TokenTarget.ts b/src/processTargets/targets/TokenTarget.ts new file mode 100644 index 0000000000..c19c77f175 --- /dev/null +++ b/src/processTargets/targets/TokenTarget.ts @@ -0,0 +1,26 @@ +import { Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import { + getTokenLeadingDelimiterTarget, + getTokenRemovalRange, + getTokenTrailingDelimiterTarget, +} from "../targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior"; +import BaseTarget from "./BaseTarget"; + +export default class TokenTarget extends BaseTarget { + insertionDelimiter = " "; + + getLeadingDelimiterTarget(): Target | undefined { + return getTokenLeadingDelimiterTarget(this); + } + getTrailingDelimiterTarget(): Target | undefined { + return getTokenTrailingDelimiterTarget(this); + } + getRemovalRange(): Range { + return getTokenRemovalRange(this); + } + + protected getCloneParameters() { + return this.state; + } +} diff --git a/src/processTargets/targets/WeakTarget.ts b/src/processTargets/targets/WeakTarget.ts new file mode 100644 index 0000000000..4186756517 --- /dev/null +++ b/src/processTargets/targets/WeakTarget.ts @@ -0,0 +1,32 @@ +import { Range } from "vscode"; +import { Target } from "../../typings/target.types"; +import { + getTokenLeadingDelimiterTarget, + getTokenRemovalRange, + getTokenTrailingDelimiterTarget, +} from "../targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior"; +import BaseTarget from "./BaseTarget"; + +/** + * - Treated as "line" for "pour", "clone", and "breakpoint" + * - Use token delimiters (space) for removal and insertion + * - Expand to nearest containing pair when asked for boundary or interior + */ +export default class WeakTarget extends BaseTarget { + insertionDelimiter = " "; + isWeak = true; + + getLeadingDelimiterTarget(): Target | undefined { + return getTokenLeadingDelimiterTarget(this); + } + getTrailingDelimiterTarget(): Target | undefined { + return getTokenTrailingDelimiterTarget(this); + } + getRemovalRange(): Range { + return getTokenRemovalRange(this); + } + + protected getCloneParameters() { + return this.state; + } +} diff --git a/src/scripts/transformRecordedTests/index.ts b/src/scripts/transformRecordedTests/index.ts index 4d8acd7c3d..60f12074ce 100644 --- a/src/scripts/transformRecordedTests/index.ts +++ b/src/scripts/transformRecordedTests/index.ts @@ -1,14 +1,13 @@ -import { updateSurroundingPairTest } from "./transformations/updateSurroundingPairTest"; -import { FixtureTransformation } from "./types"; -import { upgrade } from "./transformations/upgrade"; +import { getRecordedTestPaths } from "../../test/util/getFixturePaths"; import { identity } from "./transformations/identity"; +import { upgrade } from "./transformations/upgrade"; import { transformFile } from "./transformFile"; -import { getRecordedTestPaths } from "../../test/util/getFixturePaths"; +import { FixtureTransformation } from "./types"; const AVAILABLE_TRANSFORMATIONS: Record = { upgrade, autoFormat: identity, - custom: updateSurroundingPairTest, + // custom: MY_CUSTOM_TRANSFORMER, }; async function main(transformationName: string | undefined) { diff --git a/src/scripts/transformRecordedTests/transformations/updateSurroundingPairTest.ts b/src/scripts/transformRecordedTests/transformations/updateSurroundingPairTest.ts deleted file mode 100644 index e254d16365..0000000000 --- a/src/scripts/transformRecordedTests/transformations/updateSurroundingPairTest.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { TestCaseFixture } from "../../../testUtil/TestCase"; -import { transformPartialPrimitiveTargets } from "../../../util/getPrimitiveTargets"; -import { - DelimiterInclusion, - PartialPrimitiveTarget, -} from "../../../typings/Types"; - -// Leaving an example here in case it's helpful -export function updateSurroundingPairTest(fixture: TestCaseFixture) { - fixture.command.targets = transformPartialPrimitiveTargets( - fixture.command.targets, - (target: PartialPrimitiveTarget) => { - if (target.modifier?.type === "surroundingPair") { - let delimiterInclusion: DelimiterInclusion; - - switch (target.modifier.delimiterInclusion as any) { - case "includeDelimiters": - delimiterInclusion = undefined; - break; - case "excludeDelimiters": - delimiterInclusion = "interiorOnly"; - break; - case "delimitersOnly": - delimiterInclusion = "excludeInterior"; - break; - } - - target.modifier.delimiterInclusion = delimiterInclusion; - } - return target; - } - ); - - return fixture; -} diff --git a/src/scripts/transformRecordedTests/transformations/upgrade.ts b/src/scripts/transformRecordedTests/transformations/upgrade.ts index b9cb93b2a8..e47268c0c9 100644 --- a/src/scripts/transformRecordedTests/transformations/upgrade.ts +++ b/src/scripts/transformRecordedTests/transformations/upgrade.ts @@ -1,9 +1,9 @@ -import { TestCaseFixture } from "../../../testUtil/TestCase"; -import { canonicalizeAndValidateCommand } from "../../../util/canonicalizeAndValidateCommand"; import { flow } from "lodash"; +import { canonicalizeAndValidateCommand } from "../../../core/commandVersionUpgrades/canonicalizeAndValidateCommand"; import { cleanUpTestCaseCommand } from "../../../testUtil/cleanUpTestCaseCommand"; -import { upgradeFromVersion0 } from "./upgradeFromVersion0"; +import { TestCaseFixture } from "../../../testUtil/TestCase"; import { reorderFields } from "./reorderFields"; +import { upgradeFromVersion0 } from "./upgradeFromVersion0"; export const upgrade = flow(upgradeFromVersion0, upgradeCommand, reorderFields); diff --git a/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts b/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts index 5e878f4a43..55f45655d0 100644 --- a/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts +++ b/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts @@ -1,6 +1,6 @@ import { TestCaseFixture } from "../../../testUtil/TestCase"; import { transformPartialPrimitiveTargets } from "../../../util/getPrimitiveTargets"; -import { PartialPrimitiveTarget } from "../../../typings/Types"; +import { PartialPrimitiveTargetDescriptor } from "../../../typings/targetDescriptor.types"; export function upgradeFromVersion0(fixture: TestCaseFixture) { const { command, spokenForm: oldSpokenForm, ...rest } = fixture as any; @@ -16,7 +16,7 @@ export function upgradeFromVersion0(fixture: TestCaseFixture) { const targets = transformPartialPrimitiveTargets( newTargets ?? oldTargets, - (target: PartialPrimitiveTarget) => { + (target: PartialPrimitiveTargetDescriptor) => { if (target.mark?.type === "decoratedSymbol") { (target.mark as any).usePrePhraseSnapshot = undefined; } diff --git a/src/test/suite/fixtures/inferFullTargets.fixture.ts b/src/test/suite/fixtures/inferFullTargets.fixture.ts deleted file mode 100644 index 2c8ceed674..0000000000 --- a/src/test/suite/fixtures/inferFullTargets.fixture.ts +++ /dev/null @@ -1,2618 +0,0 @@ -// @ts-nocheck -import { - ActionPreferences, - InferenceContext, - PartialTarget, - Target, -} from "../../../typings/Types"; - -interface FixtureInput { - context: InferenceContext; - partialTargets: PartialTarget[]; - actionPreferences: ActionPreferences[]; -} - -interface Fixture { - input: FixtureInput; - expectedOutput: Target[]; -} - -const fixture: Fixture[] = [ - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - }, - end: { - type: "primitive", - position: "end", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "end", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - }, - end: { - type: "primitive", - position: "end", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "end", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - }, - end: { - type: "primitive", - selectionType: "paragraph", - mark: { - type: "decoratedSymbol", - symbolColor: "red", - character: "h", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "paragraph", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "red", - character: "h", - }, - selectionType: "paragraph", - position: "contents", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - modifier: { - type: "containingScope", - scopeType: "class", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "class", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "i", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "i", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "after", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - position: "before", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "line", - position: "before", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "line", - position: "after", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "token", - position: "after", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello\n", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "after", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "\nhello\n", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "paragraph", - position: "after", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "\nhello\n", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "paragraph", - position: "after", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello"], - isPaste: true, - clipboardContents: "\nhello\n", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "paragraph", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello\n"], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: ["\nhello\n"], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "paragraph", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello"], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello"], - isPaste: false, - }, - partialTargets: [ - { - type: "list", - elements: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - ], - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "list", - elements: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello"], - isPaste: false, - }, - partialTargets: [ - { - type: "list", - elements: [ - { - type: "primitive", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - ], - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "list", - elements: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - ], - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello"], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: ["hello"], - isPaste: false, - }, - partialTargets: [ - { - type: "list", - elements: [ - { - type: "primitive", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - { - type: "range", - start: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "e", - }, - }, - }, - ], - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "list", - elements: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "e", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 3, - endIndex: 4, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 3, - endIndex: 4, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 1, - endIndex: 3, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 1, - endIndex: 3, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 1, - endIndex: 2, - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 1, - endIndex: 2, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 2, - endIndex: 3, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 2, - endIndex: 3, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "subpiece", - pieceType: "character", - startIndex: 2, - endIndex: 6, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "character", - startIndex: 2, - endIndex: 6, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - position: "after", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 2, - endIndex: 3, - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "after", - modifier: { - type: "subpiece", - pieceType: "subtoken", - startIndex: 2, - endIndex: 3, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "subpiece", - pieceType: "character", - startIndex: 3, - endIndex: 4, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "character", - startIndex: 3, - endIndex: 4, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - modifier: { - type: "subpiece", - pieceType: "character", - startIndex: 3, - endIndex: 4, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "subpiece", - pieceType: "character", - startIndex: 3, - endIndex: 4, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "list", - elements: [ - { - type: "primitive", - selectionType: "line", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - { - type: "range", - start: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - }, - ], - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "list", - elements: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "list", - elements: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - }, - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "e", - }, - }, - ], - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "list", - elements: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "e", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - ], - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - }, - end: { - type: "primitive", - position: "end", - selectionType: "line", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "end", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - }, - end: { - type: "primitive", - position: "end", - selectionType: "paragraph", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "paragraph", - position: "end", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - end: { - type: "primitive", - position: "end", - selectionType: "line", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "line", - position: "end", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - end: { - type: "primitive", - position: "end", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "end", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - end: { - type: "primitive", - modifier: { - type: "surroundingPair", - delimiter: "doubleQuotes", - includePairDelimiter: true, - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "doubleQuotes", - includePairDelimiter: true, - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - end: { - type: "primitive", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - }, - end: { - type: "primitive", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - }, - end: { - type: "primitive", - modifier: { - type: "surroundingPair", - delimiter: "doubleQuotes", - includePairDelimiter: true, - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "doubleQuotes", - includePairDelimiter: true, - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - modifier: { - type: "surroundingPair", - delimiter: "doubleQuotes", - includePairDelimiter: false, - }, - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "f", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "doubleQuotes", - includePairDelimiter: false, - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - modifier: { - type: "surroundingPair", - delimiter: "singleQuotes", - includePairDelimiter: false, - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "singleQuotes", - includePairDelimiter: false, - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "h", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "surroundingPair", - delimiter: "parentheses", - includePairDelimiter: false, - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "identity", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "range", - start: { - type: "primitive", - selectionType: "line", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - end: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "range", - anchor: { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - active: { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "containingScope", - scopeType: "namedFunction", - }, - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "g", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: false, - }, - partialTargets: [ - { - type: "primitive", - selectionType: "line", - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - actionPreferences: [{}], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - selectionType: "line", - position: "contents", - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - }, - { - input: { - context: { - selectionContents: [""], - isPaste: true, - clipboardContents: "hello", - }, - partialTargets: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "(", - }, - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - actionPreferences: [{ position: "after" }], - }, - expectedOutput: [ - { - type: "primitive", - mark: { - type: "decoratedSymbol", - symbolColor: "default", - character: "(", - }, - selectionType: "token", - position: "contents", - modifier: { - type: "matchingPairSymbol", - }, - }, - ], - }, -]; - -export default fixture; diff --git a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterDrum.yml b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterDrum.yml index 677b64079d..87e39a9618 100644 --- a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterDrum.yml +++ b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterDrum.yml @@ -49,7 +49,7 @@ finalState: - anchor: {line: 5, character: 0} active: {line: 5, character: 0} thatMark: - - anchor: {line: 4, character: 1} + - anchor: {line: 4, character: 2} active: {line: 4, character: 7} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterItemEach.yml b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterItemEach.yml index 5e350fe8ae..cc689a2093 100644 --- a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterItemEach.yml +++ b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToAfterItemEach.yml @@ -50,7 +50,7 @@ finalState: - anchor: {line: 5, character: 0} active: {line: 5, character: 0} thatMark: - - anchor: {line: 4, character: 17} + - anchor: {line: 4, character: 19} active: {line: 4, character: 26} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeDrum.yml b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeDrum.yml index ffaf9567ea..c34166fe21 100644 --- a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeDrum.yml +++ b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeDrum.yml @@ -50,7 +50,7 @@ finalState: active: {line: 5, character: 0} thatMark: - anchor: {line: 4, character: 0} - active: {line: 4, character: 6} + active: {line: 4, character: 5} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 1} diff --git a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeItemEach.yml b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeItemEach.yml index 59a4395e1b..f2d5fdc282 100644 --- a/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeItemEach.yml +++ b/src/test/suite/fixtures/recorded/actions/bringAirAndBatAndCapToBeforeItemEach.yml @@ -51,7 +51,7 @@ finalState: active: {line: 5, character: 0} thatMark: - anchor: {line: 4, character: 16} - active: {line: 4, character: 25} + active: {line: 4, character: 23} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 1} diff --git a/src/test/suite/fixtures/recorded/actions/bringFineAfterLineVest.yml b/src/test/suite/fixtures/recorded/actions/bringFineAfterLineVest.yml new file mode 100644 index 0000000000..5234563536 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/bringFineAfterLineVest.yml @@ -0,0 +1,44 @@ +languageId: plaintext +command: + spokenForm: bring fine after line vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + modifiers: + - {type: position, position: after} + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: replaceWithTarget} +initialState: + documentContents: | + foo + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: | + foo + const value = "Hello world"; + foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 7} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: []}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/bringFineBeforeLineVest.yml b/src/test/suite/fixtures/recorded/actions/bringFineBeforeLineVest.yml new file mode 100644 index 0000000000..905ddd611f --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/bringFineBeforeLineVest.yml @@ -0,0 +1,46 @@ +languageId: plaintext +command: + spokenForm: bring fine before line vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + modifiers: + - {type: position, position: before} + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: replaceWithTarget} +initialState: + documentContents: |2 + + const value = "Hello world"; + foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 2, character: 0} + end: {line: 2, character: 3} + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: |2 + + foo + const value = "Hello world"; + foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} + sourceMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 3} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: []}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: [{type: position, position: before}, {type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToAfterDrum.yml b/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToAfterDrum.yml index 93166911b7..e01f78946a 100644 --- a/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToAfterDrum.yml +++ b/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToAfterDrum.yml @@ -53,7 +53,7 @@ finalState: - anchor: {line: 8, character: 0} active: {line: 8, character: 0} thatMark: - - anchor: {line: 4, character: 1} + - anchor: {line: 5, character: 0} active: {line: 7, character: 1} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToBeforeDrum.yml b/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToBeforeDrum.yml index 9811e5e7f0..2db341fbc0 100644 --- a/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToBeforeDrum.yml +++ b/src/test/suite/fixtures/recorded/actions/bringLineAirAndBatAndCapToBeforeDrum.yml @@ -54,7 +54,7 @@ finalState: active: {line: 8, character: 0} thatMark: - anchor: {line: 4, character: 0} - active: {line: 7, character: 0} + active: {line: 6, character: 1} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 1} diff --git a/src/test/suite/fixtures/recorded/actions/cloneArgue.yml b/src/test/suite/fixtures/recorded/actions/cloneArgue.yml new file mode 100644 index 0000000000..e4a4c6e5a8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneArgue.yml @@ -0,0 +1,44 @@ +languageId: typescript +command: + spokenForm: clone argue + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: |- + function foo(bar: number) { + + } + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: {} +finalState: + documentContents: |- + function foo(bar: number, bar: number) { + + } + selections: + - anchor: {line: 0, character: 28} + active: {line: 0, character: 28} + - anchor: {line: 0, character: 33} + active: {line: 0, character: 33} + thatMark: + - anchor: {line: 0, character: 26} + active: {line: 0, character: 37} + sourceMark: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 24} +decorations: + - name: justAddedBackground + type: token + start: {line: 0, character: 26} + end: {line: 0, character: 37} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneArgue2.yml b/src/test/suite/fixtures/recorded/actions/cloneArgue2.yml new file mode 100644 index 0000000000..561855f6ab --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneArgue2.yml @@ -0,0 +1,35 @@ +languageId: typescript +command: + spokenForm: clone argue + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: |- + function foo(bar: number) { + + } + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: {} +finalState: + documentContents: |- + function foo(bar: number, bar: number) { + + } + selections: + - anchor: {line: 0, character: 37} + active: {line: 0, character: 37} + thatMark: + - anchor: {line: 0, character: 26} + active: {line: 0, character: 37} + sourceMark: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 24} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneEveryArg.yml b/src/test/suite/fixtures/recorded/actions/cloneEveryArg.yml new file mode 100644 index 0000000000..583a7baef1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneEveryArg.yml @@ -0,0 +1,33 @@ +languageId: typescript +command: + spokenForm: clone every arg + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: "function myFunk(a: number, b: number) {}" + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: {} +finalState: + documentContents: "function myFunk(a: number, a: number, b: number, b: number) {}" + selections: + - anchor: {line: 0, character: 31} + active: {line: 0, character: 31} + thatMark: + - anchor: {line: 0, character: 27} + active: {line: 0, character: 36} + - anchor: {line: 0, character: 49} + active: {line: 0, character: 58} + sourceMark: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 25} + - anchor: {line: 0, character: 38} + active: {line: 0, character: 47} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneToken.yml b/src/test/suite/fixtures/recorded/actions/cloneToken.yml new file mode 100644 index 0000000000..62c60fc1b9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneToken.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clone token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: hello world world + selections: + - anchor: {line: 0, character: 14} + active: {line: 0, character: 14} + thatMark: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 17} + sourceMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} +decorations: + - name: justAddedBackground + type: token + start: {line: 0, character: 12} + end: {line: 0, character: 17} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneToken2.yml b/src/test/suite/fixtures/recorded/actions/cloneToken2.yml new file mode 100644 index 0000000000..ce0cda4cf8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneToken2.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clone token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +decorations: + - name: justAddedBackground + type: token + start: {line: 0, character: 6} + end: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneToken3.yml b/src/test/suite/fixtures/recorded/actions/cloneToken3.yml new file mode 100644 index 0000000000..ba314d0cea --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneToken3.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clone token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneToken4.yml b/src/test/suite/fixtures/recorded/actions/cloneToken4.yml new file mode 100644 index 0000000000..2db92264c7 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneToken4.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clone token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneToken5.yml b/src/test/suite/fixtures/recorded/actions/cloneToken5.yml new file mode 100644 index 0000000000..2edb3cc89d --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneToken5.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clone token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyAfter} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpArgue.yml b/src/test/suite/fixtures/recorded/actions/cloneUpArgue.yml new file mode 100644 index 0000000000..f4083eec4c --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpArgue.yml @@ -0,0 +1,44 @@ +languageId: typescript +command: + spokenForm: clone up argue + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: |- + function foo(bar: number) { + + } + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: {} +finalState: + documentContents: |- + function foo(bar: number, bar: number) { + + } + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + thatMark: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 24} + sourceMark: + - anchor: {line: 0, character: 26} + active: {line: 0, character: 37} +decorations: + - name: justAddedBackground + type: token + start: {line: 0, character: 13} + end: {line: 0, character: 24} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpArgue2.yml b/src/test/suite/fixtures/recorded/actions/cloneUpArgue2.yml new file mode 100644 index 0000000000..7a8e34064d --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpArgue2.yml @@ -0,0 +1,35 @@ +languageId: typescript +command: + spokenForm: clone up argue + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: |- + function foo(bar: number) { + + } + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} + marks: {} +finalState: + documentContents: |- + function foo(bar: number, bar: number) { + + } + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} + thatMark: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 24} + sourceMark: + - anchor: {line: 0, character: 26} + active: {line: 0, character: 37} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpEveryArg.yml b/src/test/suite/fixtures/recorded/actions/cloneUpEveryArg.yml new file mode 100644 index 0000000000..799d7c5f68 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpEveryArg.yml @@ -0,0 +1,33 @@ +languageId: typescript +command: + spokenForm: clone up every arg + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: "function myFunk(a: number, b: number) {}" + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: {} +finalState: + documentContents: "function myFunk(a: number, a: number, b: number, b: number) {}" + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + thatMark: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 25} + - anchor: {line: 0, character: 38} + active: {line: 0, character: 47} + sourceMark: + - anchor: {line: 0, character: 27} + active: {line: 0, character: 36} + - anchor: {line: 0, character: 49} + active: {line: 0, character: 58} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpToken.yml b/src/test/suite/fixtures/recorded/actions/cloneUpToken.yml new file mode 100644 index 0000000000..008ffbd7a0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpToken.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clone up token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: hello world world + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + sourceMark: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 17} +decorations: + - name: justAddedBackground + type: token + start: {line: 0, character: 6} + end: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpToken2.yml b/src/test/suite/fixtures/recorded/actions/cloneUpToken2.yml new file mode 100644 index 0000000000..1d02e467a6 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpToken2.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clone up token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + sourceMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} +decorations: + - name: justAddedBackground + type: token + start: {line: 0, character: 0} + end: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpToken3.yml b/src/test/suite/fixtures/recorded/actions/cloneUpToken3.yml new file mode 100644 index 0000000000..d70491b759 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpToken3.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clone up token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + sourceMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpToken4.yml b/src/test/suite/fixtures/recorded/actions/cloneUpToken4.yml new file mode 100644 index 0000000000..49604b4c05 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpToken4.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clone up token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: false + action: {name: insertCopyBefore} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + sourceMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpToken5.yml b/src/test/suite/fixtures/recorded/actions/cloneUpToken5.yml new file mode 100644 index 0000000000..416f5310b8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/cloneUpToken5.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clone up token + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: insertCopyBefore} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: hello hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + sourceMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneUpVest.yml b/src/test/suite/fixtures/recorded/actions/cloneUpVest.yml index 1a8e927f44..e47758d770 100644 --- a/src/test/suite/fixtures/recorded/actions/cloneUpVest.yml +++ b/src/test/suite/fixtures/recorded/actions/cloneUpVest.yml @@ -23,9 +23,9 @@ finalState: - anchor: {line: 0, character: 15} active: {line: 0, character: 15} thatMark: - - anchor: {line: 0, character: 0} + - anchor: {line: 0, character: 4} active: {line: 0, character: 32} sourceMark: - - anchor: {line: 1, character: 0} + - anchor: {line: 1, character: 4} active: {line: 1, character: 32} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/src/test/suite/fixtures/recorded/actions/cloneVest.yml b/src/test/suite/fixtures/recorded/actions/cloneVest.yml index ccc683f614..0d2b2f6b34 100644 --- a/src/test/suite/fixtures/recorded/actions/cloneVest.yml +++ b/src/test/suite/fixtures/recorded/actions/cloneVest.yml @@ -23,9 +23,9 @@ finalState: - anchor: {line: 1, character: 15} active: {line: 1, character: 15} thatMark: - - anchor: {line: 1, character: 0} + - anchor: {line: 1, character: 4} active: {line: 1, character: 32} sourceMark: - - anchor: {line: 0, character: 0} + - anchor: {line: 0, character: 4} active: {line: 0, character: 32} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml b/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml index ea183bad76..1ebafe1783 100644 --- a/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml +++ b/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml @@ -31,4 +31,9 @@ finalState: active: {line: 0, character: 9} - anchor: {line: 1, character: 0} active: {line: 1, character: 7} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 9} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 7} fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: parentheses}}] diff --git a/src/test/suite/fixtures/recorded/actions/drinkArg.yml b/src/test/suite/fixtures/recorded/actions/drinkArg.yml new file mode 100644 index 0000000000..292caf4341 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/drinkArg.yml @@ -0,0 +1,28 @@ +languageId: typescript +command: + spokenForm: drink arg + version: 2 + action: {name: editNewLineBefore} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: | + function helloWorld(foo: string, bar: number, baz: string) {} + selections: + - anchor: {line: 0, character: 40} + active: {line: 0, character: 40} + marks: {} +finalState: + documentContents: | + function helloWorld(foo: string, , bar: number, baz: string) {} + selections: + - anchor: {line: 0, character: 33} + active: {line: 0, character: 33} + thatMark: + - anchor: {line: 0, character: 35} + active: {line: 0, character: 46} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: argumentOrParameter}]}] diff --git a/src/test/suite/fixtures/recorded/actions/drinkBlock.yml b/src/test/suite/fixtures/recorded/actions/drinkBlock.yml new file mode 100644 index 0000000000..fcff4221d7 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/drinkBlock.yml @@ -0,0 +1,30 @@ +languageId: typescript +command: + spokenForm: drink block + version: 2 + action: {name: editNewLineBefore} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true +initialState: + documentContents: | + function helloWorld(foo: string, bar: number, baz: string) {} + selections: + - anchor: {line: 0, character: 40} + active: {line: 0, character: 40} + marks: {} +finalState: + documentContents: | + + + function helloWorld(foo: string, bar: number, baz: string) {} + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 61} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: paragraph}]}] diff --git a/src/test/suite/fixtures/recorded/actions/drinkLine.yml b/src/test/suite/fixtures/recorded/actions/drinkLine.yml new file mode 100644 index 0000000000..4f025ce87e --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/drinkLine.yml @@ -0,0 +1,33 @@ +languageId: python +command: + spokenForm: drink line + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: editNewLineBefore} +initialState: + documentContents: | + class MyClass: + def my_funk(): + pass + selections: + - anchor: {line: 1, character: 11} + active: {line: 1, character: 11} + marks: {} +finalState: + documentContents: | + class MyClass: + + def my_funk(): + pass + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 18} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/drinkVest.yml b/src/test/suite/fixtures/recorded/actions/drinkVest.yml index a1beca3ae8..d722f0ff24 100644 --- a/src/test/suite/fixtures/recorded/actions/drinkVest.yml +++ b/src/test/suite/fixtures/recorded/actions/drinkVest.yml @@ -26,6 +26,6 @@ finalState: - anchor: {line: 1, character: 0} active: {line: 1, character: 0} thatMark: - - anchor: {line: 1, character: 0} - active: {line: 1, character: 0} + - anchor: {line: 2, character: 6} + active: {line: 2, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: outside}] diff --git a/src/test/suite/fixtures/recorded/actions/drinkVest2.yml b/src/test/suite/fixtures/recorded/actions/drinkVest2.yml new file mode 100644 index 0000000000..24a73772cb --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/drinkVest2.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: drink vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + usePrePhraseSnapshot: true + action: {name: editNewLineBefore} +initialState: + documentContents: |2 + + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: |2 + + + const value = "Hello world"; + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 2, character: 10} + active: {line: 2, character: 15} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis.yml new file mode 100644 index 0000000000..e163a204f5 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineBefore} +initialState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis10.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis10.yml new file mode 100644 index 0000000000..85e18936b1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis10.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: |- + hello + there + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |- + hello + + there + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis11.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis11.yml new file mode 100644 index 0000000000..25ab7eb6dc --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis11.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: |2- + hello + there + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |2- + hello + + there + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis12.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis12.yml new file mode 100644 index 0000000000..40d89660e1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis12.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineBefore} +initialState: + documentContents: |2- + hello + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |2- + hello + + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis2.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis2.yml new file mode 100644 index 0000000000..c7ba6b5e8a --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis2.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineBefore} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: |- + + hello + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + thatMark: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis3.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis3.yml new file mode 100644 index 0000000000..c95851cdcf --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis3.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |- + + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis4.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis4.yml new file mode 100644 index 0000000000..2b47c044c9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis4.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: " hello" + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |2- + + hello + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis5.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis5.yml new file mode 100644 index 0000000000..ed800ca184 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis5.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: " hello" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |2- + + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis6.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis6.yml new file mode 100644 index 0000000000..2d291d32cd --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis6.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: " hello" + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: |2- + + hello + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + thatMark: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis7.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis7.yml new file mode 100644 index 0000000000..f5ff29d5d9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis7.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: " " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |2- + + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis8.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis8.yml new file mode 100644 index 0000000000..bab7b26fbd --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis8.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: " " + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |2- + + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis9.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis9.yml new file mode 100644 index 0000000000..3893999caa --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropThis9.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: drop this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineBefore} +initialState: + documentContents: |- + hello + there + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + marks: {} +finalState: + documentContents: |- + hello + + there + selections: + - anchor: {line: 2, character: 5} + active: {line: 2, character: 5} + thatMark: + - anchor: {line: 2, character: 5} + active: {line: 2, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/dropVest.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropVest.yml similarity index 100% rename from src/test/suite/fixtures/recorded/actions/dropVest.yml rename to src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropVest.yml diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropVest2.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropVest2.yml new file mode 100644 index 0000000000..2c0d0b5372 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/dropVest2.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: drop vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineBefore} +initialState: + documentContents: |2 + + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: |2 + + + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 10} + active: {line: 2, character: 15} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis.yml new file mode 100644 index 0000000000..44f4647525 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis10.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis10.yml new file mode 100644 index 0000000000..12141b100b --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis10.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2- + + hello + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |2 + + hello + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis11.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis11.yml new file mode 100644 index 0000000000..c7b2c96cf0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis11.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: | + there + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: |+ + there + + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + thatMark: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis12.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis12.yml new file mode 100644 index 0000000000..6b6331830a --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis12.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2 + + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |2+ + + + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis13.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis13.yml new file mode 100644 index 0000000000..cbee2b219f --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis13.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2 + there + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: |2+ + there + + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + thatMark: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis2.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis2.yml new file mode 100644 index 0000000000..ac97b8af72 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis2.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: | + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis3.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis3.yml new file mode 100644 index 0000000000..02b96e3a6b --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis3.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: " hello" + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: |2 + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis4.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis4.yml new file mode 100644 index 0000000000..7a1438b8ce --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis4.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: " " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |2 + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis5.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis5.yml new file mode 100644 index 0000000000..9892927a0d --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis5.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2- + whatever + hello + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |2 + whatever + hello + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis6.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis6.yml new file mode 100644 index 0000000000..895b0e49b6 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis6.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |- + whatever + hello + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + marks: {} +finalState: + documentContents: | + whatever + hello + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis7.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis7.yml new file mode 100644 index 0000000000..9ed220ea06 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis7.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: | + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |+ + hello + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis8.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis8.yml new file mode 100644 index 0000000000..d5ad098dbc --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis8.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2 + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |2+ + hello + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis9.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis9.yml new file mode 100644 index 0000000000..e67adbf4ef --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatThis9.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + spokenForm: float this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2- + hello + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |2 + hello + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/floatVest.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatVest.yml similarity index 87% rename from src/test/suite/fixtures/recorded/actions/floatVest.yml rename to src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatVest.yml index da3a3beb0b..fc52dd8b5d 100644 --- a/src/test/suite/fixtures/recorded/actions/floatVest.yml +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatVest.yml @@ -29,3 +29,8 @@ finalState: - anchor: {line: 1, character: 6} active: {line: 1, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatVest2.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatVest2.yml new file mode 100644 index 0000000000..81ea2b2020 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/floatVest2.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: float vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + usePrePhraseSnapshot: true + action: {name: insertEmptyLineAfter} +initialState: + documentContents: |2 + + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: |2+ + + const value = "Hello world"; + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 15} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis.yml new file mode 100644 index 0000000000..9be2896dfb --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis10.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis10.yml new file mode 100644 index 0000000000..2d042e6f3a --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis10.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2- + hello + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |2 + hello + + + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis11.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis11.yml new file mode 100644 index 0000000000..783ad2d7bc --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis11.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2- + + hello + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |2 + + + hello + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis12.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis12.yml new file mode 100644 index 0000000000..b83b5601e8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis12.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: | + there + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: |+ + + there + + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + thatMark: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis13.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis13.yml new file mode 100644 index 0000000000..07fec702bd --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis13.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: | + there + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + there + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis14.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis14.yml new file mode 100644 index 0000000000..5f2f8c0f9a --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis14.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2 + there + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |2+ + + there + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis15.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis15.yml new file mode 100644 index 0000000000..b07217e08e --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis15.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2 + there + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: |2+ + + there + + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + thatMark: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis16.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis16.yml new file mode 100644 index 0000000000..2dfa1aafa0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis16.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2 + + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |2+ + + + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis17.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis17.yml new file mode 100644 index 0000000000..c67aef5bde --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis17.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2 + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |2+ + + + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis18.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis18.yml new file mode 100644 index 0000000000..48b6b114ed --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis18.yml @@ -0,0 +1,39 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: | + hello + there + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + marks: {} +finalState: + documentContents: |+ + hello + + there + + selections: + - anchor: {line: 2, character: 5} + active: {line: 2, character: 5} + thatMark: + - anchor: {line: 2, character: 5} + active: {line: 2, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis19.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis19.yml new file mode 100644 index 0000000000..6ed741c710 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis19.yml @@ -0,0 +1,39 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |+ + hello + + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |+ + hello + + + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis2.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis2.yml new file mode 100644 index 0000000000..d927bca2b0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis2.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: hello + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: | + + hello + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis20.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis20.yml new file mode 100644 index 0000000000..136b0922dc --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis20.yml @@ -0,0 +1,41 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |- + hello + + there + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |- + hello + + + + there + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis21.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis21.yml new file mode 100644 index 0000000000..348835ed49 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis21.yml @@ -0,0 +1,41 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |- + hello + now + there + selections: + - anchor: {line: 1, character: 3} + active: {line: 1, character: 3} + marks: {} +finalState: + documentContents: |- + hello + + now + + there + selections: + - anchor: {line: 2, character: 3} + active: {line: 2, character: 3} + thatMark: + - anchor: {line: 2, character: 3} + active: {line: 2, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis22.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis22.yml new file mode 100644 index 0000000000..55af8d644f --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis22.yml @@ -0,0 +1,41 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: false + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2- + hello + now + there + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} + marks: {} +finalState: + documentContents: |2- + hello + + now + + there + selections: + - anchor: {line: 2, character: 7} + active: {line: 2, character: 7} + thatMark: + - anchor: {line: 2, character: 7} + active: {line: 2, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis3.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis3.yml new file mode 100644 index 0000000000..f102ba9976 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis3.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: " hello" + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: |2 + + hello + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis4.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis4.yml new file mode 100644 index 0000000000..cc85907453 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis4.yml @@ -0,0 +1,35 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: " " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |2 + + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 0, character: 0} + end: {line: 0, character: 0} + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis5.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis5.yml new file mode 100644 index 0000000000..d05ef8a883 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis5.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2- + whatever + hello + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |2 + whatever + + hello + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis6.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis6.yml new file mode 100644 index 0000000000..2c04455b2a --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis6.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |- + whatever + hello + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + marks: {} +finalState: + documentContents: | + whatever + + hello + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis7.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis7.yml new file mode 100644 index 0000000000..4abd1e33f1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis7.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |- + whatever + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: | + whatever + + hello + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis8.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis8.yml new file mode 100644 index 0000000000..ae301a3457 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis8.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: | + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |+ + hello + + + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis9.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis9.yml new file mode 100644 index 0000000000..b634d91653 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffThis9.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: puff this + version: 2 + targets: + - type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2 + hello + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: |2+ + hello + + + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} + - name: justAddedBackground + type: line + start: {line: 2, character: 0} + end: {line: 2, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/puffVest.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffVest.yml similarity index 79% rename from src/test/suite/fixtures/recorded/actions/puffVest.yml rename to src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffVest.yml index b6db2e5e6a..c62e9723be 100644 --- a/src/test/suite/fixtures/recorded/actions/puffVest.yml +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffVest.yml @@ -30,3 +30,12 @@ finalState: - anchor: {line: 2, character: 6} active: {line: 2, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffVest2.yml b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffVest2.yml new file mode 100644 index 0000000000..139f34296a --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/insertEmptyLines/puffVest2.yml @@ -0,0 +1,42 @@ +languageId: plaintext +command: + spokenForm: puff vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + usePrePhraseSnapshot: true + action: {name: insertEmptyLinesAround} +initialState: + documentContents: |2 + + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: |2+ + + + const value = "Hello world"; + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 10} + active: {line: 2, character: 15} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: []}] +decorations: + - name: justAddedBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 0} + - name: justAddedBackground + type: line + start: {line: 3, character: 0} + end: {line: 3, character: 0} diff --git a/src/test/suite/fixtures/recorded/actions/pourArg.yml b/src/test/suite/fixtures/recorded/actions/pourArg.yml new file mode 100644 index 0000000000..ca4a690830 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/pourArg.yml @@ -0,0 +1,28 @@ +languageId: typescript +command: + spokenForm: pour arg + version: 2 + action: {name: editNewLineAfter} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: | + function helloWorld(foo: string, bar: number, baz: string) {} + selections: + - anchor: {line: 0, character: 40} + active: {line: 0, character: 40} + marks: {} +finalState: + documentContents: | + function helloWorld(foo: string, bar: number, , baz: string) {} + selections: + - anchor: {line: 0, character: 46} + active: {line: 0, character: 46} + thatMark: + - anchor: {line: 0, character: 33} + active: {line: 0, character: 44} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: argumentOrParameter}]}] diff --git a/src/test/suite/fixtures/recorded/actions/pourBlock.yml b/src/test/suite/fixtures/recorded/actions/pourBlock.yml new file mode 100644 index 0000000000..cb640e2ada --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/pourBlock.yml @@ -0,0 +1,30 @@ +languageId: typescript +command: + spokenForm: pour block + version: 2 + action: {name: editNewLineAfter} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true +initialState: + documentContents: | + function helloWorld(foo: string, bar: number, baz: string) {} + selections: + - anchor: {line: 0, character: 40} + active: {line: 0, character: 40} + marks: {} +finalState: + documentContents: |+ + function helloWorld(foo: string, bar: number, baz: string) {} + + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 61} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: paragraph}]}] diff --git a/src/test/suite/fixtures/recorded/actions/pourLine.yml b/src/test/suite/fixtures/recorded/actions/pourLine.yml new file mode 100644 index 0000000000..ccccd8e1c5 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/pourLine.yml @@ -0,0 +1,34 @@ +languageId: python +postEditorOpenSleepTimeMs: 50 +command: + spokenForm: pour line + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: editNewLineAfter} +initialState: + documentContents: | + class MyClass: + def my_funk(): + pass + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: | + class MyClass: + def my_funk(): + + pass + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 18} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/actions/pourVest.yml b/src/test/suite/fixtures/recorded/actions/pourVest.yml index 8d7e4594d4..7284f45325 100644 --- a/src/test/suite/fixtures/recorded/actions/pourVest.yml +++ b/src/test/suite/fixtures/recorded/actions/pourVest.yml @@ -26,6 +26,6 @@ finalState: - anchor: {line: 2, character: 0} active: {line: 2, character: 0} thatMark: - - anchor: {line: 2, character: 0} - active: {line: 2, character: 0} + - anchor: {line: 1, character: 6} + active: {line: 1, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: outside}] diff --git a/src/test/suite/fixtures/recorded/actions/pourVest2.yml b/src/test/suite/fixtures/recorded/actions/pourVest2.yml new file mode 100644 index 0000000000..4ea51e852d --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/pourVest2.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: pour vest + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: v} + usePrePhraseSnapshot: true + action: {name: editNewLineAfter} +initialState: + documentContents: |2 + + const value = "Hello world"; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.v: + start: {line: 1, character: 10} + end: {line: 1, character: 15} +finalState: + documentContents: |2 + + const value = "Hello world"; + + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + thatMark: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 15} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/actions/roundWrapThis.yml b/src/test/suite/fixtures/recorded/actions/roundWrapThis.yml index 412d39cc85..d5b5fc7dd1 100644 --- a/src/test/suite/fixtures/recorded/actions/roundWrapThis.yml +++ b/src/test/suite/fixtures/recorded/actions/roundWrapThis.yml @@ -25,4 +25,7 @@ finalState: thatMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 2} + sourceMark: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}] diff --git a/src/test/suite/fixtures/recorded/actions/roundWrapVest.yml b/src/test/suite/fixtures/recorded/actions/roundWrapVest.yml index 962855a402..ce79366ac9 100644 --- a/src/test/suite/fixtures/recorded/actions/roundWrapVest.yml +++ b/src/test/suite/fixtures/recorded/actions/roundWrapVest.yml @@ -28,4 +28,7 @@ finalState: thatMark: - anchor: {line: 1, character: 6} active: {line: 1, character: 13} + sourceMark: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 12} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}] diff --git a/src/test/suite/fixtures/recorded/actions/roundWrapVest2.yml b/src/test/suite/fixtures/recorded/actions/roundWrapVest2.yml index e003ea366b..3e35851b63 100644 --- a/src/test/suite/fixtures/recorded/actions/roundWrapVest2.yml +++ b/src/test/suite/fixtures/recorded/actions/roundWrapVest2.yml @@ -28,4 +28,7 @@ finalState: thatMark: - anchor: {line: 1, character: 6} active: {line: 1, character: 13} + sourceMark: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 12} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml b/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml index 531856ea3c..63209009b0 100644 --- a/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml +++ b/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml @@ -26,4 +26,7 @@ finalState: thatMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 7} + sourceMark: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 6} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml b/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml index 4195092d77..9ca91f51ae 100644 --- a/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml +++ b/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml @@ -26,4 +26,7 @@ finalState: thatMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 7} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: (}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml b/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml index b61acd7532..a45641b3a3 100644 --- a/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml +++ b/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml @@ -23,4 +23,7 @@ finalState: thatMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 7} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 7} fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml b/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml index 3e91e57bdb..23666fd530 100644 --- a/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml +++ b/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml @@ -21,4 +21,7 @@ finalState: thatMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 7} + sourceMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/bringAirToAfterBatVerticalPastFine.yml b/src/test/suite/fixtures/recorded/compoundTargets/bringAirToAfterBatVerticalPastFine.yml index 2a20533e7b..d58d27857c 100644 --- a/src/test/suite/fixtures/recorded/compoundTargets/bringAirToAfterBatVerticalPastFine.yml +++ b/src/test/suite/fixtures/recorded/compoundTargets/bringAirToAfterBatVerticalPastFine.yml @@ -48,11 +48,11 @@ finalState: - anchor: {line: 5, character: 0} active: {line: 5, character: 0} thatMark: - - anchor: {line: 2, character: 1} + - anchor: {line: 2, character: 2} active: {line: 2, character: 3} - - anchor: {line: 3, character: 1} + - anchor: {line: 3, character: 2} active: {line: 3, character: 3} - - anchor: {line: 4, character: 1} + - anchor: {line: 4, character: 2} active: {line: 4, character: 3} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/compoundTargets/bringAirToBeforeBatVerticalPastFine.yml b/src/test/suite/fixtures/recorded/compoundTargets/bringAirToBeforeBatVerticalPastFine.yml index 2a8b817e95..48e3d66a42 100644 --- a/src/test/suite/fixtures/recorded/compoundTargets/bringAirToBeforeBatVerticalPastFine.yml +++ b/src/test/suite/fixtures/recorded/compoundTargets/bringAirToBeforeBatVerticalPastFine.yml @@ -49,11 +49,11 @@ finalState: active: {line: 5, character: 0} thatMark: - anchor: {line: 2, character: 0} - active: {line: 2, character: 2} + active: {line: 2, character: 1} - anchor: {line: 3, character: 0} - active: {line: 3, character: 2} + active: {line: 3, character: 1} - anchor: {line: 4, character: 0} - active: {line: 4, character: 2} + active: {line: 4, character: 1} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 1} diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckBlockEachBetweenLook.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckBlockEachBetweenLook.yml new file mode 100644 index 0000000000..26fa77e758 --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckBlockEachBetweenLook.yml @@ -0,0 +1,52 @@ +languageId: typescript +command: + spokenForm: chuck block each between look + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: e} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: l} + excludeAnchor: true + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + hello there + hello there + + + hello there + hello there + hello + selections: + - anchor: {line: 4, character: 9} + active: {line: 4, character: 9} + marks: + default.e: + start: {line: 1, character: 6} + end: {line: 1, character: 11} + default.l: + start: {line: 4, character: 4} + end: {line: 4, character: 9} +finalState: + documentContents: |- + hello there + hello there + hello there + hello there + hello + selections: + - anchor: {line: 2, character: 9} + active: {line: 2, character: 9} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: range, excludeAnchor: true, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: paragraph}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckBlockEachUntilLook.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckBlockEachUntilLook.yml new file mode 100644 index 0000000000..8892cd296b --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckBlockEachUntilLook.yml @@ -0,0 +1,50 @@ +languageId: typescript +command: + spokenForm: chuck block each until look + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: e} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: l} + excludeAnchor: false + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + hello there + hello there + + + hello there + hello there + hello + selections: + - anchor: {line: 4, character: 9} + active: {line: 4, character: 9} + marks: + default.e: + start: {line: 1, character: 6} + end: {line: 1, character: 11} + default.l: + start: {line: 4, character: 4} + end: {line: 4, character: 9} +finalState: + documentContents: |2- + hello there + hello there + hello + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: paragraph}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachBetweenLook.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachBetweenLook.yml new file mode 100644 index 0000000000..ef4aca5525 --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachBetweenLook.yml @@ -0,0 +1,52 @@ +languageId: plaintext +command: + spokenForm: chuck line each between look + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: e} + modifiers: + - type: containingScope + scopeType: {type: line} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: l} + excludeAnchor: true + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + hello there + hello there + + + hello there + hello there + hello + selections: + - anchor: {line: 4, character: 9} + active: {line: 4, character: 9} + marks: + default.e: + start: {line: 1, character: 6} + end: {line: 1, character: 11} + default.l: + start: {line: 4, character: 4} + end: {line: 4, character: 9} +finalState: + documentContents: |- + hello there + hello there + hello there + hello there + hello + selections: + - anchor: {line: 2, character: 9} + active: {line: 2, character: 9} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: range, excludeAnchor: true, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: line}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachBetweenLook2.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachBetweenLook2.yml new file mode 100644 index 0000000000..39b78be878 --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachBetweenLook2.yml @@ -0,0 +1,51 @@ +languageId: plaintext +command: + spokenForm: chuck line each between look + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: e} + modifiers: + - type: containingScope + scopeType: {type: line} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: l} + excludeAnchor: true + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + hello there + hello there + + hello there + hello there + hello + selections: + - anchor: {line: 3, character: 9} + active: {line: 3, character: 9} + marks: + default.e: + start: {line: 1, character: 6} + end: {line: 1, character: 11} + default.l: + start: {line: 3, character: 4} + end: {line: 3, character: 9} +finalState: + documentContents: |- + hello there + hello there + hello there + hello there + hello + selections: + - anchor: {line: 2, character: 9} + active: {line: 2, character: 9} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: range, excludeAnchor: true, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: line}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachUntilLook.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachUntilLook.yml new file mode 100644 index 0000000000..730b9ad28f --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineEachUntilLook.yml @@ -0,0 +1,51 @@ +languageId: plaintext +command: + spokenForm: chuck line each until look + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: e} + modifiers: + - type: containingScope + scopeType: {type: line} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: l} + excludeAnchor: false + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + hello there + hello there + + + hello there + hello there + hello + selections: + - anchor: {line: 4, character: 9} + active: {line: 4, character: 9} + marks: + default.e: + start: {line: 1, character: 6} + end: {line: 1, character: 11} + default.l: + start: {line: 4, character: 4} + end: {line: 4, character: 9} +finalState: + documentContents: |- + hello there + hello there + hello there + hello + selections: + - anchor: {line: 1, character: 9} + active: {line: 1, character: 9} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: line}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckLineHarpBetweenFine.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineHarpBetweenFine.yml new file mode 100644 index 0000000000..5a9f977a4e --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineHarpBetweenFine.yml @@ -0,0 +1,48 @@ +languageId: plaintext +command: + spokenForm: chuck line harp between fine + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + modifiers: + - type: containingScope + scopeType: {type: line} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + excludeAnchor: true + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: | + + hello world + + + foo bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.h: + start: {line: 1, character: 0} + end: {line: 1, character: 5} + default.f: + start: {line: 4, character: 0} + end: {line: 4, character: 3} +finalState: + documentContents: | + + hello world + foo bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: range, excludeAnchor: true, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: line}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/compoundTargets/chuckLineRiskSliceMade.yml b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineRiskSliceMade.yml new file mode 100644 index 0000000000..426a7ef371 --- /dev/null +++ b/src/test/suite/fixtures/recorded/compoundTargets/chuckLineRiskSliceMade.yml @@ -0,0 +1,51 @@ +languageId: plaintext +command: + spokenForm: chuck line risk slice made + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: r} + modifiers: + - type: containingScope + scopeType: {type: line} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: m} + excludeAnchor: false + excludeActive: false + rangeType: vertical + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + short + something longer + something even longer + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.r: + start: {line: 0, character: 0} + end: {line: 0, character: 5} + default.m: + start: {line: 2, character: 0} + end: {line: 2, character: 9} +finalState: + documentContents: |- + + hing longer + hing even longer + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: vertical, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: r}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: line}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: m}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckBlockAir.yml b/src/test/suite/fixtures/recorded/decorations/chuckBlockAir.yml new file mode 100644 index 0000000000..17523b1b2d --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckBlockAir.yml @@ -0,0 +1,47 @@ +languageId: plaintext +command: + spokenForm: chuck block air + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: | + + a + a + + + b c + d e + f g + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.a: + start: {line: 1, character: 0} + end: {line: 1, character: 1} +finalState: + documentContents: | + + b c + d e + f g + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 1, character: 0} + end: {line: 4, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckBlockAirUntilBatt.yml b/src/test/suite/fixtures/recorded/decorations/chuckBlockAirUntilBatt.yml new file mode 100644 index 0000000000..6fcc5a49d4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckBlockAirUntilBatt.yml @@ -0,0 +1,56 @@ +languageId: plaintext +command: + spokenForm: chuck block air until batt + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + excludeAnchor: false + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: | + + a + a + + + b c + d e + f g + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.a: + start: {line: 1, character: 0} + end: {line: 1, character: 1} + default.b: + start: {line: 5, character: 0} + end: {line: 5, character: 1} +finalState: + documentContents: | + b c + d e + f g + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 0, character: 0} + end: {line: 4, character: 0} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: paragraph}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: b}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckBlockBatt.yml b/src/test/suite/fixtures/recorded/decorations/chuckBlockBatt.yml new file mode 100644 index 0000000000..62fb4e3e4a --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckBlockBatt.yml @@ -0,0 +1,47 @@ +languageId: plaintext +command: + spokenForm: chuck block batt + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: | + + a + a + + + b c + d e + f g + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.b: + start: {line: 5, character: 0} + end: {line: 5, character: 1} +finalState: + documentContents: |+ + + a + a + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 4, character: 0} + active: {line: 4, character: 0} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 5, character: 0} + end: {line: 8, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: b}, modifiers: [{type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckBlockBatt2.yml b/src/test/suite/fixtures/recorded/decorations/chuckBlockBatt2.yml new file mode 100644 index 0000000000..f5a16a3c1c --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckBlockBatt2.yml @@ -0,0 +1,39 @@ +languageId: plaintext +command: + spokenForm: chuck block batt + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + + b c + d e + f g + selections: + - anchor: {line: 3, character: 5} + active: {line: 3, character: 5} + marks: + default.b: + start: {line: 1, character: 0} + end: {line: 1, character: 1} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 0, character: 0} + end: {line: 3, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: b}, modifiers: [{type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckBlockBattUntilAir.yml b/src/test/suite/fixtures/recorded/decorations/chuckBlockBattUntilAir.yml new file mode 100644 index 0000000000..f4ea08883f --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckBlockBattUntilAir.yml @@ -0,0 +1,56 @@ +languageId: plaintext +command: + spokenForm: chuck block batt until air + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + excludeAnchor: false + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: | + + a + a + + + b c + d e + f g + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.b: + start: {line: 5, character: 0} + end: {line: 5, character: 1} + default.a: + start: {line: 1, character: 0} + end: {line: 1, character: 1} +finalState: + documentContents: |- + + a + a + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 3, character: 0} + end: {line: 8, character: 0} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: b}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: paragraph}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckFine.yml b/src/test/suite/fixtures/recorded/decorations/chuckFine.yml new file mode 100644 index 0000000000..43eb4ea759 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckFine.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: chuck fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |2 + + foo ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +decorations: + - name: pendingDeleteBackground + type: token + start: {line: 1, character: 4} + end: {line: 1, character: 8} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckLineFine.yml b/src/test/suite/fixtures/recorded/decorations/chuckLineFine.yml new file mode 100644 index 0000000000..c6d1c6d155 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckLineFine.yml @@ -0,0 +1,40 @@ +languageId: plaintext +command: + spokenForm: chuck line fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |2 + + foo ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 1, character: 0} + end: {line: 1, character: 15} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: [{type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/decorations/chuckLineFineBetweenRisk.yml b/src/test/suite/fixtures/recorded/decorations/chuckLineFineBetweenRisk.yml new file mode 100644 index 0000000000..4a964e5883 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/chuckLineFineBetweenRisk.yml @@ -0,0 +1,53 @@ +languageId: plaintext +command: + spokenForm: chuck line fine between risk + version: 2 + targets: + - type: range + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: line} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: r} + excludeAnchor: true + excludeActive: true + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |2 + + foo ooo + aa + bb + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} + default.r: + start: {line: 4, character: 4} + end: {line: 4, character: 7} +finalState: + documentContents: |2 + + foo ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +decorations: + - name: pendingDeleteBackground + type: line + start: {line: 2, character: 0} + end: {line: 3, character: 2} +fullTargets: [{type: range, excludeAnchor: true, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: line}}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: r}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/decorations/clearBlockFine.yml b/src/test/suite/fixtures/recorded/decorations/clearBlockFine.yml new file mode 100644 index 0000000000..e8712165d2 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/clearBlockFine.yml @@ -0,0 +1,40 @@ +languageId: plaintext +command: + spokenForm: clear block fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |2 + + foo ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +decorations: + - name: pendingDeleteBackground + type: token + start: {line: 1, character: 4} + end: {line: 2, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: [{type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/decorations/clearFine.yml b/src/test/suite/fixtures/recorded/decorations/clearFine.yml new file mode 100644 index 0000000000..b9e9934a2c --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/clearFine.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: clear fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |2 + + foo ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + ooo + bar + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +decorations: + - name: pendingDeleteBackground + type: token + start: {line: 1, character: 4} + end: {line: 1, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/decorations/clearLineFine.yml b/src/test/suite/fixtures/recorded/decorations/clearLineFine.yml new file mode 100644 index 0000000000..ffd01ae9aa --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/clearLineFine.yml @@ -0,0 +1,41 @@ +languageId: plaintext +command: + spokenForm: clear line fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |2 + + foo ooo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + + bar + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +decorations: + - name: pendingDeleteBackground + type: token + start: {line: 1, character: 4} + end: {line: 1, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: [{type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/decorations/cutFine.yml b/src/test/suite/fixtures/recorded/decorations/cutFine.yml new file mode 100644 index 0000000000..8d9cd74e13 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/cutFine.yml @@ -0,0 +1,40 @@ +languageId: plaintext +command: + spokenForm: cut fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true + action: {name: cutToClipboard} +initialState: + documentContents: |2 + + foo bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +decorations: + - name: referencedBackground + type: token + start: {line: 1, character: 4} + end: {line: 1, character: 7} + - name: pendingDeleteBackground + type: token + start: {line: 1, character: 7} + end: {line: 1, character: 8} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/decorations/highlightFine.yml b/src/test/suite/fixtures/recorded/decorations/highlightFine.yml new file mode 100644 index 0000000000..a8ddda9123 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/highlightFine.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: highlight fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true + action: {name: highlight} +initialState: + documentContents: |2 + + foo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + foo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} +decorations: + - name: highlight0Background + type: token + start: {line: 1, character: 4} + end: {line: 1, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: []}] diff --git a/src/test/suite/fixtures/recorded/decorations/highlightLineFine.yml b/src/test/suite/fixtures/recorded/decorations/highlightLineFine.yml new file mode 100644 index 0000000000..21cb711515 --- /dev/null +++ b/src/test/suite/fixtures/recorded/decorations/highlightLineFine.yml @@ -0,0 +1,41 @@ +languageId: plaintext +command: + spokenForm: highlight line fine + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: line} + usePrePhraseSnapshot: true + action: {name: highlight} +initialState: + documentContents: |2 + + foo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: |2 + + foo + bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} +decorations: + - name: highlight0Background + type: line + start: {line: 1, character: 4} + end: {line: 1, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: [{type: containingScope, scopeType: {type: line}}]}] diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAfterWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAfterWhaleTakeWhale.yml index 30dc8f8444..7a66d00b90 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAfterWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAfterWhaleTakeWhale.yml @@ -35,7 +35,7 @@ finalState: start: {line: 0, character: 6} end: {line: 0, character: 11} thatMark: - - anchor: {line: 0, character: 11} + - anchor: {line: 0, character: 12} active: {line: 0, character: 17} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAndPointToEndOfThisAndEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAndPointToEndOfThisAndEndOfWhaleTakeWhale.yml index 47adf5fb01..27113b5834 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAndPointToEndOfThisAndEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpAndPointToEndOfThisAndEndOfWhaleTakeWhale.yml @@ -39,8 +39,8 @@ initialState: finalState: documentContents: hello. worldhello. selections: - - anchor: {line: 0, character: 12} - active: {line: 0, character: 12} + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} marks: default.h: start: {line: 0, character: 0} @@ -54,8 +54,6 @@ finalState: thatMark: - anchor: {line: 0, character: 12} active: {line: 0, character: 18} - - anchor: {line: 0, character: 12} - active: {line: 0, character: 18} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 5} diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpBeforeWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpBeforeWhaleTakeWhale.yml index 455efe3062..a12af1bd2e 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpBeforeWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringHarpBeforeWhaleTakeWhale.yml @@ -36,7 +36,7 @@ finalState: end: {line: 0, character: 18} thatMark: - anchor: {line: 0, character: 7} - active: {line: 0, character: 13} + active: {line: 0, character: 12} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 5} diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml index f31659fb46..2f1eb9460b 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml @@ -41,8 +41,8 @@ initialState: finalState: documentContents: hello. wo.rldhello selections: - - anchor: {line: 0, character: 13} - active: {line: 0, character: 13} + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} marks: default..: start: {line: 0, character: 5} diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhale.yml deleted file mode 100644 index 784ab6dbb6..0000000000 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhale.yml +++ /dev/null @@ -1,64 +0,0 @@ -languageId: plaintext -command: - version: 1 - spokenForm: bring point and harp to end of this and end of whale - action: replaceWithTarget - targets: - - type: list - elements: - - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: .} - - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: h} - - type: list - elements: - - type: primitive - position: after - insideOutsideType: inside - mark: {type: cursor} - - type: primitive - position: after - insideOutsideType: inside - mark: {type: decoratedSymbol, symbolColor: default, character: w} -marksToCheck: [default.w] -initialState: - documentContents: hello. world - selections: - - anchor: {line: 0, character: 12} - active: {line: 0, character: 12} - marks: - default..: - start: {line: 0, character: 5} - end: {line: 0, character: 6} - default.h: - start: {line: 0, character: 0} - end: {line: 0, character: 5} - default.w: - start: {line: 0, character: 7} - end: {line: 0, character: 12} -finalState: - documentContents: hello. world.hello - selections: - - anchor: {line: 0, character: 12} - active: {line: 0, character: 12} - marks: - default..: - start: {line: 0, character: 5} - end: {line: 0, character: 6} - default.h: - start: {line: 0, character: 0} - end: {line: 0, character: 5} - default.w: - start: {line: 0, character: 7} - end: {line: 0, character: 18} - thatMark: - - anchor: {line: 0, character: 12} - active: {line: 0, character: 18} - - anchor: {line: 0, character: 12} - active: {line: 0, character: 18} - sourceMark: - - anchor: {line: 0, character: 5} - active: {line: 0, character: 6} - - anchor: {line: 0, character: 0} - active: {line: 0, character: 5} -fullTargets: [{type: list, elements: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: .}, selectionType: token, position: contents, insideOutsideType: null, modifier: {type: identity}}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, selectionType: token, position: contents, insideOutsideType: null, modifier: {type: identity}}]}, {type: list, elements: [{type: primitive, mark: {type: cursor}, selectionType: token, position: after, insideOutsideType: inside, modifier: {type: identity}}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, selectionType: token, position: after, insideOutsideType: inside, modifier: {type: identity}}]}] diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhaleTakeWhale.yml index a572792994..f6ec4c6345 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfThisAndEndOfWhaleTakeWhale.yml @@ -39,8 +39,8 @@ initialState: finalState: documentContents: hello. world.hello selections: - - anchor: {line: 0, character: 12} - active: {line: 0, character: 12} + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} marks: default..: start: {line: 0, character: 5} @@ -50,12 +50,10 @@ finalState: end: {line: 0, character: 5} default.w: start: {line: 0, character: 7} - end: {line: 0, character: 18} + end: {line: 0, character: 12} thatMark: - anchor: {line: 0, character: 12} active: {line: 0, character: 18} - - anchor: {line: 0, character: 12} - active: {line: 0, character: 18} sourceMark: - anchor: {line: 0, character: 5} active: {line: 0, character: 6} diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml index ab7bc37076..6202f6bb19 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml @@ -38,8 +38,8 @@ initialState: finalState: documentContents: hello. wo.rld. selections: - - anchor: {line: 0, character: 13} - active: {line: 0, character: 13} + - anchor: {line: 0, character: 14} + active: {line: 0, character: 14} marks: default..: start: {line: 0, character: 5} diff --git a/src/test/suite/fixtures/recorded/leadingTrailing/chuckLeadingWhale.yml b/src/test/suite/fixtures/recorded/leadingTrailing/chuckLeadingWhale.yml new file mode 100644 index 0000000000..96024e1b35 --- /dev/null +++ b/src/test/suite/fixtures/recorded/leadingTrailing/chuckLeadingWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: chuck leading whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: leading} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: helloworld whatever + selections: + - anchor: {line: 0, character: 19} + active: {line: 0, character: 19} + thatMark: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: leading}]}] diff --git a/src/test/suite/fixtures/recorded/leadingTrailing/chuckPastLeadingTrap.yml b/src/test/suite/fixtures/recorded/leadingTrailing/chuckPastLeadingTrap.yml new file mode 100644 index 0000000000..3fa3ff3f2c --- /dev/null +++ b/src/test/suite/fixtures/recorded/leadingTrailing/chuckPastLeadingTrap.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: chuck past leading trap + version: 2 + targets: + - type: range + anchor: {type: primitive} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: t} + modifiers: + - {type: leading} + excludeAnchor: false + excludeActive: false + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello there + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + marks: + default.t: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: helloere + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + thatMark: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: continuous, anchor: {type: primitive, mark: {type: cursor}, modifiers: []}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: t}, modifiers: [{type: leading}]}}] diff --git a/src/test/suite/fixtures/recorded/leadingTrailing/chuckPastTrailingLook.yml b/src/test/suite/fixtures/recorded/leadingTrailing/chuckPastTrailingLook.yml new file mode 100644 index 0000000000..d7171aad08 --- /dev/null +++ b/src/test/suite/fixtures/recorded/leadingTrailing/chuckPastTrailingLook.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: chuck past trailing look + version: 2 + targets: + - type: range + anchor: {type: primitive} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: l} + modifiers: + - {type: trailing} + excludeAnchor: false + excludeActive: false + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello there + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: + default.l: + start: {line: 0, character: 0} + end: {line: 0, character: 5} +finalState: + documentContents: hethere + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + thatMark: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} +fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: continuous, anchor: {type: primitive, mark: {type: cursor}, modifiers: []}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: [{type: trailing}]}}] diff --git a/src/test/suite/fixtures/recorded/leadingTrailing/chuckTrailingDrum.yml b/src/test/suite/fixtures/recorded/leadingTrailing/chuckTrailingDrum.yml new file mode 100644 index 0000000000..c678b5efb8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/leadingTrailing/chuckTrailingDrum.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: chuck trailing drum + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: d} + modifiers: + - {type: trailing} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 6} + marks: + default.d: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello worldwhatever + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 6} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: d}, modifiers: [{type: trailing}]}] diff --git a/src/test/suite/fixtures/recorded/leadingTrailing/clearLeadingWhale.yml b/src/test/suite/fixtures/recorded/leadingTrailing/clearLeadingWhale.yml new file mode 100644 index 0000000000..a8cc4caee0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/leadingTrailing/clearLeadingWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clear leading whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: leading} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: helloworld whatever + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + thatMark: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: leading}]}] diff --git a/src/test/suite/fixtures/recorded/leadingTrailing/clearTrailingWhale.yml b/src/test/suite/fixtures/recorded/leadingTrailing/clearTrailingWhale.yml new file mode 100644 index 0000000000..9df6a3ca4f --- /dev/null +++ b/src/test/suite/fixtures/recorded/leadingTrailing/clearTrailingWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clear trailing whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: trailing} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello worldwhatever + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: trailing}]}] diff --git a/src/test/suite/fixtures/recorded/positions/bringArgMadeAndGustToEndOfJustThis.yml b/src/test/suite/fixtures/recorded/positions/bringArgMadeAndGustToEndOfJustThis.yml new file mode 100644 index 0000000000..1dcffff209 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/bringArgMadeAndGustToEndOfJustThis.yml @@ -0,0 +1,53 @@ +languageId: typescript +command: + spokenForm: bring arg made and gust to end of just this + version: 2 + targets: + - type: list + elements: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: m} + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: g} + - type: primitive + mark: {type: cursor} + modifiers: + - {type: position, position: end} + - {type: toRawSelection} + usePrePhraseSnapshot: true + action: {name: replaceWithTarget} +initialState: + documentContents: |- + function whatever(name: string, age: number, inclose: boolean) { + + } + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: + default.m: + start: {line: 0, character: 18} + end: {line: 0, character: 22} + default.g: + start: {line: 0, character: 32} + end: {line: 0, character: 35} +finalState: + documentContents: |- + function whatever(name: string, age: number, inclose: boolean) { + name: string, age: number + } + selections: + - anchor: {line: 1, character: 29} + active: {line: 1, character: 29} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 29} + sourceMark: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 30} + - anchor: {line: 0, character: 32} + active: {line: 0, character: 43} +fullTargets: [{type: list, elements: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: m}, modifiers: &ref_0 [{type: containingScope, scopeType: {type: argumentOrParameter}}]}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: g}, modifiers: *ref_0}]}, {type: primitive, mark: {type: cursor}, modifiers: [{type: position, position: end}, {type: toRawSelection}]}] diff --git a/src/test/suite/fixtures/recorded/positions/bringHarpToAfterFile.yml b/src/test/suite/fixtures/recorded/positions/bringHarpToAfterFile.yml new file mode 100644 index 0000000000..7475840bb4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/bringHarpToAfterFile.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: bring harp to after file + version: 2 + action: {name: replaceWithTarget} + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + - type: primitive + modifiers: + - {type: position, position: after} + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.h: + start: {line: 0, character: 0} + end: {line: 0, character: 5} +finalState: + documentContents: |- + hello world + hello + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 5} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: []}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: document}]}] diff --git a/src/test/suite/fixtures/recorded/positions/bringStateFineToAfterBatt.yml b/src/test/suite/fixtures/recorded/positions/bringStateFineToAfterBatt.yml new file mode 100644 index 0000000000..a7a3313118 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/bringStateFineToAfterBatt.yml @@ -0,0 +1,47 @@ +languageId: typescript +command: + spokenForm: bring state fine to after batt + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - type: containingScope + scopeType: {type: statement} + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + modifiers: + - {type: position, position: after} + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: true + action: {name: replaceWithTarget} +initialState: + documentContents: | + let foo; + let bar; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 0, character: 4} + end: {line: 0, character: 7} + default.b: + start: {line: 1, character: 4} + end: {line: 1, character: 7} +finalState: + documentContents: | + let foo; + let bar; + let foo; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 8} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 8} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: [&ref_0 {type: containingScope, scopeType: {type: statement}}]}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: b}, modifiers: [*ref_0, {type: position, position: after}]}] diff --git a/src/test/suite/fixtures/recorded/positions/bringWhaleToBeforeFile.yml b/src/test/suite/fixtures/recorded/positions/bringWhaleToBeforeFile.yml new file mode 100644 index 0000000000..7fa7920693 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/bringWhaleToBeforeFile.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: bring whale to before file + version: 2 + action: {name: replaceWithTarget} + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + - type: primitive + modifiers: + - {type: position, position: before} + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: |- + world + hello world + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + sourceMark: + - anchor: {line: 1, character: 6} + active: {line: 1, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: []}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: before}, {type: containingScope, scopeType: document}]}] diff --git a/src/test/suite/fixtures/recorded/positions/chuckAfterHarp.yml b/src/test/suite/fixtures/recorded/positions/chuckAfterHarp.yml index 56e681336d..812eaa1eba 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckAfterHarp.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckAfterHarp.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck after harp diff --git a/src/test/suite/fixtures/recorded/positions/chuckAfterLineVest.yml b/src/test/suite/fixtures/recorded/positions/chuckAfterLineVest.yml index 782e48556d..fab54835e3 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckAfterLineVest.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckAfterLineVest.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck after line vest diff --git a/src/test/suite/fixtures/recorded/positions/chuckAfterLook.yml b/src/test/suite/fixtures/recorded/positions/chuckAfterLook.yml index ca89626b92..9501d5de3d 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckAfterLook.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckAfterLook.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck after look diff --git a/src/test/suite/fixtures/recorded/positions/chuckAfterVest.yml b/src/test/suite/fixtures/recorded/positions/chuckAfterVest.yml index d199f88cce..c2895ad645 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckAfterVest.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckAfterVest.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck after vest diff --git a/src/test/suite/fixtures/recorded/positions/chuckAfterWhale.yml b/src/test/suite/fixtures/recorded/positions/chuckAfterWhale.yml new file mode 100644 index 0000000000..a2947fc505 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/chuckAfterWhale.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: chuck after whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: after} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +returnValue: null +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: after}]}] +thrownError: {name: UnsupportedError} diff --git a/src/test/suite/fixtures/recorded/positions/chuckAir.yml b/src/test/suite/fixtures/recorded/positions/chuckAir.yml index 4f4c35bc45..65d5d42c05 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckAir.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckAir.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck air diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeAir.yml b/src/test/suite/fixtures/recorded/positions/chuckBeforeAir.yml index 21effed70c..5695ca9c21 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckBeforeAir.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckBeforeAir.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck before air diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeEach.yml b/src/test/suite/fixtures/recorded/positions/chuckBeforeEach.yml index 77483adff0..cbd88701e4 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckBeforeEach.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckBeforeEach.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck before each diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeHarp.yml b/src/test/suite/fixtures/recorded/positions/chuckBeforeHarp.yml index 3b8427314e..169592d160 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckBeforeHarp.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckBeforeHarp.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck before harp diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeLineAir.yml b/src/test/suite/fixtures/recorded/positions/chuckBeforeLineAir.yml index 28d96d84d3..e9281db485 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckBeforeLineAir.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckBeforeLineAir.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck before line air diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeVest.yml b/src/test/suite/fixtures/recorded/positions/chuckBeforeVest.yml index 66a1def52e..744847de38 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckBeforeVest.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckBeforeVest.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck before vest diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeWhale.yml b/src/test/suite/fixtures/recorded/positions/chuckBeforeWhale.yml new file mode 100644 index 0000000000..b0baaff459 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/chuckBeforeWhale.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: chuck before whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: before} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +returnValue: null +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: before}]}] +thrownError: {name: UnsupportedError} diff --git a/src/test/suite/fixtures/recorded/positions/chuckEndOfWhale.yml b/src/test/suite/fixtures/recorded/positions/chuckEndOfWhale.yml new file mode 100644 index 0000000000..b18a832a0c --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/chuckEndOfWhale.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: chuck end of whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +returnValue: null +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: end}]}] +thrownError: {name: UnsupportedError} diff --git a/src/test/suite/fixtures/recorded/positions/chuckBeforeBlockAir.yml b/src/test/suite/fixtures/recorded/positions/chuckLeadingBlockAir.yml similarity index 53% rename from src/test/suite/fixtures/recorded/positions/chuckBeforeBlockAir.yml rename to src/test/suite/fixtures/recorded/positions/chuckLeadingBlockAir.yml index 31d34664c0..94c10e6fb6 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckBeforeBlockAir.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckLeadingBlockAir.yml @@ -1,13 +1,16 @@ -languageId: typescript +languageId: plaintext command: - version: 1 - spokenForm: chuck before block air - action: remove + spokenForm: chuck leading block air + version: 2 targets: - type: primitive - position: before - selectionType: paragraph mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - {type: leading} + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: remove} initialState: documentContents: |- @@ -24,11 +27,12 @@ initialState: finalState: documentContents: |- - const value = "Hello world"; const value = "Hello world"; + const value = "Hello world"; + const value = "Hello world"; selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} thatMark: - - anchor: {line: 1, character: 28} - active: {line: 1, character: 28} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, selectionType: paragraph, position: before, modifier: {type: identity}, insideOutsideType: outside}] + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: position, position: before}, {type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/positions/chuckPastAfterLook.yml b/src/test/suite/fixtures/recorded/positions/chuckPastAfterLook.yml deleted file mode 100644 index 005c07a9f3..0000000000 --- a/src/test/suite/fixtures/recorded/positions/chuckPastAfterLook.yml +++ /dev/null @@ -1,32 +0,0 @@ -languageId: typescript -command: - version: 1 - spokenForm: chuck past after look - action: remove - targets: - - type: range - start: {type: primitive} - end: - type: primitive - position: after - mark: {type: decoratedSymbol, symbolColor: default, character: l} - excludeStart: false - excludeEnd: false -initialState: - documentContents: hello there - selections: - - anchor: {line: 0, character: 2} - active: {line: 0, character: 2} - marks: - default.l: - start: {line: 0, character: 0} - end: {line: 0, character: 5} -finalState: - documentContents: hethere - selections: - - anchor: {line: 0, character: 2} - active: {line: 0, character: 2} - thatMark: - - anchor: {line: 0, character: 2} - active: {line: 0, character: 2} -fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: outside}, end: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, selectionType: token, position: after, modifier: {type: identity}, insideOutsideType: outside}}] diff --git a/src/test/suite/fixtures/recorded/positions/chuckPastBeforeTrap.yml b/src/test/suite/fixtures/recorded/positions/chuckPastBeforeTrap.yml deleted file mode 100644 index e9940b52ad..0000000000 --- a/src/test/suite/fixtures/recorded/positions/chuckPastBeforeTrap.yml +++ /dev/null @@ -1,32 +0,0 @@ -languageId: typescript -command: - version: 1 - spokenForm: chuck past before trap - action: remove - targets: - - type: range - start: {type: primitive} - end: - type: primitive - position: before - mark: {type: decoratedSymbol, symbolColor: default, character: t} - excludeStart: false - excludeEnd: false -initialState: - documentContents: hello there - selections: - - anchor: {line: 0, character: 8} - active: {line: 0, character: 8} - marks: - default.t: - start: {line: 0, character: 6} - end: {line: 0, character: 11} -finalState: - documentContents: helloere - selections: - - anchor: {line: 0, character: 5} - active: {line: 0, character: 5} - thatMark: - - anchor: {line: 0, character: 5} - active: {line: 0, character: 5} -fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: outside}, end: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: t}, selectionType: token, position: before, modifier: {type: identity}, insideOutsideType: outside}}] diff --git a/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml b/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml index 2aa194fa69..23f9986b7b 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck past end of line diff --git a/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLook.yml b/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLook.yml index 7effdfdc01..78c3a15c6f 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLook.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckPastEndOfLook.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck past end of look diff --git a/src/test/suite/fixtures/recorded/positions/chuckPastStartOfTrap.yml b/src/test/suite/fixtures/recorded/positions/chuckPastStartOfTrap.yml index 1bf8a8c856..cd5bb2dd3a 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckPastStartOfTrap.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckPastStartOfTrap.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck past start of trap diff --git a/src/test/suite/fixtures/recorded/positions/chuckStartOfWhale.yml b/src/test/suite/fixtures/recorded/positions/chuckStartOfWhale.yml new file mode 100644 index 0000000000..e3773ace82 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/chuckStartOfWhale.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: chuck start of whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: start} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +returnValue: null +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: start}]}] +thrownError: {name: UnsupportedError} diff --git a/src/test/suite/fixtures/recorded/positions/chuckAfterBlockVest.yml b/src/test/suite/fixtures/recorded/positions/chuckTrailingBlockVest.yml similarity index 53% rename from src/test/suite/fixtures/recorded/positions/chuckAfterBlockVest.yml rename to src/test/suite/fixtures/recorded/positions/chuckTrailingBlockVest.yml index cfdd988bfe..3053fa513c 100644 --- a/src/test/suite/fixtures/recorded/positions/chuckAfterBlockVest.yml +++ b/src/test/suite/fixtures/recorded/positions/chuckTrailingBlockVest.yml @@ -1,13 +1,16 @@ -languageId: typescript +languageId: plaintext command: - version: 1 - spokenForm: chuck after block vest - action: remove + spokenForm: chuck trailing block vest + version: 2 targets: - type: primitive - position: after - selectionType: paragraph mark: {type: decoratedSymbol, symbolColor: default, character: v} + modifiers: + - {type: trailing} + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: remove} initialState: documentContents: |- @@ -24,11 +27,12 @@ initialState: finalState: documentContents: |- - const value = "Hello world"; const value = "Hello world"; + const value = "Hello world"; + const value = "Hello world"; selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} thatMark: - - anchor: {line: 1, character: 28} - active: {line: 1, character: 28} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: paragraph, position: after, modifier: {type: identity}, insideOutsideType: outside}] + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/positions/replaceAfterVestWithHallo.yml b/src/test/suite/fixtures/recorded/positions/replaceAfterVestWithHallo.yml index ea845c269f..e1b1396232 100644 --- a/src/test/suite/fixtures/recorded/positions/replaceAfterVestWithHallo.yml +++ b/src/test/suite/fixtures/recorded/positions/replaceAfterVestWithHallo.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: replace after vest with hallo @@ -28,6 +28,6 @@ finalState: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} thatMark: - - anchor: {line: 1, character: 17} - active: {line: 1, character: 17} + - anchor: {line: 1, character: 11} + active: {line: 1, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: after, modifier: {type: identity}, insideOutsideType: null}] diff --git a/src/test/suite/fixtures/recorded/positions/replaceBeforeVestWithHello.yml b/src/test/suite/fixtures/recorded/positions/replaceBeforeVestWithHello.yml index c461c1a82e..f73f55b0f2 100644 --- a/src/test/suite/fixtures/recorded/positions/replaceBeforeVestWithHello.yml +++ b/src/test/suite/fixtures/recorded/positions/replaceBeforeVestWithHello.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: replace before vest with hello diff --git a/src/test/suite/fixtures/recorded/positions/replaceEndOfVestWithHello.yml b/src/test/suite/fixtures/recorded/positions/replaceEndOfVestWithHello.yml index 695a21abc6..85d0ab5572 100644 --- a/src/test/suite/fixtures/recorded/positions/replaceEndOfVestWithHello.yml +++ b/src/test/suite/fixtures/recorded/positions/replaceEndOfVestWithHello.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: replace end of vest with hello diff --git a/src/test/suite/fixtures/recorded/positions/replaceStartOfVestWithHello.yml b/src/test/suite/fixtures/recorded/positions/replaceStartOfVestWithHello.yml index 8ff693acd0..87db443c78 100644 --- a/src/test/suite/fixtures/recorded/positions/replaceStartOfVestWithHello.yml +++ b/src/test/suite/fixtures/recorded/positions/replaceStartOfVestWithHello.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: replace start of vest with hello diff --git a/src/test/suite/fixtures/recorded/positions/takeAfterWhale.yml b/src/test/suite/fixtures/recorded/positions/takeAfterWhale.yml new file mode 100644 index 0000000000..8f88fdc1d6 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/takeAfterWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: take after whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: after} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: after}]}] diff --git a/src/test/suite/fixtures/recorded/positions/takeBeforeWhale.yml b/src/test/suite/fixtures/recorded/positions/takeBeforeWhale.yml new file mode 100644 index 0000000000..5c80bf5464 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/takeBeforeWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: take before whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: before} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: before}]}] diff --git a/src/test/suite/fixtures/recorded/positions/takeEndOfWhale.yml b/src/test/suite/fixtures/recorded/positions/takeEndOfWhale.yml new file mode 100644 index 0000000000..96f457a00f --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/takeEndOfWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: take end of whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: end}]}] diff --git a/src/test/suite/fixtures/recorded/positions/takeStartOfWhale.yml b/src/test/suite/fixtures/recorded/positions/takeStartOfWhale.yml new file mode 100644 index 0000000000..32898faeb2 --- /dev/null +++ b/src/test/suite/fixtures/recorded/positions/takeStartOfWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: take start of whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: start} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + thatMark: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: start}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/bringHarpToAfterFile.yml b/src/test/suite/fixtures/recorded/selectionTypes/bringHarpToAfterFile.yml new file mode 100644 index 0000000000..a5640e4535 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/bringHarpToAfterFile.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: bring harp to after file + version: 2 + action: {name: replaceWithTarget} + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + modifiers: [] + - type: primitive + modifiers: + - {type: position, position: after} + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + marks: + default.h: + start: {line: 0, character: 0} + end: {line: 0, character: 5} +finalState: + documentContents: |- + hello world + hello + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 5} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: []}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: document}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/bringWhaleToBeforeFile.yml b/src/test/suite/fixtures/recorded/selectionTypes/bringWhaleToBeforeFile.yml new file mode 100644 index 0000000000..e2ee67f1e6 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/bringWhaleToBeforeFile.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: bring whale to before file + version: 2 + action: {name: replaceWithTarget} + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: [] + - type: primitive + modifiers: + - {type: position, position: before} + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: |- + world + hello world + selections: + - anchor: {line: 1, character: 11} + active: {line: 1, character: 11} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} + sourceMark: + - anchor: {line: 1, character: 6} + active: {line: 1, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: []}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: before}, {type: containingScope, scopeType: document}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockAir.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockAir.yml index fd8f46eb1d..27f2d0c7c2 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockAir.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockAir.yml @@ -22,14 +22,13 @@ initialState: start: {line: 3, character: 6} end: {line: 3, character: 11} finalState: - documentContents: |+ + documentContents: | const value = "Hello world"; - selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} thatMark: - - anchor: {line: 3, character: 0} - active: {line: 3, character: 0} + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, selectionType: paragraph, position: contents, modifier: {type: identity}, insideOutsideType: outside}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockHarpBetweenFine.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockHarpBetweenFine.yml new file mode 100644 index 0000000000..b2047db96b --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockHarpBetweenFine.yml @@ -0,0 +1,48 @@ +languageId: plaintext +command: + spokenForm: chuck block harp between fine + version: 2 + action: {name: remove} + targets: + - type: range + excludeAnchor: true + excludeActive: true + anchor: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + active: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + hello world + + + foo bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.h: + start: {line: 1, character: 0} + end: {line: 1, character: 5} + default.f: + start: {line: 4, character: 0} + end: {line: 4, character: 3} +finalState: + documentContents: | + + hello world + foo bar + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: range, excludeAnchor: true, excludeActive: true, rangeType: continuous, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: &ref_0 [{type: containingScope, scopeType: paragraph}]}, active: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: *ref_0}}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockVest.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockVest.yml index 34c5915180..32dbc37684 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockVest.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckBlockVest.yml @@ -1,4 +1,4 @@ -languageId: typescript +languageId: plaintext command: version: 1 spokenForm: chuck block vest diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckFile2.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckFile2.yml new file mode 100644 index 0000000000..893e2b7e68 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckFile2.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: chuck file + version: 2 + action: {name: remove} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true +initialState: + documentContents: |2+ + + + foo + + bar + + selections: + - anchor: {line: 6, character: 0} + active: {line: 6, character: 0} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: document}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearLinePair.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearLinePair.yml new file mode 100644 index 0000000000..f2b0820e14 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearLinePair.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + spokenForm: clear line pair + version: 2 + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: line} + - type: containingScope + scopeType: {type: surroundingPair, delimiter: any} + usePrePhraseSnapshot: true +initialState: + documentContents: foo (bar) baz + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: line}, {type: surroundingPair, delimiter: any}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/drinkBlock.yml b/src/test/suite/fixtures/recorded/selectionTypes/drinkBlock.yml new file mode 100644 index 0000000000..415d6ea4f0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/drinkBlock.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: drink block + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: editNewLineBefore} +initialState: + documentContents: |2 + hello there + hello there + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |2 + + + hello there + hello there + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 2, character: 4} + active: {line: 3, character: 15} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/drinkCell.yml b/src/test/suite/fixtures/recorded/selectionTypes/drinkCell.yml index b86c6b5711..6aa20bf0ee 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/drinkCell.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/drinkCell.yml @@ -21,6 +21,6 @@ finalState: - anchor: {line: 1, character: 0} active: {line: 1, character: 0} thatMark: - - anchor: {line: 1, character: 0} - active: {line: 1, character: 0} + - anchor: {line: 3, character: 12} + active: {line: 3, character: 12} fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: notebookCell, position: contents, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/drinkCellEach.yml b/src/test/suite/fixtures/recorded/selectionTypes/drinkCellEach.yml index bc152f2168..18344eaed4 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/drinkCellEach.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/drinkCellEach.yml @@ -32,6 +32,6 @@ finalState: - anchor: {line: 1, character: 0} active: {line: 1, character: 0} thatMark: - - anchor: {line: 1, character: 0} - active: {line: 1, character: 0} + - anchor: {line: 3, character: 7} + active: {line: 3, character: 12} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, selectionType: notebookCell, position: contents, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/drinkEveryArg.yml b/src/test/suite/fixtures/recorded/selectionTypes/drinkEveryArg.yml new file mode 100644 index 0000000000..fa6878b933 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/drinkEveryArg.yml @@ -0,0 +1,40 @@ +languageId: typescript +command: + spokenForm: drink every arg + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: editNewLineBefore} +initialState: + documentContents: |- + function whatever(a: number, b: number, c: number) { + + } + selections: + - anchor: {line: 0, character: 29} + active: {line: 0, character: 29} + marks: {} +finalState: + documentContents: |- + function whatever(, a: number, , b: number, , c: number) { + + } + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + - anchor: {line: 0, character: 31} + active: {line: 0, character: 31} + - anchor: {line: 0, character: 44} + active: {line: 0, character: 44} + thatMark: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 29} + - anchor: {line: 0, character: 33} + active: {line: 0, character: 42} + - anchor: {line: 0, character: 46} + active: {line: 0, character: 55} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/drinkJustFine.yml b/src/test/suite/fixtures/recorded/selectionTypes/drinkJustFine.yml new file mode 100644 index 0000000000..6cbc05151c --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/drinkJustFine.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: drink just fine + version: 2 + action: {name: editNewLineBefore} + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + modifiers: + - {type: toRawSelection} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: f}, modifiers: [{type: toRawSelection}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/drinkToken.yml b/src/test/suite/fixtures/recorded/selectionTypes/drinkToken.yml new file mode 100644 index 0000000000..269d794b11 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/drinkToken.yml @@ -0,0 +1,26 @@ +languageId: python +command: + spokenForm: drink token + version: 2 + action: {name: editNewLineBefore} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: " foo" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: token}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/pourBlock.yml b/src/test/suite/fixtures/recorded/selectionTypes/pourBlock.yml new file mode 100644 index 0000000000..99cf49af82 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/pourBlock.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: pour block + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: editNewLineAfter} +initialState: + documentContents: |2 + hello there + hello there + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 9} + marks: {} +finalState: + documentContents: |2 + hello there + hello there + + + selections: + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 1, character: 15} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/pourCell.yml b/src/test/suite/fixtures/recorded/selectionTypes/pourCell.yml index 4a4c3f5252..36c9c030b6 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/pourCell.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/pourCell.yml @@ -21,6 +21,6 @@ finalState: - anchor: {line: 3, character: 0} active: {line: 3, character: 0} thatMark: - - anchor: {line: 3, character: 0} - active: {line: 3, character: 0} + - anchor: {line: 1, character: 12} + active: {line: 1, character: 12} fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: notebookCell, position: contents, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/pourCellEach.yml b/src/test/suite/fixtures/recorded/selectionTypes/pourCellEach.yml index e034cdd54d..16de857e1d 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/pourCellEach.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/pourCellEach.yml @@ -32,6 +32,6 @@ finalState: - anchor: {line: 4, character: 0} active: {line: 4, character: 0} thatMark: - - anchor: {line: 4, character: 0} - active: {line: 4, character: 0} + - anchor: {line: 1, character: 7} + active: {line: 1, character: 12} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: e}, selectionType: notebookCell, position: contents, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/pourEveryArg.yml b/src/test/suite/fixtures/recorded/selectionTypes/pourEveryArg.yml new file mode 100644 index 0000000000..a538ba34da --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/pourEveryArg.yml @@ -0,0 +1,40 @@ +languageId: typescript +command: + spokenForm: pour every arg + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: editNewLineAfter} +initialState: + documentContents: |- + function whatever(a: number, b: number, c: number) { + + } + selections: + - anchor: {line: 0, character: 29} + active: {line: 0, character: 29} + marks: {} +finalState: + documentContents: |- + function whatever(a: number, , b: number, , c: number, ) { + + } + selections: + - anchor: {line: 0, character: 29} + active: {line: 0, character: 29} + - anchor: {line: 0, character: 42} + active: {line: 0, character: 42} + - anchor: {line: 0, character: 55} + active: {line: 0, character: 55} + thatMark: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 27} + - anchor: {line: 0, character: 31} + active: {line: 0, character: 40} + - anchor: {line: 0, character: 44} + active: {line: 0, character: 53} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock.yml new file mode 100644 index 0000000000..c909fde190 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock.yml @@ -0,0 +1,42 @@ +languageId: plaintext +command: + spokenForm: take every block + version: 2 + action: {name: setSelection} + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 6, character: 0} + active: {line: 6, character: 0} + marks: {} +finalState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 5, character: 5} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 5, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: paragraph}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock2.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock2.yml new file mode 100644 index 0000000000..beba4f46e9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock2.yml @@ -0,0 +1,42 @@ +languageId: plaintext +command: + spokenForm: take every block + version: 2 + action: {name: setSelection} + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 4, character: 5} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 1} + active: {line: 1, character: 0} + - anchor: {line: 5, character: 5} + active: {line: 3, character: 0} + thatMark: + - anchor: {line: 1, character: 1} + active: {line: 1, character: 0} + - anchor: {line: 5, character: 5} + active: {line: 3, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: paragraph}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock3.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock3.yml new file mode 100644 index 0000000000..2f1baaa1ed --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryBlock3.yml @@ -0,0 +1,42 @@ +languageId: plaintext +command: + spokenForm: take every block + version: 2 + action: {name: setSelection} + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 0} + active: {line: 4, character: 5} + marks: {} +finalState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 5, character: 5} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 5, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: paragraph}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryFile.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryFile.yml new file mode 100644 index 0000000000..8d11d3f324 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryFile.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: take every file + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: document} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |2+ + + + foo + + bar + + selections: + - anchor: {line: 6, character: 0} + active: {line: 6, character: 0} + marks: {} +finalState: + documentContents: |2+ + + + foo + + bar + + selections: + - anchor: {line: 0, character: 0} + active: {line: 6, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 6, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: document}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine.yml new file mode 100644 index 0000000000..cbef2964cc --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine.yml @@ -0,0 +1,50 @@ +languageId: plaintext +command: + spokenForm: take every line + version: 2 + action: {name: setSelection} + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: line} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 6, character: 0} + active: {line: 6, character: 0} + marks: {} +finalState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 3, character: 5} + - anchor: {line: 4, character: 0} + active: {line: 4, character: 5} + - anchor: {line: 5, character: 0} + active: {line: 5, character: 5} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 3, character: 5} + - anchor: {line: 4, character: 0} + active: {line: 4, character: 5} + - anchor: {line: 5, character: 0} + active: {line: 5, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: line}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine2.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine2.yml new file mode 100644 index 0000000000..fcd020adf4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine2.yml @@ -0,0 +1,46 @@ +languageId: plaintext +command: + spokenForm: take every line + version: 2 + action: {name: setSelection} + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: line} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 4, character: 5} + active: {line: 1, character: 0} + marks: {} +finalState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 1} + active: {line: 1, character: 0} + - anchor: {line: 3, character: 5} + active: {line: 3, character: 0} + - anchor: {line: 4, character: 5} + active: {line: 4, character: 0} + thatMark: + - anchor: {line: 1, character: 1} + active: {line: 1, character: 0} + - anchor: {line: 3, character: 5} + active: {line: 3, character: 0} + - anchor: {line: 4, character: 5} + active: {line: 4, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: line}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine3.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine3.yml new file mode 100644 index 0000000000..a473f3c06b --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryLine3.yml @@ -0,0 +1,46 @@ +languageId: plaintext +command: + spokenForm: take every line + version: 2 + action: {name: setSelection} + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: line} + usePrePhraseSnapshot: true +initialState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 0} + active: {line: 4, character: 5} + marks: {} +finalState: + documentContents: | + + a + + b c + d e + f g + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 3, character: 5} + - anchor: {line: 4, character: 0} + active: {line: 4, character: 5} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 3, character: 0} + active: {line: 3, character: 5} + - anchor: {line: 4, character: 0} + active: {line: 4, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: line}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeFile.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeFile.yml index 7d546f6ee1..874f7e2711 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/takeFile.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeFile.yml @@ -1,26 +1,38 @@ -languageId: typescript +languageId: plaintext command: - version: 1 spokenForm: take file - action: setSelection + version: 2 targets: - - {type: primitive, selectionType: document} + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true + action: {name: setSelection} initialState: - documentContents: | + documentContents: |2+ + + + foo + + bar - const value = "Hello world"; selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} + - anchor: {line: 6, character: 0} + active: {line: 6, character: 0} marks: {} finalState: - documentContents: | + documentContents: |2+ + + + foo + + bar - const value = "Hello world"; selections: - anchor: {line: 0, character: 0} - active: {line: 2, character: 0} + active: {line: 6, character: 0} thatMark: - anchor: {line: 0, character: 0} - active: {line: 2, character: 0} -fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: document, position: contents, modifier: {type: identity}, insideOutsideType: inside}] + active: {line: 6, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: document}}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml new file mode 100644 index 0000000000..cf8ae2f724 --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml @@ -0,0 +1,37 @@ +languageId: typescript +command: + spokenForm: take core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + - type: containingScope + scopeType: {type: surroundingPair, delimiter: any} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: | + async (editor, targets) => { + const edits = targets.map((target, i) =>( + text: i%2 === 0?left:right + ) + } + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + marks: {} +finalState: + documentContents: | + async (editor, targets) => { + const edits = targets.map((target, i) =>( + text: i%2 === 0?left:right + ) + } + selections: + - anchor: {line: 1, character: 45} + active: {line: 3, character: 4} + thatMark: + - anchor: {line: 1, character: 45} + active: {line: 3, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: surroundingPair, delimiter: any}}, {type: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairDouble.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsDouble.yml similarity index 96% rename from src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairDouble.yml rename to src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsDouble.yml index 4512d75e4d..4f1f3520bc 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairDouble.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsDouble.yml @@ -1,7 +1,7 @@ languageId: typescript command: version: 1 - spokenForm: clear pair double + spokenForm: clear bounds double action: clearAndSetSelection targets: - type: primitive diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairRound.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsRound.yml similarity index 95% rename from src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairRound.yml rename to src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsRound.yml index 8368988817..384343be9d 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairRound.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsRound.yml @@ -1,7 +1,7 @@ languageId: typescript command: version: 1 - spokenForm: clear pair round + spokenForm: clear bounds round action: clearAndSetSelection targets: - type: primitive diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairRound2.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsRound2.yml similarity index 96% rename from src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairRound2.yml rename to src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsRound2.yml index 41102831e0..f31d48c666 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/takePairRound2.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTreeParity/clearBoundsRound2.yml @@ -1,7 +1,7 @@ languageId: typescript command: version: 1 - spokenForm: clear pair round + spokenForm: clear bounds round action: clearAndSetSelection targets: - type: primitive diff --git a/src/test/suite/fixtures/recorded/surroundingPair/textual/takePairDouble.yml b/src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsDouble.yml similarity index 96% rename from src/test/suite/fixtures/recorded/surroundingPair/textual/takePairDouble.yml rename to src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsDouble.yml index 00ae470728..d58ca06205 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/textual/takePairDouble.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsDouble.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 1 - spokenForm: clear pair double + spokenForm: clear bounds double action: clearAndSetSelection targets: - type: primitive diff --git a/src/test/suite/fixtures/recorded/surroundingPair/textual/takePairRound.yml b/src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsRound.yml similarity index 95% rename from src/test/suite/fixtures/recorded/surroundingPair/textual/takePairRound.yml rename to src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsRound.yml index 61767e1f87..1226f21476 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/textual/takePairRound.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsRound.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 1 - spokenForm: clear pair round + spokenForm: clear bounds round action: clearAndSetSelection targets: - type: primitive diff --git a/src/test/suite/fixtures/recorded/surroundingPair/textual/takePairRound2.yml b/src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsRound2.yml similarity index 96% rename from src/test/suite/fixtures/recorded/surroundingPair/textual/takePairRound2.yml rename to src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsRound2.yml index bc2c5d6d41..aefae8a424 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/textual/takePairRound2.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/textual/clearBoundsRound2.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 1 - spokenForm: clear pair round + spokenForm: clear bounds round action: clearAndSetSelection targets: - type: primitive diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml b/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml index 10f6234510..1263de63de 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml @@ -24,7 +24,7 @@ finalState: - anchor: {line: 0, character: 4} active: {line: 0, character: 4} thatMark: - - anchor: {line: 0, character: 4} + - anchor: {line: 0, character: 5} active: {line: 0, character: 8} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis2.yml b/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis2.yml index 1267f27294..0a8ea09faf 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis2.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringFineAfterThis2.yml @@ -24,7 +24,7 @@ finalState: - anchor: {line: 0, character: 9} active: {line: 0, character: 9} thatMark: - - anchor: {line: 0, character: 9} + - anchor: {line: 0, character: 10} active: {line: 0, character: 13} sourceMark: - anchor: {line: 0, character: 0} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringFineBeforeThis.yml b/src/test/suite/fixtures/recorded/updateSelections/bringFineBeforeThis.yml index bce7f981d2..c8e880d6fd 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringFineBeforeThis.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringFineBeforeThis.yml @@ -25,7 +25,7 @@ finalState: active: {line: 0, character: 8} thatMark: - anchor: {line: 0, character: 4} - active: {line: 0, character: 8} + active: {line: 0, character: 7} sourceMark: - anchor: {line: 0, character: 0} active: {line: 0, character: 3} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterFine.yml b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterFine.yml index 94b44d205b..12a3be2b2b 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterFine.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterFine.yml @@ -27,7 +27,7 @@ finalState: - anchor: {line: 0, character: 3} active: {line: 0, character: 3} thatMark: - - anchor: {line: 0, character: 3} + - anchor: {line: 0, character: 4} active: {line: 0, character: 9} sourceMark: - anchor: {line: 0, character: 10} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis.yml b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis.yml index e96ae1b561..381bbd053f 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis.yml @@ -24,7 +24,7 @@ finalState: - anchor: {line: 0, character: 3} active: {line: 0, character: 3} thatMark: - - anchor: {line: 0, character: 3} + - anchor: {line: 0, character: 4} active: {line: 0, character: 9} sourceMark: - anchor: {line: 0, character: 10} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis2.yml b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis2.yml index e96ae1b561..381bbd053f 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis2.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleAfterThis2.yml @@ -24,7 +24,7 @@ finalState: - anchor: {line: 0, character: 3} active: {line: 0, character: 3} thatMark: - - anchor: {line: 0, character: 3} + - anchor: {line: 0, character: 4} active: {line: 0, character: 9} sourceMark: - anchor: {line: 0, character: 10} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml index dbd2c11525..3170e30f05 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml @@ -25,7 +25,7 @@ finalState: active: {line: 0, character: 9} thatMark: - anchor: {line: 0, character: 3} - active: {line: 0, character: 9} + active: {line: 0, character: 8} sourceMark: - anchor: {line: 0, character: 10} active: {line: 0, character: 15} diff --git a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleToEndOfFine.yml b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleToEndOfFine.yml index 6447d9784f..df49740867 100644 --- a/src/test/suite/fixtures/recorded/updateSelections/bringWhaleToEndOfFine.yml +++ b/src/test/suite/fixtures/recorded/updateSelections/bringWhaleToEndOfFine.yml @@ -25,8 +25,8 @@ initialState: finalState: documentContents: fooworld world selections: - - anchor: {line: 0, character: 3} - active: {line: 0, character: 3} + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} thatMark: - anchor: {line: 0, character: 3} active: {line: 0, character: 8} diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 2cb7d7d8b3..5939c8e50d 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,12 +1,14 @@ -import * as path from "path"; -import * as Mocha from "mocha"; import * as glob from "glob"; +import * as Mocha from "mocha"; +import * as path from "path"; +import { runTestSubset, TEST_SUBSET_GREP_STRING } from "./runTestSubset"; export function run(): Promise { // Create the mocha test const mocha = new Mocha({ ui: "tdd", color: true, + grep: runTestSubset() ? TEST_SUBSET_GREP_STRING : undefined, // Only run a subset of tests }); const testsRoot = path.resolve(__dirname, ".."); diff --git a/src/test/suite/inferFullTargets.test.ts b/src/test/suite/inferFullTargets.test.ts deleted file mode 100644 index 052237e6e8..0000000000 --- a/src/test/suite/inferFullTargets.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as assert from "assert"; - -import inferFullTargets from "../../core/inferFullTargets"; -import fixture from "./fixtures/inferFullTargets.fixture"; - -/* - * FIXME: These tests are outdated and thus disabled for now - */ -suite.skip("inferFullTargets", () => { - fixture.forEach(({ input, expectedOutput }, index) => { - test(`inferFullTargets ${index}`, () => { - assert.deepStrictEqual( - inferFullTargets(input.partialTargets, input.actionPreferences), - expectedOutput - ); - }); - }); -}); diff --git a/src/test/suite/recorded.test.ts b/src/test/suite/recorded.test.ts index 48ba988c4b..24aebf2f6a 100644 --- a/src/test/suite/recorded.test.ts +++ b/src/test/suite/recorded.test.ts @@ -1,28 +1,30 @@ import * as assert from "assert"; -import serialize from "../../testUtil/serialize"; import { promises as fsp } from "fs"; import * as yaml from "js-yaml"; +import * as sinon from "sinon"; import * as vscode from "vscode"; -import { TestCaseFixture } from "../../testUtil/TestCase"; import HatTokenMap from "../../core/HatTokenMap"; -import * as sinon from "sinon"; -import { Clipboard } from "../../util/Clipboard"; +import { ReadOnlyHatMap } from "../../core/IndividualHatMap"; +import { extractTargetedMarks } from "../../testUtil/extractTargetedMarks"; +import serialize from "../../testUtil/serialize"; import { ExcludableSnapshotField, takeSnapshot, } from "../../testUtil/takeSnapshot"; +import { TestCaseFixture } from "../../testUtil/TestCase"; import { marksToPlainObject, PositionPlainObject, rangeToPlainObject, SelectionPlainObject, SerializedMarks, + testDecorationsToPlainObject, } from "../../testUtil/toPlainObject"; +import { Clipboard } from "../../util/Clipboard"; import { getCursorlessApi } from "../../util/getExtensionApi"; -import { extractTargetedMarks } from "../../testUtil/extractTargetedMarks"; -import asyncSafety from "../util/asyncSafety"; -import { ReadOnlyHatMap } from "../../core/IndividualHatMap"; +import sleep from "../../util/sleep"; import { openNewEditor } from "../openNewEditor"; +import asyncSafety from "../util/asyncSafety"; import { getRecordedTestPaths } from "../util/getFixturePaths"; function createPosition(position: PositionPlainObject) { @@ -43,6 +45,11 @@ suite("recorded test cases", async function () { sinon.restore(); }); + suiteSetup(async () => { + // Necessary because opening a notebook opens the panel for some reason + await vscode.commands.executeCommand("workbench.action.closePanel"); + }); + getRecordedTestPaths().forEach((path) => test( path.split(".")[0], @@ -56,14 +63,23 @@ async function runTest(file: string) { const fixture = yaml.load(buffer.toString()) as TestCaseFixture; const excludeFields: ExcludableSnapshotField[] = []; + // TODO The snapshot gets messed up with timing issues when running the recorded tests + // "Couldn't find token default.a" + const usePrePhraseSnapshot = false; + const cursorlessApi = await getCursorlessApi(); const graph = cursorlessApi.graph!; + graph.editStyles.testDecorations = []; const editor = await openNewEditor( fixture.initialState.documentContents, fixture.languageId ); + if (fixture.postEditorOpenSleepTimeMs != null) { + await sleep(fixture.postEditorOpenSleepTimeMs); + } + if (!fixture.initialState.documentContents.includes("\n")) { await editor.edit((editBuilder) => { editBuilder.setEndOfLine(vscode.EndOfLine.LF); @@ -101,7 +117,9 @@ async function runTest(file: string) { await graph.hatTokenMap.addDecorations(); - const readableHatMap = await graph.hatTokenMap.getReadableMap(false); + const readableHatMap = await graph.hatTokenMap.getReadableMap( + usePrePhraseSnapshot + ); // Assert that recorded decorations are present checkMarks(fixture.initialState.marks, readableHatMap); @@ -124,7 +142,7 @@ async function runTest(file: string) { const returnValue = await vscode.commands.executeCommand( "cursorless.command", - fixture.command + { ...fixture.command, usePrePhraseSnapshot } ); const marks = @@ -147,8 +165,19 @@ async function runTest(file: string) { marks ); + const actualDecorations = + fixture.decorations == null + ? undefined + : testDecorationsToPlainObject(graph.editStyles.testDecorations); + if (process.env.CURSORLESS_TEST_UPDATE_FIXTURES === "true") { - const outputFixture = { ...fixture, finalState: resultState, returnValue }; + const outputFixture = { + ...fixture, + finalState: resultState, + decorations: actualDecorations, + returnValue, + }; + await fsp.writeFile(file, serialize(outputFixture)); } else { assert.deepStrictEqual( @@ -157,6 +186,12 @@ async function runTest(file: string) { "Unexpected final state" ); + assert.deepStrictEqual( + actualDecorations, + fixture.decorations, + "Unexpected decorations" + ); + assert.deepStrictEqual( returnValue, fixture.returnValue, diff --git a/src/test/suite/runTestSubset.ts b/src/test/suite/runTestSubset.ts new file mode 100644 index 0000000000..c3c37bf1d4 --- /dev/null +++ b/src/test/suite/runTestSubset.ts @@ -0,0 +1,16 @@ +/** + * The grep string to pass to Mocha when running a subset of tests. This grep + * string will be used with the "Run Single Extension Test" launch + * configuration. + * See https://mochajs.org/#-grep-regexp-g-regexp for supported syntax + */ +export const TEST_SUBSET_GREP_STRING = "actions/insertEmptyLines"; + +/** + * Determine whether we should run just the subset of the tests specified by + * {@link TEST_SUBSET_GREP_STRING}. + * @returns `true` if we are using the run test subset launch config + */ +export function runTestSubset() { + return process.env.CURSORLESS_RUN_TEST_SUBSET === "true"; +} diff --git a/src/testUtil/TestCase.ts b/src/testUtil/TestCase.ts index b8bd083dd6..1bb6248968 100644 --- a/src/testUtil/TestCase.ts +++ b/src/testUtil/TestCase.ts @@ -1,37 +1,53 @@ +import { pick } from "lodash"; import * as vscode from "vscode"; +import { CommandLatest } from "../core/commandRunner/command.types"; +import { TestDecoration } from "../core/editStyles"; +import { ReadOnlyHatMap } from "../core/IndividualHatMap"; import { ThatMark } from "../core/ThatMark"; -import { Target, Token } from "../typings/Types"; +import { TargetDescriptor } from "../typings/targetDescriptor.types"; +import { Token } from "../typings/Types"; +import { cleanUpTestCaseCommand } from "./cleanUpTestCaseCommand"; import { extractTargetedMarks, extractTargetKeys, } from "./extractTargetedMarks"; -import { marksToPlainObject, SerializedMarks } from "./toPlainObject"; +import serialize from "./serialize"; import { ExtraSnapshotField, takeSnapshot, TestCaseSnapshot, } from "./takeSnapshot"; -import serialize from "./serialize"; -import { pick } from "lodash"; -import { ReadOnlyHatMap } from "../core/IndividualHatMap"; -import { CommandArgument } from "../core/commandRunner/types"; -import { cleanUpTestCaseCommand } from "./cleanUpTestCaseCommand"; +import { + marksToPlainObject, + PositionPlainObject, + SerializedMarks, + testDecorationsToPlainObject, +} from "./toPlainObject"; -export type TestCaseCommand = CommandArgument; +export type TestCaseCommand = CommandLatest; export type TestCaseContext = { thatMark: ThatMark; sourceMark: ThatMark; - targets: Target[]; + targets: TargetDescriptor[]; + decorations: TestDecoration[]; hatTokenMap: ReadOnlyHatMap; }; +interface PlainTestDecoration { + name: string; + type: "token" | "line"; + start: PositionPlainObject; + end: PositionPlainObject; +} + export type ThrownError = { name: string; }; export type TestCaseFixture = { languageId: string; + postEditorOpenSleepTimeMs?: number; command: TestCaseCommand; /** @@ -40,19 +56,21 @@ export type TestCaseFixture = { marksToCheck?: string[]; initialState: TestCaseSnapshot; + decorations?: PlainTestDecoration[]; /** The final state after a command is issued. Undefined if we are testing a non-match(error) case. */ finalState?: TestCaseSnapshot; /** Used to assert if an error has been thrown. */ thrownError?: ThrownError; returnValue: unknown; /** Inferred full targets added for context; not currently used in testing */ - fullTargets: Target[]; + fullTargets: TargetDescriptor[]; }; export class TestCase { languageId: string; - fullTargets: Target[]; + fullTargets: TargetDescriptor[]; initialState: TestCaseSnapshot | null = null; + decorations?: PlainTestDecoration[]; finalState?: TestCaseSnapshot; thrownError?: ThrownError; returnValue: unknown = null; @@ -65,6 +83,7 @@ export class TestCase { command: TestCaseCommand, private context: TestCaseContext, private isHatTokenMapTest: boolean = false, + private isDecorationsTest: boolean = false, private startTimestamp: bigint, private extraSnapshotFields?: ExtraSnapshotField[] ) { @@ -80,6 +99,13 @@ export class TestCase { this._awaitingFinalMarkInfo = isHatTokenMapTest; } + recordDecorations() { + const decorations = this.context.decorations; + if (this.isDecorationsTest && decorations.length > 0) { + this.decorations = testDecorationsToPlainObject(decorations); + } + } + private getMarks() { let marks: Record; @@ -97,7 +123,7 @@ export class TestCase { return marksToPlainObject(marks); } - private includesThatMark(target: Target, type: string): boolean { + private includesThatMark(target: TargetDescriptor, type: string): boolean { if (target.type === "primitive" && target.mark.type === type) { return true; } else if (target.type === "list") { @@ -114,7 +140,7 @@ export class TestCase { private getExcludedFields(context?: { initialSnapshot?: boolean }) { const excludableFields = { - clipboard: !["copy", "paste"].includes(this.command.action), + clipboard: !["copy", "paste"].includes(this.command.action.name), thatMark: context?.initialSnapshot && !this.fullTargets.some((target) => @@ -131,7 +157,7 @@ export class TestCase { "scrollToBottom", "scrollToCenter", "scrollToTop", - ].includes(this.command.action), + ].includes(this.command.action.name), }; return Object.keys(excludableFields).filter( @@ -152,6 +178,7 @@ export class TestCase { marksToCheck: this.marksToCheck, initialState: this.initialState, finalState: this.finalState, + decorations: this.decorations, returnValue: this.returnValue, fullTargets: this.fullTargets, thrownError: this.thrownError, diff --git a/src/testUtil/TestCaseRecorder.ts b/src/testUtil/TestCaseRecorder.ts index caa86314b6..665ef06b1e 100644 --- a/src/testUtil/TestCaseRecorder.ts +++ b/src/testUtil/TestCaseRecorder.ts @@ -3,7 +3,8 @@ import { invariant } from "immutability-helper"; import * as path from "path"; import * as vscode from "vscode"; import HatTokenMap from "../core/HatTokenMap"; -import { DecoratedSymbol, Graph } from "../typings/Types"; +import { Graph } from "../typings/Types"; +import { DecoratedSymbolMark } from "../typings/targetDescriptor.types"; import { getDocumentRange } from "../util/range"; import sleep from "../util/sleep"; import { extractTargetedMarks } from "./extractTargetedMarks"; @@ -28,6 +29,9 @@ interface RecordTestCaseCommandArg { */ isHatTokenMapTest?: boolean; + /** If true decorations will be added to the test fixture */ + isDecorationsTest?: boolean; + /** * The directory in which to store the test cases that we record. If left out * the user will be prompted to select a directory within the default recorded @@ -63,6 +67,7 @@ export class TestCaseRecorder { private targetDirectory: string | null = null; private testCase: TestCase | null = null; private isHatTokenMapTest: boolean = false; + private isDecorationsTest: boolean = false; private disposables: vscode.Disposable[] = []; private isSilent?: boolean; private startTimestamp?: bigint; @@ -130,7 +135,7 @@ export class TestCaseRecorder { async ( outPath: string, metadata: unknown, - targetedMarks: DecoratedSymbol[], + targetedMarks: DecoratedSymbolMark[], usePrePhraseSnapshot: boolean ) => { let marks: SerializedMarks | undefined; @@ -171,6 +176,7 @@ export class TestCaseRecorder { async start(arg?: RecordTestCaseCommandArg) { const { isHatTokenMapTest = false, + isDecorationsTest = false, directory, isSilent = false, extraSnapshotFields = [], @@ -193,6 +199,7 @@ export class TestCaseRecorder { this.startTimestamp = process.hrtime.bigint(); const timestampISO = new Date().toISOString(); this.isHatTokenMapTest = isHatTokenMapTest; + this.isDecorationsTest = isDecorationsTest; this.isSilent = isSilent; this.extraSnapshotFields = extraSnapshotFields; this.isErrorTest = isErrorTest; @@ -244,6 +251,7 @@ export class TestCaseRecorder { command, context, this.isHatTokenMapTest, + this.isDecorationsTest, this.startTimestamp!, this.extraSnapshotFields ); @@ -267,6 +275,8 @@ export class TestCaseRecorder { return; } + this.testCase.recordDecorations(); + await this.finishTestCase(); } diff --git a/src/testUtil/cleanUpTestCaseCommand.ts b/src/testUtil/cleanUpTestCaseCommand.ts index c1822abba2..63edfa6083 100644 --- a/src/testUtil/cleanUpTestCaseCommand.ts +++ b/src/testUtil/cleanUpTestCaseCommand.ts @@ -3,15 +3,14 @@ import { TestCaseCommand } from "./TestCase"; export function cleanUpTestCaseCommand( command: TestCaseCommand ): TestCaseCommand { - const { extraArgs, usePrePhraseSnapshot, ...rest } = command; + const { action, ...rest } = command; + const { args } = action; return { ...rest, - extraArgs: - extraArgs == null - ? undefined - : extraArgs.length === 0 - ? undefined - : extraArgs, + action: { + ...action, + args: args == null ? undefined : args.length === 0 ? undefined : args, + }, }; } diff --git a/src/testUtil/extractTargetedMarks.ts b/src/testUtil/extractTargetedMarks.ts index dc4366e089..11235db1d3 100644 --- a/src/testUtil/extractTargetedMarks.ts +++ b/src/testUtil/extractTargetedMarks.ts @@ -1,8 +1,12 @@ import { ReadOnlyHatMap } from "../core/IndividualHatMap"; import HatTokenMap from "../core/HatTokenMap"; -import { PrimitiveTarget, Target, Token } from "../typings/Types"; +import { Token } from "../typings/Types"; +import { + PrimitiveTargetDescriptor, + TargetDescriptor, +} from "../typings/targetDescriptor.types"; -function extractPrimitiveTargetKeys(...targets: PrimitiveTarget[]) { +function extractPrimitiveTargetKeys(...targets: PrimitiveTargetDescriptor[]) { const keys: string[] = []; targets.forEach((target) => { if (target.mark.type === "decoratedSymbol") { @@ -13,7 +17,7 @@ function extractPrimitiveTargetKeys(...targets: PrimitiveTarget[]) { return keys; } -export function extractTargetKeys(target: Target): string[] { +export function extractTargetKeys(target: TargetDescriptor): string[] { switch (target.type) { case "primitive": return extractPrimitiveTargetKeys(target); diff --git a/src/testUtil/toPlainObject.ts b/src/testUtil/toPlainObject.ts index 8d4ac974de..fe415bdb3f 100644 --- a/src/testUtil/toPlainObject.ts +++ b/src/testUtil/toPlainObject.ts @@ -1,4 +1,5 @@ import { Selection, Position, Range } from "vscode"; +import { TestDecoration } from "../core/editStyles"; import { Token } from "../typings/Types"; export type PositionPlainObject = { @@ -50,3 +51,12 @@ export function marksToPlainObject(marks: { ); return serializedMarks; } + +export function testDecorationsToPlainObject(decorations: TestDecoration[]) { + return decorations.map(({ name, type, start, end }) => ({ + name, + type, + start: positionToPlainObject(start), + end: positionToPlainObject(end), + })); +} diff --git a/src/typings/Types.ts b/src/typings/Types.ts index 4a47ceb11f..00cc00bcef 100644 --- a/src/typings/Types.ts +++ b/src/typings/Types.ts @@ -1,18 +1,19 @@ -import { SyntaxNode } from "web-tree-sitter"; import * as vscode from "vscode"; -import { ExtensionContext, Location, Selection } from "vscode"; -import { HatStyleName } from "../core/constants"; +import { ExtensionContext, Location } from "vscode"; +import { SyntaxNode } from "web-tree-sitter"; +import { ActionRecord } from "../actions/actions.types"; +import Debug from "../core/Debug"; +import Decorations from "../core/Decorations"; import { EditStyles } from "../core/editStyles"; +import FontMeasurements from "../core/FontMeasurements"; import HatTokenMap from "../core/HatTokenMap"; +import { ReadOnlyHatMap } from "../core/IndividualHatMap"; import { Snippets } from "../core/Snippets"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { FullRangeInfo } from "./updateSelections"; -import Decorations from "../core/Decorations"; -import FontMeasurements from "../core/FontMeasurements"; -import { CommandServerApi } from "../util/getExtensionApi"; -import { ReadOnlyHatMap } from "../core/IndividualHatMap"; -import Debug from "../core/Debug"; +import { ModifierStage } from "../processTargets/PipelineStages.types"; import { TestCaseRecorder } from "../testUtil/TestCaseRecorder"; +import { CommandServerApi } from "../util/getExtensionApi"; +import { FullRangeInfo } from "./updateSelections"; /** * A token within a text editor, including the current display line of the token @@ -22,256 +23,8 @@ export interface Token extends FullRangeInfo { displayLine: number; } -export interface CursorMark { - type: "cursor"; -} - -export interface CursorMarkToken { - type: "cursorToken"; -} - -export interface That { - type: "that"; -} - -export interface Source { - type: "source"; -} - -export interface Nothing { - type: "nothing"; -} - -export interface LastCursorPosition { - type: "lastCursorPosition"; -} - -export interface DecoratedSymbol { - type: "decoratedSymbol"; - symbolColor: HatStyleName; - character: string; -} - -export type LineNumberType = "absolute" | "relative" | "modulo100"; - -export interface LineNumberPosition { - type: LineNumberType; - lineNumber: number; -} - -export interface LineNumber { - type: "lineNumber"; - anchor: LineNumberPosition; - active: LineNumberPosition; -} - -export type Mark = - | CursorMark - | CursorMarkToken - | That - | Source - // | LastCursorPosition Not implemented yet - | DecoratedSymbol - | Nothing - | LineNumber; - -export type SimpleSurroundingPairName = - | "angleBrackets" - | "backtickQuotes" - | "curlyBrackets" - | "doubleQuotes" - | "escapedDoubleQuotes" - | "escapedParentheses" - | "escapedSquareBrackets" - | "escapedSingleQuotes" - | "parentheses" - | "singleQuotes" - | "squareBrackets"; -export type ComplexSurroundingPairName = "string" | "any"; -export type SurroundingPairName = - | SimpleSurroundingPairName - | ComplexSurroundingPairName; - -export type ScopeType = - | "argumentOrParameter" - | "anonymousFunction" - | "attribute" - | "class" - | "className" - | "collectionItem" - | "collectionKey" - | "comment" - | "functionCall" - | "functionName" - | "ifStatement" - | "list" - | "map" - | "name" - | "namedFunction" - | "regularExpression" - | "statement" - | "string" - | "type" - | "value" - | "condition" - | "section" - | "sectionLevelOne" - | "sectionLevelTwo" - | "sectionLevelThree" - | "sectionLevelFour" - | "sectionLevelFive" - | "sectionLevelSix" - | "selector" - | "xmlBothTags" - | "xmlElement" - | "xmlEndTag" - | "xmlStartTag"; - -export type SubTokenType = "word" | "character"; - -/** - * Indicates whether to include or exclude delimiters in a surrounding pair - * modifier. In the future, these will become proper modifiers that can be - * applied in many places, such as to restrict to the body of an if statement. - * By default, a surrounding pair modifier refers to the entire surrounding - * range, so if delimiter inclusion is undefined, it's equivalent to not having - * one of these modifiers; ie include the delimiters. - */ -export type DelimiterInclusion = "excludeInterior" | "interiorOnly" | undefined; - -export type SurroundingPairDirection = "left" | "right"; -export interface SurroundingPairModifier { - type: "surroundingPair"; - delimiter: SurroundingPairName; - delimiterInclusion: DelimiterInclusion; - forceDirection?: SurroundingPairDirection; -} - -export interface ContainingScopeModifier { - type: "containingScope"; - scopeType: ScopeType; - valueOnly?: boolean; - includeSiblings?: boolean; -} - -export interface SubTokenModifier { - type: "subpiece"; - pieceType: SubTokenType; - anchor: number; - active: number; - excludeAnchor?: boolean; - excludeActive?: boolean; -} - -export interface MatchingPairSymbolModifier { - type: "matchingPairSymbol"; -} - -export interface IdentityModifier { - type: "identity"; -} - -/** - * Converts its input to a raw selection with no type information so for - * example if it is the destination of a bring or move it should inherit the - * type information such as delimiters from its source. - */ -export interface RawSelectionModifier { - type: "toRawSelection"; -} - -export interface HeadModifier { - type: "head"; -} - -export interface TailModifier { - type: "tail"; -} - -export type Modifier = - | IdentityModifier - | SurroundingPairModifier - | ContainingScopeModifier - | SubTokenModifier - // | MatchingPairSymbolModifier Not implemented - | HeadModifier - | TailModifier - | RawSelectionModifier; - -export type SelectionType = - // | "character" Not implemented - | "token" - | "line" - | "notebookCell" - | "paragraph" - | "document" - | "nonWhitespaceSequence" - | "url"; - -export type Position = "before" | "after" | "contents"; - -export type InsideOutsideType = "inside" | "outside" | null; - -export interface PartialPrimitiveTarget { - type: "primitive"; - mark?: Mark; - modifier?: Modifier; - selectionType?: SelectionType; - position?: Position; - insideOutsideType?: InsideOutsideType; - isImplicit?: boolean; -} - -export interface PartialRangeTarget { - type: "range"; - start: PartialPrimitiveTarget; - end: PartialPrimitiveTarget; - excludeStart?: boolean; - excludeEnd?: boolean; - rangeType?: RangeType; -} - -export interface PartialListTarget { - type: "list"; - elements: (PartialPrimitiveTarget | PartialRangeTarget)[]; -} - -export type PartialTarget = - | PartialPrimitiveTarget - | PartialRangeTarget - | PartialListTarget; - -export interface PrimitiveTarget { - type: "primitive"; - mark: Mark; - modifier: Modifier; - selectionType: SelectionType; - position: Position; - insideOutsideType: InsideOutsideType; - isImplicit: boolean; -} - -export interface RangeTarget { - type: "range"; - anchor: PrimitiveTarget; - active: PrimitiveTarget; - excludeAnchor: boolean; - excludeActive: boolean; - rangeType: RangeType; -} - -// continuous is one single continuous selection between the two targets -// vertical puts a selection on each line vertically between the two targets -export type RangeType = "continuous" | "vertical"; - -export interface ListTarget { - type: "list"; - elements: (PrimitiveTarget | RangeTarget)[]; -} - -export type Target = PrimitiveTarget | RangeTarget | ListTarget; - export interface ProcessedTargetsContext { + finalStages: ModifierStage[]; currentSelections: SelectionWithEditor[]; currentEditor: vscode.TextEditor | undefined; hatTokenMap: ReadOnlyHatMap; @@ -285,146 +38,40 @@ export interface SelectionWithEditor { editor: vscode.TextEditor; } +export interface RangeWithEditor { + range: vscode.Range; + editor: vscode.TextEditor; +} + export interface SelectionContext { - isInDelimitedList?: boolean; - containingListDelimiter?: string | null; + containingListDelimiter?: string; /** * Selection used for outside selection */ - outerSelection?: vscode.Selection | null; + removalRange?: vscode.Range; /** * The range of the delimiter before the selection */ - leadingDelimiterRange?: vscode.Range | null; + leadingDelimiterRange?: vscode.Range; /** * The range of the delimiter after the selection */ - trailingDelimiterRange?: vscode.Range | null; - - isNotebookCell?: boolean; - - /** - * Represents the boundary ranges of this selection. For example, for a - * surrounding pair this would be the opening and closing delimiter. For an if - * statement this would be the line of the guard as well as the closing brace. - */ - boundary?: SelectionWithContext[]; - - /** - * Represents the interior ranges of this selection. For example, for a - * surrounding pair this would exclude the opening and closing delimiter. For an if - * statement this would be the statements in the body. - */ - interior?: SelectionWithContext[]; - - /** - * Indicates that this is a raw selection with no type information so for - * example if it is the destination of a bring or move it should inherit the - * type information such as delimiters from its source - */ - isRawSelection?: boolean; + trailingDelimiterRange?: vscode.Range; } -/** - * Represents a selection in a particular document along with potential rich - * context information such as how to remove the given selection - */ -export interface TypedSelection { - /** - * The selection. If insideOutsideType is non-null, it will be adjusted to - * include delimiter if outside - */ +export type SelectionWithEditorWithContext = { selection: SelectionWithEditor; - selectionType: SelectionType; - selectionContext: SelectionContext; - - /** - * Is a boolean if user specifically requested inside or outside - */ - insideOutsideType: InsideOutsideType; - - /** - * Mirrored from the target from which this selection was constructed - */ - position: Position; -} - -export interface ActionPreferences { - position?: Position; - insideOutsideType: InsideOutsideType; - selectionType?: SelectionType; - modifier?: Modifier; -} + context: SelectionContext; +}; export interface SelectionWithContext { selection: vscode.Selection; context: SelectionContext; } -export interface ActionReturnValue { - returnValue?: any; - thatMark?: SelectionWithEditor[]; - sourceMark?: SelectionWithEditor[]; -} - -export interface Action { - run(targets: TypedSelection[][], ...args: any[]): Promise; - - /** - * Used to define default values for parts of target during inference. - * @param args Extra args to command - */ - getTargetPreferences(...args: any[]): ActionPreferences[]; -} - -export type ActionType = - | "callAsFunction" - | "clearAndSetSelection" - | "copyToClipboard" - | "cutToClipboard" - | "deselect" - | "editNewLineAfter" - | "editNewLineBefore" - | "executeCommand" - | "extractVariable" - | "findInWorkspace" - | "foldRegion" - | "followLink" - | "getText" - | "highlight" - | "indentLine" - | "insertCopyAfter" - | "insertCopyBefore" - | "insertEmptyLineAfter" - | "insertEmptyLineBefore" - | "insertEmptyLinesAround" - | "moveToTarget" - | "outdentLine" - | "pasteFromClipboard" - | "remove" - | "replace" - | "replaceWithTarget" - | "reverseTargets" - | "rewrapWithPairedDelimiter" - | "scrollToBottom" - | "scrollToCenter" - | "scrollToTop" - | "setSelection" - | "setSelectionAfter" - | "setSelectionBefore" - | "sortTargets" - | "swapTargets" - | "toggleLineBreakpoint" - | "toggleLineComment" - | "unfoldRegion" - | "wrapWithPairedDelimiter" - | "wrapWithSnippet"; - -export type ActionRecord = Record; - export interface Graph { /** * Keeps a map from action names to objects that implement the given action @@ -533,3 +180,7 @@ export interface Edit { */ isReplace?: boolean; } + +export interface EditWithRangeUpdater extends Edit { + updateRange: (range: vscode.Range) => vscode.Range; +} diff --git a/src/typings/snippet.ts b/src/typings/snippet.ts index 4eb7ef3c08..940a2f9391 100644 --- a/src/typings/snippet.ts +++ b/src/typings/snippet.ts @@ -1,4 +1,4 @@ -import { ScopeType } from "./Types"; +import { ScopeType, SimpleScopeTypeType } from "./targetDescriptor.types"; export interface SnippetScope { langIds?: string[]; @@ -21,7 +21,7 @@ export interface SnippetVariable { * Default to this scope type when wrapping a target without scope type * specified. */ - wrapperScopeType?: ScopeType; + wrapperScopeType?: SimpleScopeTypeType; /** * Description of the snippet variable diff --git a/src/typings/target.types.ts b/src/typings/target.types.ts new file mode 100644 index 0000000000..080e571a21 --- /dev/null +++ b/src/typings/target.types.ts @@ -0,0 +1,71 @@ +import { Range, Selection, TextEditor } from "vscode"; +import { EditWithRangeUpdater } from "./Types"; + +export interface EditNewCommandContext { + type: "command"; + command: string; +} +export interface EditNewDelimiterContext { + type: "delimiter"; + delimiter: string; +} + +export type EditNewContext = EditNewCommandContext | EditNewDelimiterContext; + +export interface Target { + /** The text editor used for all ranges */ + readonly editor: TextEditor; + + /** If true active is before anchor */ + readonly isReversed: boolean; + + /** The range of the content */ + readonly contentRange: Range; + + /** If this selection has a delimiter use it for inserting before or after the target. For example, new line for a line or paragraph and comma for a list or argument */ + readonly insertionDelimiter: string; + + /** If true this target should be treated as a line */ + readonly isLine: boolean; + + /** If true this target is weak and can be transformed/upgraded */ + readonly isWeak: boolean; + + /** If true this target is a raw selection and its insertion delimiter should not be used on bring action */ + readonly isRaw: boolean; + + /** If true this target is a notebook cell */ + readonly isNotebookCell: boolean; + + /** The text contained in the content range */ + readonly contentText: string; + + /** The content range and is reversed turned into a selection */ + readonly contentSelection: Selection; + + /** Internal target that should be used for the that mark */ + readonly thatTarget: Target; + + getInteriorStrict(): Target[]; + getBoundaryStrict(): Target[]; + /** The range of the delimiter before the content selection */ + getLeadingDelimiterTarget(): Target | undefined; + /** The range of the delimiter after the content selection */ + getTrailingDelimiterTarget(): Target | undefined; + getRemovalRange(): Range; + getRemovalHighlightRange(): Range | undefined; + getEditNewContext(isBefore: boolean): EditNewContext; + withThatTarget(thatTarget: Target): Target; + withContentRange(contentRange: Range): Target; + createContinuousRangeTarget( + isReversed: boolean, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean + ): Target; + /** Constructs change/insertion edit. Adds delimiter before/after if needed */ + constructChangeEdit(text: string): EditWithRangeUpdater; + /** Constructs removal edit */ + constructRemovalEdit(): EditWithRangeUpdater; + isEqual(target: Target): boolean; +} diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts new file mode 100644 index 0000000000..042b70a050 --- /dev/null +++ b/src/typings/targetDescriptor.types.ts @@ -0,0 +1,268 @@ +import { HatStyleName } from "../core/constants"; + +export interface CursorMark { + type: "cursor"; +} + +export interface ThatMark { + type: "that"; +} + +export interface SourceMark { + type: "source"; +} + +export interface NothingMark { + type: "nothing"; +} + +export interface LastCursorPositionMark { + type: "lastCursorPosition"; +} + +export interface DecoratedSymbolMark { + type: "decoratedSymbol"; + symbolColor: HatStyleName; + character: string; +} + +export type LineNumberType = "absolute" | "relative" | "modulo100"; + +export interface LineNumberPosition { + type: LineNumberType; + lineNumber: number; +} + +export interface LineNumberMark { + type: "lineNumber"; + anchor: LineNumberPosition; + active: LineNumberPosition; +} + +export type Mark = + | CursorMark + | ThatMark + | SourceMark + // | LastCursorPositionMark Not implemented yet + | DecoratedSymbolMark + | NothingMark + | LineNumberMark; + +export type SimpleSurroundingPairName = + | "angleBrackets" + | "backtickQuotes" + | "curlyBrackets" + | "doubleQuotes" + | "escapedDoubleQuotes" + | "escapedParentheses" + | "escapedSquareBrackets" + | "escapedSingleQuotes" + | "parentheses" + | "singleQuotes" + | "squareBrackets"; +export type ComplexSurroundingPairName = "string" | "any"; +export type SurroundingPairName = + | SimpleSurroundingPairName + | ComplexSurroundingPairName; + +export type SimpleScopeTypeType = + | "argumentOrParameter" + | "anonymousFunction" + | "attribute" + | "class" + | "className" + | "collectionItem" + | "collectionKey" + | "comment" + | "functionCall" + | "functionName" + | "ifStatement" + | "list" + | "map" + | "name" + | "namedFunction" + | "regularExpression" + | "statement" + | "string" + | "type" + | "value" + | "condition" + | "section" + | "sectionLevelOne" + | "sectionLevelTwo" + | "sectionLevelThree" + | "sectionLevelFour" + | "sectionLevelFive" + | "sectionLevelSix" + | "selector" + | "xmlBothTags" + | "xmlElement" + | "xmlEndTag" + | "xmlStartTag" + // Text based scopes + | "token" + | "line" + | "notebookCell" + | "paragraph" + | "document" + | "character" + | "word" + | "nonWhitespaceSequence" + | "url"; + +export interface SimpleScopeType { + type: SimpleScopeTypeType; +} + +export type SurroundingPairDirection = "left" | "right"; +export interface SurroundingPairScopeType { + type: "surroundingPair"; + delimiter: SurroundingPairName; + forceDirection?: SurroundingPairDirection; +} + +export type ScopeType = SimpleScopeType | SurroundingPairScopeType; + +export interface ContainingSurroundingPairModifier + extends ContainingScopeModifier { + scopeType: SurroundingPairScopeType; +} + +export interface InteriorOnlyModifier { + type: "interiorOnly"; +} + +export interface ExcludeInteriorModifier { + type: "excludeInterior"; +} + +export interface ContainingScopeModifier { + type: "containingScope"; + scopeType: ScopeType; +} + +export interface EveryScopeModifier { + type: "everyScope"; + scopeType: ScopeType; +} + +export interface OrdinalRangeModifier { + type: "ordinalRange"; + scopeType: ScopeType; + anchor: number; + active: number; + excludeAnchor?: boolean; + excludeActive?: boolean; +} +/** + * Converts its input to a raw selection with no type information so for + * example if it is the destination of a bring or move it should inherit the + * type information such as delimiters from its source. + */ + +export interface RawSelectionModifier { + type: "toRawSelection"; +} + +export interface HeadModifier { + type: "extendThroughStartOf"; +} + +export interface TailModifier { + type: "extendThroughEndOf"; +} + +export interface LeadingModifier { + type: "leading"; +} + +export interface TrailingModifier { + type: "trailing"; +} + +export type Position = "before" | "after" | "start" | "end"; + +export interface PositionModifier { + type: "position"; + position: Position; +} + +export interface PartialPrimitiveTargetDescriptor { + type: "primitive"; + mark?: Mark; + modifiers?: Modifier[]; + isImplicit?: boolean; +} + +export type Modifier = + | PositionModifier + | InteriorOnlyModifier + | ExcludeInteriorModifier + | ContainingScopeModifier + | EveryScopeModifier + | OrdinalRangeModifier + | HeadModifier + | TailModifier + | LeadingModifier + | TrailingModifier + | RawSelectionModifier; + +export interface PartialRangeTargetDescriptor { + type: "range"; + anchor: PartialPrimitiveTargetDescriptor; + active: PartialPrimitiveTargetDescriptor; + excludeAnchor: boolean; + excludeActive: boolean; + rangeType?: RangeType; +} + +export interface PartialListTargetDescriptor { + type: "list"; + elements: (PartialPrimitiveTargetDescriptor | PartialRangeTargetDescriptor)[]; +} + +export type PartialTargetDescriptor = + | PartialPrimitiveTargetDescriptor + | PartialRangeTargetDescriptor + | PartialListTargetDescriptor; + +export interface PrimitiveTargetDescriptor + extends PartialPrimitiveTargetDescriptor { + /** + * The mark, eg "air", "this", "that", etc + */ + mark: Mark; + + /** + * Zero or more modifiers that will be applied in sequence to the output from + * the mark. Note that the modifiers will be applied in reverse order. For + * example, if the user says "take first char name air", then we will apply + * "name" to the output of "air" to select the name of the function or + * statement containing "air", then apply "first char" to select the first + * character of the name. + */ + modifiers: Modifier[]; +} + +export interface RangeTargetDescriptor { + type: "range"; + anchor: PrimitiveTargetDescriptor; + active: PrimitiveTargetDescriptor; + excludeAnchor: boolean; + excludeActive: boolean; + rangeType: RangeType; +} +// continuous is one single continuous selection between the two targets +// vertical puts a selection on each line vertically between the two targets + +export type RangeType = "continuous" | "vertical"; + +export interface ListTargetDescriptor { + type: "list"; + elements: (PrimitiveTargetDescriptor | RangeTargetDescriptor)[]; +} + +export type TargetDescriptor = + | PrimitiveTargetDescriptor + | RangeTargetDescriptor + | ListTargetDescriptor; diff --git a/src/util/canonicalizeAndValidateCommand.ts b/src/util/canonicalizeAndValidateCommand.ts deleted file mode 100644 index 5036475373..0000000000 --- a/src/util/canonicalizeAndValidateCommand.ts +++ /dev/null @@ -1,84 +0,0 @@ -import canonicalizeActionName from "./canonicalizeActionName"; -import canonicalizeTargets from "./canonicalizeTargets"; -import { ActionType, PartialTarget, SelectionType } from "../typings/Types"; -import { getPartialPrimitiveTargets } from "./getPrimitiveTargets"; -import { - CommandArgument, - CommandArgumentComplete, - LATEST_VERSION, -} from "../core/commandRunner/types"; -import { ActionableError } from "../errors"; -import { commands } from "vscode"; - -/** - * Given a command argument which comes from the client, normalize it so that it - * conforms to the latest version of the expected cursorless command argument. - * - * @param commandArgument The command argument to normalize - * @returns The normalized command argument - */ -export function canonicalizeAndValidateCommand( - commandArgument: CommandArgument -): CommandArgumentComplete { - const { - action: inputActionName, - targets: inputPartialTargets, - extraArgs: inputExtraArgs = [], - usePrePhraseSnapshot = false, - version, - ...rest - } = commandArgument; - - if (version > LATEST_VERSION) { - throw new ActionableError( - "Cursorless Talon version is ahead of Cursorless VSCode extension version. Please update Cursorless VSCode.", - [ - { - name: "Check for updates", - action: () => - commands.executeCommand( - "workbench.extensions.action.checkForUpdates" - ), - }, - ] - ); - } - - const actionName = canonicalizeActionName(inputActionName); - const partialTargets = canonicalizeTargets(inputPartialTargets); - const extraArgs = inputExtraArgs; - - validateCommand(actionName, partialTargets); - - return { - ...rest, - version: LATEST_VERSION, - action: actionName, - targets: partialTargets, - extraArgs: extraArgs, - usePrePhraseSnapshot, - }; -} - -export function validateCommand( - actionName: ActionType, - partialTargets: PartialTarget[] -) { - if ( - usesSelectionType("notebookCell", partialTargets) && - !["editNewLineBefore", "editNewLineAfter"].includes(actionName) - ) { - throw new Error( - "The notebookCell scope type is currently only supported with the actions editNewLineAbove and editNewLineBelow" - ); - } -} - -function usesSelectionType( - selectionType: SelectionType, - partialTargets: PartialTarget[] -) { - return getPartialPrimitiveTargets(partialTargets).some( - (partialTarget) => partialTarget.selectionType === selectionType - ); -} diff --git a/src/util/canonicalizeTargets.ts b/src/util/canonicalizeTargets.ts deleted file mode 100644 index f2efb50eb8..0000000000 --- a/src/util/canonicalizeTargets.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - PartialPrimitiveTarget, - PartialTarget, - ScopeType, -} from "../typings/Types"; -import update from "immutability-helper"; -import { transformPartialPrimitiveTargets } from "./getPrimitiveTargets"; -import { HatStyleName } from "../core/constants"; -import { flow } from "lodash"; -import { isDeepStrictEqual } from "util"; - -const SCOPE_TYPE_CANONICALIZATION_MAPPING: Record = { - arrowFunction: "anonymousFunction", - dictionary: "map", - regex: "regularExpression", -}; - -const COLOR_CANONICALIZATION_MAPPING: Record = { - purple: "pink", -}; - -const canonicalizeScopeTypes = ( - target: PartialPrimitiveTarget -): PartialPrimitiveTarget => - target.modifier?.type === "containingScope" - ? update(target, { - modifier: { - scopeType: (scopeType: string) => - SCOPE_TYPE_CANONICALIZATION_MAPPING[scopeType] ?? scopeType, - }, - }) - : target; - -const canonicalizeColors = ( - target: PartialPrimitiveTarget -): PartialPrimitiveTarget => - target.mark?.type === "decoratedSymbol" - ? update(target, { - mark: { - symbolColor: (symbolColor: string) => - COLOR_CANONICALIZATION_MAPPING[symbolColor] ?? symbolColor, - }, - }) - : target; - -const STRICT_HERE = { - type: "primitive", - mark: { type: "cursor" }, - selectionType: "token", - position: "contents", - modifier: { type: "identity" }, - insideOutsideType: "inside", -}; - -const IMPLICIT_TARGET: PartialPrimitiveTarget = { - type: "primitive", - isImplicit: true, -}; - -const upgradeStrictHere = ( - target: PartialPrimitiveTarget -): PartialPrimitiveTarget => - isDeepStrictEqual(target, STRICT_HERE) ? IMPLICIT_TARGET : target; - -export default function canonicalizeTargets(partialTargets: PartialTarget[]) { - return transformPartialPrimitiveTargets( - partialTargets, - flow(canonicalizeScopeTypes, canonicalizeColors, upgradeStrictHere) - ); -} diff --git a/src/util/editDisplayUtils.ts b/src/util/editDisplayUtils.ts deleted file mode 100644 index 33800ff545..0000000000 --- a/src/util/editDisplayUtils.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { TextEditorDecorationType, window, workspace } from "vscode"; -import { TypedSelection, SelectionWithEditor } from "../typings/Types"; -import { isLineSelectionType } from "./selectionType"; -import { runOnTargetsForEachEditor, runForEachEditor } from "./targetUtils"; -import { EditStyle } from "../core/editStyles"; -import isTesting from "../testUtil/isTesting"; -import sleep from "./sleep"; - -const getPendingEditDecorationTime = () => - workspace - .getConfiguration("cursorless") - .get("pendingEditDecorationTime")!; - -export async function decorationSleep() { - if (isTesting()) { - return; - } - - await sleep(getPendingEditDecorationTime()); -} - -export async function displayPendingEditDecorationsForSelection( - selections: SelectionWithEditor[], - style: TextEditorDecorationType -) { - await runForEachEditor( - selections, - (selection) => selection.editor, - async (editor, selections) => { - editor.setDecorations( - style, - selections.map((selection) => selection.selection) - ); - } - ); - - await decorationSleep(); - - await runForEachEditor( - selections, - (selection) => selection.editor, - async (editor) => { - editor.setDecorations(style, []); - } - ); -} - -export default async function displayPendingEditDecorations( - targets: TypedSelection[], - editStyle: EditStyle -) { - await setDecorations(targets, editStyle); - - await decorationSleep(); - - clearDecorations(editStyle); -} - -export function clearDecorations(editStyle: EditStyle) { - window.visibleTextEditors.map((editor) => { - editor.setDecorations(editStyle.token, []); - editor.setDecorations(editStyle.line, []); - }); -} - -export async function setDecorations( - targets: TypedSelection[], - editStyle: EditStyle -) { - await runOnTargetsForEachEditor(targets, async (editor, selections) => { - editor.setDecorations( - editStyle.token, - selections - .filter((selection) => !useLineDecorations(selection)) - .map((selection) => selection.selection.selection) - ); - - editor.setDecorations( - editStyle.line, - selections - .filter((selection) => useLineDecorations(selection)) - .map((selection) => { - const { document } = selection.selection.editor; - const { start, end } = selection.selection.selection; - const startLine = document.lineAt(start); - const hasLeadingLine = - start.character === startLine.range.end.character; - if ( - end.character === 0 && - (!hasLeadingLine || start.character === 0) - ) { - // NB: We move end up one line because it is at beginning of - // next line - return selection.selection.selection.with({ - end: end.translate(-1), - }); - } - if (hasLeadingLine) { - // NB: We move start down one line because it is at end of - // previous line - return selection.selection.selection.with({ - start: start.translate(1), - }); - } - return selection.selection.selection; - }) - ); - }); -} - -function useLineDecorations(selection: TypedSelection) { - return ( - isLineSelectionType(selection.selectionType) && - selection.position === "contents" - ); -} diff --git a/src/util/expandToContainingLine.ts b/src/util/expandToContainingLine.ts deleted file mode 100644 index f17690cea1..0000000000 --- a/src/util/expandToContainingLine.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Range, TextEditor } from "vscode"; - -export default function expandToContainingLine( - editor: TextEditor, - range: Range -) { - const start = range.start.with({ character: 0 }); - const end = editor.document.lineAt(range.end).range.end; - return new Range(start, end); -} diff --git a/src/util/getLinks.ts b/src/util/getLinks.ts index de5dbd8ca8..a36fa6d761 100644 --- a/src/util/getLinks.ts +++ b/src/util/getLinks.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode"; -import { TypedSelection } from "../typings/Types"; +import { Target } from "../typings/target.types"; export async function getLinksForSelections( editor: vscode.TextEditor, @@ -11,9 +11,9 @@ export async function getLinksForSelections( ); } -export async function getLinkForTarget(target: TypedSelection) { - const links = await getLinksForEditor(target.selection.editor); - return links.find((link) => link.range.contains(target.selection.selection)); +export async function getLinkForTarget(target: Target) { + const links = await getLinksForEditor(target.editor); + return links.find((link) => link.range.contains(target.contentRange)); } function getLinksForEditor(editor: vscode.TextEditor) { diff --git a/src/util/getPrimitiveTargets.ts b/src/util/getPrimitiveTargets.ts index 8206390edf..686451fd71 100644 --- a/src/util/getPrimitiveTargets.ts +++ b/src/util/getPrimitiveTargets.ts @@ -1,10 +1,10 @@ import { - PartialPrimitiveTarget, - PartialRangeTarget, - PartialTarget, - PrimitiveTarget, - Target, -} from "../typings/Types"; + PartialPrimitiveTargetDescriptor, + PartialRangeTargetDescriptor, + PartialTargetDescriptor, + PrimitiveTargetDescriptor, + TargetDescriptor, +} from "../typings/targetDescriptor.types"; /** * Given a list of targets, recursively descends all targets and returns every @@ -13,20 +13,20 @@ import { * @param targets The targets to extract from * @returns A list of primitive targets */ -export function getPartialPrimitiveTargets(targets: PartialTarget[]) { +export function getPartialPrimitiveTargets(targets: PartialTargetDescriptor[]) { return targets.flatMap(getPartialPrimitiveTargetsHelper); } function getPartialPrimitiveTargetsHelper( - target: PartialTarget -): PartialPrimitiveTarget[] { + target: PartialTargetDescriptor +): PartialPrimitiveTargetDescriptor[] { switch (target.type) { case "primitive": return [target]; case "list": return target.elements.flatMap(getPartialPrimitiveTargetsHelper); case "range": - return [target.start, target.end]; + return [target.anchor, target.active]; } } /** @@ -36,11 +36,13 @@ function getPartialPrimitiveTargetsHelper( * @param targets The targets to extract from * @returns A list of primitive targets */ -export function getPrimitiveTargets(targets: Target[]) { +export function getPrimitiveTargets(targets: TargetDescriptor[]) { return targets.flatMap(getPrimitiveTargetsHelper); } -function getPrimitiveTargetsHelper(target: Target): PrimitiveTarget[] { +function getPrimitiveTargetsHelper( + target: TargetDescriptor +): PrimitiveTargetDescriptor[] { switch (target.type) { case "primitive": return [target]; @@ -59,8 +61,10 @@ function getPrimitiveTargetsHelper(target: Target): PrimitiveTarget[] { * @returns A list of primitive targets */ export function transformPartialPrimitiveTargets( - targets: PartialTarget[], - func: (target: PartialPrimitiveTarget) => PartialPrimitiveTarget + targets: PartialTargetDescriptor[], + func: ( + target: PartialPrimitiveTargetDescriptor + ) => PartialPrimitiveTargetDescriptor ) { return targets.map((target) => transformPartialPrimitiveTargetsHelper(target, func) @@ -68,9 +72,11 @@ export function transformPartialPrimitiveTargets( } function transformPartialPrimitiveTargetsHelper( - target: PartialTarget, - func: (target: PartialPrimitiveTarget) => PartialPrimitiveTarget -): PartialTarget { + target: PartialTargetDescriptor, + func: ( + target: PartialPrimitiveTargetDescriptor + ) => PartialPrimitiveTargetDescriptor +): PartialTargetDescriptor { switch (target.type) { case "primitive": return func(target); @@ -80,11 +86,15 @@ function transformPartialPrimitiveTargetsHelper( elements: target.elements.map( (element) => transformPartialPrimitiveTargetsHelper(element, func) as - | PartialPrimitiveTarget - | PartialRangeTarget + | PartialPrimitiveTargetDescriptor + | PartialRangeTargetDescriptor ), }; case "range": - return { ...target, start: func(target.start), end: func(target.end) }; + return { + ...target, + anchor: func(target.anchor), + active: func(target.active), + }; } } diff --git a/src/util/getTextWithPossibleDelimiter.ts b/src/util/getTextWithPossibleDelimiter.ts deleted file mode 100644 index 977e4b78d8..0000000000 --- a/src/util/getTextWithPossibleDelimiter.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TypedSelection } from "../typings/Types"; - -/** Get text from selection. Possibly add delimiter for positions before/after */ -export function getTextWithPossibleDelimiter( - source: TypedSelection, - destination: TypedSelection -) { - const sourceText = source.selection.editor.document.getText( - source.selection.selection - ); - return maybeAddDelimiter(sourceText, destination); -} - -/** Possibly add delimiter for positions before/after */ -export function maybeAddDelimiter( - sourceText: string, - destination: TypedSelection -) { - const { insideOutsideType, position } = destination; - const containingListDelimiter = - destination.selectionContext.containingListDelimiter; - return containingListDelimiter == null || - position === "contents" || - insideOutsideType === "inside" - ? sourceText - : destination.position === "after" - ? containingListDelimiter + sourceText - : sourceText + containingListDelimiter; -} diff --git a/src/util/nodeMatchers.ts b/src/util/nodeMatchers.ts index f29ea9197e..e4bc87f8ec 100644 --- a/src/util/nodeMatchers.ts +++ b/src/util/nodeMatchers.ts @@ -4,9 +4,9 @@ import { NodeFinder, SelectionExtractor, NodeMatcherAlternative, - ScopeType, SelectionWithEditor, } from "../typings/Types"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; import { simpleSelectionExtractor, argumentSelectionExtractor, @@ -176,9 +176,9 @@ export const notSupported: NodeMatcher = ( }; export function createPatternMatchers( - nodeMatchers: Partial> -): Record { - Object.keys(nodeMatchers).forEach((scopeType: ScopeType) => { + nodeMatchers: Partial> +): Record { + Object.keys(nodeMatchers).forEach((scopeType: SimpleScopeTypeType) => { let matcher = nodeMatchers[scopeType]; if (Array.isArray(matcher)) { nodeMatchers[scopeType] = patternMatcher(...matcher); @@ -186,5 +186,5 @@ export function createPatternMatchers( nodeMatchers[scopeType] = patternMatcher(matcher); } }); - return nodeMatchers as Record; + return nodeMatchers as Record; } diff --git a/src/util/nodeSelectors.ts b/src/util/nodeSelectors.ts index 588130d560..5423d96a34 100644 --- a/src/util/nodeSelectors.ts +++ b/src/util/nodeSelectors.ts @@ -317,9 +317,9 @@ export function delimitedSelector( getEndNode: (node: SyntaxNode) => SyntaxNode = identity ): SelectionExtractor { return (editor: TextEditor, node: SyntaxNode) => { - let containingListDelimiter: string | null = null; - let leadingDelimiterRange: Range | null = null; - let trailingDelimiterRange: Range | null = null; + let containingListDelimiter: string | undefined; + let leadingDelimiterRange: Range | undefined; + let trailingDelimiterRange: Range | undefined; const startNode = getStartNode(node); const endNode = getEndNode(node); @@ -367,7 +367,6 @@ export function delimitedSelector( new Position(endNode.endPosition.row, endNode.endPosition.column) ), context: { - isInDelimitedList: true, containingListDelimiter, leadingDelimiterRange, trailingDelimiterRange, diff --git a/src/util/performInsideOutsideAdjustment.ts b/src/util/performInsideOutsideAdjustment.ts deleted file mode 100644 index 9d8326980e..0000000000 --- a/src/util/performInsideOutsideAdjustment.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { InsideOutsideType, TypedSelection } from "../typings/Types"; -import { updateTypedSelectionRange } from "./selectionUtils"; - -export function performInsideOutsideAdjustment( - selection: TypedSelection, - preferredInsideOutsideType: InsideOutsideType = null -): TypedSelection { - const insideOutsideType = - selection.insideOutsideType ?? preferredInsideOutsideType; - - if (insideOutsideType === "outside") { - if (selection.position !== "contents") { - const delimiterRange = - selection.position === "before" - ? selection.selectionContext.leadingDelimiterRange - : selection.selectionContext.trailingDelimiterRange; - - return delimiterRange == null - ? selection - : updateTypedSelectionRange(selection, delimiterRange); - } - - const usedSelection = - selection.selectionContext.outerSelection ?? - selection.selection.selection; - - const delimiterRange = - selection.selectionContext.trailingDelimiterRange ?? - selection.selectionContext.leadingDelimiterRange; - - const range = - delimiterRange != null - ? usedSelection.union(delimiterRange) - : usedSelection; - - return updateTypedSelectionRange(selection, range); - } - - return selection; -} - -export function performInsideAdjustment(selection: TypedSelection) { - return performInsideOutsideAdjustment(selection, "inside"); -} - -export function performOutsideAdjustment(selection: TypedSelection) { - return performInsideOutsideAdjustment(selection, "outside"); -} diff --git a/src/util/rangeUtils.ts b/src/util/rangeUtils.ts new file mode 100644 index 0000000000..a812180221 --- /dev/null +++ b/src/util/rangeUtils.ts @@ -0,0 +1,30 @@ +import { Position, Range, TextEditor } from "vscode"; + +export function isAtEndOfLine(editor: TextEditor, position: Position) { + const endLine = editor.document.lineAt(position); + + return position.isEqual(endLine.range.end); +} + +export function isAtStartOfLine(position: Position) { + return position.character === 0; +} + +/** + * Expands the given range to in the full line(s) containing it, including + * leading and trailing white space. + * + * @param editor The editor + * @param range The range to expand + * @returns The expanded range + */ +export function expandToFullLine(editor: TextEditor, range: Range) { + return new Range( + new Position(range.start.line, 0), + editor.document.lineAt(range.end).range.end + ); +} + +export function makeEmptyRange(position: Position) { + return new Range(position, position); +} diff --git a/src/util/selectionType.ts b/src/util/selectionType.ts deleted file mode 100644 index a500dffbaf..0000000000 --- a/src/util/selectionType.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SelectionType } from "../typings/Types"; - -export function isLineSelectionType(selectionType: SelectionType) { - return selectionType === "line" || selectionType === "paragraph"; -} diff --git a/src/util/selectionUtils.ts b/src/util/selectionUtils.ts index 6ce7e891fb..8e443b0ef4 100644 --- a/src/util/selectionUtils.ts +++ b/src/util/selectionUtils.ts @@ -1,29 +1,14 @@ -import { Range, Selection, Position } from "vscode"; -import update from "immutability-helper"; -import { TypedSelection, SelectionWithEditor } from "../typings/Types"; - -export function selectionFromRange( - selection: Selection, - range: Range -): Selection { - return selectionFromPositions(selection, range.start, range.end); -} - -export function selectionFromPositions( - selection: Selection, - start: Position, - end: Position -): Selection { - // The built in isReversed is bugged on empty selection. don't use - return isForward(selection) - ? new Selection(start, end) - : new Selection(end, start); -} +import { Position, Range, Selection } from "vscode"; +import { SelectionWithEditor } from "../typings/Types"; export function isForward(selection: Selection) { return selection.active.isAfterOrEqual(selection.anchor); } +export function isReversed(selection: Selection) { + return selection.active.isBefore(selection.anchor); +} + export function selectionWithEditorFromRange( selection: SelectionWithEditor, range: Range @@ -31,7 +16,7 @@ export function selectionWithEditorFromRange( return selectionWithEditorFromPositions(selection, range.start, range.end); } -export function selectionWithEditorFromPositions( +function selectionWithEditorFromPositions( selection: SelectionWithEditor, start: Position, end: Position @@ -42,21 +27,18 @@ export function selectionWithEditorFromPositions( }; } -/** - * Returns a copy of the given typed selection so that the new selection has the new given range - * preserving the direction of the original selection - * - * @param selection The original typed selection to Update - * @param range The new range for the given selection - * @returns The updated typed selection - */ -export function updateTypedSelectionRange( - selection: TypedSelection, - range: Range -): TypedSelection { - return update(selection, { - selection: { - selection: () => selectionFromRange(selection.selection.selection, range), - }, - }); +function selectionFromPositions( + selection: Selection, + start: Position, + end: Position +): Selection { + // The built in isReversed is bugged on empty selection. don't use + return isForward(selection) + ? new Selection(start, end) + : new Selection(end, start); +} + +export function selectionFromRange(isReversed: boolean, range: Range) { + const { start, end } = range; + return isReversed ? new Selection(end, start) : new Selection(start, end); } diff --git a/src/util/setSelectionsAndFocusEditor.ts b/src/util/setSelectionsAndFocusEditor.ts index 004b6081b3..4c9401e3e4 100644 --- a/src/util/setSelectionsAndFocusEditor.ts +++ b/src/util/setSelectionsAndFocusEditor.ts @@ -1,6 +1,7 @@ import { range } from "lodash"; import { commands, Selection, TextEditor, ViewColumn, window } from "vscode"; import { getCellIndex, getNotebookFromCellDocument } from "./notebook"; +import uniqDeep from "./uniqDeep"; const columnFocusCommands = { [ViewColumn.One]: "workbench.action.focusFirstEditorGroup", @@ -21,7 +22,7 @@ export async function setSelectionsAndFocusEditor( selections: Selection[], revealRange: boolean = true ) { - editor.selections = selections; + setSelectionsWithoutFocusingEditor(editor, selections); if (revealRange) { editor.revealRange(editor.selection); @@ -32,6 +33,13 @@ export async function setSelectionsAndFocusEditor( await focusEditor(editor); } +export function setSelectionsWithoutFocusingEditor( + editor: TextEditor, + selections: Selection[] +) { + editor.selections = uniqDeep(selections); +} + export async function focusEditor(editor: TextEditor) { if (editor.viewColumn != null) { await commands.executeCommand(columnFocusCommands[editor.viewColumn]); diff --git a/src/util/targetUtils.ts b/src/util/targetUtils.ts index 86b3b97ac1..e0fff7ee76 100644 --- a/src/util/targetUtils.ts +++ b/src/util/targetUtils.ts @@ -1,13 +1,15 @@ -import { TextEditor, Selection, Position } from "vscode"; +import { zip } from "lodash"; +import { Range, Selection, TextEditor } from "vscode"; +import { Target } from "../typings/target.types"; +import { SelectionWithEditor } from "../typings/Types"; import { groupBy } from "./itertools"; -import { TypedSelection } from "../typings/Types"; -export function ensureSingleEditor(targets: TypedSelection[]) { +export function ensureSingleEditor(targets: Target[]) { if (targets.length === 0) { throw new Error("Require at least one target with this action"); } - const editors = targets.map((target) => target.selection.editor); + const editors = targets.map((target) => target.editor); if (new Set(editors).size > 1) { throw new Error("Can only have one editor with this action"); @@ -16,7 +18,7 @@ export function ensureSingleEditor(targets: TypedSelection[]) { return editors[0]; } -export function ensureSingleTarget(targets: TypedSelection[]) { +export function ensureSingleTarget(targets: Target[]) { if (targets.length !== 1) { throw new Error("Can only have one target with this action"); } @@ -37,14 +39,14 @@ export async function runForEachEditor( } export async function runOnTargetsForEachEditor( - targets: TypedSelection[], - func: (editor: TextEditor, selections: TypedSelection[]) => Promise + targets: Target[], + func: (editor: TextEditor, targets: Target[]) => Promise ): Promise { - return runForEachEditor(targets, (target) => target.selection.editor, func); + return runForEachEditor(targets, (target) => target.editor, func); } -export function groupTargetsForEachEditor(targets: TypedSelection[]) { - return groupForEachEditor(targets, (target) => target.selection.editor); +export function groupTargetsForEachEditor(targets: Target[]) { + return groupForEachEditor(targets, (target) => target.editor); } export function groupForEachEditor( @@ -61,46 +63,43 @@ export function groupForEachEditor( }); } -/** Get the possible leading and trailing overflow ranges of the outside target compared to the inside target */ +/** Get the possible leading and trailing overflow ranges of the outside range compared to the inside range */ export function getOutsideOverflow( - insideTarget: TypedSelection, - outsideTarget: TypedSelection -): TypedSelection[] { - const { start: insideStart, end: insideEnd } = - insideTarget.selection.selection; - const { start: outsideStart, end: outsideEnd } = - outsideTarget.selection.selection; + editor: TextEditor, + insideRange: Range, + outsideRange: Range +): Range[] { + const { start: insideStart, end: insideEnd } = insideRange; + const { start: outsideStart, end: outsideEnd } = outsideRange; const result = []; if (outsideStart.isBefore(insideStart)) { - result.push( - createTypeSelection( - insideTarget.selection.editor, - outsideStart, - insideStart - ) - ); + result.push(new Range(outsideStart, insideStart)); } if (outsideEnd.isAfter(insideEnd)) { - result.push( - createTypeSelection(insideTarget.selection.editor, insideEnd, outsideEnd) - ); + result.push(new Range(insideEnd, outsideEnd)); } return result; } -function createTypeSelection( - editor: TextEditor, - start: Position, - end: Position -): TypedSelection { - return { - selection: { - editor, - selection: new Selection(start, end), - }, - selectionType: "token", - selectionContext: {}, - insideOutsideType: "inside", - position: "contents", - }; +export function getContentRange(target: Target) { + return target.contentRange; +} + +export function createThatMark( + targets: Target[], + ranges?: Range[] +): SelectionWithEditor[] { + const thatMark = + ranges != null + ? zip(targets, ranges).map(([target, range]) => ({ + editor: target!.editor, + selection: target?.isReversed + ? new Selection(range!.end, range!.start) + : new Selection(range!.start, range!.end), + })) + : targets.map((target) => ({ + editor: target!.editor, + selection: target.contentSelection, + })); + return thatMark; } diff --git a/src/util/tryConstructTarget.ts b/src/util/tryConstructTarget.ts new file mode 100644 index 0000000000..220489f7c2 --- /dev/null +++ b/src/util/tryConstructTarget.ts @@ -0,0 +1,68 @@ +import { Range, TextEditor } from "vscode"; +import { CommonTargetParameters } from "../processTargets/targets/BaseTarget"; +import LineTarget from "../processTargets/targets/LineTarget"; +import PlainTarget from "../processTargets/targets/PlainTarget"; +import { Target } from "../typings/target.types"; + +type TargetConstructor = new ( + parameters: CommonTargetParameters +) => T; + +/** + * Constructs a target from the given range or returns undefined if the range is + * undefined + * @param constructor The type of target to construct + * @param editor The editor containing the range + * @param range The range to convert into a target + * @param isReversed Whether the rain should be backward + * @returns A new target constructed from the given range, or null if the range + * is undefined + */ +export function tryConstructTarget( + constructor: TargetConstructor, + editor: TextEditor, + range: Range | undefined, + isReversed: boolean +): T | undefined { + return range == null + ? undefined + : new constructor({ + editor, + isReversed, + contentRange: range, + }); +} + +/** + * Constructs a {@link PlainTarget} from the given range, or returns undefined + * if the range is undefined + * @param editor The editor containing the range + * @param range The range to convert into a target + * @param isReversed Whether the rain should be backward + * @returns A new {@link PlainTarget} constructed from the given range, or null + * if the range is undefined + */ +export function tryConstructPlainTarget( + editor: TextEditor, + range: Range | undefined, + isReversed: boolean +): PlainTarget | undefined { + return tryConstructTarget(PlainTarget, editor, range, isReversed); +} + +/** + * Constructs a {@link LineTarget} from the given range, or returns undefined + * if the range is undefined + * @param editor The editor containing the range + * @param range The range to convert into a target + * @param isReversed Whether the rain should be backward + * @returns A new {@link LineTarget} constructed from the given range, or null + * if the range is undefined + */ +export function constructLineTarget( + editor: TextEditor, + range: Range | undefined, + isReversed: boolean +): LineTarget | undefined { + return tryConstructTarget(LineTarget, editor, range, isReversed); +} diff --git a/src/util/typeUtils.ts b/src/util/typeUtils.ts new file mode 100644 index 0000000000..da390ba0b3 --- /dev/null +++ b/src/util/typeUtils.ts @@ -0,0 +1,13 @@ +/** + * Determines whether two objects have the same constructor. + * + * @param a The first object + * @param b The second object + * @returns True if `a` and `b` have the same constructor + */ +export function isSameType(a: unknown, b: unknown): boolean { + return ( + Object.getPrototypeOf(a).constructor === + Object.getPrototypeOf(b).constructor + ); +} diff --git a/src/util/unifyRanges.ts b/src/util/unifyRanges.ts index dc12103b0c..8324207cff 100644 --- a/src/util/unifyRanges.ts +++ b/src/util/unifyRanges.ts @@ -1,6 +1,6 @@ -import { Range, Selection } from "vscode"; -import { TypedSelection } from "../typings/Types"; -import { performInsideOutsideAdjustment } from "./performInsideOutsideAdjustment"; +import { Range } from "vscode"; +import { targetsToContinuousTarget } from "../processTargets/processTargets"; +import { Target } from "../typings/target.types"; import { groupTargetsForEachEditor } from "./targetUtils"; /** Unifies overlapping/intersecting ranges */ @@ -36,12 +36,8 @@ function unifyRangesOnePass(ranges: Range[]): [Range[], boolean] { return [result, madeChanges]; } -/** - * Unifies overlapping/intersecting targets - * FIXME This code probably needs to update once we have objected oriented targets - * https://github.com/cursorless-dev/cursorless/issues/210 - */ -export function unifyTargets(targets: TypedSelection[]): TypedSelection[] { +/** Unifies overlapping/intersecting targets */ +export function unifyRemovalTargets(targets: Target[]): Target[] { if (targets.length < 2) { return targets; } @@ -51,7 +47,7 @@ export function unifyTargets(targets: TypedSelection[]): TypedSelection[] { } let results = [...targets]; results.sort((a, b) => - a.selection.selection.start.compareTo(b.selection.selection.start) + a.contentRange.start.compareTo(b.contentRange.start) ); let run = true; // Merge targets untill there are no overlaps/intersections @@ -62,14 +58,12 @@ export function unifyTargets(targets: TypedSelection[]): TypedSelection[] { }); } -function unifyTargetsOnePass( - targets: TypedSelection[] -): [TypedSelection[], boolean] { +function unifyTargetsOnePass(targets: Target[]): [Target[], boolean] { if (targets.length < 2) { return [targets, false]; } - const results: TypedSelection[] = []; - let currentGroup: TypedSelection[] = []; + const results: Target[] = []; + let currentGroup: Target[] = []; targets.forEach((target) => { // No intersection. Mark start of new group if ( @@ -86,33 +80,15 @@ function unifyTargetsOnePass( return [results, results.length !== targets.length]; } -function mergeTargets(targets: TypedSelection[]): TypedSelection { +function mergeTargets(targets: Target[]): Target { if (targets.length === 1) { return targets[0]; } const first = targets[0]; const last = targets[targets.length - 1]; - const typeSelection: TypedSelection = { - selection: { - editor: first.selection.editor, - selection: new Selection( - first.selection.selection.start, - last.selection.selection.end - ), - }, - position: "contents", - selectionType: first.selectionType, - insideOutsideType: first.insideOutsideType, - selectionContext: { - leadingDelimiterRange: first.selectionContext.leadingDelimiterRange, - trailingDelimiterRange: last.selectionContext.trailingDelimiterRange, - }, - }; - return performInsideOutsideAdjustment(typeSelection); + return targetsToContinuousTarget(first, last); } -function intersects(targetA: TypedSelection, targetB: TypedSelection) { - return !!targetA.selection.selection.intersection( - targetB.selection.selection - ); +function intersects(targetA: Target, targetB: Target) { + return !!targetA.getRemovalRange().intersection(targetB.getRemovalRange()); } diff --git a/src/util/uniqDeep.ts b/src/util/uniqDeep.ts new file mode 100644 index 0000000000..182226a01e --- /dev/null +++ b/src/util/uniqDeep.ts @@ -0,0 +1,5 @@ +import { uniqWith, isEqual } from "lodash"; + +export default (array: T[]): T[] => { + return uniqWith(array, isEqual); +}; diff --git a/yarn.lock b/yarn.lock index ad493155b4..5c81a2053a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -110,6 +110,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + "@types/lodash@^4.14.168": version "4.14.181" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.181.tgz#d1d3740c379fda17ab175165ba04e2d03389385d" @@ -1093,6 +1098,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + just-extend@^4.0.2: version "4.2.1" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" @@ -1184,7 +1196,7 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -1634,6 +1646,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -1692,6 +1709,24 @@ treeify@^1.0.1, treeify@^1.1.0: resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== +ts-unused-exports@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ts-unused-exports/-/ts-unused-exports-8.0.0.tgz#6dd15ff26286e0b7e5663cda3b98c77ea6f3ffe7" + integrity sha512-gylHFyJqC80PSb4zy35KTckykEW1vmKjnOHjBeX9iKBo4b/SzqQIcXXbYSuif4YMgNm6ewFF62VM1C9z0bGZPw== + dependencies: + chalk "^4.0.0" + tsconfig-paths "^3.9.0" + +tsconfig-paths@^3.9.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"