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

Contiguous scope #2101

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fdcd03e
Started working on contiguous scope modifier
AndreasArvidsson Dec 5, 2023
f223d84
Merge branch 'main' into contiguous_scope
AndreasArvidsson Dec 5, 2023
b549ca5
more work
AndreasArvidsson Dec 5, 2023
a71f017
Change continuous scope modifier into continuous scope type
AndreasArvidsson Dec 6, 2023
da8d6ec
Added tests
AndreasArvidsson Dec 6, 2023
a3fa351
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 6, 2023
53351e8
Moved tests
AndreasArvidsson Dec 6, 2023
d370690
Added comment
AndreasArvidsson Dec 6, 2023
48f97ee
cleanup
AndreasArvidsson Dec 6, 2023
e2ec5c8
more cleanup
AndreasArvidsson Dec 6, 2023
d1020a8
Update packages/cursorless-engine/src/processTargets/modifiers/scopeH…
AndreasArvidsson Dec 6, 2023
02a295e
Import
AndreasArvidsson Dec 6, 2023
9d75ead
Update packages/cursorless-engine/src/processTargets/modifiers/scopeH…
AndreasArvidsson Dec 6, 2023
48c7a65
Update packages/cursorless-engine/src/processTargets/modifiers/scopeH…
AndreasArvidsson Dec 6, 2023
fa57ab2
Update packages/cursorless-engine/src/processTargets/modifiers/scopeH…
AndreasArvidsson Dec 6, 2023
c943542
Update packages/cursorless-engine/src/processTargets/modifiers/scopeH…
AndreasArvidsson Dec 6, 2023
c26d704
Update packages/cursorless-engine/src/processTargets/modifiers/scopeH…
AndreasArvidsson Dec 6, 2023
e0b6b5b
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 6, 2023
2988adb
rename
AndreasArvidsson Dec 6, 2023
c2f942b
Merge
AndreasArvidsson Dec 6, 2023
34cbbb1
update
AndreasArvidsson Dec 6, 2023
8b5a95f
Use proximal and distal
AndreasArvidsson Dec 7, 2023
bd86a64
Continues target
AndreasArvidsson Dec 7, 2023
5a905ea
Added tests
AndreasArvidsson Dec 7, 2023
6612818
cleanup
AndreasArvidsson Dec 7, 2023
2852e37
Merge branch 'main' into contiguous_scope
AndreasArvidsson Dec 7, 2023
d3eb687
clean up
AndreasArvidsson Dec 8, 2023
fe7e8e5
Merge branch 'contiguous_scope' of github.com:cursorless-dev/cursorle…
AndreasArvidsson Dec 8, 2023
2f15dc6
Only do contiguous for comments
AndreasArvidsson Dec 14, 2023
0ccdcc6
cleanup
AndreasArvidsson Dec 14, 2023
fd93230
Merge branch 'main' into contiguous_scope
AndreasArvidsson Dec 14, 2023
d898f26
cleanup
AndreasArvidsson Dec 14, 2023
ea5a542
Update test
AndreasArvidsson Dec 14, 2023
66731e4
Added predicate
AndreasArvidsson Dec 15, 2023
58bfb9a
Merge branch 'main' into contiguous_scope
AndreasArvidsson Dec 15, 2023
b6ac60f
Added comment facets
AndreasArvidsson Dec 15, 2023
17b0507
Remove pattern argument from contiguous predicate
AndreasArvidsson Feb 26, 2024
6f0de5c
Merge branch 'main' into contiguous_scope
AndreasArvidsson Feb 26, 2024
9f156a6
Update scope fixtures
AndreasArvidsson Feb 26, 2024
38ab206
Merge branch 'main' into contiguous_scope
AndreasArvidsson Jun 14, 2024
883eb1e
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jun 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions cursorless-talon/src/modifiers/scopes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from talon import Module

mod = Module()
Expand All @@ -12,28 +14,45 @@
"cursorless_custom_regex_scope_type_plural",
desc="Supported plural custom regular expression scope types",
)
mod.list(
"cursorless_contiguous_scope_type",
desc="Cursorless contiguous scope type",
)


@mod.capture(
rule="{user.cursorless_scope_type} | {user.cursorless_custom_regex_scope_type}"
rule="[{user.cursorless_contiguous_scope_type}] ({user.cursorless_scope_type} | {user.cursorless_custom_regex_scope_type})"
)
def cursorless_scope_type(m) -> dict[str, str]:
def cursorless_scope_type(m) -> dict[str, Any]:
"""Cursorless scope type singular"""
try:
return {"type": m.cursorless_scope_type}
scope_type = {"type": m.cursorless_scope_type}
except AttributeError:
return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type}
scope_type = {
"type": "customRegex",
"regex": m.cursorless_custom_regex_scope_type,
}

try:
return {"type": m.cursorless_contiguous_scope_type, "scopeType": scope_type}
except AttributeError:
return scope_type


