mirror of
https://github.com/cursorless-dev/cursorless.git
synced 2024-10-05 05:17:38 +03:00
Iteration-based scope handler interface (#1096)
* Iteration-based scope handler interface * Remove `getPreferredScopeTouchingPosition` * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * `isPreferredOver` skeleton * Reintroduce token preferences * Attempt at more simplification * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tweak hints * Cleanup * Remove `allowNoOverlap` from requirements * New RelativeInclusive semantics * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add tests * Migrate early stopping to base class * More doc tweaks * More doc tweaks * More docs * More docs * More docs * More doc strings * More cleanup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * More docs * More docs * More docs * Fixes * More docs * More docstring tweaks * Tweak docs * Address PR comments * Tweak token `isPreferredOver` Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
cf66411ba8
commit
1871635283
@ -5,7 +5,7 @@ from .head_tail import head_tail_modifiers
|
||||
from .interior import interior_modifiers
|
||||
from .ordinal_scope import first_modifiers, last_modifiers
|
||||
from .range_type import range_types
|
||||
from .relative_scope import backward_modifiers, previous_next_modifiers
|
||||
from .relative_scope import forward_backward_modifiers, previous_next_modifiers
|
||||
from .simple_scope_modifier import simple_scope_modifiers
|
||||
|
||||
mod = Module()
|
||||
@ -81,7 +81,7 @@ def on_ready():
|
||||
"first_modifier": first_modifiers,
|
||||
"last_modifier": last_modifiers,
|
||||
"previous_next_modifier": previous_next_modifiers,
|
||||
"backward_modifier": backward_modifiers,
|
||||
"forward_backward_modifier": forward_backward_modifiers,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -3,12 +3,17 @@ from typing import Any
|
||||
from talon import Module
|
||||
|
||||
previous_next_modifiers = {"previous": "previous", "next": "next"}
|
||||
backward_modifiers = {"backward": "backward"}
|
||||
forward_backward_modifiers = {
|
||||
"forward": "forward",
|
||||
"backward": "backward",
|
||||
}
|
||||
|
||||
mod = Module()
|
||||
|
||||
mod.list("cursorless_previous_next_modifier", desc="Cursorless previous/next modifiers")
|
||||
mod.list("cursorless_backward_modifier", desc="Cursorless backward modifiers")
|
||||
mod.list(
|
||||
"cursorless_forward_backward_modifier", desc="Cursorless forward/backward modifiers"
|
||||
)
|
||||
|
||||
|
||||
@mod.capture(rule="{user.cursorless_previous_next_modifier}")
|
||||
@ -44,7 +49,7 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]:
|
||||
|
||||
|
||||
@mod.capture(
|
||||
rule="<user.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{user.cursorless_backward_modifier}]"
|
||||
rule="<user.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{user.cursorless_forward_backward_modifier}]"
|
||||
)
|
||||
def cursorless_relative_scope_count(m) -> dict[str, Any]:
|
||||
"""Relative count scope. `three funks`"""
|
||||
@ -52,18 +57,20 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]:
|
||||
m.cursorless_scope_type_plural,
|
||||
0,
|
||||
m.private_cursorless_number_small,
|
||||
getattr(m, "cursorless_backward_modifier", "forward"),
|
||||
getattr(m, "cursorless_forward_backward_modifier", "forward"),
|
||||
)
|
||||
|
||||
|
||||
@mod.capture(rule="<user.cursorless_scope_type> {user.cursorless_backward_modifier}")
|
||||
@mod.capture(
|
||||
rule="<user.cursorless_scope_type> {user.cursorless_forward_backward_modifier}"
|
||||
)
|
||||
def cursorless_relative_scope_one_backward(m) -> dict[str, Any]:
|
||||
"""Take scope backward, eg `funk backward`"""
|
||||
return create_relative_scope_modifier(
|
||||
m.cursorless_scope_type,
|
||||
0,
|
||||
1,
|
||||
m.cursorless_backward_modifier,
|
||||
m.cursorless_forward_backward_modifier,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { NoContainingScopeError } from "../../errors";
|
||||
import type { Target } from "../../typings/target.types";
|
||||
import type { ContainingScopeModifier } from "../../typings/targetDescriptor.types";
|
||||
import type {
|
||||
ContainingScopeModifier,
|
||||
Direction,
|
||||
} from "../../typings/targetDescriptor.types";
|
||||
import type { ProcessedTargetsContext } from "../../typings/Types";
|
||||
import getScopeHandler from "./scopeHandlers/getScopeHandler";
|
||||
import type { ModifierStage } from "../PipelineStages.types";
|
||||
import { constructScopeRangeTarget } from "./constructScopeRangeTarget";
|
||||
import getLegacyScopeStage from "./getLegacyScopeStage";
|
||||
import {
|
||||
getLeftScope,
|
||||
getPreferredScope,
|
||||
getRightScope,
|
||||
} from "./getPreferredScope";
|
||||
import { TargetScope } from "./scopeHandlers/scope.types";
|
||||
import { TextEditor, Position } from "vscode";
|
||||
import { ScopeHandler } from "./scopeHandlers/scopeHandler.types";
|
||||
import { getContainingScope } from "./getContainingScope";
|
||||
|
||||
/**
|
||||
* This modifier stage expands from the input target to the smallest containing
|
||||
@ -19,8 +21,8 @@ import {
|
||||
* 1. Expand to smallest scope(s) touching start position of input target's
|
||||
* content range
|
||||
* 2. If input target has an empty content range, return the start scope,
|
||||
* breaking ties as defined by {@link getPreferredScope} when more than one
|
||||
* scope touches content range
|
||||
* breaking ties as defined by {@link ScopeHandler.isPreferredOver} when more
|
||||
* than one scope touches content range
|
||||
* 3. Otherwise, if end of input target is weakly contained by the domain of the
|
||||
* rightmost start scope, return rightmost start scope. We return rightmost
|
||||
* because that will have non-empty intersection with input target content
|
||||
@ -39,7 +41,7 @@ export class ContainingScopeStage implements ModifierStage {
|
||||
editor,
|
||||
contentRange: { start, end },
|
||||
} = target;
|
||||
const { scopeType, ancestorIndex } = this.modifier;
|
||||
const { scopeType, ancestorIndex = 0 } = this.modifier;
|
||||
|
||||
const scopeHandler = getScopeHandler(
|
||||
scopeType,
|
||||
@ -50,41 +52,58 @@ export class ContainingScopeStage implements ModifierStage {
|
||||
return getLegacyScopeStage(this.modifier).run(context, target);
|
||||
}
|
||||
|
||||
const startScopes = scopeHandler.getScopesTouchingPosition(
|
||||
if (end.isEqual(start)) {
|
||||
// Input target is empty; return the preferred scope touching target
|
||||
let scope = getPreferredScopeTouchingPosition(
|
||||
scopeHandler,
|
||||
editor,
|
||||
start,
|
||||
);
|
||||
|
||||
if (scope == null) {
|
||||
throw new NoContainingScopeError(this.modifier.scopeType.type);
|
||||
}
|
||||
|
||||
if (ancestorIndex > 0) {
|
||||
scope = expandFromPosition(
|
||||
scopeHandler,
|
||||
editor,
|
||||
scope.domain.end,
|
||||
"forward",
|
||||
ancestorIndex - 1,
|
||||
);
|
||||
}
|
||||
|
||||
if (scope == null) {
|
||||
throw new NoContainingScopeError(this.modifier.scopeType.type);
|
||||
}
|
||||
|
||||
return [scope.getTarget(isReversed)];
|
||||
}
|
||||
|
||||
const startScope = expandFromPosition(
|
||||
scopeHandler,
|
||||
editor,
|
||||
start,
|
||||
"forward",
|
||||
ancestorIndex,
|
||||
);
|
||||
|
||||
if (startScopes.length === 0) {
|
||||
if (startScope == null) {
|
||||
throw new NoContainingScopeError(this.modifier.scopeType.type);
|
||||
}
|
||||
|
||||
if (end.isEqual(start)) {
|
||||
// Input target is empty; return the preferred scope touching target
|
||||
return [getPreferredScope(startScopes)!.getTarget(isReversed)];
|
||||
}
|
||||
|
||||
// Target is non-empty; use the rightmost scope touching `startScope`
|
||||
// because that will have non-empty overlap with input content range
|
||||
const startScope = getRightScope(startScopes)!;
|
||||
|
||||
if (startScope.domain.contains(end)) {
|
||||
// End of input target is contained in domain of start scope; return start
|
||||
// scope
|
||||
return [startScope.getTarget(isReversed)];
|
||||
}
|
||||
|
||||
// End of input target is after end of start scope; we need to make a range
|
||||
// between start and end scopes. For the end scope, we break ties to the
|
||||
// left so that the scope will have non-empty overlap with input target
|
||||
// content range.
|
||||
const endScopes = scopeHandler.getScopesTouchingPosition(
|
||||
const endScope = expandFromPosition(
|
||||
scopeHandler,
|
||||
editor,
|
||||
end,
|
||||
"backward",
|
||||
ancestorIndex,
|
||||
);
|
||||
const endScope = getLeftScope(endScopes);
|
||||
|
||||
if (endScope == null) {
|
||||
throw new NoContainingScopeError(this.modifier.scopeType.type);
|
||||
@ -93,3 +112,71 @@ export class ContainingScopeStage implements ModifierStage {
|
||||
return [constructScopeRangeTarget(isReversed, startScope, endScope)];
|
||||
}
|
||||
}
|
||||
|
||||
function expandFromPosition(
|
||||
scopeHandler: ScopeHandler,
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
ancestorIndex: number,
|
||||
): TargetScope | undefined {
|
||||
let nextAncestorIndex = 0;
|
||||
for (const scope of scopeHandler.generateScopes(editor, position, direction, {
|
||||
containment: "required",
|
||||
})) {
|
||||
if (nextAncestorIndex === ancestorIndex) {
|
||||
return scope;
|
||||
}
|
||||
|
||||
// Because containment is required, and we are moving in a consistent
|
||||
// direction (ie forward or backward), each scope will be progressively
|
||||
// larger
|
||||
nextAncestorIndex += 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getPreferredScopeTouchingPosition(
|
||||
scopeHandler: ScopeHandler,
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
): TargetScope | undefined {
|
||||
const forwardScope = getContainingScope(
|
||||
scopeHandler,
|
||||
editor,
|
||||
position,
|
||||
"forward",
|
||||
);
|
||||
|
||||
if (forwardScope == null) {
|
||||
return getContainingScope(scopeHandler, editor, position, "backward");
|
||||
}
|
||||
|
||||
if (
|
||||
scopeHandler.isPreferredOver == null ||
|
||||
forwardScope.domain.start.isBefore(position)
|
||||
) {
|
||||
return forwardScope;
|
||||
}
|
||||
|
||||
const backwardScope = getContainingScope(
|
||||
scopeHandler,
|
||||
editor,
|
||||
position,
|
||||
"backward",
|
||||
);
|
||||
|
||||
// If there is no backward scope, or if the backward scope is an ancestor of
|
||||
// forward scope, return forward scope
|
||||
if (
|
||||
backwardScope == null ||
|
||||
backwardScope.domain.contains(forwardScope.domain)
|
||||
) {
|
||||
return forwardScope;
|
||||
}
|
||||
|
||||
return scopeHandler.isPreferredOver(backwardScope, forwardScope) ?? false
|
||||
? backwardScope
|
||||
: forwardScope;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import getModifierStage from "../getModifierStage";
|
||||
import type { ModifierStage } from "../PipelineStages.types";
|
||||
import getLegacyScopeStage from "./getLegacyScopeStage";
|
||||
import getScopeHandler from "./scopeHandlers/getScopeHandler";
|
||||
import getScopesOverlappingRange from "./scopeHandlers/getScopesOverlappingRange";
|
||||
import { TargetScope } from "./scopeHandlers/scope.types";
|
||||
import { ScopeHandler } from "./scopeHandlers/scopeHandler.types";
|
||||
|
||||
@ -46,7 +47,8 @@ export class EveryScopeStage implements ModifierStage {
|
||||
let scopes: TargetScope[] | undefined;
|
||||
|
||||
if (target.hasExplicitRange) {
|
||||
scopes = scopeHandler.getScopesOverlappingRange(
|
||||
scopes = getScopesOverlappingRange(
|
||||
scopeHandler,
|
||||
editor,
|
||||
target.contentRange,
|
||||
);
|
||||
@ -65,7 +67,8 @@ export class EveryScopeStage implements ModifierStage {
|
||||
if (scopes == null) {
|
||||
// If target had no explicit range, or was contained by a single target
|
||||
// instance, expand to iteration scope before overlapping
|
||||
scopes = scopeHandler.getScopesOverlappingRange(
|
||||
scopes = getScopesOverlappingRange(
|
||||
scopeHandler,
|
||||
editor,
|
||||
this.getDefaultIterationRange(context, scopeHandler, target),
|
||||
);
|
||||
|
@ -1,32 +1,19 @@
|
||||
import type { Position, TextEditor } from "vscode";
|
||||
import type { Target } from "../../typings/target.types";
|
||||
import type { RelativeScopeModifier } from "../../typings/targetDescriptor.types";
|
||||
import type { ProcessedTargetsContext } from "../../typings/Types";
|
||||
import type { ModifierStage } from "../PipelineStages.types";
|
||||
import { constructScopeRangeTarget } from "./constructScopeRangeTarget";
|
||||
import { getLeftScope, getRightScope } from "./getPreferredScope";
|
||||
import { runLegacy } from "./relativeScopeLegacy";
|
||||
import getScopeHandler from "./scopeHandlers/getScopeHandler";
|
||||
import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types";
|
||||
import { TargetScope } from "./scopeHandlers/scope.types";
|
||||
import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types";
|
||||
import { OutOfRangeError } from "./targetSequenceUtils";
|
||||
|
||||
/**
|
||||
* Handles relative modifiers that don't include targets intersecting with the
|
||||
* input, eg "next funk", "previous two tokens". Proceeds as follows:
|
||||
*
|
||||
* 1. If the input is empty, skips past any scopes that are directly adjacent to
|
||||
* input target in the direction of movement. Eg if the cursor is at the
|
||||
* very start of a token, we first jump past that token for "next token".
|
||||
* 2. Otherwise, we start at the `end` of the input range (`start` if
|
||||
* {@link RelativeScopeModifier.direction} is `"backward"`).
|
||||
* 3. Asks the scope handler for the scope at
|
||||
* {@link RelativeScopeModifier.offset} in given
|
||||
* {@link RelativeScopeModifier.direction} by calling
|
||||
* {@link ScopeHandler.getScopeRelativeToPosition}.
|
||||
* 4. If {@link RelativeScopeModifier.length} is 1, returns that scope
|
||||
* 5. Otherwise, asks scope handler for scope at offset
|
||||
* {@link RelativeScopeModifier.length} - 1, starting from the end of
|
||||
* {@link Scope.domain} of that scope (start for "backward"), and forms a
|
||||
* range target.
|
||||
* input, eg "next funk", "previous two tokens". Proceeds by running
|
||||
* {@link ScopeHandler.generateScopes} to get the desired scopes, skipping the
|
||||
* first scope if input range is empty and is at start of that scope.
|
||||
*/
|
||||
export default class RelativeExclusiveScopeStage implements ModifierStage {
|
||||
constructor(private modifier: RelativeScopeModifier) {}
|
||||
@ -44,72 +31,48 @@ export default class RelativeExclusiveScopeStage implements ModifierStage {
|
||||
const { isReversed, editor, contentRange: inputRange } = target;
|
||||
const { length: desiredScopeCount, direction, offset } = this.modifier;
|
||||
|
||||
const initialPosition = inputRange.isEmpty
|
||||
? getInitialPositionForEmptyInputRange(
|
||||
scopeHandler,
|
||||
direction,
|
||||
editor,
|
||||
inputRange.start,
|
||||
)
|
||||
: direction === "forward"
|
||||
? inputRange.end
|
||||
: inputRange.start;
|
||||
const initialPosition =
|
||||
direction === "forward" ? inputRange.end : inputRange.start;
|
||||
|
||||
const proximalScope = scopeHandler.getScopeRelativeToPosition(
|
||||
// If inputRange is empty, then we skip past any scopes that start at
|
||||
// inputRange. Otherwise just disallow any scopes that start strictly
|
||||
// before the end of input range (strictly after for "backward").
|
||||
const containment: ContainmentPolicy | undefined = inputRange.isEmpty
|
||||
? "disallowed"
|
||||
: "disallowedIfStrict";
|
||||
|
||||
let scopeCount = 0;
|
||||
let proximalScope: TargetScope | undefined;
|
||||
for (const scope of scopeHandler.generateScopes(
|
||||
editor,
|
||||
initialPosition,
|
||||
offset,
|
||||
direction,
|
||||
);
|
||||
{ containment },
|
||||
)) {
|
||||
scopeCount += 1;
|
||||
|
||||
if (desiredScopeCount === 1) {
|
||||
return [proximalScope.getTarget(isReversed)];
|
||||
if (scopeCount < offset) {
|
||||
// Skip until we hit `offset`
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scopeCount === offset) {
|
||||
// When we hit offset, that becomes proximal scope
|
||||
if (desiredScopeCount === 1) {
|
||||
// Just yield it if we only want 1 scope
|
||||
return [scope.getTarget(isReversed)];
|
||||
}
|
||||
|
||||
proximalScope = scope;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scopeCount === offset + desiredScopeCount - 1) {
|
||||
// Then make a range when we get the desired number of scopes
|
||||
return [constructScopeRangeTarget(isReversed, proximalScope!, scope)];
|
||||
}
|
||||
}
|
||||
|
||||
const distalScope = scopeHandler.getScopeRelativeToPosition(
|
||||
editor,
|
||||
direction === "forward"
|
||||
? proximalScope.domain.end
|
||||
: proximalScope.domain.start,
|
||||
desiredScopeCount - 1,
|
||||
direction,
|
||||
);
|
||||
|
||||
return [constructScopeRangeTarget(isReversed, proximalScope, distalScope)];
|
||||
throw new OutOfRangeError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the position to pass in to
|
||||
* {@link ScopeHandler.getScopeRelativeToPosition}. If input target is empty,
|
||||
* we skip past one scope if it is direclty adjacent to us in the direction
|
||||
* we're going. Otherwise we just use end or start of input target, depending
|
||||
* which direction we're going (`end` for `"forward"`).
|
||||
* @param scopeHandler The scope handler to ask
|
||||
* @param direction The direction we are going
|
||||
* @param editor The editor containing {@link inputPosition}
|
||||
* @param inputPosition The position of the input target
|
||||
* @returns
|
||||
*/
|
||||
function getInitialPositionForEmptyInputRange(
|
||||
scopeHandler: ScopeHandler,
|
||||
direction: string,
|
||||
editor: TextEditor,
|
||||
inputPosition: Position,
|
||||
) {
|
||||
const scopesTouchingPosition = scopeHandler.getScopesTouchingPosition(
|
||||
editor,
|
||||
inputPosition,
|
||||
);
|
||||
|
||||
const skipScope =
|
||||
direction === "forward"
|
||||
? getRightScope(scopesTouchingPosition)
|
||||
: getLeftScope(scopesTouchingPosition);
|
||||
|
||||
return (
|
||||
(direction === "forward"
|
||||
? skipScope?.domain.end
|
||||
: skipScope?.domain.start) ?? inputPosition
|
||||
);
|
||||
}
|
||||
|
@ -8,9 +8,11 @@ import type {
|
||||
import type { ProcessedTargetsContext } from "../../typings/Types";
|
||||
import type { ModifierStage } from "../PipelineStages.types";
|
||||
import { constructScopeRangeTarget } from "./constructScopeRangeTarget";
|
||||
import { getLeftScope, getRightScope } from "./getPreferredScope";
|
||||
import { getContainingScope } from "./getContainingScope";
|
||||
import { runLegacy } from "./relativeScopeLegacy";
|
||||
import getScopeHandler from "./scopeHandlers/getScopeHandler";
|
||||
import getScopeRelativeToPosition from "./scopeHandlers/getScopeRelativeToPosition";
|
||||
import getScopesOverlappingRange from "./scopeHandlers/getScopesOverlappingRange";
|
||||
import type { TargetScope } from "./scopeHandlers/scope.types";
|
||||
import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types";
|
||||
import { TooFewScopesError } from "./TooFewScopesError";
|
||||
@ -54,6 +56,8 @@ export class RelativeInclusiveScopeStage implements ModifierStage {
|
||||
const { isReversed, editor, contentRange: inputRange } = target;
|
||||
const { scopeType, length: desiredScopeCount, direction } = this.modifier;
|
||||
|
||||
// FIXME: Figure out how to just continue iteration rather than starting
|
||||
// over after getting offset 0 scopes
|
||||
const offset0Scopes = getOffset0Scopes(
|
||||
scopeHandler,
|
||||
direction,
|
||||
@ -85,7 +89,8 @@ export class RelativeInclusiveScopeStage implements ModifierStage {
|
||||
|
||||
const distalScope =
|
||||
desiredScopeCount > offset0ScopeCount
|
||||
? scopeHandler.getScopeRelativeToPosition(
|
||||
? getScopeRelativeToPosition(
|
||||
scopeHandler,
|
||||
editor,
|
||||
initialPosition,
|
||||
desiredScopeCount - offset0ScopeCount,
|
||||
@ -118,20 +123,18 @@ function getOffset0Scopes(
|
||||
range: Range,
|
||||
): TargetScope[] {
|
||||
if (range.isEmpty) {
|
||||
const inputPosition = range.start;
|
||||
// First try scope in correct direction, falling back to opposite direction
|
||||
const containingScope =
|
||||
getContainingScope(scopeHandler, editor, range.start, direction) ??
|
||||
getContainingScope(
|
||||
scopeHandler,
|
||||
editor,
|
||||
range.start,
|
||||
direction === "forward" ? "backward" : "forward",
|
||||
);
|
||||
|
||||
const scopesTouchingPosition = scopeHandler.getScopesTouchingPosition(
|
||||
editor,
|
||||
inputPosition,
|
||||
);
|
||||
|
||||
const preferredScope =
|
||||
direction === "forward"
|
||||
? getRightScope(scopesTouchingPosition)
|
||||
: getLeftScope(scopesTouchingPosition);
|
||||
|
||||
return preferredScope == null ? [] : [preferredScope];
|
||||
return containingScope == null ? [] : [containingScope];
|
||||
}
|
||||
|
||||
return scopeHandler.getScopesOverlappingRange(editor, range);
|
||||
return getScopesOverlappingRange(scopeHandler, editor, range);
|
||||
}
|
||||
|
30
src/processTargets/modifiers/getContainingScope.ts
Normal file
30
src/processTargets/modifiers/getContainingScope.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Direction } from "../../typings/targetDescriptor.types";
|
||||
import { TextEditor, Position } from "vscode";
|
||||
import { ScopeHandler } from "./scopeHandlers/scopeHandler.types";
|
||||
|
||||
/**
|
||||
* Gets the smallest containing scope, preferring scopes in direction
|
||||
* {@link direction}
|
||||
* @param scopeHandler
|
||||
* @param editor
|
||||
* @param position
|
||||
* @param direction
|
||||
* @returns
|
||||
*/
|
||||
export function getContainingScope(
|
||||
scopeHandler: ScopeHandler,
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
) {
|
||||
return getOne(
|
||||
scopeHandler.generateScopes(editor, position, direction, {
|
||||
containment: "required",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function getOne<T>(iterable: Iterable<T>): T | undefined {
|
||||
const { value, done } = iterable[Symbol.iterator]().next();
|
||||
return done ? undefined : value;
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import { TargetScope } from "./scopeHandlers/scope.types";
|
||||
|
||||
/**
|
||||
* Given a list of scopes, returns the preferred scope, or `undefined` if
|
||||
* {@link scopes} is empty. The preferred scope will always be the rightmost
|
||||
* scope.
|
||||
* @param scopes A list of scopes to choose from
|
||||
* @returns A single preferred scope, or `undefined` if {@link scopes} is empty
|
||||
*/
|
||||
export function getPreferredScope(
|
||||
scopes: TargetScope[],
|
||||
): TargetScope | undefined {
|
||||
return getRightScope(scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of scopes, returns the leftmost scope, or `undefined` if
|
||||
* {@link scopes} is empty.
|
||||
* @param scopes A list of scopes to choose from
|
||||
* @returns A single preferred scope, or `undefined` if {@link scopes} is empty
|
||||
*/
|
||||
export function getLeftScope(scopes: TargetScope[]): TargetScope | undefined {
|
||||
return getScopeHelper(scopes, (scope1, scope2) =>
|
||||
scope1.domain.start.isBefore(scope2.domain.start),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of scopes, returns the rightmost scope, or `undefined` if
|
||||
* {@link scopes} is empty.
|
||||
* @param scopes A list of scopes to choose from
|
||||
* @returns A single preferred scope, or `undefined` if {@link scopes} is empty
|
||||
*/
|
||||
export function getRightScope(scopes: TargetScope[]): TargetScope | undefined {
|
||||
return getScopeHelper(scopes, (scope1, scope2) =>
|
||||
scope1.domain.start.isAfter(scope2.domain.start),
|
||||
);
|
||||
}
|
||||
|
||||
function getScopeHelper(
|
||||
scopes: TargetScope[],
|
||||
isScope1Preferred: (scope1: TargetScope, scope2: TargetScope) => boolean,
|
||||
): TargetScope | undefined {
|
||||
if (scopes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (scopes.length === 1) {
|
||||
return scopes[0];
|
||||
}
|
||||
|
||||
if (scopes.length > 2) {
|
||||
throw Error("Cannot compare more than two scopes.");
|
||||
}
|
||||
|
||||
const [scope1, scope2] = scopes;
|
||||
|
||||
return isScope1Preferred(scope1, scope2) ? scope1 : scope2;
|
||||
}
|
167
src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts
Normal file
167
src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts
Normal file
@ -0,0 +1,167 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { Position, Range, TextEditor } from "vscode";
|
||||
import type {
|
||||
Direction,
|
||||
ScopeType,
|
||||
} from "../../../typings/targetDescriptor.types";
|
||||
import type { TargetScope } from "./scope.types";
|
||||
import type {
|
||||
ScopeHandler,
|
||||
ScopeIteratorRequirements,
|
||||
} from "./scopeHandler.types";
|
||||
import { shouldYieldScope } from "./shouldYieldScope";
|
||||
|
||||
/**
|
||||
* All scope handlers should derive from this base class
|
||||
*/
|
||||
export default abstract class BaseScopeHandler implements ScopeHandler {
|
||||
public abstract readonly scopeType: ScopeType;
|
||||
public abstract readonly iterationScopeType: ScopeType;
|
||||
|
||||
/**
|
||||
* Indicates whether scopes are allowed to contain one another. If `false`, we
|
||||
* can optimise the algorithm by making certain assumptions.
|
||||
*/
|
||||
protected abstract readonly isHierarchical: boolean;
|
||||
|
||||
/**
|
||||
* Returns an iterable that yields scopes.
|
||||
*
|
||||
* If your scope type is *not* hierarchical, and {@link direction} is
|
||||
* `"forward"`, yield all scopes whose {@link TargetScope.domain|domain}'s
|
||||
* {@link Range.end|end} is equal to or after {@link position} in document
|
||||
* order.
|
||||
*
|
||||
* If your scope type is *not* hierarchical, and {@link direction} is
|
||||
* `"backward"`, yield all scopes whose {@link TargetScope.domain|domain}'s
|
||||
* {@link Range.start|start} is equal to or before {@link position}, in
|
||||
* reverse document order.
|
||||
*
|
||||
* If your scope type *is* hierarchical, and {@link direction} is `"forward"`,
|
||||
* walk forward starting at {@link position} (including position). Any time a
|
||||
* scope's {@link TargetScope.domain|domain} ends or starts, yield that scope.
|
||||
* If multiple domains start or end at a particular point, break ties as
|
||||
* follows:
|
||||
*
|
||||
* 1. First yield any scopes with empty domain.
|
||||
* 2. Then yield any scopes whose domains are ending, in reverse order of
|
||||
* where they start.
|
||||
* 3. Then yield the scope with minimal domain that is starting. Any time you
|
||||
* yield a scope, advance your position to the end of the scope, but when
|
||||
* considering this new position, don't return this scope again.
|
||||
*
|
||||
* If your scope type *is* hierarchical, and {@link direction} is
|
||||
* `"backward"`, walk backward starting at {@link position} (including
|
||||
* position). Any time a scope's {@link TargetScope.domain|domain} ends or
|
||||
* starts, yield that scope. If multiple domains start or end at a particular
|
||||
* point, break ties as follows:
|
||||
*
|
||||
* 1. First yield any scopes with empty domain.
|
||||
* 2. Then yield any scopes whose domains are starting, in order of where they
|
||||
* end.
|
||||
* 3. Then yield the scope with minimal domain that is ending. Any time you
|
||||
* yield a scope, advance your position to the start of the scope, but when
|
||||
* considering this new position, don't return this scope again.
|
||||
*
|
||||
* Note that the {@link hints} argument can be ignored, but you are welcome to
|
||||
* use it to improve performance. For example, knowing the
|
||||
* {@link ScopeIteratorRequirements.distalPosition} can be useful if you need
|
||||
* to query a list of scopes in bulk.
|
||||
*
|
||||
* Some notes:
|
||||
*
|
||||
* - Once you have yielded a scope, you do not need to yield any scopes
|
||||
* contained by that scope.
|
||||
* - You can yield the same scope more than once if it makes your life easier
|
||||
*
|
||||
* The only strict requirements are that
|
||||
*
|
||||
* - you yield every scope that might meet the requirements
|
||||
* - you yield scopes in the correct order
|
||||
*
|
||||
* @param editor The editor containing {@link position}
|
||||
* @param position The position from which to start
|
||||
* @param direction The direction to go relative to {@link position}
|
||||
* @param hints Optional hints about which scopes should be returned
|
||||
*/
|
||||
protected abstract generateScopeCandidates(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
hints?: ScopeIteratorRequirements | undefined,
|
||||
): Iterable<TargetScope>;
|
||||
|
||||
*generateScopes(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
requirements: ScopeIteratorRequirements | undefined = {},
|
||||
): Iterable<TargetScope> {
|
||||
let previousScope: TargetScope | undefined = undefined;
|
||||
|
||||
for (const scope of this.generateScopeCandidates(
|
||||
editor,
|
||||
position,
|
||||
direction,
|
||||
requirements,
|
||||
)) {
|
||||
if (
|
||||
shouldYieldScope(
|
||||
position,
|
||||
direction,
|
||||
requirements,
|
||||
previousScope,
|
||||
scope,
|
||||
)
|
||||
) {
|
||||
yield scope;
|
||||
}
|
||||
|
||||
if (this.canStopEarly(position, direction, requirements, scope)) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousScope = scope;
|
||||
}
|
||||
}
|
||||
|
||||
private canStopEarly(
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
requirements: ScopeIteratorRequirements,
|
||||
{ domain }: TargetScope,
|
||||
) {
|
||||
const { containment, distalPosition } = requirements;
|
||||
|
||||
if (this.isHierarchical) {
|
||||
// Don't try anything fancy if scope is hierarchical
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
containment === "required" &&
|
||||
(direction === "forward"
|
||||
? domain.end.isAfter(position)
|
||||
: domain.start.isBefore(position))
|
||||
) {
|
||||
// If we require containment, then if we have already yielded something
|
||||
// ending strictly after position, we won't yield anything else containing
|
||||
// position
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
distalPosition != null &&
|
||||
(direction === "forward"
|
||||
? domain.end.isAfterOrEqual(distalPosition)
|
||||
: domain.start.isBeforeOrEqual(distalPosition))
|
||||
) {
|
||||
// If we have a distal position, and we have yielded something that ends
|
||||
// at or after distal position, we won't be able to yield anything else
|
||||
// that starts before distal position
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,59 +1,35 @@
|
||||
import { Position, Range, TextEditor } from "vscode";
|
||||
import { Position, TextEditor } from "vscode";
|
||||
import { Direction, ScopeType } from "../../../typings/targetDescriptor.types";
|
||||
import { getDocumentRange } from "../../../util/rangeUtils";
|
||||
import { DocumentTarget } from "../../targets";
|
||||
import { OutOfRangeError } from "../targetSequenceUtils";
|
||||
import NotHierarchicalScopeError from "./NotHierarchicalScopeError";
|
||||
import BaseScopeHandler from "./BaseScopeHandler";
|
||||
import { TargetScope } from "./scope.types";
|
||||
import { ScopeHandler } from "./scopeHandler.types";
|
||||
|
||||
export default class DocumentScopeHandler implements ScopeHandler {
|
||||
export default class DocumentScopeHandler extends BaseScopeHandler {
|
||||
public readonly scopeType = { type: "document" } as const;
|
||||
public readonly iterationScopeType = { type: "document" } as const;
|
||||
protected readonly isHierarchical = false;
|
||||
|
||||
constructor(_scopeType: ScopeType, _languageId: string) {
|
||||
// Empty
|
||||
super();
|
||||
}
|
||||
|
||||
getScopesTouchingPosition(
|
||||
protected *generateScopeCandidates(
|
||||
editor: TextEditor,
|
||||
_position: Position,
|
||||
ancestorIndex: number = 0,
|
||||
): TargetScope[] {
|
||||
if (ancestorIndex !== 0) {
|
||||
throw new NotHierarchicalScopeError(this.scopeType);
|
||||
}
|
||||
|
||||
return [getDocumentScope(editor)];
|
||||
}
|
||||
|
||||
getScopesOverlappingRange(editor: TextEditor, _range: Range): TargetScope[] {
|
||||
return [getDocumentScope(editor)];
|
||||
}
|
||||
|
||||
getScopeRelativeToPosition(
|
||||
_editor: TextEditor,
|
||||
_position: Position,
|
||||
_offset: number,
|
||||
_direction: Direction,
|
||||
): TargetScope {
|
||||
// NB: offset will always be greater than or equal to 1, so this will be an
|
||||
// error
|
||||
throw new OutOfRangeError();
|
||||
): Iterable<TargetScope> {
|
||||
const contentRange = getDocumentRange(editor.document);
|
||||
|
||||
yield {
|
||||
editor,
|
||||
domain: contentRange,
|
||||
getTarget: (isReversed) =>
|
||||
new DocumentTarget({
|
||||
editor,
|
||||
isReversed,
|
||||
contentRange,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getDocumentScope(editor: TextEditor): TargetScope {
|
||||
const contentRange = getDocumentRange(editor.document);
|
||||
|
||||
return {
|
||||
editor,
|
||||
domain: contentRange,
|
||||
getTarget: (isReversed) =>
|
||||
new DocumentTarget({
|
||||
editor,
|
||||
isReversed,
|
||||
contentRange,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
64
src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts
Normal file
64
src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Allows us to keep track of an iterator along with the most recently yielded
|
||||
* value
|
||||
*/
|
||||
interface IteratorInfo<T> {
|
||||
iterator: Iterator<T>;
|
||||
value: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks each iterator for its first value, and constructs {@link IteratorInfo}s
|
||||
* to keep track of iterators along with their values, pruning any iterators
|
||||
* that didn't yield a first value.
|
||||
* @param iterators The iterators to keep track of
|
||||
* @returns A list of {@link IteratorInfo}s containing each iterator along with
|
||||
* the first yielded value, removing any iterators that didn't yield a first
|
||||
* value
|
||||
*/
|
||||
export function getInitialIteratorInfos<T>(
|
||||
iterators: Iterator<T>[],
|
||||
): IteratorInfo<T>[] {
|
||||
return iterators.flatMap((iterator) => {
|
||||
const { value, done } = iterator.next();
|
||||
return done
|
||||
? []
|
||||
: [
|
||||
{
|
||||
iterator,
|
||||
value,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances each iterator until {@link criterion} is `true`, pruning iterators
|
||||
* that terminate without {@link criterion} becoming `true`.
|
||||
*
|
||||
* @param iteratorInfos A list of iterator infos
|
||||
* @param criterion The criterion to check
|
||||
* @returns A new set of iterator infos that have been advanced until
|
||||
* {@link criterion} is `true`. Any iterators that terminate without meeting
|
||||
* {@link criterion} are removed
|
||||
*/
|
||||
export function advanceIteratorsUntil<T>(
|
||||
iteratorInfos: IteratorInfo<T>[],
|
||||
criterion: (arg: T) => boolean,
|
||||
): IteratorInfo<T>[] {
|
||||
return iteratorInfos.flatMap((iteratorInfo) => {
|
||||
const { iterator } = iteratorInfo;
|
||||
let { value } = iteratorInfo;
|
||||
|
||||
let done: boolean | undefined = false;
|
||||
while (!criterion(value) && !done) {
|
||||
({ value, done } = iterator.next());
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{ iterator, value }];
|
||||
});
|
||||
}
|
@ -1,55 +1,32 @@
|
||||
import { range } from "lodash";
|
||||
import { Position, Range, TextEditor } from "vscode";
|
||||
import { Direction, ScopeType } from "../../../typings/targetDescriptor.types";
|
||||
import { LineTarget } from "../../targets";
|
||||
import { OutOfRangeError } from "../targetSequenceUtils";
|
||||
import NotHierarchicalScopeError from "./NotHierarchicalScopeError";
|
||||
import BaseScopeHandler from "./BaseScopeHandler";
|
||||
import type { TargetScope } from "./scope.types";
|
||||
import type { ScopeHandler } from "./scopeHandler.types";
|
||||
|
||||
export default class LineScopeHandler implements ScopeHandler {
|
||||
export default class LineScopeHandler extends BaseScopeHandler {
|
||||
public readonly scopeType = { type: "line" } as const;
|
||||
public readonly iterationScopeType = { type: "document" } as const;
|
||||
protected readonly isHierarchical = false;
|
||||
|
||||
constructor(_scopeType: ScopeType, _languageId: string) {
|
||||
// Empty
|
||||
super();
|
||||
}
|
||||
|
||||
getScopesTouchingPosition(
|
||||
*generateScopeCandidates(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
ancestorIndex: number = 0,
|
||||
): TargetScope[] {
|
||||
if (ancestorIndex !== 0) {
|
||||
throw new NotHierarchicalScopeError(this.scopeType);
|
||||
}
|
||||
|
||||
return [lineNumberToScope(editor, position.line)];
|
||||
}
|
||||
|
||||
getScopesOverlappingRange(
|
||||
editor: TextEditor,
|
||||
{ start, end }: Range,
|
||||
): TargetScope[] {
|
||||
return range(start.line, end.line + 1).map((lineNumber) =>
|
||||
lineNumberToScope(editor, lineNumber),
|
||||
);
|
||||
}
|
||||
|
||||
getScopeRelativeToPosition(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
offset: number,
|
||||
direction: Direction,
|
||||
): TargetScope {
|
||||
const lineNumber =
|
||||
direction === "forward" ? position.line + offset : position.line - offset;
|
||||
|
||||
if (lineNumber < 0 || lineNumber >= editor.document.lineCount) {
|
||||
throw new OutOfRangeError();
|
||||
): Iterable<TargetScope> {
|
||||
if (direction === "forward") {
|
||||
for (let i = position.line; i < editor.document.lineCount; i++) {
|
||||
yield lineNumberToScope(editor, i);
|
||||
}
|
||||
} else {
|
||||
for (let i = position.line; i >= 0; i--) {
|
||||
yield lineNumberToScope(editor, i);
|
||||
}
|
||||
}
|
||||
|
||||
return lineNumberToScope(editor, lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
import type { Position, Range, TextEditor } from "vscode";
|
||||
import type { Position, TextEditor } from "vscode";
|
||||
import { getScopeHandler } from ".";
|
||||
import type {
|
||||
Direction,
|
||||
ScopeType,
|
||||
} from "../../../typings/targetDescriptor.types";
|
||||
import { getLeftScope, getRightScope } from "../getPreferredScope";
|
||||
import { OutOfRangeError } from "../targetSequenceUtils";
|
||||
import NotHierarchicalScopeError from "./NotHierarchicalScopeError";
|
||||
import BaseScopeHandler from "./BaseScopeHandler";
|
||||
import type { TargetScope } from "./scope.types";
|
||||
import type { ScopeHandler } from "./scopeHandler.types";
|
||||
import type {
|
||||
ScopeHandler,
|
||||
ScopeIteratorRequirements,
|
||||
} from "./scopeHandler.types";
|
||||
|
||||
/**
|
||||
* This class can be used to define scope types that are most easily defined by
|
||||
@ -17,9 +18,14 @@ import type { ScopeHandler } from "./scopeHandler.types";
|
||||
* regex can't cross line boundaries. In this case the
|
||||
* {@link iterationScopeType} will be `line`, and we just return a list of all
|
||||
* regex matches to this base class and let it handle the rest.
|
||||
*
|
||||
* Note that this base class only works for non-hierarchical scope types. In
|
||||
* the future we may define a nested scope handler that supports hierarchical
|
||||
* scope types.
|
||||
*/
|
||||
export default abstract class NestedScopeHandler implements ScopeHandler {
|
||||
export default abstract class NestedScopeHandler extends BaseScopeHandler {
|
||||
public abstract readonly iterationScopeType: ScopeType;
|
||||
protected readonly isHierarchical = false;
|
||||
|
||||
/**
|
||||
* We expand to this scope type before looking for instances of the scope type
|
||||
@ -49,7 +55,9 @@ export default abstract class NestedScopeHandler implements ScopeHandler {
|
||||
constructor(
|
||||
public readonly scopeType: ScopeType,
|
||||
protected languageId: string,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private get searchScopeHandler(): ScopeHandler {
|
||||
if (this._searchScopeHandler == null) {
|
||||
@ -62,56 +70,6 @@ export default abstract class NestedScopeHandler implements ScopeHandler {
|
||||
return this._searchScopeHandler;
|
||||
}
|
||||
|
||||
getScopesTouchingPosition(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
ancestorIndex: number = 0,
|
||||
): TargetScope[] {
|
||||
if (ancestorIndex !== 0) {
|
||||
throw new NotHierarchicalScopeError(this.scopeType);
|
||||
}
|
||||
|
||||
return this.searchScopeHandler
|
||||
.getScopesTouchingPosition(editor, position)
|
||||
.flatMap((searchScope) => this.getScopesInSearchScope(searchScope))
|
||||
.filter(({ domain }) => domain.contains(position));
|
||||
}
|
||||
|
||||
getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] {
|
||||
return this.searchScopeHandler
|
||||
.getScopesOverlappingRange(editor, range)
|
||||
.flatMap((searchScope) => this.getScopesInSearchScope(searchScope))
|
||||
.filter(({ domain }) => {
|
||||
const intersection = domain.intersection(range);
|
||||
return intersection != null && !intersection.isEmpty;
|
||||
});
|
||||
}
|
||||
|
||||
getScopeRelativeToPosition(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
offset: number,
|
||||
direction: Direction,
|
||||
): TargetScope {
|
||||
let remainingOffset = offset;
|
||||
|
||||
// Note that most of the heavy lifting is done by iterateScopeGroups; here
|
||||
// we just repeatedly subtract `scopes.length` until we have seen as many
|
||||
// scopes as required by `offset`.
|
||||
const iterator = this.iterateScopeGroups(editor, position, direction);
|
||||
for (const scopes of iterator) {
|
||||
if (scopes.length >= remainingOffset) {
|
||||
return direction === "forward"
|
||||
? scopes.at(remainingOffset - 1)!
|
||||
: scopes.at(-remainingOffset)!;
|
||||
}
|
||||
|
||||
remainingOffset -= scopes.length;
|
||||
}
|
||||
|
||||
throw new OutOfRangeError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields groups of scopes for use in {@link getScopeRelativeToPosition}.
|
||||
* Begins by returning a list of all scopes in the search scope containing
|
||||
@ -127,58 +85,29 @@ export default abstract class NestedScopeHandler implements ScopeHandler {
|
||||
* @param direction The direction passed in to
|
||||
* {@link getScopeRelativeToPosition}
|
||||
*/
|
||||
private *iterateScopeGroups(
|
||||
protected *generateScopeCandidates(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
): Generator<TargetScope[], void, unknown> {
|
||||
const containingSearchScopes =
|
||||
this.searchScopeHandler.getScopesTouchingPosition(editor, position);
|
||||
hints: ScopeIteratorRequirements | undefined = {},
|
||||
): Iterable<TargetScope> {
|
||||
const { containment, ...rest } = hints;
|
||||
const generator = this.searchScopeHandler.generateScopes(
|
||||
editor,
|
||||
position,
|
||||
direction,
|
||||
// If containment is disallowed, we need to unset that for the search
|
||||
// scope, because the search scope could contain position but nested
|
||||
// scopes do not.
|
||||
{
|
||||
containment: containment === "required" ? "required" : undefined,
|
||||
...rest,
|
||||
},
|
||||
);
|
||||
|
||||
const containingSearchScope =
|
||||
direction === "forward"
|
||||
? getRightScope(containingSearchScopes)
|
||||
: getLeftScope(containingSearchScopes);
|
||||
|
||||
let currentPosition = position;
|
||||
|
||||
if (containingSearchScope != null) {
|
||||
yield this.getScopesInSearchScope(containingSearchScope).filter(
|
||||
({ domain }) =>
|
||||
direction === "forward"
|
||||
? domain.start.isAfterOrEqual(position)
|
||||
: domain.end.isBeforeOrEqual(position),
|
||||
);
|
||||
|
||||
// Move current position past containing scope so that asking for next
|
||||
// parent search scope won't just give us back the same on
|
||||
currentPosition =
|
||||
direction === "forward"
|
||||
? containingSearchScope.domain.end
|
||||
: containingSearchScope.domain.start;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// Note that we always use an `offset` of 1 here. We could instead have
|
||||
// left `currentPosition` unchanged and incremented offset, but in some
|
||||
// cases it will be more efficient not to ask parent to walk past the same
|
||||
// scopes over and over again. Eg for surrounding pair this can help us.
|
||||
// For line it makes no difference.
|
||||
const searchScope = this.searchScopeHandler.getScopeRelativeToPosition(
|
||||
editor,
|
||||
currentPosition,
|
||||
1,
|
||||
direction,
|
||||
);
|
||||
|
||||
yield this.getScopesInSearchScope(searchScope);
|
||||
|
||||
// Move current position past the scope we just used so that asking for next
|
||||
// parent search scope won't just give us back the same on
|
||||
currentPosition =
|
||||
direction === "forward"
|
||||
? searchScope.domain.end
|
||||
: searchScope.domain.start;
|
||||
for (const searchScope of generator) {
|
||||
const scopes = this.getScopesInSearchScope(searchScope);
|
||||
yield* direction === "backward" ? [...scopes].reverse() : scopes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { maxBy, minBy } from "lodash";
|
||||
import { Position, Range, TextEditor } from "vscode";
|
||||
import { Position, TextEditor } from "vscode";
|
||||
import { getScopeHandler } from ".";
|
||||
import {
|
||||
Direction,
|
||||
OneOfScopeType,
|
||||
} from "../../../typings/targetDescriptor.types";
|
||||
import { OutOfRangeError } from "../targetSequenceUtils";
|
||||
import BaseScopeHandler from "./BaseScopeHandler";
|
||||
import { compareTargetScopes } from "./compareTargetScopes";
|
||||
import { getInitialIteratorInfos, advanceIteratorsUntil } from "./IteratorInfo";
|
||||
import type { TargetScope } from "./scope.types";
|
||||
import { ScopeHandler } from "./scopeHandler.types";
|
||||
import { ScopeHandler, ScopeIteratorRequirements } from "./scopeHandler.types";
|
||||
|
||||
export default class OneOfScopeHandler extends BaseScopeHandler {
|
||||
protected isHierarchical = true;
|
||||
|
||||
export default class OneOfScopeHandler implements ScopeHandler {
|
||||
private scopeHandlers: ScopeHandler[] = this.scopeType.scopeTypes.map(
|
||||
(scopeType) => {
|
||||
const handler = getScopeHandler(scopeType, this.languageId);
|
||||
@ -30,104 +33,40 @@ export default class OneOfScopeHandler implements ScopeHandler {
|
||||
constructor(
|
||||
public readonly scopeType: OneOfScopeType,
|
||||
private languageId: string,
|
||||
) {}
|
||||
|
||||
getScopesTouchingPosition(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
ancestorIndex?: number,
|
||||
): TargetScope[] {
|
||||
if (ancestorIndex !== 0) {
|
||||
// FIXME: We could support this one, but it will be a bit of work.
|
||||
throw new Error("`grand` not yet supported for compound scopes.");
|
||||
}
|
||||
|
||||
return keepOnlyBottomLevelScopes(
|
||||
this.scopeHandlers.flatMap((scopeHandler) =>
|
||||
scopeHandler.getScopesTouchingPosition(editor, position, ancestorIndex),
|
||||
),
|
||||
);
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* We proceed as follows:
|
||||
*
|
||||
* 1. Get all scopes returned by
|
||||
* {@link ScopeHandler.getScopesOverlappingRange} from each of
|
||||
* {@link scopeHandlers}.
|
||||
* 2. If any of these scopes has a {@link TargetScope.domain|domain} that
|
||||
* terminates within {@link range}, return all such maximal scopes.
|
||||
* 3. Otherwise, return a list containing just the minimal scope containing
|
||||
* {@link range}.
|
||||
*/
|
||||
getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] {
|
||||
const candidateScopes = this.scopeHandlers.flatMap((scopeHandler) =>
|
||||
scopeHandler.getScopesOverlappingRange(editor, range),
|
||||
);
|
||||
|
||||
const scopesTerminatingInRange = candidateScopes.filter(
|
||||
({ domain }) => !domain.contains(range),
|
||||
);
|
||||
|
||||
return scopesTerminatingInRange.length > 0
|
||||
? keepOnlyTopLevelScopes(scopesTerminatingInRange)
|
||||
: keepOnlyBottomLevelScopes(candidateScopes);
|
||||
}
|
||||
|
||||
getScopeRelativeToPosition(
|
||||
*generateScopeCandidates(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
offset: number,
|
||||
direction: Direction,
|
||||
): TargetScope {
|
||||
let currentPosition = position;
|
||||
let currentScope: TargetScope;
|
||||
hints?: ScopeIteratorRequirements | undefined,
|
||||
): Iterable<TargetScope> {
|
||||
const iterators = this.scopeHandlers.map((scopeHandler) =>
|
||||
scopeHandler
|
||||
.generateScopes(editor, position, direction, hints)
|
||||
[Symbol.iterator](),
|
||||
);
|
||||
|
||||
if (this.scopeHandlers.length === 0) {
|
||||
throw new OutOfRangeError();
|
||||
}
|
||||
let iteratorInfos = getInitialIteratorInfos(iterators);
|
||||
|
||||
for (let i = 0; i < offset; i++) {
|
||||
const candidateScopes = this.scopeHandlers.map((scopeHandler) =>
|
||||
scopeHandler.getScopeRelativeToPosition(
|
||||
editor,
|
||||
currentPosition,
|
||||
1,
|
||||
direction,
|
||||
),
|
||||
while (iteratorInfos.length > 0) {
|
||||
iteratorInfos.sort((a, b) =>
|
||||
compareTargetScopes(direction, position, a.value, b.value),
|
||||
);
|
||||
|
||||
currentScope =
|
||||
direction === "forward"
|
||||
? minBy(candidateScopes, ({ domain: start }) => start)!
|
||||
: maxBy(candidateScopes, ({ domain: end }) => end)!;
|
||||
// Pick minimum scope according to canonical scope ordering
|
||||
const currentScope = iteratorInfos[0].value;
|
||||
|
||||
currentPosition =
|
||||
direction === "forward"
|
||||
? currentScope.domain.end
|
||||
: currentScope.domain.start;
|
||||
yield currentScope;
|
||||
|
||||
// Advance all iterators past the scope that was yielded
|
||||
iteratorInfos = advanceIteratorsUntil(
|
||||
iteratorInfos,
|
||||
(scope) =>
|
||||
compareTargetScopes(direction, position, currentScope, scope) < 0,
|
||||
);
|
||||
}
|
||||
|
||||
return currentScope!;
|
||||
}
|
||||
}
|
||||
|
||||
function keepOnlyTopLevelScopes(candidateScopes: TargetScope[]): TargetScope[] {
|
||||
return candidateScopes.filter(
|
||||
({ domain }) =>
|
||||
!candidateScopes.some(({ domain: otherDomain }) =>
|
||||
otherDomain.contains(domain),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function keepOnlyBottomLevelScopes(
|
||||
candidateScopes: TargetScope[],
|
||||
): TargetScope[] {
|
||||
return candidateScopes.filter(
|
||||
({ domain }) =>
|
||||
!candidateScopes.some(({ domain: otherDomain }) =>
|
||||
domain.contains(otherDomain),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,48 +1,31 @@
|
||||
import { Position, Range, TextEditor } from "vscode";
|
||||
import { Position, TextEditor } from "vscode";
|
||||
import {
|
||||
Direction,
|
||||
SurroundingPairScopeType,
|
||||
} from "../../../typings/targetDescriptor.types";
|
||||
import BaseScopeHandler from "./BaseScopeHandler";
|
||||
import { TargetScope } from "./scope.types";
|
||||
import { ScopeHandler } from "./scopeHandler.types";
|
||||
import { ScopeIteratorRequirements } from "./scopeHandler.types";
|
||||
|
||||
export default class SurroundingPairScopeHandler implements ScopeHandler {
|
||||
export default class SurroundingPairScopeHandler extends BaseScopeHandler {
|
||||
public readonly iterationScopeType;
|
||||
|
||||
protected isHierarchical = true;
|
||||
|
||||
constructor(
|
||||
public readonly scopeType: SurroundingPairScopeType,
|
||||
_languageId: string,
|
||||
) {
|
||||
// FIXME: Figure out the actual iteration scope type
|
||||
super();
|
||||
this.iterationScopeType = this.scopeType;
|
||||
}
|
||||
|
||||
getScopesTouchingPosition(
|
||||
generateScopeCandidates(
|
||||
_editor: TextEditor,
|
||||
_position: Position,
|
||||
_ancestorIndex: number = 0,
|
||||
): TargetScope[] {
|
||||
// TODO: Run existing surrounding pair code on empty range constructed from
|
||||
// position, returning both if position is adjacent to two
|
||||
// TODO: Handle ancestor index
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getScopesOverlappingRange(_editor: TextEditor, _range: Range): TargetScope[] {
|
||||
// TODO: Implement https://github.com/cursorless-dev/cursorless/pull/1031#issuecomment-1276777449
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getScopeRelativeToPosition(
|
||||
_editor: TextEditor,
|
||||
_position: Position,
|
||||
_offset: number,
|
||||
_direction: Direction,
|
||||
): TargetScope {
|
||||
// TODO: Walk forward until we hit either an opening or closing delimiter.
|
||||
// If we hit an opening delimiter then we walk over as many pairs as we need
|
||||
// to until we have offset. If we *first* instead hit a closing PR en then we
|
||||
// expand containing and walk forward from that
|
||||
_hints?: ScopeIteratorRequirements | undefined,
|
||||
): Iterable<TargetScope> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NestedScopeHandler } from ".";
|
||||
import { getMatcher } from "../../../core/tokenizer";
|
||||
import type { ScopeType } from "../../../typings/targetDescriptor.types";
|
||||
import { getTokensInRange } from "../../../util/getTokensInRange";
|
||||
import { TokenTarget } from "../../targets";
|
||||
@ -23,4 +24,20 @@ export default class TokenScopeHandler extends NestedScopeHandler {
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
isPreferredOver(
|
||||
scopeA: TargetScope,
|
||||
scopeB: TargetScope,
|
||||
): boolean | undefined {
|
||||
const {
|
||||
editor: { document },
|
||||
} = scopeA;
|
||||
const { identifierMatcher } = getMatcher(document.languageId);
|
||||
|
||||
return identifierMatcher.test(document.getText(scopeA.domain))
|
||||
? true
|
||||
: identifierMatcher.test(document.getText(scopeB.domain))
|
||||
? false
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,114 @@
|
||||
import { Position, Range } from "vscode";
|
||||
import { Direction } from "../../../typings/targetDescriptor.types";
|
||||
import { TargetScope } from "./scope.types";
|
||||
|
||||
/**
|
||||
* Defines the canonical scope ordering.
|
||||
*
|
||||
* @param direction The direction of iteration
|
||||
* @param position The initial position
|
||||
* @param param2 Scope A
|
||||
* @param param3 Scope B
|
||||
* @returns -1 if scope A should be yielded before scope B, 1 if scope A should
|
||||
* be yielded after scope B, and 0 if they are equivalent by this ordering
|
||||
*/
|
||||
export function compareTargetScopes(
|
||||
direction: Direction,
|
||||
position: Position,
|
||||
{ domain: a }: TargetScope,
|
||||
{ domain: b }: TargetScope,
|
||||
): number {
|
||||
return direction === "forward"
|
||||
? compareTargetScopesForward(position, a, b)
|
||||
: compareTargetScopesBackward(position, a, b);
|
||||
}
|
||||
|
||||
function compareTargetScopesForward(
|
||||
position: Position,
|
||||
a: Range,
|
||||
b: Range,
|
||||
): number {
|
||||
// First determine whether the start occurs before position. If so, we will
|
||||
// only get to see the end when iterating forward.
|
||||
const aIsStartVisible = a.start.isAfterOrEqual(position);
|
||||
const bIsStartVisible = b.start.isAfterOrEqual(position);
|
||||
|
||||
if (aIsStartVisible && bIsStartVisible) {
|
||||
// If both of them occur after or equal position, yield them according to
|
||||
// when they start, or yield smaller one first if they start at the same
|
||||
// place
|
||||
const value = a.start.compareTo(b.start);
|
||||
|
||||
return value === 0 ? a.end.compareTo(b.end) : value;
|
||||
}
|
||||
|
||||
if (!aIsStartVisible && !bIsStartVisible) {
|
||||
// If both of them start before position, compare their endpoints, yielding
|
||||
// the one that ends first, breaking ties by yielding smaller one first
|
||||
const value = a.end.compareTo(b.end);
|
||||
|
||||
return value === 0 ? -a.start.compareTo(b.start) : value;
|
||||
}
|
||||
|
||||
if (!aIsStartVisible && bIsStartVisible) {
|
||||
// If `a` starts before position, but `b` does not, then compare the end of
|
||||
// `a` to the start of `b`, returning whichever is smaller.
|
||||
const value = a.end.compareTo(b.start);
|
||||
|
||||
// If they are tied, then start with `a` if it is empty, otherwise start
|
||||
// with `b` because it is ending and `a` is starting.
|
||||
return value !== 0 ? value : b.isEmpty ? 1 : -1;
|
||||
}
|
||||
|
||||
// Otherwise `b` starts before position, but `a` does not. Apply reverse
|
||||
// logic to last `if` statement above
|
||||
const value = a.start.compareTo(b.end);
|
||||
|
||||
return value !== 0 ? value : a.isEmpty ? -1 : 1;
|
||||
}
|
||||
|
||||
// FIXME: Unify this function with compareTargetScopesForward by constructing
|
||||
// distal / proximal versions of these ranges
|
||||
function compareTargetScopesBackward(
|
||||
position: Position,
|
||||
a: Range,
|
||||
b: Range,
|
||||
): number {
|
||||
// First determine whether the end occurs after position. If so, we will
|
||||
// only get to see the start when iterating backward.
|
||||
const aIsEndVisible = a.end.isBeforeOrEqual(position);
|
||||
const bIsEndVisible = b.end.isBeforeOrEqual(position);
|
||||
|
||||
if (aIsEndVisible && bIsEndVisible) {
|
||||
// If both of them occur before or equal position, yield them according to
|
||||
// when they end, or yield smaller one first if they end at the same
|
||||
// place
|
||||
const value = -a.end.compareTo(b.end);
|
||||
|
||||
return value === 0 ? -a.start.compareTo(b.start) : value;
|
||||
}
|
||||
|
||||
if (!aIsEndVisible && !bIsEndVisible) {
|
||||
// If both of them end after position, compare their startpoints, yielding
|
||||
// the one that starts first, breaking ties by yielding smaller one first
|
||||
const value = -a.start.compareTo(b.start);
|
||||
|
||||
return value === 0 ? a.end.compareTo(b.end) : value;
|
||||
}
|
||||
|
||||
if (!aIsEndVisible && bIsEndVisible) {
|
||||
// If `a` ends after position, but `b` does not, then compare the start of
|
||||
// `a` to the end of `b`, returning whichever is greater.
|
||||
const value = -a.start.compareTo(b.end);
|
||||
|
||||
// If they are tied, then start with `a` if it is empty, otherwise start
|
||||
// with `b` because it is starting and `a` is ending.
|
||||
return value !== 0 ? value : b.isEmpty ? 1 : -1;
|
||||
}
|
||||
|
||||
// Otherwise `b` starts before position, but `a` does not. Apply reverse
|
||||
// logic to last `if` statement above
|
||||
const value = -a.end.compareTo(b.start);
|
||||
|
||||
return value !== 0 ? value : a.isEmpty ? -1 : 1;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { Position, TextEditor } from "vscode";
|
||||
import { Direction } from "../../../typings/targetDescriptor.types";
|
||||
import { OutOfRangeError } from "../targetSequenceUtils";
|
||||
import { TargetScope } from "./scope.types";
|
||||
import { ScopeHandler } from "./scopeHandler.types";
|
||||
|
||||
/**
|
||||
* Runs the scope generator until `offset` scopes have been yielded and returns
|
||||
* that scope, only yielding scopes that start after or equal {@link position}
|
||||
* @param scopeHandler
|
||||
* @param editor
|
||||
* @param position
|
||||
* @param offset
|
||||
* @param direction
|
||||
* @returns
|
||||
*/
|
||||
export default function getScopeRelativeToPosition(
|
||||
scopeHandler: ScopeHandler,
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
offset: number,
|
||||
direction: Direction,
|
||||
): TargetScope {
|
||||
let scopeCount = 0;
|
||||
const iterator = scopeHandler.generateScopes(editor, position, direction, {
|
||||
containment: "disallowedIfStrict",
|
||||
});
|
||||
for (const scope of iterator) {
|
||||
scopeCount += 1;
|
||||
|
||||
if (scopeCount === offset) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
|
||||
throw new OutOfRangeError();
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { Range, TextEditor } from "vscode";
|
||||
import { TargetScope } from "./scope.types";
|
||||
import { ScopeHandler } from "./scopeHandler.types";
|
||||
|
||||
/**
|
||||
* Returns a list of all scopes that have nonempty overlap with {@link range}.
|
||||
* @param scopeHandler
|
||||
* @param editor
|
||||
* @param param2
|
||||
* @returns
|
||||
*/
|
||||
export default function getScopesOverlappingRange(
|
||||
scopeHandler: ScopeHandler,
|
||||
editor: TextEditor,
|
||||
{ start, end }: Range,
|
||||
): TargetScope[] {
|
||||
return Array.from(
|
||||
scopeHandler.generateScopes(editor, start, "forward", {
|
||||
distalPosition: end,
|
||||
}),
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import type { Position, Range, TextEditor } from "vscode";
|
||||
import type { Position, TextEditor } from "vscode";
|
||||
import type {
|
||||
Direction,
|
||||
ScopeType,
|
||||
@ -7,13 +7,9 @@ import type { TargetScope } from "./scope.types";
|
||||
|
||||
/**
|
||||
* Represents a scope type. The functions in this interface allow us to find
|
||||
* specific instances of the given scope type in a document. For example, there
|
||||
* is a function to find the scopes touching a given position
|
||||
* ({@link getScopesTouchingPosition}), a function to find every instance of
|
||||
* the scope overlapping a range ({@link getScopesOverlappingRange}), etc.
|
||||
* These functions are used by the various modifier stages to implement
|
||||
* modifiers that involve the given scope type, such as containing, every,
|
||||
* next, etc.
|
||||
* specific instances of the given scope type in a document. These functions are
|
||||
* used by the various modifier stages to implement modifiers that involve the
|
||||
* given scope type, such as containing, every, next, etc.
|
||||
*
|
||||
* Note that some scope types are hierarchical, ie one scope of the given type
|
||||
* can contain another scope of the same type. For example, a function can
|
||||
@ -21,11 +17,10 @@ import type { TargetScope } from "./scope.types";
|
||||
* are also hierarchical, as they can be nested. Many scope types are not
|
||||
* hierarchical, though, eg line, token, word, etc.
|
||||
*
|
||||
* In the case of a hierarchical scope type, these functions should never
|
||||
* return two scopes that contain one another. Ie if we return a surrounding
|
||||
* pair, we shouldn't also return any surrounding pairs contained within, or
|
||||
* if we return a function, we shouldn't also return a function nested within
|
||||
* that function.
|
||||
* Note also that scope's domains are never allowed to partially overlap.
|
||||
* Scopes can be directly adjacent to one another, or have one or more
|
||||
* characters between them, or, for hierarchical scopes, one scope can
|
||||
* completely contain another scope.
|
||||
*
|
||||
* Note that there are helpers that can sometimes be used to avoid implementing
|
||||
* a scope handler from scratch, eg {@link NestedScopeHandler}.
|
||||
@ -44,110 +39,67 @@ export interface ScopeHandler {
|
||||
readonly iterationScopeType: ScopeType;
|
||||
|
||||
/**
|
||||
* Return all scope(s) touching the given position. A scope is considered to
|
||||
* touch a position if its {@link TargetScope.domain|domain} contains the
|
||||
* position or is directly adjacent to the position. In other words, return
|
||||
* all scopes for which the following is true:
|
||||
*
|
||||
* ```typescript
|
||||
* scope.domain.start <= position && scope.domain.end >= position
|
||||
* ```
|
||||
*
|
||||
* If the position is directly adjacent to two scopes, return both. If no
|
||||
* scope touches the given position, return an empty list.
|
||||
*
|
||||
* Note that if this scope type is hierarchical, return only minimal scopes if
|
||||
* {@link ancestorIndex} is omitted or is 0. Ie if scope A and scope B both
|
||||
* touch {@link position}, and scope A contains scope B, return scope B but
|
||||
* not scope A.
|
||||
*
|
||||
* If {@link ancestorIndex} is supplied and is greater than 0, throw a
|
||||
* {@link NotHierarchicalScopeError} if the scope type is not hierarchical.
|
||||
*
|
||||
* If the scope type is hierarchical, then if {@link ancestorIndex} is 1,
|
||||
* return all scopes touching {@link position} that have a child that is a
|
||||
* minimal scope touching {@link position} (ie they have a child that has an
|
||||
* {@link ancestorIndex} of 1 with respect to {@link position}). If
|
||||
* {@link ancestorIndex} is 2, return all scopes touching {@link position}
|
||||
* that have a child with {@link ancestorIndex} of 1 with respect to
|
||||
* {@link position}, etc.
|
||||
*
|
||||
* The {@link ancestorIndex} parameter is primarily to be used by `"grand"`
|
||||
* scopes (#124).
|
||||
*
|
||||
* @param editor The editor containing {@link position}
|
||||
* @param position The position from which to expand
|
||||
* @param ancestorIndex If supplied, skip this many ancestors up the
|
||||
* hierarchy.
|
||||
*/
|
||||
getScopesTouchingPosition(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
ancestorIndex?: number,
|
||||
): TargetScope[];
|
||||
|
||||
/**
|
||||
* Return a list of all scopes that overlap with {@link range}. A scope is
|
||||
* considered to overlap with a range if its {@link TargetScope.domain|domain}
|
||||
* has a non-empty intersection with the range. In other words, return all
|
||||
* scopes for which the following is true:
|
||||
*
|
||||
* ```typescript
|
||||
* const intersection = scope.domain.intersection(range);
|
||||
* return intersection != null && !intersection.isEmpty;
|
||||
* ```
|
||||
*
|
||||
* If the scope type is hierarchical, then there can be nested scopes that
|
||||
* both overlap with {@link range}. As mentioned in the JSDoc for
|
||||
* {@link ScopeHandler}, you should never return two scopes that contain one
|
||||
* another. Ie if scope A and scope B both overlap with {@link range}, you
|
||||
* must return only one of them. Here's how to decide which scopes to return:
|
||||
*
|
||||
* 1. If there exists any scope whose {@link TargetScope.domain|domain} starts
|
||||
* or ends within {@link range}, then return all maximal scopes whose
|
||||
* {@link TargetScope.domain|domain} starts or ends within {@link range}.
|
||||
* Ie if scope A and scope B both have domains starting or ending in
|
||||
* {@link range} and scope A contains scope B, return scope A.
|
||||
* 2. Otherwise, ie if no scope terminates within {@link range}, return the
|
||||
* minimal scope whose {@link TargetScope.domain|domain} contains
|
||||
* {@link range}, if any such scope exists. Ie if scope A and scope B both
|
||||
* have domains containing {@link range} and scope A contains scope B,
|
||||
* return scope B.
|
||||
*
|
||||
* @param editor The editor containing {@link range}
|
||||
* @param range The range with which to find overlapping scopes
|
||||
*/
|
||||
getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[];
|
||||
|
||||
/**
|
||||
* Returns a scope before or after {@link position}, depending on
|
||||
* {@link direction}. If {@link direction} is `"forward"` and {@link offset}
|
||||
* is 1, return the leftmost scope whose {@link TargetScope.domain|domain}'s
|
||||
* {@link Range.start|start} is equal or after {@link position}. If
|
||||
* {@link direction} is `"forward"` and {@link offset} is 2, return the
|
||||
* leftmost scope whose {@link TargetScope.domain|domain}'s
|
||||
* {@link Range.start|start} is equal or after the {@link Range.end|end} of
|
||||
* {@link TargetScope.domain|domain} of the scope at `offset` 1. Etc.
|
||||
*
|
||||
* If {@link direction} is `"backward"` and {@link offset} is 1, return the
|
||||
* rightmost scope whose {@link TargetScope.domain|domain}'s
|
||||
* {@link Range.end|end} is equal or before {@link position}. If
|
||||
* {@link direction} is `"backward"` and {@link offset} is 2, return the
|
||||
* rightmost scope whose {@link TargetScope.domain|domain}'s
|
||||
* {@link Range.end|end} is equal or before the {@link Range.start|start} of
|
||||
* {@link TargetScope.domain|domain} of the scope at `offset` 1. Etc.
|
||||
*
|
||||
* Note that {@link offset} will always be greater than or equal to 1.
|
||||
* Returns an iterable of scopes meeting the requirements in
|
||||
* {@link requirements}, yielded in a specific order. See
|
||||
* {@link generateScopeCandidates} and {@link compareTargetScopes} for more on
|
||||
* the order.
|
||||
*
|
||||
* @param editor The editor containing {@link position}
|
||||
* @param position The position from which to start
|
||||
* @param offset Which scope before / after position to return
|
||||
* @param direction The direction to go relative to {@link position}
|
||||
* @param requirements Extra requirements of the scopes being returned
|
||||
* @returns An iterable of scopes
|
||||
*/
|
||||
getScopeRelativeToPosition(
|
||||
generateScopes(
|
||||
editor: TextEditor,
|
||||
position: Position,
|
||||
offset: number,
|
||||
direction: Direction,
|
||||
): TargetScope;
|
||||
requirements?: ScopeIteratorRequirements,
|
||||
): Iterable<TargetScope>;
|
||||
|
||||
/**
|
||||
* This optional function can be defined to indicate a preference when the
|
||||
* containing scope modifier is applied to an empty target that is directly in
|
||||
* between two instances of scope. By default we prefer the right scope, but
|
||||
* if you define this function you can indicate another way to break these
|
||||
* ties.
|
||||
* @param scopeA A scope
|
||||
* @param scopeB Another scope
|
||||
* @returns A boolean indicating if {@link scopeA} is preferred over
|
||||
* {@link scopeB}. A value of `undefined` indicates no preference.
|
||||
*/
|
||||
isPreferredOver?(
|
||||
scopeA: TargetScope,
|
||||
scopeB: TargetScope,
|
||||
): boolean | undefined;
|
||||
}
|
||||
|
||||
export type ContainmentPolicy =
|
||||
| "required"
|
||||
| "disallowed"
|
||||
| "disallowedIfStrict";
|
||||
|
||||
export interface ScopeIteratorRequirements {
|
||||
/**
|
||||
* Indicates whether the scopes must / must not contain the input position.
|
||||
* The values are as follows:
|
||||
*
|
||||
* - `"required"` means that the scope's {@link TargetScope.domain|domain}
|
||||
* must contain position. If position is directly adjacent to the domain,
|
||||
* that counts as containment
|
||||
* - `"disallowed"` means that the scope's {@link TargetScope.domain|domain}
|
||||
* may not contain position. If position is directly adjacent to the
|
||||
* domain, that is also disallowed
|
||||
* - `"disallowedIfStrict"` means that the scope's
|
||||
* {@link TargetScope.domain|domain} may not strictly contain position. If
|
||||
* position is directly adjacent to the domain, that *is* allowed.
|
||||
*/
|
||||
containment?: ContainmentPolicy;
|
||||
|
||||
/**
|
||||
* Indicates that the {@link TargetScope.domain|domain} of the scopes must
|
||||
* start strictly before this position for `"forward"`, or strictly after this
|
||||
* position for `"backward"`.
|
||||
*/
|
||||
distalPosition?: Position;
|
||||
}
|
||||
|
102
src/processTargets/modifiers/scopeHandlers/shouldYieldScope.ts
Normal file
102
src/processTargets/modifiers/scopeHandlers/shouldYieldScope.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Position } from "vscode";
|
||||
import { Direction } from "../../../typings/targetDescriptor.types";
|
||||
import { strictlyContains } from "../../../util/rangeUtils";
|
||||
import { compareTargetScopes } from "./compareTargetScopes";
|
||||
import { TargetScope } from "./scope.types";
|
||||
import { ScopeIteratorRequirements } from "./scopeHandler.types";
|
||||
|
||||
/**
|
||||
* This function is used to filter out scopes that don't meet the required
|
||||
* criteria.
|
||||
*
|
||||
* @param position
|
||||
* @param direction
|
||||
* @param hints
|
||||
* @param previousScope
|
||||
* @param scope
|
||||
* @returns `true` if {@link scope} meets the criteria laid out in
|
||||
* {@link hints}, as well as the default semantics.
|
||||
*/
|
||||
export function shouldYieldScope(
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
hints: ScopeIteratorRequirements,
|
||||
previousScope: TargetScope | undefined,
|
||||
scope: TargetScope,
|
||||
): boolean {
|
||||
const { containment, distalPosition } = hints;
|
||||
const { domain } = scope;
|
||||
|
||||
if (
|
||||
previousScope != null &&
|
||||
compareTargetScopes(direction, position, previousScope, scope) >= 0
|
||||
) {
|
||||
// Don't yield any scopes that are considered prior to a scope that has
|
||||
// already been yielded
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple containment checks
|
||||
switch (containment) {
|
||||
case "disallowed":
|
||||
if (domain.contains(position)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "disallowedIfStrict":
|
||||
if (strictlyContains(domain, position)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "required":
|
||||
if (!domain.contains(position)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Don't yield scopes that end before the iteration is supposed to start
|
||||
if (
|
||||
direction === "forward"
|
||||
? domain.end.isBefore(position)
|
||||
: domain.start.isAfter(position)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't return non-empty scopes that end where the iteration is supposed to
|
||||
// start
|
||||
if (
|
||||
!domain.isEmpty &&
|
||||
(direction === "forward"
|
||||
? domain.end.isEqual(position)
|
||||
: domain.start.isEqual(position))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (distalPosition != null) {
|
||||
if (
|
||||
direction === "forward"
|
||||
? domain.start.isAfter(distalPosition)
|
||||
: domain.end.isBefore(distalPosition)
|
||||
) {
|
||||
// If a distalPosition was given, don't yield scopes that start after the
|
||||
// distalPosition
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!domain.isEmpty &&
|
||||
(direction === "forward"
|
||||
? domain.start.isEqual(distalPosition)
|
||||
: domain.end.isEqual(distalPosition))
|
||||
) {
|
||||
// If a distalPosition was given, don't yield non-empty scopes that start
|
||||
// at distalPosition
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -23,7 +23,7 @@ finalState:
|
||||
documentContents: |-
|
||||
const foo = "hello";
|
||||
|
||||
const bar = "helloconst;
|
||||
const bar = "const";
|
||||
selections:
|
||||
- anchor: {line: 2, character: 18}
|
||||
active: {line: 2, character: 18}
|
||||
|
@ -30,5 +30,5 @@ finalState:
|
||||
const bar = "hello";
|
||||
selections:
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 2, character: 19}
|
||||
active: {line: 2, character: 18}
|
||||
fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, anchor: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: o}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: identity}}, active: {type: primitive, mark: {type: cursorToken}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: identity}}}]
|
||||
|
@ -0,0 +1,29 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear line backward
|
||||
version: 3
|
||||
targets:
|
||||
- type: primitive
|
||||
modifiers:
|
||||
- type: relativeScope
|
||||
scopeType: {type: line}
|
||||
offset: 0
|
||||
length: 1
|
||||
direction: backward
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: |-
|
||||
aaa bbb
|
||||
ccc ddd
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |
|
||||
aaa bbb
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: line}, offset: 0, length: 1, direction: backward}]}]
|
@ -1,4 +1,4 @@
|
||||
languageId: markdown
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear next token
|
||||
version: 3
|
||||
@ -13,20 +13,14 @@ command:
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: |-
|
||||
aaa
|
||||
bbb
|
||||
ccc
|
||||
documentContents: .foo bar
|
||||
selections:
|
||||
- anchor: {line: 0, character: 1}
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 0, character: 1}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |-
|
||||
aaa
|
||||
|
||||
ccc
|
||||
documentContents: . bar
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
- anchor: {line: 0, character: 1}
|
||||
active: {line: 0, character: 1}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}]}]
|
||||
|
@ -1,4 +1,4 @@
|
||||
languageId: plaintext
|
||||
languageId: markdown
|
||||
command:
|
||||
spokenForm: clear next token
|
||||
version: 3
|
||||
@ -13,14 +13,20 @@ command:
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: .foo bar
|
||||
selections:
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 0, character: 1}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: . bar
|
||||
documentContents: |-
|
||||
aaa
|
||||
bbb
|
||||
ccc
|
||||
selections:
|
||||
- anchor: {line: 0, character: 1}
|
||||
active: {line: 0, character: 1}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |-
|
||||
aaa
|
||||
|
||||
ccc
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 1, length: 1, direction: forward}]}]
|
@ -0,0 +1,30 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear one lines
|
||||
version: 3
|
||||
targets:
|
||||
- type: primitive
|
||||
modifiers:
|
||||
- type: relativeScope
|
||||
scopeType: {type: line}
|
||||
offset: 0
|
||||
length: 1
|
||||
direction: forward
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: |-
|
||||
aaa bbb
|
||||
ccc ddd
|
||||
selections:
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |-
|
||||
|
||||
ccc ddd
|
||||
selections:
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 0, character: 0}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: line}, offset: 0, length: 1, direction: forward}]}]
|
@ -0,0 +1,26 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear one tokens
|
||||
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
|
||||
selections:
|
||||
- anchor: {line: 0, character: 3}
|
||||
active: {line: 0, character: 3}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: " bbb"
|
||||
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: 1, direction: forward}]}]
|
@ -0,0 +1,26 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear token backward
|
||||
version: 3
|
||||
targets:
|
||||
- type: primitive
|
||||
modifiers:
|
||||
- type: relativeScope
|
||||
scopeType: {type: token}
|
||||
offset: 0
|
||||
length: 1
|
||||
direction: backward
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: aaa bbb
|
||||
selections:
|
||||
- anchor: {line: 0, character: 4}
|
||||
active: {line: 0, character: 4}
|
||||
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: 1, direction: backward}]}]
|
@ -0,0 +1,28 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear two lines backward
|
||||
version: 3
|
||||
targets:
|
||||
- type: primitive
|
||||
modifiers:
|
||||
- type: relativeScope
|
||||
scopeType: {type: line}
|
||||
offset: 0
|
||||
length: 2
|
||||
direction: backward
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: |-
|
||||
aaa bbb
|
||||
ccc ddd
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: ""
|
||||
selections:
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 0, character: 0}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: line}, offset: 0, length: 2, direction: backward}]}]
|
@ -1,4 +1,4 @@
|
||||
languageId: markdown
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear two tokens
|
||||
version: 3
|
||||
@ -13,19 +13,14 @@ command:
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: |-
|
||||
aaa
|
||||
bbb
|
||||
ccc
|
||||
documentContents: aaa bbb. ccc
|
||||
selections:
|
||||
- anchor: {line: 0, character: 1}
|
||||
active: {line: 0, character: 1}
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |-
|
||||
|
||||
ccc
|
||||
documentContents: aaa bbb
|
||||
selections:
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 0, character: 0}
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: forward}]}]
|
||||
|
@ -0,0 +1,31 @@
|
||||
languageId: markdown
|
||||
command:
|
||||
spokenForm: clear two tokens
|
||||
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: 1}
|
||||
active: {line: 0, character: 1}
|
||||
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: forward}]}]
|
@ -13,14 +13,14 @@ command:
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: aaa bbb. ccc
|
||||
documentContents: aaa bbb ccc
|
||||
selections:
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
- anchor: {line: 0, character: 3}
|
||||
active: {line: 0, character: 3}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: aaa bbb
|
||||
documentContents: " ccc"
|
||||
selections:
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
- 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: forward}]}]
|
@ -1,4 +1,4 @@
|
||||
languageId: markdown
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear two tokens backward
|
||||
version: 3
|
||||
@ -13,18 +13,14 @@ command:
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: |-
|
||||
aaa
|
||||
bbb
|
||||
ccc
|
||||
documentContents: aaa bbb. ccc
|
||||
selections:
|
||||
- anchor: {line: 2, character: 1}
|
||||
active: {line: 2, character: 1}
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |
|
||||
aaa
|
||||
documentContents: . ccc
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
- 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}]}]
|
||||
|
@ -0,0 +1,30 @@
|
||||
languageId: markdown
|
||||
command:
|
||||
spokenForm: clear two tokens 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: 2, character: 1}
|
||||
active: {line: 2, character: 1}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: |
|
||||
aaa
|
||||
selections:
|
||||
- anchor: {line: 1, character: 0}
|
||||
active: {line: 1, character: 0}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 2, direction: backward}]}]
|
@ -13,14 +13,14 @@ command:
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: aaa bbb. ccc
|
||||
documentContents: aaa bbb ccc
|
||||
selections:
|
||||
- anchor: {line: 0, character: 7}
|
||||
active: {line: 0, character: 7}
|
||||
- anchor: {line: 0, character: 8}
|
||||
active: {line: 0, character: 8}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: . ccc
|
||||
documentContents: "aaa "
|
||||
selections:
|
||||
- anchor: {line: 0, character: 0}
|
||||
active: {line: 0, character: 0}
|
||||
- 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: backward}]}]
|
@ -0,0 +1,26 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear token backward
|
||||
version: 3
|
||||
targets:
|
||||
- type: primitive
|
||||
modifiers:
|
||||
- type: relativeScope
|
||||
scopeType: {type: token}
|
||||
offset: 0
|
||||
length: 1
|
||||
direction: backward
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: aaa.
|
||||
selections:
|
||||
- anchor: {line: 0, character: 3}
|
||||
active: {line: 0, character: 3}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: .
|
||||
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: 1, direction: backward}]}]
|
@ -0,0 +1,26 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear token backward
|
||||
version: 3
|
||||
targets:
|
||||
- type: primitive
|
||||
modifiers:
|
||||
- type: relativeScope
|
||||
scopeType: {type: token}
|
||||
offset: 0
|
||||
length: 1
|
||||
direction: backward
|
||||
usePrePhraseSnapshot: true
|
||||
action: {name: clearAndSetSelection}
|
||||
initialState:
|
||||
documentContents: .aaa
|
||||
selections:
|
||||
- anchor: {line: 0, character: 1}
|
||||
active: {line: 0, character: 1}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: aaa
|
||||
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: 1, direction: backward}]}]
|
@ -0,0 +1,26 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear token forward
|
||||
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.
|
||||
selections:
|
||||
- anchor: {line: 0, character: 3}
|
||||
active: {line: 0, character: 3}
|
||||
marks: {}
|
||||
finalState:
|
||||
documentContents: aaa
|
||||
selections:
|
||||
- anchor: {line: 0, character: 3}
|
||||
active: {line: 0, character: 3}
|
||||
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: relativeScope, scopeType: {type: token}, offset: 0, length: 1, direction: forward}]}]
|
@ -0,0 +1,26 @@
|
||||
languageId: plaintext
|
||||
command:
|
||||
spokenForm: clear token forward
|
||||
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
|
||||
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: relativeScope, scopeType: {type: token}, offset: 0, length: 1, direction: forward}]}]
|
@ -43,12 +43,18 @@ export function getRangeLength(editor: TextEditor, range: Range) {
|
||||
* range1.start < range2.start && range1.end > range2.end
|
||||
* ```
|
||||
* @param range1 One of the ranges to compare
|
||||
* @param range2 The other range to compare
|
||||
* @param rangeOrPosition The other range or position to compare
|
||||
* @returns A boolean indicating whether {@link range1} completely contains
|
||||
* {@link range2} without it touching either boundary
|
||||
* {@link rangeOrPosition} without it touching either boundary
|
||||
*/
|
||||
export function strictlyContains(range1: Range, range2: Range): boolean {
|
||||
return range1.start.isBefore(range2.start) && range1.end.isAfter(range2.end);
|
||||
export function strictlyContains(
|
||||
range1: Range,
|
||||
rangeOrPosition: Range | Position,
|
||||
): boolean {
|
||||
const start =
|
||||
"start" in rangeOrPosition ? rangeOrPosition.start : rangeOrPosition;
|
||||
const end = "end" in rangeOrPosition ? rangeOrPosition.end : rangeOrPosition;
|
||||
return range1.start.isBefore(start) && range1.end.isAfter(end);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user