Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added spread support for relative and ordinal scopes #2254

Merged
merged 17 commits into from
Mar 25, 2024
9 changes: 9 additions & 0 deletions changelog/2024-03-addedSpreadModifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
tags: [enhancement]
pullRequest: 2254
---

- Added increment spread modifier. Turn relative and ordinal range modifiers into multiple target selections instead of contiguous range.
pokey marked this conversation as resolved.
Show resolved Hide resolved

- `"take spread two tokens"` selects two tokens as separate selections
- `"take spread first two tokens"` selects two tokens as separate selections
pokey marked this conversation as resolved.
Show resolved Hide resolved
23 changes: 18 additions & 5 deletions cursorless-talon/src/modifiers/ordinal_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,23 @@ def cursorless_ordinal_range(m) -> dict[str, Any]:


@mod.capture(
rule="({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
rule="[{user.cursorless_spread_scope_modifier}] ({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
)
def cursorless_first_last(m) -> dict[str, Any]:
"""First/last `n` scopes; eg "first three funks"""
if m[0] == "first":
spread = hasattr(m, "cursorless_spread_scope_modifier")
if hasattr(m, "cursorless_first_modifier"):
return create_ordinal_scope_modifier(
m.cursorless_scope_type_plural, 0, m.private_cursorless_number_small
m.cursorless_scope_type_plural,
0,
m.private_cursorless_number_small,
spread,
)
return create_ordinal_scope_modifier(
m.cursorless_scope_type_plural,
-m.private_cursorless_number_small,
m.private_cursorless_number_small,
spread,
)


Expand All @@ -65,10 +70,18 @@ def cursorless_ordinal_scope(m) -> dict[str, Any]:
return m[0]


def create_ordinal_scope_modifier(scope_type: dict, start: int, length: int = 1):
return {
def create_ordinal_scope_modifier(
scope_type: dict,
start: int,
length: int = 1,
spread: bool = False,
):
res = {
"type": "ordinalScope",
"scopeType": scope_type,
"start": start,
"length": length,
}
if spread:
res["spread"] = True
return res
20 changes: 16 additions & 4 deletions cursorless-talon/src/modifiers/relative_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
mod.list(
"cursorless_forward_backward_modifier", desc="Cursorless forward/backward modifiers"
)
mod.list("cursorless_spread_scope_modifier", desc="Cursorless spread modifiers")


@mod.capture(rule="{user.cursorless_previous_next_modifier}")
Expand All @@ -26,11 +27,12 @@ def cursorless_relative_scope_singular(m) -> dict[str, Any]:
getattr(m, "ordinals_small", 1),
1,
m.cursorless_relative_direction,
False,
)


@mod.capture(
rule="<user.cursorless_relative_direction> <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
rule="[{user.cursorless_spread_scope_modifier}] <user.cursorless_relative_direction> <user.private_cursorless_number_small> <user.cursorless_scope_type_plural>"
)
def cursorless_relative_scope_plural(m) -> dict[str, Any]:
"""Relative previous/next plural scope. `next three funks`"""
Expand All @@ -39,11 +41,12 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]:
1,
m.private_cursorless_number_small,
m.cursorless_relative_direction,
hasattr(m, "cursorless_spread_scope_modifier"),
)


@mod.capture(
rule="<user.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{user.cursorless_forward_backward_modifier}]"
rule="[{user.cursorless_spread_scope_modifier}] <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`"""
Expand All @@ -52,6 +55,7 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]:
0,
m.private_cursorless_number_small,
getattr(m, "cursorless_forward_backward_modifier", "forward"),
hasattr(m, "cursorless_spread_scope_modifier"),
)


Expand All @@ -65,6 +69,7 @@ def cursorless_relative_scope_one_backward(m) -> dict[str, Any]:
0,
1,
m.cursorless_forward_backward_modifier,
False,
)


Expand All @@ -82,12 +87,19 @@ def cursorless_relative_scope(m) -> dict[str, Any]:


