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 every/spread ordinal/relative modifier. Turns relative and ordinal range modifiers into multiple target selections instead of contiguous range.

- `"take every two tokens"` selects two tokens as separate selections
- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors)
27 changes: 22 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,27 @@ 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_every_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":
is_every = hasattr(m, "cursorless_every_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,
is_every,
)
return create_ordinal_scope_modifier(
m.cursorless_scope_type_plural,
-m.private_cursorless_number_small,
m.private_cursorless_number_small,
is_every,
)


Expand All @@ -65,10 +74,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,
is_every: bool = False,
):
res = {
"type": "ordinalScope",
"scopeType": scope_type,
"start": start,
"length": length,
}
if is_every:
res["isEvery"] = True
return res
19 changes: 15 additions & 4 deletions cursorless-talon/src/modifiers/relative_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,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_every_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 +40,12 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]:
1,
m.private_cursorless_number_small,
m.cursorless_relative_direction,
hasattr(m, "cursorless_every_scope_modifier"),
)


@mod.capture(
rule="<user.private_cursorless_number_small> <user.cursorless_scope_type_plural> [{user.cursorless_forward_backward_modifier}]"
rule="[{user.cursorless_every_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 +54,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_every_scope_modifier"),
)


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


Expand All @@ -82,12 +86,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,
is_every: bool,
) -> dict[str, Any]:
return {
res = {
"type": "relativeScope",
"scopeType": scope_type,
"offset": offset,
"length": length,
"direction": direction,
}
if is_every:
res["isEvery"] = True
return res
40 changes: 22 additions & 18 deletions cursorless-talon/src/modifiers/simple_scope_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,35 @@
mod = Module()

mod.list(
"cursorless_simple_scope_modifier",
desc='Cursorless simple scope modifiers, eg "every"',
"cursorless_every_scope_modifier",
desc="Cursorless every scope modifiers",
)
mod.list(
"cursorless_ancestor_scope_modifier",
desc="Cursorless ancestor scope modifiers",
)


@mod.capture(
rule="[{user.cursorless_simple_scope_modifier}] <user.cursorless_scope_type>"
rule=(
"[{user.cursorless_every_scope_modifier} | {user.cursorless_ancestor_scope_modifier}] "
"<user.cursorless_scope_type>"
),
)
def cursorless_simple_scope_modifier(m) -> dict[str, Any]:
"""Containing scope, every scope, etc"""
if hasattr(m, "cursorless_simple_scope_modifier"):
modifier = m.cursorless_simple_scope_modifier

if modifier == "every":
return {
"type": "everyScope",
"scopeType": m.cursorless_scope_type,
}

if modifier == "ancestor":
return {
"type": "containingScope",
"scopeType": m.cursorless_scope_type,
"ancestorIndex": 1,
}
if hasattr(m, "cursorless_every_scope_modifier"):
return {
"type": "everyScope",
"scopeType": m.cursorless_scope_type,
}

if hasattr(m, "cursorless_ancestor_scope_modifier"):
return {
"type": "containingScope",
"scopeType": m.cursorless_scope_type,
"ancestorIndex": 1,
}

return {
"type": "containingScope",
Expand Down
3 changes: 2 additions & 1 deletion cursorless-talon/src/spoken_forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
"its": "inferPreviousMark",
"visible": "visible"
},
"simple_scope_modifier": { "every": "every", "grand": "ancestor" },
"every_scope_modifier": { "every": "every" },
"ancestor_scope_modifier": { "grand": "ancestor" },
"interior_modifier": {
"inside": "interiorOnly"
},
Expand Down
9 changes: 9 additions & 0 deletions docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ 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"` |

You can prefix the modifier with `"every"` to yield multiple targets rather than a range. For example, `"take every two tokens"` selects two tokens as separate selections.

##### `"every"`

The modifier `"every"` can be used to select a syntactic element and all of its matching siblings.
Expand All @@ -215,6 +217,13 @@ The modifier `"every"` can be used to select a syntactic element and all of its

For example, the command `"take every key [blue] air"` will select every key in the map/object/dict including the token with a blue hat over the letter 'a'.

###### Use with relative / ordinal modifiers

The modifier `every` can also be used to cause [relative / ordinal modifiers](#previous--next--ordinal--number) to yield multiple targets rather than a range:

- `"take every two tokens"` selects two tokens as separate selections
- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors)

##### `"grand"`

The modifier `"grand"` can be used to select the parent of the containing syntactic element.
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, yields individual targets instead of contiguous range. Defaults to `false` */
isEvery?: boolean;
}

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 use individual targets instead of combined range */
isEvery?: 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 isEvery = modifier.isEvery
? this.spokenFormMap.simpleModifier.everyScope
: [];

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

const number = numberToSpokenForm(modifier.length);

if (modifier.start === 0) {
return [
isEvery,
this.spokenFormMap.modifierExtra.first,
number,
pluralize(scope),
];
}
if (modifier.start === -modifier.length) {
return [
isEvery,
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 isEvery = modifier.isEvery
? this.spokenFormMap.simpleModifier.everyScope
: [];

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

// token forward/backward
return [scope, direction];
return [isEvery, 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 [isEvery, length, scopePlural];
}

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

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

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

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

const scopePlural = pluralize(scope);

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

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

throw new NoSpokenFormError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { ModifierStageFactory } from "../ModifierStageFactory";
import type { ModifierStage } from "../PipelineStages.types";
import { PlainTarget } from "../targets";
import { ContainingTokenIfUntypedEmptyStage } from "./ConditionalModifierStages";
import { OutOfRangeError } from "./targetSequenceUtils";
import { StoredTargetMap } from "../..";
import { OutOfRangeError } from "./listUtils";

export class InstanceStage implements ModifierStage {
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createRangeTargetFromIndices,
getEveryScopeTargets,
} from "./targetSequenceUtils";
import { sliceStrict } from "./listUtils";

export class OrdinalScopeStage implements ModifierStage {
constructor(
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.isEvery) {
return sliceStrict(targets, startIndex, endIndex);
}

return [
createRangeTargetFromIndices(
target.isReversed,
Expand Down
Loading
Loading