Skip to content

Commit

Permalink
Support multiple targets per scope
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed May 28, 2023
1 parent 81fbb1a commit da0805c
Show file tree
Hide file tree
Showing 21 changed files with 112 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 @@ -13,6 +13,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 +37,26 @@ 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),
(a, b) => {
return {
...a,
getTargets(isReversed: boolean) {
return [...a.getTargets(isReversed), ...b.getTargets(isReversed)];
},
};
},
);
}

/**
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,28 @@
/**
* 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 two elements
* @returns A new list with adjacent elements merged
*/
export function mergeAdjacentBy<T>(
input: T[],
isEqual: (a: T, b: T) => boolean,
merge: (a: T, b: T) => T,
): T[] {
const output: T[] = [];
for (const item of input) {
if (output.length === 0) {
output.push(item);
} else {
const last = output[output.length - 1];
if (isEqual(last, item)) {
output[output.length - 1] = merge(last, item);
} else {
output.push(item);
}
}
}
return output;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,20 @@ export default class WordScopeHandler extends NestedScopeHandler {
return contentRanges.map((range, i) => ({
editor,
domain: range,
getTarget: (isReversed) => {
getTargets: (isReversed) => {
const previousContentRange = i > 0 ? contentRanges[i - 1] : null;
const nextContentRange =
i + 1 < contentRanges.length ? contentRanges[i + 1] : null;

return constructTarget(
isReversed,
editor,
previousContentRange,
range,
nextContentRange,
);
return [
constructTarget(
isReversed,
editor,
previousContentRange,
range,
nextContentRange,
),
];
},
}));
}
Expand Down
Loading

0 comments on commit da0805c

Please sign in to comment.