def create_relative_scope_modifier(
scope_type: dict, offset: int, length: int, direction: str
scope_type: dict,
offset: int,
length: int,
direction: str,
spread: bool,
) -> dict[str, Any]:
return {
res = {
"type": "relativeScope",
"scopeType": scope_type,
"offset": offset,
"length": length,
"direction": direction,
}
if spread:
res["spread"] = True
return res
3 changes: 3 additions & 0 deletions cursorless-talon/src/spoken_forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@
"visible": "visible"
},
"simple_scope_modifier": { "every": "every", "grand": "ancestor" },
"spread_scope_modifier": {
"spread": "spread"
pokey marked this conversation as resolved.
Show resolved Hide resolved
},
"interior_modifier": {
"inside": "interiorOnly"
},
Expand Down
7 changes: 7 additions & 0 deletions docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ And here is a table of the spoken forms:
| `"previous [number] [scope]s"` | previous `[number]` instances of `[scope]` | `"take previous three funks"` |
| `"previous [scope]"` | Previous instance of `[scope]` | `"take previous funk"` |

##### `spread`

The modifier `spread` can be used to get multiple selections from the above numbered scopes instead of one contiguous range.

- `"take spread two tokens"` selects two tokens as separate selections
- `"take spread first two tokens"` selects two tokens as separate selections
pokey marked this conversation as resolved.
Show resolved Hide resolved

##### `"every"`

The modifier `"every"` can be used to select a syntactic element and all of its matching siblings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ export interface OrdinalScopeModifier {

/** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */
length: number;

/** If true spread to individual targets instead of combined range */
spread?: boolean;
pokey marked this conversation as resolved.
Show resolved Hide resolved
}

