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";