diff --git a/cursorless-talon/src/cheatsheet/sections/scopes.py b/cursorless-talon/src/cheatsheet/sections/scopes.py index 1df251ac69..c4704f5352 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", "subtoken_scope_type"], + ["scope_type"], {"argumentOrParameter": "Argument"}, ), "

": "Paired delimiter", diff --git a/cursorless-talon/src/cheatsheet_html/sections/scopes.py b/cursorless-talon/src/cheatsheet_html/sections/scopes.py index 9f7056f55a..49722a814a 100644 --- a/cursorless-talon/src/cheatsheet_html/sections/scopes.py +++ b/cursorless-talon/src/cheatsheet_html/sections/scopes.py @@ -3,7 +3,7 @@ def get_scopes(): return get_lists( - ["scope_type", "subtoken_scope_type"], + ["scope_type"], "scopeType", {"argumentOrParameter": "Argument"}, ) diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index 8b16f6a888..fb42cb821c 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -135,7 +135,7 @@ def construct_cursorless_command_argument( use_pre_phrase_snapshot = False return { - "version": 2, + "version": 3, "spokenForm": get_spoken_form(), "action": { "name": action, diff --git a/cursorless-talon/src/marks/lines_number.py b/cursorless-talon/src/marks/lines_number.py index 3e47042dd9..50a39bfdc8 100644 --- a/cursorless-talon/src/marks/lines_number.py +++ b/cursorless-talon/src/marks/lines_number.py @@ -37,13 +37,8 @@ class CustomizableTerm: @mod.capture(rule="{user.cursorless_line_direction} ") def cursorless_line_number(m) -> dict[str, Any]: direction = directions_map[m.cursorless_line_direction] - line_number = m.number_small - line = { - "lineNumber": direction.formatter(line_number), - "type": direction.type, - } return { "type": "lineNumber", - "anchor": line, - "active": line, + "lineNumberType": direction.type, + "lineNumber": direction.formatter(m.number_small), } diff --git a/cursorless-talon/src/modifiers/containing_scope.py b/cursorless-talon/src/modifiers/containing_scope.py index 3aa344ef68..573c48d6c7 100644 --- a/cursorless-talon/src/modifiers/containing_scope.py +++ b/cursorless-talon/src/modifiers/containing_scope.py @@ -52,6 +52,8 @@ "start tag": "xmlStartTag", "end tag": "xmlEndTag", # Text-based scope types + "char": "character", + "word": "word", "block": "paragraph", "cell": "notebookCell", "file": "document", @@ -71,40 +73,26 @@ } -@mod.capture(rule="{user.cursorless_scope_type}") +@mod.capture( + rule="{user.cursorless_scope_type} | {user.cursorless_custom_regex_scope_type}" +) def cursorless_scope_type(m) -> dict[str, str]: """Simple cursorless scope type that only need to specify their type""" - return {"type": m.cursorless_scope_type} - - -@mod.capture(rule="{user.cursorless_custom_regex_scope_type}") -def cursorless_custom_regex_scope_type(m) -> dict[str, str]: - """Cursorless custom regular expression scope type""" - return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type} + try: + return {"type": m.cursorless_scope_type} + except AttributeError: + return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type} -@mod.capture( - rule="[every] ( | )" -) +@mod.capture(rule="[every] ") def cursorless_containing_scope(m) -> dict[str, Any]: """Expand to containing scope""" - try: - scope_type = m.cursorless_scope_type - except AttributeError: - scope_type = m.cursorless_custom_regex_scope_type return { "type": "everyScope" if m[0] == "every" else "containingScope", - "scopeType": scope_type, + "scopeType": 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 = { - "word": "word", - "char": "character", -} - # NOTE: Please do not change these dicts. Use the CSVs for customization. # See https://www.cursorless.org/docs/user/customization/ # NB: This is a hack until we support having inside and outside on arbitrary @@ -119,7 +107,6 @@ def on_ready(): "modifier_scope_types", { "scope_type": scope_types, - "subtoken_scope_type": subtoken_scope_types, "surrounding_pair_scope_type": surrounding_pair_scope_types, }, ) diff --git a/cursorless-talon/src/modifiers/modifiers.py b/cursorless-talon/src/modifiers/modifiers.py index 90ec8ff51a..9d275d4d11 100644 --- a/cursorless-talon/src/modifiers/modifiers.py +++ b/cursorless-talon/src/modifiers/modifiers.py @@ -39,7 +39,8 @@ def cursorless_simple_modifier(m) -> dict[str, str]: head_tail_swallowed_modifiers = [ "", # bounds, just, leading, trailing "", # funk, state, class - "", # first past second word + "", # first past second word + "", # next funk "", # matching/pair [curly, round] ] diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py new file mode 100644 index 0000000000..cb2a110de4 --- /dev/null +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -0,0 +1,68 @@ +from typing import Any + +from talon import Module + +from ..compound_targets import is_active_included, is_anchor_included + +mod = Module() + + +@mod.capture(rule=" | last") +def ordinal_or_last(m) -> int: + """An ordinal or the word 'last'""" + if m[0] == "last": + return -1 + return m.ordinals_small - 1 + + +@mod.capture( + rule=" [{user.cursorless_range_connective} ] " +) +def cursorless_ordinal_range(m) -> dict[str, Any]: + """Ordinal range""" + if len(m.ordinal_or_last_list) > 1: + range_connective = m.cursorless_range_connective + include_anchor = is_anchor_included(range_connective) + include_active = is_active_included(range_connective) + anchor = create_ordinal_scope_modifier( + m.cursorless_scope_type, m.ordinal_or_last_list[0] + ) + active = create_ordinal_scope_modifier( + m.cursorless_scope_type, m.ordinal_or_last_list[1] + ) + return { + "type": "range", + "anchor": anchor, + "active": active, + "excludeAnchor": not include_anchor, + "excludeActive": not include_active, + } + else: + return create_ordinal_scope_modifier( + m.cursorless_scope_type, m.ordinal_or_last_list[0] + ) + + +@mod.capture(rule="(first | last) ") +def cursorless_first_last(m) -> dict[str, Any]: + """First/last `n` scopes; eg "first three funk""" + if m[0] == "first": + return create_ordinal_scope_modifier(m.cursorless_scope_type, 0, m.number_small) + return create_ordinal_scope_modifier( + m.cursorless_scope_type, -m.number_small, m.number_small + ) + + +@mod.capture(rule=" | ") +def cursorless_ordinal_scope(m) -> dict[str, Any]: + """Ordinal ranges such as subwords or characters""" + return m[0] + + +def create_ordinal_scope_modifier(scope_type: Any, start: int, length: int = 1): + return { + "type": "ordinalScope", + "scopeType": scope_type, + "start": start, + "length": length, + } diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py new file mode 100644 index 0000000000..dc142d3eaf --- /dev/null +++ b/cursorless-talon/src/modifiers/relative_scope.py @@ -0,0 +1,17 @@ +from typing import Any + +from talon import Module + +mod = Module() + + +@mod.capture(rule="(previous | next) ") +def cursorless_relative_scope(m) -> dict[str, Any]: + """Previous/next scope""" + return { + "type": "relativeScope", + "scopeType": m.cursorless_scope_type, + "offset": 1, + "length": 1, + "direction": "backward" if m[0] == "previous" else "forward", + } diff --git a/cursorless-talon/src/modifiers/sub_token.py b/cursorless-talon/src/modifiers/sub_token.py deleted file mode 100644 index 1121dd92a1..0000000000 --- a/cursorless-talon/src/modifiers/sub_token.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Any - -from talon import Module - -from ..compound_targets import is_active_included, is_anchor_included - -mod = Module() - - -mod.list("cursorless_subtoken_scope_type", desc="Supported subtoken scope types") - - -@mod.capture(rule=" | last") -def ordinal_or_last(m) -> int: - """An ordinal or the word 'last'""" - if m[0] == "last": - return -1 - return m.ordinals_small - 1 - - -@mod.capture( - rule=" [{user.cursorless_range_connective} ]" -) -def cursorless_ordinal_range(m) -> dict[str, Any]: - """Ordinal range""" - try: - range_connective = m.cursorless_range_connective - include_anchor = is_anchor_included(range_connective) - include_active = is_active_included(range_connective) - except AttributeError: - include_anchor = True - include_active = True - - return { - "anchor": m.ordinal_or_last_list[0], - "active": m.ordinal_or_last_list[-1], - "excludeAnchor": not include_anchor, - "excludeActive": not include_active, - } - - -@mod.capture(rule="(first | last) ") -def cursorless_first_last_range(m) -> dict[str, Any]: - """First/last range""" - if m[0] == "first": - return {"anchor": 0, "active": m.number_small - 1} - return {"anchor": -m.number_small, "active": -1} - - -@mod.capture( - rule=( - "( | ) " - "{user.cursorless_subtoken_scope_type}" - ) -) -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 { - "type": "ordinalRange", - "scopeType": { - "type": m.cursorless_subtoken_scope_type, - }, - **range, - } diff --git a/src/core/TokenGraphemeSplitter.ts b/src/core/TokenGraphemeSplitter.ts index 44dc6a8496..dbfd661394 100644 --- a/src/core/TokenGraphemeSplitter.ts +++ b/src/core/TokenGraphemeSplitter.ts @@ -68,7 +68,7 @@ export const UNKNOWN = "[unk]"; /** * Regex used to split a token into graphemes. */ -export const GRAPHEME_SPLIT_REGEX = /\p{L}\p{M}*|\P{L}/gu; +export const GRAPHEME_SPLIT_REGEX = /\p{L}\p{M}*|[\p{N}\p{P}\p{S}]/gu; export class TokenGraphemeSplitter { private disposables: Disposable[] = []; diff --git a/src/core/commandRunner/command.types.ts b/src/core/commandRunner/command.types.ts index 4d22a22175..e3261371c0 100644 --- a/src/core/commandRunner/command.types.ts +++ b/src/core/commandRunner/command.types.ts @@ -4,17 +4,18 @@ import { CommandV0, CommandV1, } from "../commandVersionUpgrades/upgradeV1ToV2/commandV1.types"; +import { CommandV2 } from "../commandVersionUpgrades/upgradeV2ToV3/commandV2.types"; export type CommandComplete = Required> & Pick & { action: Required }; -export const LATEST_VERSION = 2 as const; +export const LATEST_VERSION = 3 as const; export type CommandLatest = Command & { version: typeof LATEST_VERSION; }; -export type Command = CommandV0 | CommandV1 | CommandV2; +export type Command = CommandV0 | CommandV1 | CommandV2 | CommandV3; interface ActionCommand { /** @@ -28,11 +29,11 @@ interface ActionCommand { args?: unknown[]; } -export interface CommandV2 { +export interface CommandV3 { /** * The version number of the command API */ - version: 2; + version: 3; /** * The spoken form of the command if issued from a voice command system diff --git a/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts b/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts index 139db87e27..e6b672c539 100644 --- a/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts +++ b/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts @@ -16,6 +16,7 @@ import canonicalizeActionName from "./canonicalizeActionName"; import canonicalizeTargets from "./canonicalizeTargets"; import { upgradeV0ToV1 } from "./upgradeV0ToV1"; import { upgradeV1ToV2 } from "./upgradeV1ToV2"; +import { upgradeV2ToV3 } from "./upgradeV2ToV3"; /** * Given a command argument which comes from the client, normalize it so that it @@ -66,6 +67,9 @@ function upgradeCommand(command: Command): CommandLatest { case 1: command = upgradeV1ToV2(command); break; + case 2: + command = upgradeV2ToV3(command); + break; default: throw new Error( `Can't upgrade from unknown version ${command.version}` diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts index 95f15c3b44..84c0125aaf 100644 --- a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeStrictHere.ts @@ -1,5 +1,5 @@ import { isDeepStrictEqual } from "util"; -import { PartialPrimitiveTargetDescriptor } from "../../../typings/targetDescriptor.types"; +import { PartialPrimitiveTargetDescriptorV2 } from "../upgradeV2ToV3/targetDescriptorV2.types"; const STRICT_HERE = { type: "primitive", @@ -9,11 +9,11 @@ const STRICT_HERE = { modifier: { type: "identity" }, insideOutsideType: "inside", }; -const IMPLICIT_TARGET: PartialPrimitiveTargetDescriptor = { +const IMPLICIT_TARGET: PartialPrimitiveTargetDescriptorV2 = { type: "primitive", isImplicit: true, }; export const upgradeStrictHere = ( - target: PartialPrimitiveTargetDescriptor -): PartialPrimitiveTargetDescriptor => + target: PartialPrimitiveTargetDescriptorV2 +): PartialPrimitiveTargetDescriptorV2 => isDeepStrictEqual(target, STRICT_HERE) ? IMPLICIT_TARGET : target; diff --git a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts index 2997447830..471d2f568e 100644 --- a/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts +++ b/src/core/commandVersionUpgrades/upgradeV1ToV2/upgradeV1ToV2.ts @@ -1,14 +1,12 @@ -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 { SimpleScopeTypeType } from "../../../typings/targetDescriptor.types"; +import { CommandV2 } from "../upgradeV2ToV3/commandV2.types"; +import { + ModifierV2, + PartialPrimitiveTargetDescriptorV2, + PartialRangeTargetDescriptorV2, + PartialTargetDescriptorV2, +} from "../upgradeV2ToV3/targetDescriptorV2.types"; import { CommandV1, ModifierV0V1, @@ -31,7 +29,7 @@ export function upgradeV1ToV2(command: CommandV1): CommandV2 { }; } -function upgradeModifier(modifier: ModifierV0V1): Modifier[] { +function upgradeModifier(modifier: ModifierV0V1): ModifierV2[] { switch (modifier.type) { case "identity": return []; @@ -93,7 +91,7 @@ function upgradeModifier(modifier: ModifierV0V1): Modifier[] { function upgradePrimitiveTarget( target: PartialPrimitiveTargetV0V1, action: ActionType -): PartialPrimitiveTargetDescriptor { +): PartialPrimitiveTargetDescriptorV2 { const { type, isImplicit, @@ -103,7 +101,7 @@ function upgradePrimitiveTarget( selectionType, position, } = target; - const modifiers: Modifier[] = []; + const modifiers: ModifierV2[] = []; if (position && position !== "contents") { if (position === "before") { @@ -162,7 +160,7 @@ function upgradePrimitiveTarget( function upgradeTarget( target: PartialTargetV0V1, action: ActionType -): PartialTargetDescriptor { +): PartialTargetDescriptorV2 { switch (target.type) { case "list": return { @@ -170,8 +168,8 @@ function upgradeTarget( elements: target.elements.map( (target) => upgradeTarget(target, action) as - | PartialPrimitiveTargetDescriptor - | PartialRangeTargetDescriptor + | PartialPrimitiveTargetDescriptorV2 + | PartialRangeTargetDescriptorV2 ), }; case "range": { @@ -193,12 +191,10 @@ function upgradeTarget( function upgradeTargets( partialTargets: PartialTargetV0V1[], action: ActionType -) { - const partialTargetsV2: PartialTargetDescriptor[] = partialTargets.map( - (target) => upgradeTarget(target, action) - ); - return transformPartialPrimitiveTargets( - partialTargetsV2, - flow(upgradeStrictHere) - ); +): PartialTargetDescriptorV2[] { + return partialTargets + .map((target) => upgradeTarget(target, action)) + .map((target) => + target.type === "primitive" ? upgradeStrictHere(target) : target + ); } diff --git a/src/core/commandVersionUpgrades/upgradeV2ToV3/commandV2.types.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/commandV2.types.ts new file mode 100644 index 0000000000..4f2b10ec72 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/commandV2.types.ts @@ -0,0 +1,42 @@ +import { ActionType } from "../../../actions/actions.types"; +import { PartialTargetDescriptorV2 } from "./targetDescriptorV2.types"; + +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: PartialTargetDescriptorV2[]; +} diff --git a/src/core/commandVersionUpgrades/upgradeV2ToV3/index.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/index.ts new file mode 100644 index 0000000000..7ba24e7423 --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/index.ts @@ -0,0 +1 @@ +export * from "./upgradeV2ToV3"; diff --git a/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts new file mode 100644 index 0000000000..2a62f6bf4c --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/targetDescriptorV2.types.ts @@ -0,0 +1,330 @@ +import { HatStyleName } from "../../hatStyles"; + +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 LineNumberPositionV2 { + type: LineNumberType; + lineNumber: number; +} + +export interface LineNumberMarkV2 { + type: "lineNumber"; + anchor: LineNumberPositionV2; + active: LineNumberPositionV2; +} + +export type MarkV2 = + | CursorMark + | ThatMark + | SourceMark + // | LastCursorPositionMark Not implemented yet + | DecoratedSymbolMark + | NothingMark + | LineNumberMarkV2; + +export type SimpleSurroundingPairName = + | "angleBrackets" + | "backtickQuotes" + | "curlyBrackets" + | "doubleQuotes" + | "escapedDoubleQuotes" + | "escapedParentheses" + | "escapedSquareBrackets" + | "escapedSingleQuotes" + | "parentheses" + | "singleQuotes" + | "squareBrackets"; +export type ComplexSurroundingPairName = + | "string" + | "any" + | "collectionBoundary"; +export type SurroundingPairName = + | SimpleSurroundingPairName + | ComplexSurroundingPairName; + +export type SimpleScopeTypeType = + | "argumentOrParameter" + | "anonymousFunction" + | "attribute" + | "class" + | "className" + | "collectionItem" + | "collectionKey" + | "comment" + | "functionCall" + | "functionCallee" + | "functionName" + | "ifStatement" + | "list" + | "map" + | "name" + | "namedFunction" + | "regularExpression" + | "statement" + | "string" + | "type" + | "value" + | "condition" + | "section" + | "sectionLevelOne" + | "sectionLevelTwo" + | "sectionLevelThree" + | "sectionLevelFour" + | "sectionLevelFive" + | "sectionLevelSix" + | "selector" + | "xmlBothTags" + | "xmlElement" + | "xmlEndTag" + | "xmlStartTag" + // Latex scope types + | "part" + | "chapter" + | "subSection" + | "subSubSection" + | "namedParagraph" + | "subParagraph" + | "environment" + // Text based scopes + | "token" + | "line" + | "notebookCell" + | "paragraph" + | "document" + | "character" + | "word" + | "nonWhitespaceSequence" + | "boundedNonWhitespaceSequence" + | "url"; + +export interface SimpleScopeType { + type: SimpleScopeTypeType; +} + +export interface CustomRegexScopeType { + type: "customRegex"; + regex: string; +} + +export type SurroundingPairDirection = "left" | "right"; +export interface SurroundingPairScopeType { + type: "surroundingPair"; + delimiter: SurroundingPairName; + forceDirection?: SurroundingPairDirection; + + /** + * If `true`, then only accept pairs where the pair completely contains the + * selection, ie without the edges touching. + */ + requireStrongContainment?: boolean; +} + +export type ScopeType = + | SimpleScopeType + | SurroundingPairScopeType + | CustomRegexScopeType; + +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 LeadingModifier { + type: "leading"; +} + +export interface TrailingModifier { + type: "trailing"; +} + +export type Position = "before" | "after" | "start" | "end"; + +export interface PositionModifier { + type: "position"; + position: Position; +} + +export interface PartialPrimitiveTargetDescriptorV2 { + type: "primitive"; + mark?: MarkV2; + modifiers?: ModifierV2[]; + isImplicit?: boolean; +} + +export interface HeadTailModifier { + type: "extendThroughStartOf" | "extendThroughEndOf"; + modifiers?: ModifierV2[]; +} + +/** + * Runs {@link modifier} if the target has no explicit scope type, ie if + * {@link Target.hasExplicitScopeType} is `false`. + */ +export interface ModifyIfUntypedModifier { + type: "modifyIfUntyped"; + + /** + * The modifier to apply if the target is untyped + */ + modifier: ModifierV2; +} + +/** + * Tries each of the modifiers in {@link modifiers} in turn until one of them + * doesn't throw an error, returning the output from the first modifier not + * throwing an error. + */ +export interface CascadingModifier { + type: "cascading"; + + /** + * The modifiers to try in turn + */ + modifiers: ModifierV2[]; +} + +export type ModifierV2 = + | PositionModifier + | InteriorOnlyModifier + | ExcludeInteriorModifier + | ContainingScopeModifier + | EveryScopeModifier + | OrdinalRangeModifier + | HeadTailModifier + | LeadingModifier + | TrailingModifier + | RawSelectionModifier + | ModifyIfUntypedModifier + | CascadingModifier; + +export interface PartialRangeTargetDescriptorV2 { + type: "range"; + anchor: PartialPrimitiveTargetDescriptorV2; + active: PartialPrimitiveTargetDescriptorV2; + excludeAnchor: boolean; + excludeActive: boolean; + rangeType?: RangeType; +} + +export interface PartialListTargetDescriptorV2 { + type: "list"; + elements: ( + | PartialPrimitiveTargetDescriptorV2 + | PartialRangeTargetDescriptorV2 + )[]; +} + +export type PartialTargetDescriptorV2 = + | PartialPrimitiveTargetDescriptorV2 + | PartialRangeTargetDescriptorV2 + | PartialListTargetDescriptorV2; + +export interface PrimitiveTargetDescriptor + extends PartialPrimitiveTargetDescriptorV2 { + /** + * The mark, eg "air", "this", "that", etc + */ + mark: MarkV2; + + /** + * 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: ModifierV2[]; + + /** + * We separate the positional modifier from the other modifiers because it + * behaves differently and and makes the target behave like a destination for + * example for bring. This change is the first step toward #803 + */ + positionModifier?: PositionModifier; +} + +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/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts b/src/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts new file mode 100644 index 0000000000..619763c86e --- /dev/null +++ b/src/core/commandVersionUpgrades/upgradeV2ToV3/upgradeV2ToV3.ts @@ -0,0 +1,142 @@ +import { isEqual } from "lodash"; +import { + OrdinalScopeModifier, + LineNumberMark, + Mark, + Modifier, + PartialPrimitiveTargetDescriptor, + PartialRangeTargetDescriptor, + PartialTargetDescriptor, + RangeMark, + RangeModifier, +} from "../../../typings/targetDescriptor.types"; +import { CommandV3 } from "../../commandRunner/command.types"; +import { CommandV2 } from "./commandV2.types"; +import { + LineNumberMarkV2, + LineNumberPositionV2, + MarkV2, + ModifierV2, + OrdinalRangeModifier, + PartialPrimitiveTargetDescriptorV2, + PartialTargetDescriptorV2, + ScopeType, +} from "./targetDescriptorV2.types"; + +export function upgradeV2ToV3(command: CommandV2): CommandV3 { + return { + ...command, + version: 3, + targets: command.targets.map(upgradeTarget), + }; +} + +function upgradeTarget( + target: PartialTargetDescriptorV2 +): PartialTargetDescriptor { + switch (target.type) { + case "list": + return { + ...target, + elements: target.elements.map( + (target) => + upgradeTarget(target) as + | PartialPrimitiveTargetDescriptor + | PartialRangeTargetDescriptor + ), + }; + case "range": { + const { anchor, active, ...rest } = target; + return { + anchor: upgradePrimitiveTarget(anchor), + active: upgradePrimitiveTarget(active), + ...rest, + }; + } + case "primitive": + return upgradePrimitiveTarget(target); + } +} + +function upgradePrimitiveTarget( + target: PartialPrimitiveTargetDescriptorV2 +): PartialPrimitiveTargetDescriptor { + return { + ...target, + mark: target.mark != null ? updateMark(target.mark) : undefined, + modifiers: + target.modifiers != null + ? target.modifiers.map(updateModifier) + : undefined, + }; +} + +function updateMark(mark: MarkV2): Mark { + switch (mark.type) { + case "lineNumber": + return createLineNumberMark(mark); + default: + return mark as Mark; + } +} + +function updateModifier(modifier: ModifierV2): Modifier { + switch (modifier.type) { + case "ordinalRange": + return createOrdinalModifier(modifier); + default: + return modifier as Modifier; + } +} + +function createLineNumberMark( + mark: LineNumberMarkV2 +): LineNumberMark | RangeMark { + if (isEqual(mark.anchor, mark.active)) { + return createLineNumberMarkFromPos(mark.anchor); + } + + return { + type: "range", + anchor: createLineNumberMarkFromPos(mark.anchor), + active: createLineNumberMarkFromPos(mark.active), + }; +} + +function createOrdinalModifier( + modifier: OrdinalRangeModifier +): OrdinalScopeModifier | RangeModifier { + if (modifier.anchor === modifier.active) { + return createAbsoluteOrdinalModifier(modifier.scopeType, modifier.anchor); + } + + return { + type: "range", + anchor: createAbsoluteOrdinalModifier(modifier.scopeType, modifier.anchor), + active: createAbsoluteOrdinalModifier(modifier.scopeType, modifier.active), + excludeAnchor: modifier.excludeAnchor, + excludeActive: modifier.excludeActive, + }; +} + +function createLineNumberMarkFromPos( + position: LineNumberPositionV2 +): LineNumberMark { + return { + type: "lineNumber", + lineNumberType: position.type, + lineNumber: position.lineNumber, + }; +} + +function createAbsoluteOrdinalModifier( + scopeType: ScopeType, + start: number +): OrdinalScopeModifier { + return { + type: "ordinalScope", + scopeType, + start, + length: 1, + }; +} diff --git a/src/processTargets/getMarkStage.ts b/src/processTargets/getMarkStage.ts index 5339542be7..a3174e0d81 100644 --- a/src/processTargets/getMarkStage.ts +++ b/src/processTargets/getMarkStage.ts @@ -3,6 +3,7 @@ import CursorStage from "./marks/CursorStage"; import DecoratedSymbolStage from "./marks/DecoratedSymbolStage"; import LineNumberStage from "./marks/LineNumberStage"; import NothingStage from "./marks/NothingStage"; +import RangeMarkStage from "./marks/RangeMarkStage"; import { SourceStage, ThatStage } from "./marks/ThatStage"; import { MarkStage } from "./PipelineStages.types"; @@ -18,6 +19,8 @@ export default (mark: Mark): MarkStage => { return new DecoratedSymbolStage(mark); case "lineNumber": return new LineNumberStage(mark); + case "range": + return new RangeMarkStage(mark); case "nothing": return new NothingStage(mark); } diff --git a/src/processTargets/getModifierStage.ts b/src/processTargets/getModifierStage.ts index a48e1c8468..a2ea266f4c 100644 --- a/src/processTargets/getModifierStage.ts +++ b/src/processTargets/getModifierStage.ts @@ -4,7 +4,6 @@ import { EveryScopeModifier, Modifier, } from "../typings/targetDescriptor.types"; -import BoundedNonWhitespaceSequenceStage from "./modifiers/BoundedNonWhitespaceStage"; import CascadingStage from "./modifiers/CascadingStage"; import { HeadStage, TailStage } from "./modifiers/HeadTailStage"; import { @@ -14,11 +13,12 @@ import { import ItemStage from "./modifiers/ItemStage"; import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; import ModifyIfUntypedStage from "./modifiers/ModifyIfUntypedStage"; -import OrdinalRangeSubTokenStage, { - OrdinalRangeSubTokenModifier, -} from "./modifiers/OrdinalRangeSubTokenStage"; +import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; import PositionStage from "./modifiers/PositionStage"; +import RangeModifierStage from "./modifiers/RangeModifierStage"; import RawSelectionStage from "./modifiers/RawSelectionStage"; +import { RelativeScopeStage } from "./modifiers/RelativeScopeStage"; +import BoundedNonWhitespaceSequenceStage from "./modifiers/scopeTypeStages/BoundedNonWhitespaceStage"; import ContainingSyntaxScopeStage, { SimpleContainingScopeModifier, } from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage"; @@ -27,11 +27,15 @@ import LineStage from "./modifiers/scopeTypeStages/LineStage"; import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage"; import ParagraphStage from "./modifiers/scopeTypeStages/ParagraphStage"; import { - NonWhitespaceSequenceStage, CustomRegexModifier, CustomRegexStage, + NonWhitespaceSequenceStage, UrlStage, } from "./modifiers/scopeTypeStages/RegexStage"; +import { + CharacterStage, + WordStage, +} from "./modifiers/scopeTypeStages/SubTokenStages"; import TokenStage from "./modifiers/scopeTypeStages/TokenStage"; import SurroundingPairStage from "./modifiers/SurroundingPairStage"; import { ModifierStage } from "./PipelineStages.types"; @@ -57,19 +61,16 @@ export default (modifier: Modifier): ModifierStage => { 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 - ); + case "ordinalScope": + return new OrdinalScopeStage(modifier); + case "relativeScope": + return new RelativeScopeStage(modifier); case "cascading": return new CascadingStage(modifier); case "modifyIfUntyped": return new ModifyIfUntypedStage(modifier); + case "range": + return new RangeModifierStage(modifier); } }; @@ -102,8 +103,9 @@ const getContainingScopeStage = ( modifier as ContainingSurroundingPairModifier ); case "word": + return new WordStage(modifier); case "character": - throw new Error(`Unsupported scope type ${modifier.scopeType.type}`); + return new CharacterStage(modifier); default: // Default to containing syntax scope using tree sitter return new ContainingSyntaxScopeStage( diff --git a/src/processTargets/marks/CursorStage.ts b/src/processTargets/marks/CursorStage.ts index 6d8b1be179..d49951337f 100644 --- a/src/processTargets/marks/CursorStage.ts +++ b/src/processTargets/marks/CursorStage.ts @@ -6,7 +6,7 @@ import type { MarkStage } from "../PipelineStages.types"; import { UntypedTarget } from "../targets"; export default class CursorStage implements MarkStage { - constructor(private modifier: CursorMark) {} + constructor(private mark: CursorMark) {} run(context: ProcessedTargetsContext): Target[] { return context.currentSelections.map( diff --git a/src/processTargets/marks/DecoratedSymbolStage.ts b/src/processTargets/marks/DecoratedSymbolStage.ts index 171a6bd180..ec7cf91dd7 100644 --- a/src/processTargets/marks/DecoratedSymbolStage.ts +++ b/src/processTargets/marks/DecoratedSymbolStage.ts @@ -5,17 +5,17 @@ import { MarkStage } from "../PipelineStages.types"; import { UntypedTarget } from "../targets"; export default class implements MarkStage { - constructor(private modifier: DecoratedSymbolMark) {} + constructor(private mark: DecoratedSymbolMark) {} run(context: ProcessedTargetsContext): Target[] { const token = context.hatTokenMap.getToken( - this.modifier.symbolColor, - this.modifier.character + this.mark.symbolColor, + this.mark.character ); if (token == null) { throw new Error( - `Couldn't find mark ${this.modifier.symbolColor} '${this.modifier.character}'` + `Couldn't find mark ${this.mark.symbolColor} '${this.mark.character}'` ); } diff --git a/src/processTargets/marks/LineNumberStage.ts b/src/processTargets/marks/LineNumberStage.ts index 4c0f62df49..4a1ddb178a 100644 --- a/src/processTargets/marks/LineNumberStage.ts +++ b/src/processTargets/marks/LineNumberStage.ts @@ -1,7 +1,7 @@ import type { TextEditor } from "vscode"; import type { LineNumberMark, - LineNumberPosition, + LineNumberType, } from "../../typings/targetDescriptor.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; import { createLineTarget } from "../modifiers/scopeTypeStages/LineStage"; @@ -9,29 +9,33 @@ import type { MarkStage } from "../PipelineStages.types"; import { LineTarget } from "../targets"; export default class implements MarkStage { - constructor(private modifier: LineNumberMark) {} + constructor(private mark: 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, isReversed, contentRange)]; + const lineNumber = getLineNumber( + editor, + this.mark.lineNumberType, + this.mark.lineNumber + ); + const contentRange = editor.document.lineAt(lineNumber).range; + return [createLineTarget(editor, false, contentRange)]; } } -const getLine = (editor: TextEditor, linePosition: LineNumberPosition) => { - switch (linePosition.type) { +const getLineNumber = ( + editor: TextEditor, + lineNumberType: LineNumberType, + lineNumber: number +) => { + switch (lineNumberType) { case "absolute": - return linePosition.lineNumber; + return lineNumber; case "relative": - return editor.selection.active.line + linePosition.lineNumber; + return editor.selection.active.line + lineNumber; case "modulo100": { const stepSize = 100; const startLine = editor.visibleRanges[0].start.line; @@ -40,19 +44,21 @@ const getLine = (editor: TextEditor, linePosition: LineNumberPosition) => { const base = Math.floor(startLine / stepSize) * stepSize; const visibleLines = []; const invisibleLines = []; - let lineNumber = base + linePosition.lineNumber; - while (lineNumber <= endLine) { - if (lineNumber >= startLine) { + let currentLineNumber = base + lineNumber; + while (currentLineNumber <= endLine) { + if (currentLineNumber >= startLine) { const visible = editor.visibleRanges.find( - (r) => lineNumber >= r.start.line && lineNumber <= r.end.line + (r) => + currentLineNumber >= r.start.line && + currentLineNumber <= r.end.line ); if (visible) { - visibleLines.push(lineNumber); + visibleLines.push(currentLineNumber); } else { - invisibleLines.push(lineNumber); + invisibleLines.push(currentLineNumber); } } - lineNumber += stepSize; + currentLineNumber += stepSize; } if (visibleLines.length === 1) { return visibleLines[0]; diff --git a/src/processTargets/marks/NothingStage.ts b/src/processTargets/marks/NothingStage.ts index 818b242173..d4c3944186 100644 --- a/src/processTargets/marks/NothingStage.ts +++ b/src/processTargets/marks/NothingStage.ts @@ -3,7 +3,7 @@ import { NothingMark } from "../../typings/targetDescriptor.types"; import { MarkStage } from "../PipelineStages.types"; export default class implements MarkStage { - constructor(private modifier: NothingMark) {} + constructor(private mark: NothingMark) {} run(): Target[] { return []; diff --git a/src/processTargets/marks/RangeMarkStage.ts b/src/processTargets/marks/RangeMarkStage.ts new file mode 100644 index 0000000000..a75f1d8891 --- /dev/null +++ b/src/processTargets/marks/RangeMarkStage.ts @@ -0,0 +1,30 @@ +import { Target } from "../../typings/target.types"; +import { RangeMark } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import getMarkStage from "../getMarkStage"; +import { MarkStage } from "../PipelineStages.types"; +import { targetsToContinuousTarget } from "../processTargets"; + +export default class RangeMarkStage implements MarkStage { + constructor(private mark: RangeMark) {} + + run(context: ProcessedTargetsContext): Target[] { + const anchorStage = getMarkStage(this.mark.anchor); + const activeStage = getMarkStage(this.mark.active); + const anchorTargets = anchorStage.run(context); + const activeTargets = activeStage.run(context); + + if (anchorTargets.length !== 1 || activeTargets.length !== 1) { + throw new Error("Expected single anchor and active target"); + } + + return [ + targetsToContinuousTarget( + anchorTargets[0], + activeTargets[0], + this.mark.excludeAnchor, + this.mark.excludeActive + ), + ]; + } +} diff --git a/src/processTargets/marks/ThatStage.ts b/src/processTargets/marks/ThatStage.ts index 952f646757..92c545d47c 100644 --- a/src/processTargets/marks/ThatStage.ts +++ b/src/processTargets/marks/ThatStage.ts @@ -4,7 +4,7 @@ import { ProcessedTargetsContext } from "../../typings/Types"; import { MarkStage } from "../PipelineStages.types"; export class ThatStage implements MarkStage { - constructor(private modifier: ThatMark) {} + constructor(private mark: ThatMark) {} run(context: ProcessedTargetsContext): Target[] { if (context.thatMark.length === 0) { @@ -16,7 +16,7 @@ export class ThatStage implements MarkStage { } export class SourceStage implements MarkStage { - constructor(private modifier: SourceMark) {} + constructor(private mark: SourceMark) {} run(context: ProcessedTargetsContext): Target[] { if (context.sourceMark.length === 0) { diff --git a/src/processTargets/modifiers/OrdinalRangeSubTokenStage.ts b/src/processTargets/modifiers/OrdinalRangeSubTokenStage.ts deleted file mode 100644 index 8f418fce84..0000000000 --- a/src/processTargets/modifiers/OrdinalRangeSubTokenStage.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Range } from "vscode"; -import { GRAPHEME_SPLIT_REGEX } from "../../core/TokenGraphemeSplitter"; -import type { Target } from "../../typings/target.types"; -import type { - OrdinalRangeModifier, - SimpleScopeType, -} from "../../typings/targetDescriptor.types"; -import type { ProcessedTargetsContext } from "../../typings/Types"; -import type { ModifierStage } from "../PipelineStages.types"; -import { PlainTarget, SubTokenWordTarget } from "../targets"; -import { getTokenRangeForSelection } from "./scopeTypeStages/TokenStage"; -import { SUBWORD_MATCHER } from "./subToken"; - -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; - // If the target has an explicit range use that. Otherwise expand to the token. - const tokenContentRange = target.hasExplicitRange - ? target.contentRange - : getTokenRangeForSelection(target.editor, 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"); - } - - const regex = - this.modifier.scopeType.type === "word" - ? SUBWORD_MATCHER - : GRAPHEME_SPLIT_REGEX; - pieces = [...tokenText.matchAll(regex)].map((match) => ({ - start: match.index!, - end: match.index! + match[0].length, - })); - - 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/OrdinalScopeStage.ts b/src/processTargets/modifiers/OrdinalScopeStage.ts new file mode 100644 index 0000000000..8ea114e82e --- /dev/null +++ b/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -0,0 +1,33 @@ +import { Target } from "../../typings/target.types"; +import { OrdinalScopeModifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import { + createRangeTargetFromIndices, + getEveryScopeTargets, +} from "./targetSequenceUtils"; + +export class OrdinalScopeStage implements ModifierStage { + constructor(private modifier: OrdinalScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const targets = getEveryScopeTargets( + context, + target, + this.modifier.scopeType + ); + + const startIndex = + this.modifier.start + (this.modifier.start < 0 ? targets.length : 0); + const endIndex = startIndex + this.modifier.length - 1; + + return [ + createRangeTargetFromIndices( + target.isReversed, + targets, + startIndex, + endIndex + ), + ]; + } +} diff --git a/src/processTargets/modifiers/RangeModifierStage.ts b/src/processTargets/modifiers/RangeModifierStage.ts new file mode 100644 index 0000000000..a5822ac6a4 --- /dev/null +++ b/src/processTargets/modifiers/RangeModifierStage.ts @@ -0,0 +1,30 @@ +import { Target } from "../../typings/target.types"; +import { RangeModifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import getModifierStage from "../getModifierStage"; +import { ModifierStage } from "../PipelineStages.types"; +import { targetsToContinuousTarget } from "../processTargets"; + +export default class RangeModifierStage implements ModifierStage { + constructor(private modifier: RangeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const anchorStage = getModifierStage(this.modifier.anchor); + const activeStage = getModifierStage(this.modifier.active); + const anchorTargets = anchorStage.run(context, target); + const activeTargets = activeStage.run(context, target); + + if (anchorTargets.length !== 1 || activeTargets.length !== 1) { + throw new Error("Expected single anchor and active target"); + } + + return [ + targetsToContinuousTarget( + anchorTargets[0], + activeTargets[0], + this.modifier.excludeAnchor, + this.modifier.excludeActive + ), + ]; + } +} diff --git a/src/processTargets/modifiers/RelativeScopeStage.ts b/src/processTargets/modifiers/RelativeScopeStage.ts new file mode 100644 index 0000000000..3540dc4432 --- /dev/null +++ b/src/processTargets/modifiers/RelativeScopeStage.ts @@ -0,0 +1,186 @@ +import { findLastIndex } from "lodash"; +import { Range } from "vscode"; +import { NoContainingScopeError } from "../../errors"; +import { Target } from "../../typings/target.types"; +import { RelativeScopeModifier } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStage } from "../PipelineStages.types"; +import { UntypedTarget } from "../targets"; +import { + createRangeTargetFromIndices, + getEveryScopeTargets, + OutOfRangeError, +} from "./targetSequenceUtils"; + +export class RelativeScopeStage implements ModifierStage { + constructor(private modifier: RelativeScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const isForward = this.modifier.direction === "forward"; + + /** + * A list of targets in the iteration scope for the input {@link target}. + * Note that we convert {@link target} to have no explicit range so that we + * get all targets in the iteration scope rather than just the intersecting + * targets. + * + * FIXME: In the future we should probably use a better abstraction for this, but + * that will rely on #629 + */ + const targets = getEveryScopeTargets( + context, + createTargetWithoutExplicitRange(target), + this.modifier.scopeType + ); + + /** Proximal index. This is the index closest to the target content range. */ + const proximalIndex = this.computeProximalIndex( + target.contentRange, + targets, + isForward + ); + + /** Index of range farther from input target */ + const distalIndex = isForward + ? proximalIndex + this.modifier.length - 1 + : proximalIndex - this.modifier.length + 1; + + const startIndex = Math.min(proximalIndex, distalIndex); + const endIndex = Math.max(proximalIndex, distalIndex); + + return [ + createRangeTargetFromIndices( + target.isReversed, + targets, + startIndex, + endIndex + ), + ]; + } + + /** + * Compute the index of the target that will form the near end of the range. + * + * @param inputTargetRange The range of the input target to the modifier stage + * @param targets A list of all targets under consideration (eg in iteration + * scope) + * @param isForward `true` if we are handling "next", `false` if "previous" + * @returns The index into {@link targets} that will form the near end of the range. + */ + private computeProximalIndex( + inputTargetRange: Range, + targets: Target[], + isForward: boolean + ) { + const includeIntersectingScopes = this.modifier.offset === 0; + + const intersectingIndices = getIntersectingTargetIndices( + inputTargetRange, + targets + ); + + if (intersectingIndices.length === 0) { + const adjacentTargetIndex = isForward + ? targets.findIndex((t) => + t.contentRange.start.isAfter(inputTargetRange.start) + ) + : findLastIndex(targets, (t) => + t.contentRange.start.isBefore(inputTargetRange.start) + ); + + if (adjacentTargetIndex === -1) { + throw new OutOfRangeError(); + } + + // For convenience, if they ask to include intersecting indices, we just + // start with the nearest one in the correct direction. So eg if you say + // "two funks" between functions, it will take two functions to the right + // of you. + if (includeIntersectingScopes) { + return adjacentTargetIndex; + } + + return isForward + ? adjacentTargetIndex + this.modifier.offset - 1 + : adjacentTargetIndex - this.modifier.offset + 1; + } + + // If we've made it here, then there are scopes intersecting with + // {@link inputTargetRange} + + const intersectingStartIndex = intersectingIndices[0]; + const intersectingEndIndex = intersectingIndices.at(-1)!; + + if (includeIntersectingScopes) { + // Number of scopes intersecting with input target is already greater than + // desired length; throw error. This occurs if user says "two funks", and + // they have 3 functions selected. Not clear what to do in that case so + // we throw error. + if (intersectingIndices.length > this.modifier.length) { + throw new TooFewScopesError( + this.modifier.length, + intersectingIndices.length, + this.modifier.scopeType.type + ); + } + + // This ensures that we count intersecting scopes in "three funks", so + // that we will never get more than 3 functions. + return isForward ? intersectingStartIndex : intersectingEndIndex; + } + + // If we are excluding the intersecting scopes, then we set 0 to be such + // that the next scope will be the first non-intersecting. + return isForward + ? intersectingEndIndex + this.modifier.offset + : intersectingStartIndex - this.modifier.offset; + } +} + +class TooFewScopesError extends Error { + constructor( + requestedLength: number, + currentLength: number, + scopeType: string + ) { + super( + `Requested ${requestedLength} ${scopeType}s, but ${currentLength} are already selected.` + ); + this.name = "TooFewScopesError"; + } +} + +/** Get indices of all targets in {@link targets} intersecting with + * {@link inputTargetRange} */ +function getIntersectingTargetIndices( + inputTargetRange: Range, + targets: Target[] +): number[] { + const targetsWithIntersection = targets + .map((t, i) => ({ + index: i, + intersection: t.contentRange.intersection(inputTargetRange), + })) + .filter((t) => t.intersection != null); + + // Input target range is empty. Use rightmost target and accept weak + // containment. + if (inputTargetRange.isEmpty) { + return targetsWithIntersection.slice(-1).map((t) => t.index); + } + + // Input target range is not empty. Use all targets with non empty + // intersections. + return targetsWithIntersection + .filter((t) => !t.intersection!.isEmpty) + .map((t) => t.index); +} + +function createTargetWithoutExplicitRange(target: Target) { + return new UntypedTarget({ + editor: target.editor, + isReversed: target.isReversed, + contentRange: target.contentRange, + hasExplicitRange: false, + }); +} diff --git a/src/processTargets/modifiers/BoundedNonWhitespaceStage.ts b/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts similarity index 77% rename from src/processTargets/modifiers/BoundedNonWhitespaceStage.ts rename to src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts index 2b700bebd9..6f92eed1c2 100644 --- a/src/processTargets/modifiers/BoundedNonWhitespaceStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts @@ -1,14 +1,14 @@ -import { Target } from "../../typings/target.types"; +import { Target } from "../../../typings/target.types"; import { ContainingScopeModifier, EveryScopeModifier, -} from "../../typings/targetDescriptor.types"; -import { ProcessedTargetsContext } from "../../typings/Types"; -import { ModifierStage } from "../PipelineStages.types"; -import { TokenTarget } from "../targets"; -import getModifierStage from "../getModifierStage"; -import { processSurroundingPair } from "./surroundingPair"; -import { NoContainingScopeError } from "../../errors"; +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import { TokenTarget } from "../../targets"; +import getModifierStage from "../../getModifierStage"; +import { processSurroundingPair } from "../surroundingPair"; +import { NoContainingScopeError } from "../../../errors"; /** * Intersection of NonWhitespaceSequenceStage and a surrounding pair diff --git a/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts new file mode 100644 index 0000000000..06be51abd1 --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/SubTokenStages.ts @@ -0,0 +1,193 @@ +import { Range, TextEditor } from "vscode"; +import { GRAPHEME_SPLIT_REGEX } from "../../../core/TokenGraphemeSplitter"; +import { NoContainingScopeError } from "../../../errors"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { matchAll } from "../../../util/regex"; +import { ModifierStage } from "../../PipelineStages.types"; +import { PlainTarget, SubTokenWordTarget } from "../../targets"; +import { SUBWORD_MATCHER } from "../subToken"; +import { getTokenRangeForSelection } from "./TokenStage"; + +abstract class SubTokenStage implements ModifierStage { + constructor( + private modifier: ContainingScopeModifier | EveryScopeModifier, + private regex: RegExp + ) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + const { document } = target.editor; + const tokenRange = getTokenRangeForSelection( + target.editor, + target.contentRange + ); + const text = document.getText(tokenRange); + const offset = document.offsetAt(tokenRange.start); + + const contentRanges = matchAll( + text, + this.regex, + (match) => + new Range( + document.positionAt(offset + match.index!), + document.positionAt(offset + match.index! + match[0].length) + ) + ); + + const targets = this.createTargetsFromRanges( + target.isReversed, + target.editor, + contentRanges + ); + + // If target has explicit range filter to scopes in that range. Otherwise expand to all scopes in iteration scope. + const filteredTargets = target.hasExplicitRange + ? filterTargets(target, targets) + : targets; + + if (filteredTargets.length === 0) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + if (this.modifier.type === "everyScope") { + return filteredTargets; + } + + return [this.getSingleTarget(target, filteredTargets)]; + } + + /** + * Constructs a single range target containing all targets from + * {@link allTargets} that intersect with {@link inputTarget}. + * @param inputTarget The input target to this stage + * @param allTargets A list of all targets under consideration + * @returns A single target constructed by forming a range containing all + * targets that intersect with {@link inputTarget} + */ + private getSingleTarget(inputTarget: Target, allTargets: Target[]): Target { + let intersectingTargets = allTargets + .map((t) => ({ + target: t, + intersection: t.contentRange.intersection(inputTarget.contentRange), + })) + .filter((it) => it.intersection != null); + + if (intersectingTargets.length === 0) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + // Empty range utilize single adjacent target to the right of {@link inputTarget} + if (inputTarget.contentRange.isEmpty) { + return intersectingTargets.at(-1)!.target; + } + + // On non empty input range, utilize all targets with a non-empty + // intersection with {@link inputTarget} + intersectingTargets = intersectingTargets.filter( + (it) => !it.intersection!.isEmpty + ); + + if (intersectingTargets.length === 0) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + if (intersectingTargets.length === 1) { + return intersectingTargets[0].target; + } + + return intersectingTargets[0].target.createContinuousRangeTarget( + inputTarget.isReversed, + intersectingTargets.at(-1)!.target, + true, + true + ); + } + + /** + * Create one target for each element of {@link contentRanges} + */ + protected abstract createTargetsFromRanges( + isReversed: boolean, + editor: TextEditor, + contentRanges: Range[] + ): Target[]; +} + +export class WordStage extends SubTokenStage { + constructor(modifier: ContainingScopeModifier | EveryScopeModifier) { + super(modifier, SUBWORD_MATCHER); + } + + protected createTargetsFromRanges( + isReversed: boolean, + editor: TextEditor, + contentRanges: Range[] + ): Target[] { + return contentRanges.map((contentRange, i) => { + const previousContentRange = i > 0 ? contentRanges[i - 1] : null; + const nextContentRange = + i + 1 < contentRanges.length ? contentRanges[i + 1] : null; + + const leadingDelimiterRange = + previousContentRange != null && + contentRange.start.isAfter(previousContentRange.end) + ? new Range(previousContentRange.end, contentRange.start) + : undefined; + + const trailingDelimiterRange = + nextContentRange != null && + nextContentRange.start.isAfter(contentRange.end) + ? new Range(contentRange.end, nextContentRange.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, + }); + }); + } +} + +export class CharacterStage extends SubTokenStage { + constructor(modifier: ContainingScopeModifier | EveryScopeModifier) { + super(modifier, GRAPHEME_SPLIT_REGEX); + } + + protected createTargetsFromRanges( + isReversed: boolean, + editor: TextEditor, + contentRanges: Range[] + ): Target[] { + return contentRanges.map( + (contentRange) => + new PlainTarget({ + editor, + isReversed, + contentRange, + }) + ); + } +} + +function filterTargets(target: Target, targets: Target[]): Target[] { + return targets.filter((t) => { + const intersection = t.contentRange.intersection(target.contentRange); + return intersection != null && !intersection.isEmpty; + }); +} diff --git a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts b/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts index f791e0f65c..298885f37d 100644 --- a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts @@ -89,15 +89,14 @@ export function getTokenRangeForSelection( if (lengthDiff !== 0) { return lengthDiff; } - // Lastly sort on start position. ie leftmost - return a.offsets.start - b.offsets.start; + // Lastly sort on start position in reverse. ie prefer rightmost + return b.offsets.start - a.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"); diff --git a/src/processTargets/modifiers/targetSequenceUtils.ts b/src/processTargets/modifiers/targetSequenceUtils.ts new file mode 100644 index 0000000000..120f7a9ae6 --- /dev/null +++ b/src/processTargets/modifiers/targetSequenceUtils.ts @@ -0,0 +1,54 @@ +import { Target } from "../../typings/target.types"; +import { ScopeType } from "../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../typings/Types"; +import getModifierStage from "../getModifierStage"; + +export class OutOfRangeError extends Error { + constructor() { + super("Scope index out of range"); + this.name = "OutOfRangeError"; + } +} + +/** + * Construct a single range target between two targets in a list of targets, + * inclusive + * @param targets The list of targets to index into + * @param startIndex The index of the target in {@link targets} that will form + * the start of the range + * @param endIndex The index of the target in {@link targets} that will form the + * end of the range + */ +export function createRangeTargetFromIndices( + isReversed: boolean, + targets: Target[], + startIndex: number, + endIndex: number +): Target { + if (startIndex < 0 || endIndex >= targets.length) { + throw new OutOfRangeError(); + } + + if (startIndex === endIndex) { + return targets[startIndex]; + } + + return targets[startIndex].createContinuousRangeTarget( + isReversed, + targets[endIndex], + true, + true + ); +} + +export function getEveryScopeTargets( + context: ProcessedTargetsContext, + target: Target, + scopeType: ScopeType +): Target[] { + const containingStage = getModifierStage({ + type: "everyScope", + scopeType, + }); + return containingStage.run(context, target); +} diff --git a/src/processTargets/processTargets.ts b/src/processTargets/processTargets.ts index bb56993ed7..2955ab9199 100644 --- a/src/processTargets/processTargets.ts +++ b/src/processTargets/processTargets.ts @@ -8,7 +8,6 @@ import { TargetDescriptor, } from "../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../typings/Types"; -import { ensureSingleEditor } from "../util/targetUtils"; import getMarkStage from "./getMarkStage"; import getModifierStage from "./getModifierStage"; import { ModifierStage } from "./PipelineStages.types"; @@ -61,19 +60,13 @@ function processRangeTarget( 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" - ); + throw new Error("AnchorTargets and activeTargets lengths don't match"); } switch (targetDesc.rangeType) { case "continuous": return [ - processContinuousRangeTarget( + targetsToContinuousTarget( anchorTarget, activeTarget, targetDesc.excludeAnchor, @@ -81,7 +74,7 @@ function processRangeTarget( ), ]; case "vertical": - return processVerticalRangeTarget( + return targetsToVerticalTarget( anchorTarget, activeTarget, targetDesc.excludeAnchor, @@ -92,13 +85,14 @@ function processRangeTarget( ); } -function processContinuousRangeTarget( +export function targetsToContinuousTarget( anchorTarget: Target, activeTarget: Target, - excludeAnchor: boolean, - excludeActive: boolean + excludeAnchor: boolean = false, + excludeActive: boolean = false ): Target { - ensureSingleEditor([anchorTarget, activeTarget]); + ensureSingleEditor(anchorTarget, activeTarget); + const isReversed = calcIsReversed(anchorTarget, activeTarget); const startTarget = isReversed ? activeTarget : anchorTarget; const endTarget = isReversed ? anchorTarget : activeTarget; @@ -113,19 +107,14 @@ function processContinuousRangeTarget( ); } -export function targetsToContinuousTarget( - anchorTarget: Target, - activeTarget: Target -): Target { - return processContinuousRangeTarget(anchorTarget, activeTarget, false, false); -} - -function processVerticalRangeTarget( +function targetsToVerticalTarget( anchorTarget: Target, activeTarget: Target, excludeAnchor: boolean, excludeActive: boolean ): Target[] { + ensureSingleEditor(anchorTarget, activeTarget); + const isReversed = calcIsReversed(anchorTarget, activeTarget); const delta = isReversed ? -1 : 1; @@ -246,3 +235,9 @@ function calcIsReversed(anchor: Target, active: Target) { function uniqTargets(array: Target[]): Target[] { return uniqWith(array, (a, b) => a.isEqual(b)); } + +function ensureSingleEditor(anchorTarget: Target, activeTarget: Target) { + if (anchorTarget.editor !== activeTarget.editor) { + throw new Error("Cannot form range between targets in different editors"); + } +} diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoToken.yml new file mode 100644 index 0000000000..6afd2f6105 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: "aaa bbb " + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoToken2.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoToken2.yml new file mode 100644 index 0000000000..2336f15051 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearNextTwoToken2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearOneToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearOneToken.yml new file mode 100644 index 0000000000..fe98c76a63 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearOneToken.yml @@ -0,0 +1,22 @@ +languageId: plaintext +command: + spokenForm: clear one token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc ddd eee fff + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 13} + marks: {} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 1, direction: forward}]}] +thrownError: {name: TooFewScopesError} diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken.yml new file mode 100644 index 0000000000..cef13a0b80 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear previous two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: " ccc" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken2.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken2.yml new file mode 100644 index 0000000000..17548bc8f3 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear previous two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 10} + active: {line: 0, character: 10} + marks: {} +finalState: + documentContents: " ccc" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken3.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken3.yml new file mode 100644 index 0000000000..861f78345f --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearPreviousTwoToken3.yml @@ -0,0 +1,22 @@ +languageId: plaintext +command: + spokenForm: clear previous two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 2, direction: backward}]}] +thrownError: {name: OutOfRangeError} diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearThreeToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearThreeToken.yml new file mode 100644 index 0000000000..edc3b82639 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearThreeToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear three token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 3 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc ddd eee fff + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 13} + marks: {} +finalState: + documentContents: aaa bbb fff + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 3, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearThreeTokenBackward.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearThreeTokenBackward.yml new file mode 100644 index 0000000000..db7cf96129 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearThreeTokenBackward.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear three token backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 3 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc ddd eee fff + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 13} + marks: {} +finalState: + documentContents: aaa eee fff + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 3, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken.yml new file mode 100644 index 0000000000..7dfa406d92 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc ddd eee fff + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 13} + marks: {} +finalState: + documentContents: aaa bbb eee fff + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken2.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken2.yml new file mode 100644 index 0000000000..fa09c28380 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml new file mode 100644 index 0000000000..4fad6e3887 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken4.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken4.yml new file mode 100644 index 0000000000..fb9df8934a --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoToken4.yml @@ -0,0 +1,22 @@ +languageId: plaintext +command: + spokenForm: clear two token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}] +thrownError: {name: OutOfRangeError} diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward.yml new file mode 100644 index 0000000000..3c14d82247 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two token backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: " ccc" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml new file mode 100644 index 0000000000..072321a958 --- /dev/null +++ b/src/test/suite/fixtures/recorded/relativeScopes/clearTwoTokenBackward2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear two token backward + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa bbb ccc + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: " ccc" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckLastTwoItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckLastTwoItem.yml new file mode 100644 index 0000000000..2ace070a49 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckLastTwoItem.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: chuck last two item + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: collectionItem} + start: -2 + length: 2 + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: a, b + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: collectionItem}, start: -2, length: 2}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckWord.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckWord.yml new file mode 100644 index 0000000000..004a43fefc --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckWord.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: chuck word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: aaa_bbb_ccc + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: aaa_ccc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/chuckWord2.yml b/src/test/suite/fixtures/recorded/selectionTypes/chuckWord2.yml new file mode 100644 index 0000000000..d26c4f98a1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/chuckWord2.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: chuck word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aaaCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCar.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCar.yml new file mode 100644 index 0000000000..d61c179349 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCar.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear car + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: character} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aaaBbCcc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: character}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCar2.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCar2.yml new file mode 100644 index 0000000000..b680b3c245 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCar2.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear car + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: character} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aaabbCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: character}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCar3.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCar3.yml new file mode 100644 index 0000000000..2126236eb4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCar3.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear car + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: character} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aabbCcc + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: character}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCar4.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCar4.yml new file mode 100644 index 0000000000..48980db8c8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCar4.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear car + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: character} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: " a" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: " " + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: character}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCar5.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCar5.yml new file mode 100644 index 0000000000..60d428b4e0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCar5.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear car + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: character} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: "a " + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: " " + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: character}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearFirstTwoItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearFirstTwoItem.yml new file mode 100644 index 0000000000..59e0aadf8d --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearFirstTwoItem.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear first two item + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: collectionItem} + start: 0 + length: 2 + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: ", c, d" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: collectionItem}, start: 0, length: 2}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearLastTwoItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearLastTwoItem.yml new file mode 100644 index 0000000000..fd7016eec1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearLastTwoItem.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear last two item + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: collectionItem} + start: -2 + length: 2 + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "a, b, " + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: collectionItem}, start: -2, length: 2}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearNextFunk.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearNextFunk.yml new file mode 100644 index 0000000000..dea9907f6b --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearNextFunk.yml @@ -0,0 +1,31 @@ +languageId: typescript +command: + spokenForm: clear next funk + version: 2 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: namedFunction} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + function foo() {} + + function bar() {} + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + function foo() {} + + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: nextContainingScope, scopeType: {type: namedFunction}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearNextToken.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearNextToken.yml new file mode 100644 index 0000000000..78ea9e2770 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearNextToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a b + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: "a " + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord.yml new file mode 100644 index 0000000000..4aa1be02d9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCccDdd + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord2.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord2.yml new file mode 100644 index 0000000000..5a3c20442d --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCccDdd + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord3.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord3.yml new file mode 100644 index 0000000000..0379d60e54 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearNextWord3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear next word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCccDdd + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: forward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousFunk.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousFunk.yml new file mode 100644 index 0000000000..5278c034ce --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousFunk.yml @@ -0,0 +1,32 @@ +languageId: typescript +command: + spokenForm: clear previous funk + version: 2 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: namedFunction} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + function foo() {} + + function bar() {} + selections: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 0} + marks: {} +finalState: + documentContents: |- + + + function bar() {} + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: previousContainingScope, scopeType: {type: namedFunction}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousToken.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousToken.yml new file mode 100644 index 0000000000..3bb06a0364 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousToken.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear previous token + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a b + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: " b" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml new file mode 100644 index 0000000000..5ae206e97a --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear previous word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCccDdd + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: aaaCccDdd + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord2.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord2.yml new file mode 100644 index 0000000000..c6e60edc01 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord2.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear previous word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCccDdd + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: aaaCccDdd + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord3.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord3.yml new file mode 100644 index 0000000000..874a8bde15 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearPreviousWord3.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear previous word + version: 3 + targets: + - type: primitive + modifiers: + - type: relativeScope + scopeType: {type: word} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCccDdd + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 7} + marks: {} +finalState: + documentContents: BbbCccDdd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: word}, offset: 1, length: 1, direction: backward}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearSecondBetweenThirdItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondBetweenThirdItem.yml new file mode 100644 index 0000000000..3ac6abcd09 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondBetweenThirdItem.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clear second between third item + version: 3 + targets: + - type: primitive + modifiers: + - type: range + anchor: + type: ordinalScope + scopeType: {type: collectionItem} + start: 1 + length: 1 + active: + type: ordinalScope + scopeType: {type: collectionItem} + start: 2 + length: 1 + excludeAnchor: true + excludeActive: true + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: a, bc, d + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: range, anchor: {type: ordinalScope, scopeType: {type: collectionItem}, start: 1, length: 1}, active: {type: ordinalScope, scopeType: {type: collectionItem}, start: 2, length: 1}, excludeAnchor: true, excludeActive: true}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearSecondItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondItem.yml new file mode 100644 index 0000000000..d4b0f7fac9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondItem.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear second item + version: 3 + targets: + - type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: collectionItem} + start: 1 + length: 1 + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: a, , c, d + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: ordinalScope, scopeType: {type: collectionItem}, start: 1, length: 1}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearSecondPastThirdItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondPastThirdItem.yml new file mode 100644 index 0000000000..cbd2bd6f6b --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondPastThirdItem.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clear second past third item + version: 3 + targets: + - type: primitive + modifiers: + - type: range + anchor: + type: ordinalScope + scopeType: {type: collectionItem} + start: 1 + length: 1 + active: + type: ordinalScope + scopeType: {type: collectionItem} + start: 2 + length: 1 + excludeAnchor: false + excludeActive: false + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: a, , d + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: range, anchor: {type: ordinalScope, scopeType: {type: collectionItem}, start: 1, length: 1}, active: {type: ordinalScope, scopeType: {type: collectionItem}, start: 2, length: 1}, excludeAnchor: false, excludeActive: false}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearSecondUntilFourthItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondUntilFourthItem.yml new file mode 100644 index 0000000000..57273426f0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearSecondUntilFourthItem.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clear second until fourth item + version: 3 + targets: + - type: primitive + modifiers: + - type: range + anchor: + type: ordinalScope + scopeType: {type: collectionItem} + start: 1 + length: 1 + active: + type: ordinalScope + scopeType: {type: collectionItem} + start: 3 + length: 1 + excludeAnchor: false + excludeActive: true + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, b, c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: a, d + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: range, anchor: {type: ordinalScope, scopeType: {type: collectionItem}, start: 1, length: 1}, active: {type: ordinalScope, scopeType: {type: collectionItem}, start: 3, length: 1}, excludeAnchor: false, excludeActive: true}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord.yml new file mode 100644 index 0000000000..dc6d674c8e --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aaaCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord2.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord2.yml new file mode 100644 index 0000000000..dd1ace6460 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord2.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: aaaCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord3.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord3.yml new file mode 100644 index 0000000000..f7bc901bf8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord3.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: Ccc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord4.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord4.yml new file mode 100644 index 0000000000..a84f3ee2d9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord4.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: aaaCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord5.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord5.yml new file mode 100644 index 0000000000..4a5c99d622 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord5.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: BbbCcc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord6.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord6.yml new file mode 100644 index 0000000000..92a3ed8d36 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord6.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: aaaCcc + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearWord7.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearWord7.yml new file mode 100644 index 0000000000..850d9c397e --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearWord7.yml @@ -0,0 +1,23 @@ +languageId: plaintext +command: + spokenForm: clear word + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: aaaBbb + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryCar.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryCar.yml new file mode 100644 index 0000000000..e9a7611b45 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryCar.yml @@ -0,0 +1,39 @@ +languageId: plaintext +command: + spokenForm: take every car + version: 3 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: character} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 2} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 3} + - anchor: {line: 0, character: 3} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 6} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} + - anchor: {line: 0, character: 7} + active: {line: 0, character: 8} + - anchor: {line: 0, character: 8} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: character}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryCarBlock.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryCarBlock.yml new file mode 100644 index 0000000000..7f404d9b1a --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryCarBlock.yml @@ -0,0 +1,47 @@ +languageId: plaintext +command: + spokenForm: take every car block + version: 3 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: character} + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + a*a + b-b + c/c + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |- + a*a + b-b + c/c + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 2} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 3} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 1, character: 1} + active: {line: 1, character: 2} + - anchor: {line: 1, character: 2} + active: {line: 1, character: 3} + - anchor: {line: 2, character: 0} + active: {line: 2, character: 1} + - anchor: {line: 2, character: 1} + active: {line: 2, character: 2} + - anchor: {line: 2, character: 2} + active: {line: 2, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: character}}, {type: containingScope, scopeType: {type: paragraph}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryWord.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryWord.yml new file mode 100644 index 0000000000..fcf7d054ae --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryWord.yml @@ -0,0 +1,27 @@ +languageId: plaintext +command: + spokenForm: take every word + version: 3 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: word} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: aaaBbbCcc + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} + - anchor: {line: 0, character: 3} + active: {line: 0, character: 6} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: word}}]}] diff --git a/src/test/suite/fixtures/recorded/subtoken/takeFirstChar5.yml b/src/test/suite/fixtures/recorded/subtoken/takeFirstChar5.yml index 0202958d81..b9dc05d088 100644 --- a/src/test/suite/fixtures/recorded/subtoken/takeFirstChar5.yml +++ b/src/test/suite/fixtures/recorded/subtoken/takeFirstChar5.yml @@ -16,6 +16,6 @@ initialState: finalState: documentContents: //** selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 3} fullTargets: [{type: primitive, mark: {type: cursorToken}, selectionType: token, position: contents, modifier: {type: subpiece, pieceType: character, anchor: 0, active: 0}, insideOutsideType: inside}] diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index 1058bcb375..c4720f4904 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -31,25 +31,31 @@ export interface DecoratedSymbolMark { export type LineNumberType = "absolute" | "relative" | "modulo100"; -export interface LineNumberPosition { - type: LineNumberType; +export interface LineNumberMark { + type: "lineNumber"; + lineNumberType: LineNumberType; lineNumber: number; } -export interface LineNumberMark { - type: "lineNumber"; - anchor: LineNumberPosition; - active: LineNumberPosition; +/** + * Constructs a range between {@link anchor} and {@link active} + */ +export interface RangeMark { + type: "range"; + anchor: Mark; + active: Mark; + excludeAnchor?: boolean; + excludeActive?: boolean; } export type Mark = | CursorMark | ThatMark | SourceMark - // | LastCursorPositionMark Not implemented yet | DecoratedSymbolMark | NothingMark - | LineNumberMark; + | LineNumberMark + | RangeMark; export type SimpleSurroundingPairName = | "angleBrackets" @@ -176,20 +182,50 @@ export interface EveryScopeModifier { scopeType: ScopeType; } -export interface OrdinalRangeModifier { - type: "ordinalRange"; +/** + * Refer to scopes by absolute index relative to iteration scope, eg "first + * funk" to refer to the first function in a class. + */ +export interface OrdinalScopeModifier { + type: "ordinalScope"; + scopeType: ScopeType; - anchor: number; - active: number; - excludeAnchor?: boolean; - excludeActive?: boolean; + + /** The start of the range. Start from end of iteration scope if `start` is negative */ + start: number; + + /** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */ + length: number; +} + +/** + * Refer to scopes by offset relative to input target, eg "next + * funk" to refer to the first function after the function containing the target input. + */ +export interface RelativeScopeModifier { + type: "relativeScope"; + + scopeType: ScopeType; + + /** Indicates how many scopes away to start relative to the input target. + * Note that if {@link direction} is `"backward"`, then this scope will be the + * end of the output range. */ + offset: number; + + /** The number of scopes to include. Will always be positive. If greater + * than 1, will include scopes in the direction of {@link direction} */ + length: number; + + /** Indicates which direction both {@link offset} and {@link length} go + * relative to input target */ + direction: "forward" | "backward"; } + /** * 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"; } @@ -248,19 +284,33 @@ export interface CascadingModifier { modifiers: Modifier[]; } +/** + * First applies {@link anchor} to input, then independently applies + * {@link active}, and forms a range between the two resulting targets + */ +export interface RangeModifier { + type: "range"; + anchor: Modifier; + active: Modifier; + excludeAnchor?: boolean; + excludeActive?: boolean; +} + export type Modifier = | PositionModifier | InteriorOnlyModifier | ExcludeInteriorModifier | ContainingScopeModifier | EveryScopeModifier - | OrdinalRangeModifier + | OrdinalScopeModifier + | RelativeScopeModifier | HeadTailModifier | LeadingModifier | TrailingModifier | RawSelectionModifier | ModifyIfUntypedModifier - | CascadingModifier; + | CascadingModifier + | RangeModifier; export interface PartialRangeTargetDescriptor { type: "range";