export type Direction = "forward" | "backward";
Expand All @@ -297,6 +300,9 @@ export interface RelativeScopeModifier {
/** Indicates which direction both {@link offset} and {@link length} go
* relative to input target */
direction: Direction;

/** If true spread to individual targets instead of combined range */
spread?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,35 +83,41 @@ export class PrimitiveTargetSpokenFormGenerator {

case "ordinalScope": {
const scope = this.handleScopeType(modifier.scopeType);
const spread = modifier.spread
? this.spokenFormMap.modifierExtra.spread
: [];

if (modifier.length === 1) {
if (modifier.start === -1) {
return [this.spokenFormMap.modifierExtra.last, scope];
return [spread, this.spokenFormMap.modifierExtra.last, scope];
}
if (modifier.start === 0) {
return [this.spokenFormMap.modifierExtra.first, scope];
return [spread, this.spokenFormMap.modifierExtra.first, scope];
}
if (modifier.start < 0) {
return [
spread,
ordinalToSpokenForm(Math.abs(modifier.start)),
this.spokenFormMap.modifierExtra.last,
scope,
];
}
return [ordinalToSpokenForm(modifier.start + 1), scope];
return [spread, ordinalToSpokenForm(modifier.start + 1), scope];
}

const number = numberToSpokenForm(modifier.length);

if (modifier.start === 0) {
return [
spread,
this.spokenFormMap.modifierExtra.first,
number,
pluralize(scope),
];
}
if (modifier.start === -modifier.length) {
return [
spread,
this.spokenFormMap.modifierExtra.last,
number,
pluralize(scope),
Expand Down Expand Up @@ -157,6 +163,9 @@ export class PrimitiveTargetSpokenFormGenerator {
modifier: RelativeScopeModifier,
): SpokenFormComponent {
const scope = this.handleScopeType(modifier.scopeType);
const spread = modifier.spread
? this.spokenFormMap.modifierExtra.spread
: [];

if (modifier.length === 1) {
const direction =
Expand All @@ -165,7 +174,7 @@ export class PrimitiveTargetSpokenFormGenerator {
: connectives.backward;

// token forward/backward
return [scope, direction];
return [spread, scope, direction];
}

const length = numberToSpokenForm(modifier.length);
Expand All @@ -174,11 +183,11 @@ export class PrimitiveTargetSpokenFormGenerator {
// two tokens
// This could also have been "two tokens forward"; there is no way to disambiguate.
if (modifier.direction === "forward") {
return [length, scopePlural];
return [spread, length, scopePlural];
}

// two tokens backward
return [length, scopePlural, connectives.backward];
return [spread, length, scopePlural, connectives.backward];
}

private handleRelativeScopeExclusive(
Expand All @@ -189,25 +198,28 @@ export class PrimitiveTargetSpokenFormGenerator {
modifier.direction === "forward"
? connectives.next
: connectives.previous;
const spread = modifier.spread
? this.spokenFormMap.modifierExtra.spread
: [];

if (modifier.offset === 1) {
const number = numberToSpokenForm(modifier.length);

if (modifier.length === 1) {
// next/previous token
return [direction, scope];
return [spread, direction, scope];
}

const scopePlural = pluralize(scope);

// next/previous two tokens
return [direction, number, scopePlural];
return [spread, direction, number, scopePlural];
}

if (modifier.length === 1) {
const ordinal = ordinalToSpokenForm(modifier.offset);
// second next/previous token
return [ordinal, direction, scope];
return [spread, ordinal, direction, scope];
}

throw new NoSpokenFormError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ModifierStageFactory } from "../ModifierStageFactory";
import { ModifierStage } from "../PipelineStages.types";
import {
createRangeTargetFromIndices,
sliceTargetsByIndices,
getEveryScopeTargets,
} from "./targetSequenceUtils";

Expand All @@ -24,6 +25,10 @@ export class OrdinalScopeStage implements ModifierStage {
this.modifier.start + (this.modifier.start < 0 ? targets.length : 0);
const endIndex = startIndex + this.modifier.length - 1;

if (this.modifier.spread) {
return sliceTargetsByIndices(targets, startIndex, endIndex);
}

return [
createRangeTargetFromIndices(
target.isReversed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import type { RelativeScopeModifier } from "@cursorless/common";
import type { Target } from "../../typings/target.types";
import { ModifierStageFactory } from "../ModifierStageFactory";
import type { ModifierStage } from "../PipelineStages.types";
import { constructScopeRangeTarget } from "./constructScopeRangeTarget";
import {
constructScopeRangeTarget,
constructTargetsFromScopes,
} from "./constructScopeRangeTarget";
import { runLegacy } from "./relativeScopeLegacy";
import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory";
import { TargetScope } from "./scopeHandlers/scope.types";
import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types";
import type {
ContainmentPolicy,
ScopeHandler,
} from "./scopeHandlers/scopeHandler.types";
import { OutOfRangeError } from "./targetSequenceUtils";

/**
Expand All @@ -32,7 +38,26 @@ export class RelativeExclusiveScopeStage implements ModifierStage {
return runLegacy(this.modifierStageFactory, this.modifier, target);
}

const { isReversed, editor, contentRange: inputRange } = target;
const scopes = this.getsScopes(scopeHandler, target);
const { isReversed } = target;

if (this.modifier.spread) {
return constructTargetsFromScopes(isReversed, scopes);
}

// Then make a range when we get the desired number of scopes
return constructScopeRangeTarget(
isReversed,
scopes[0],
scopes[scopes.length - 1],
);
}

private getsScopes(
scopeHandler: ScopeHandler,
target: Target,
): TargetScope[] {
const { editor, contentRange: inputRange } = target;
const { length: desiredScopeCount, direction, offset } = this.modifier;

const initialPosition =
Expand All @@ -45,8 +70,8 @@ export class RelativeExclusiveScopeStage implements ModifierStage {
? "disallowed"
: "disallowedIfStrict";

const scopes: TargetScope[] = [];
let scopeCount = 0;
let proximalScope: TargetScope | undefined;
for (const scope of scopeHandler.generateScopes(
editor,
initialPosition,
Expand All @@ -63,17 +88,20 @@ export class RelativeExclusiveScopeStage implements ModifierStage {
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.getTargets(isReversed);
// Just return it if we only want 1 scope
return [scope];
}

proximalScope = scope;
scopes.push(scope);

continue;
}

if (scopeCount === offset + desiredScopeCount - 1) {
scopes.push(scope);

// Then make a range when we get the desired number of scopes
return constructScopeRangeTarget(isReversed, proximalScope!, scope);
return scopes;
}
}

Expand Down
Loading
Loading