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:
Pokey Rule 2022-10-29 16:18:23 +01:00 committed by GitHub
parent cf66411ba8
commit 1871635283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1282 additions and 668 deletions

View File

@ -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,
},
)

View File

@ -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,
)

View File

@ -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;
}

View File

@ -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),
);

View File

@ -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
);
}

View File

@ -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);
}

View 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;
}

View File

@ -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;
}

View 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;
}
}

View File

@ -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,
}),
};
}

View 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 }];
});
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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),
),
);
}

View File

@ -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.");
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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,
}),
);
}

View File

@ -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;
}

View 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;
}

View File

@ -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}

View File

@ -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}}}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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}]}]

View File

@ -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);
}
/**