Skip to content

Commit

Permalink
Support multiple targets per scope (#1509)
Browse files Browse the repository at this point in the history
- Depends on #1506

## Checklist

- [ ] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [ ] I have updated the
[docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and
[cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet)
- [ ] I have not broken the cheatsheet
  • Loading branch information
pokey authored Jun 6, 2023
1 parent 135b5b5 commit 0b7756e
Show file tree
Hide file tree
Showing 21 changed files with 123 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ export class ContainingScopeStage implements ModifierStage {
throw new NoContainingScopeError(this.modifier.scopeType.type);
}

return [containingScope];
return containingScope;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,29 +73,27 @@ 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 = getScopesOverlappingRange(
scopes = this.getDefaultIterationRange(
scopeHandler,
editor,
this.getDefaultIterationRange(
scopeHandler,
this.scopeHandlerFactory,
target,
),
this.scopeHandlerFactory,
target,
).flatMap((iterationRange) =>
getScopesOverlappingRange(scopeHandler, editor, iterationRange),
);
}

if (scopes.length === 0) {
throw new NoContainingScopeError(scopeType.type);
}

return scopes.map((scope) => scope.getTarget(isReversed));
return scopes.flatMap((scope) => scope.getTargets(isReversed));
}

getDefaultIterationRange(
scopeHandler: ScopeHandler,
scopeHandlerFactory: ScopeHandlerFactory,
target: Target,
): Range {
): Range[] {
const iterationScopeHandler = scopeHandlerFactory.create(
scopeHandler.iterationScopeType,
target.editor.document.languageId,
Expand All @@ -116,7 +114,7 @@ export class EveryScopeStage implements ModifierStage {
);
}

return iterationScopeTarget.contentRange;
return iterationScopeTarget.map((target) => target.contentRange);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default class RelativeExclusiveScopeStage implements ModifierStage {
// When we hit offset, that becomes proximal scope
if (desiredScopeCount === 1) {
// Just yield it if we only want 1 scope
return [scope.getTarget(isReversed)];
return scope.getTargets(isReversed);
}

proximalScope = scope;
Expand All @@ -73,7 +73,7 @@ export default class RelativeExclusiveScopeStage implements ModifierStage {

if (scopeCount === offset + desiredScopeCount - 1) {
// Then make a range when we get the desired number of scopes
return [constructScopeRangeTarget(isReversed, proximalScope!, scope)];
return constructScopeRangeTarget(isReversed, proximalScope!, scope);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,10 @@ export class RelativeInclusiveScopeStage implements ModifierStage {
throw new OutOfRangeError();
}

return [
constructScopeRangeTarget(
isReversed,
scopes[0],
scopes[scopes.length - 1],
),
];
return constructScopeRangeTarget(
isReversed,
scopes[0],
scopes[scopes.length - 1],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ export function constructScopeRangeTarget(
isReversed: boolean,
scope1: TargetScope,
scope2: TargetScope,
): Target {
): Target[] {
if (scope1 === scope2) {
return scope1.getTarget(isReversed);
return scope1.getTargets(isReversed);
}

const target1 = scope1.getTarget(isReversed);
const target2 = scope2.getTarget(isReversed);
const targets1 = scope1.getTargets(isReversed);
const targets2 = scope2.getTargets(isReversed);

if (targets1.length !== 1 || targets2.length !== 1) {
throw Error("Scope range targets must be single-target");
}

const [target1] = targets1;
const [target2] = targets2;

const isScope2After = target2.contentRange.start.isAfterOrEqual(
target1.contentRange.start,
Expand All @@ -33,10 +40,7 @@ export function constructScopeRangeTarget(
? [target1, target2]
: [target2, target1];

return startTarget.createContinuousRangeTarget(
isReversed,
endTarget,
true,
true,
);
return [
startTarget.createContinuousRangeTarget(isReversed, endTarget, true, true),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function getContainingScopeTarget(
target: Target,
scopeHandler: ScopeHandler,
ancestorIndex: number = 0,
): Target | undefined {
): Target[] | undefined {
const {
isReversed,
editor,
Expand Down Expand Up @@ -46,7 +46,7 @@ export function getContainingScopeTarget(
return undefined;
}

return scope.getTarget(isReversed);
return scope.getTargets(isReversed);
}

const startScope = expandFromPosition(
Expand All @@ -62,7 +62,7 @@ export function getContainingScopeTarget(
}

if (startScope.domain.contains(end)) {
return startScope.getTarget(isReversed);
return startScope.getTargets(isReversed);
}

const endScope = expandFromPosition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ suite("BaseScopeHandler", () => {
const inputScopes = testCase.scopes.map((scope) => ({
editor,
domain: toRange(scope.start, scope.end),
getTarget: () => undefined as any,
getTargets: () => undefined as any,
}));

assert.deepStrictEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export default class CharacterScopeHandler extends NestedScopeHandler {
(range) => ({
editor,
domain: range,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new PlainTarget({
editor,
contentRange: range,
isReversed,
isToken: false,
}),
],
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export default class DocumentScopeHandler extends BaseScopeHandler {
yield {
editor,
domain: contentRange,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new DocumentTarget({
editor,
isReversed,
contentRange,
}),
],
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ export default class IdentifierScopeHandler extends NestedScopeHandler {
(range) => ({
editor,
domain: range,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new TokenTarget({
editor,
contentRange: range,
isReversed,
}),
],
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function lineNumberToScope(
return {
editor,
domain: range,
getTarget: (isReversed) => createLineTarget(editor, isReversed, range),
getTargets: (isReversed) => [createLineTarget(editor, isReversed, range)],
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ function createScope(editor: TextEditor, domain: Range): TargetScope {
return {
editor,
domain,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new ParagraphTarget({
editor,
isReversed,
contentRange: fitRangeToLineContent(editor, domain),
}),
],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ export default class TokenScopeHandler extends NestedScopeHandler {
(range) => ({
editor,
domain: range,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new TokenTarget({
editor,
contentRange: range,
isReversed,
}),
],
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TextDocument,
TextEditor,
} from "@cursorless/common";
import { uniqWith } from "lodash";
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
import { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
import BaseScopeHandler from "../BaseScopeHandler";
Expand All @@ -13,6 +14,7 @@ import {
ContainmentPolicy,
ScopeIteratorRequirements,
} from "../scopeHandler.types";
import { mergeAdjacentBy } from "./mergeAdjacentBy";

/** Base scope handler to use for both tree-sitter scopes and their iteration scopes */
export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
Expand All @@ -36,11 +38,33 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
hints,
);

yield* this.query
const scopes = this.query
.matches(document, start, end)
.map((match) => this.matchToScope(editor, match))
.filter((scope): scope is TargetScope => scope != null)
.sort((a, b) => compareTargetScopes(direction, position, a, b));

// Merge scopes that have the same domain into a single scope with multiple
// targets
yield* mergeAdjacentBy(
scopes,
(a, b) => a.domain.isRangeEqual(b.domain),
(equivalentScopes) => {
if (equivalentScopes.length === 1) {
return equivalentScopes[0];
}

return {
...equivalentScopes[0],
getTargets(isReversed: boolean) {
return uniqWith(
equivalentScopes.flatMap((scope) => scope.getTargets(isReversed)),
(a, b) => a.isEqual(b),
);
},
};
},
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler
return {
editor,
domain,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new PlainTarget({
editor,
isReversed,
contentRange,
}),
],
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
return {
editor,
domain,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new ScopeTypeTarget({
scopeTypeType,
editor,
Expand All @@ -77,6 +77,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
interiorRange,
// FIXME: Add delimiter text
}),
],
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ export class TreeSitterTextFragmentScopeHandler extends BaseTreeSitterScopeHandl
return {
editor,
domain: contentRange,
getTarget: (isReversed) =>
getTargets: (isReversed) => [
new PlainTarget({
editor,
isReversed,
contentRange,
}),
],
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Merges adjacent elements of a list using a predicate and a merge function.
* Adjacent elements are merged if the predicate returns true for them.
* @param input The input list to merge adjacent elements of
* @param isEqual A function that returns true if two elements should be merged
* @param merge A function that merges multiple elements
* @returns A new list with adjacent elements merged
*/
export function mergeAdjacentBy<T>(
input: T[],
isEqual: (a: T, b: T) => boolean,
merge: (a: T[]) => T,
): T[] {
const result: T[] = [];
let current: T[] = [];

for (const elem of input) {
if (current.length === 0 || isEqual(current[current.length - 1], elem)) {
current.push(elem);
} else {
result.push(merge(current));
current = [elem];
}
}

if (current.length > 0) {
result.push(merge(current));
}

return result;
}
Loading

0 comments on commit 0b7756e

Please sign in to comment.