@mod.capture(
rule="{user.cursorless_scope_type_plural} | {user.cursorless_custom_regex_scope_type_plural}"
rule="[{user.cursorless_contiguous_scope_type}] ({user.cursorless_scope_type_plural} | {user.cursorless_custom_regex_scope_type_plural})"
)
def cursorless_scope_type_plural(m) -> dict[str, str]:
def cursorless_scope_type_plural(m) -> dict[str, Any]:
"""Cursorless scope type plural"""
try:
return {"type": m.cursorless_scope_type_plural}
scope_type = {"type": m.cursorless_scope_type_plural}
except AttributeError:
return {
scope_type = {
"type": "customRegex",
"regex": m.cursorless_custom_regex_scope_type_plural,
}

try:
return {"type": m.cursorless_contiguous_scope_type, "scopeType": scope_type}
except AttributeError:
return scope_type
1 change: 1 addition & 0 deletions cursorless-talon/src/spoken_forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"visible": "visible"
},
"simple_scope_modifier": { "every": "every" },
"contiguous_scope_type": { "fat": "contiguous" },
"interior_modifier": {
"inside": "interiorOnly"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,17 @@ export interface OneOfScopeType {
scopeTypes: ScopeType[];
}

export interface ContiguousScopeType {
type: "contiguous";
scopeType: ScopeType;
}

export type ScopeType =
| SimpleScopeType
| SurroundingPairScopeType
| CustomRegexScopeType
| OneOfScopeType;
| OneOfScopeType
| ContiguousScopeType;

export interface ContainingSurroundingPairModifier
extends ContainingScopeModifier {
Expand Down
14 changes: 14 additions & 0 deletions packages/common/src/util/itertools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,17 @@ export function isEmptyIterable(iterable: Iterable<unknown>): boolean {

return true;
}

/**
* Returns the first element of the given iterable, or `undefined` if the
* iterable is empty
* @param iterable The iterable to get the first element of
* @returns The first element of the iterable, or `undefined` if the iterable
* is empty
*/
export function next<T>(generator: Iterable<T>): T | undefined {
for (const value of generator) {
return value;
}
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ export class PrimitiveTargetSpokenFormGenerator {
switch (scopeType.type) {
case "oneOf":
throw new NoSpokenFormError(`Scope type '${scopeType.type}'`);

case "contiguous": {
const scope = this.handleScopeType(scopeType.scopeType);
return [this.spokenFormMap.modifierExtra.contiguousScope, scope];
}

case "surroundingPair": {
const pair = this.spokenFormMap.pairedDelimiter[scopeType.delimiter];
if (scopeType.forceDirection != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
ContiguousScopeType,
Direction,
Position,
Range,
ScopeType,
TextEditor,
next,
} from "@cursorless/common";
import { ScopeHandlerFactory } from ".";
import { Target } from "../../../typings/target.types";
import { BaseScopeHandler } from "./BaseScopeHandler";
import type { TargetScope } from "./scope.types";
import { CustomScopeType, ScopeHandler } from "./scopeHandler.types";

export class ContiguousScopeHandler extends BaseScopeHandler {
protected readonly isHierarchical = false;
private readonly scopeHandler: ScopeHandler;

constructor(
private scopeHandlerFactory: ScopeHandlerFactory,
public scopeType: ContiguousScopeType,
languageId: string,
) {
super();
const handler = scopeHandlerFactory.create(scopeType.scopeType, languageId);
if (handler == null) {
throw new Error(
`No available scope handler for '${scopeType.scopeType.type}'`,
);
}
this.scopeHandler = handler;
}

get iterationScopeType(): ScopeType | CustomScopeType {
return this.scopeHandler.iterationScopeType;
}

generateScopeCandidates(
editor: TextEditor,
position: Position,
direction: Direction,
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
): Iterable<TargetScope> {
return direction === "backward"
? this.generateScopeCandidatesBackward(editor, position)
: this.generateScopeCandidatesForward(editor, position);
}

*generateScopeCandidatesBackward(
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
editor: TextEditor,
position: Position,
): Iterable<TargetScope> {
let targetsForward = next(
getTargetsInDirection(this.scopeHandler, editor, position, "forward"),
);
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved

const targetsBackward = getTargetsInDirection(
this.scopeHandler,
editor,
position,
"backward",
);
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved

for (const targets of targetsBackward) {
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
if (targetsForward != null && isAdjacent(targets[1], targetsForward[0])) {
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
yield targetsToScope(targets[0], targetsForward[1]);
targetsForward = undefined;
} else {
yield targetsToScope(...targets);
}
}
}

*generateScopeCandidatesForward(
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
editor: TextEditor,
position: Position,
): Iterable<TargetScope> {
let targetsBackward = next(
getTargetsInDirection(this.scopeHandler, editor, position, "backward"),
);

const targetsForward = getTargetsInDirection(
this.scopeHandler,
editor,
position,
"forward",
);

for (const targets of targetsForward) {
if (
targetsBackward != null &&
isAdjacent(targetsBackward[1], targets[0])
) {
yield targetsToScope(targetsBackward[0], targets[1]);
targetsBackward = undefined;
} else {
yield targetsToScope(...targets);
}
}
}
}

function targetsToScope(
leadingTarget: Target,
trailingTarget: Target,
): TargetScope {
if (leadingTarget.contentRange.isRangeEqual(trailingTarget.contentRange)) {
return {
editor: leadingTarget.editor,
domain: leadingTarget.contentRange,
getTargets: () => [leadingTarget],
};
}
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved

const range = leadingTarget.contentRange.union(trailingTarget.contentRange);
return {
editor: leadingTarget.editor,
domain: range,
getTargets: () => [leadingTarget.withContentRange(range)],
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
};
}

function* getTargetsInDirection(
scopeHandler: ScopeHandler,
editor: TextEditor,
position: Position,
direction: Direction,
): Iterable<[Target, Target]> {
const isForward = direction === "forward";
let first, last: Target | undefined;
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved

const generator = scopeHandler.generateScopes(editor, position, direction, {
allowAdjacentScopes: true,
skipAncestorScopes: true,
});

for (const scope of generator) {
for (const target of scope.getTargets(false)) {
if (first == null) {
first = target;
}

if (last != null) {
const [leadingTarget, trailingTarget] = isForward
? [last, target]
: [target, last];
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved

if (!isAdjacent(leadingTarget, trailingTarget)) {
yield isForward ? [first, last] : [last, first];
first = target;
}
}

last = target;
}
}

if (first != null && last != null) {
yield isForward ? [first, last] : [last, first];
}
}

function isAdjacent(leadingTarget: Target, trailingTarget: Target): boolean {
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
if (
leadingTarget.contentRange.intersection(trailingTarget.contentRange) != null
) {
return true;
}

const leadingRange =
leadingTarget.getTrailingDelimiterTarget()?.contentRange ??
leadingTarget.contentRange;
const trailingRange =
trailingTarget.getLeadingDelimiterTarget()?.contentRange ??
trailingTarget.contentRange;

if (leadingRange.intersection(trailingRange) != null) {
return true;
}

if (
!leadingTarget.isLine &&
trailingRange.start.line - leadingRange.end.line > 1
) {
return false;
}

const rangeBetween = new Range(leadingRange.end, trailingRange.start);
const text = leadingTarget.editor.document.getText(rangeBetween);
return /^\s*$/.test(text);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type { ScopeType } from "@cursorless/common";
import { LanguageDefinitions } from "../../../languages/LanguageDefinitions";
import { CharacterScopeHandler } from "./CharacterScopeHandler";
import { CustomRegexScopeHandler } from "./RegexScopeHandler";
import { ContiguousScopeHandler } from "./ContiguousScopeHandler";
import { DocumentScopeHandler } from "./DocumentScopeHandler";
import { IdentifierScopeHandler } from "./IdentifierScopeHandler";
import { LineScopeHandler } from "./LineScopeHandler";
import { NonWhitespaceSequenceScopeHandler } from "./RegexScopeHandler";
import { OneOfScopeHandler } from "./OneOfScopeHandler";
import { ParagraphScopeHandler } from "./ParagraphScopeHandler";
import {
CustomRegexScopeHandler,
NonWhitespaceSequenceScopeHandler,
UrlScopeHandler,
} from "./RegexScopeHandler";
import { ScopeHandlerFactory } from "./ScopeHandlerFactory";
import { SentenceScopeHandler } from "./SentenceScopeHandler/SentenceScopeHandler";
import { TokenScopeHandler } from "./TokenScopeHandler";
import { UrlScopeHandler } from "./RegexScopeHandler";
import { WordScopeHandler } from "./WordScopeHandler/WordScopeHandler";
import { LanguageDefinitions } from "../../../languages/LanguageDefinitions";
import type { CustomScopeType, ScopeHandler } from "./scopeHandler.types";

/**
Expand Down Expand Up @@ -72,6 +75,8 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory {
return new CustomRegexScopeHandler(this, scopeType, languageId);
case "custom":
return scopeType.scopeHandler;
case "contiguous":
return new ContiguousScopeHandler(this, scopeType, languageId);
case "instance":
// Handle instance pseudoscope with its own special modifier
throw Error("Unexpected scope type 'instance'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ function isLanguageSpecific(scopeType: ScopeType): boolean {
return false;

case "oneOf":
case "contiguous":
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
throw Error(
`Can't decide whether scope type ${JSON.stringify(
scopeType,
Expand Down
3 changes: 2 additions & 1 deletion packages/cursorless-engine/src/spokenForms/SpokenFormType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type ModifierExtra =
| "previous"
| "next"
| "forward"
| "backward";
| "backward"
| "contiguousScope";

export type SpokenFormType = keyof SpokenFormMapKeyTypes;
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = {
next: "next",
forward: "forward",
backward: "backward",
contiguousScope: "fat",
AndreasArvidsson marked this conversation as resolved.
Show resolved Hide resolved
},

customRegex: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
languageId: plaintext
command:
version: 6
spokenForm: change fat block
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: containingScope
scopeType:
type: contiguous
scopeType: {type: paragraph}
usePrePhraseSnapshot: true
initialState:
documentContents: |-
aaa
aaa

bbb
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: ""
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
Loading