Skip to content

One of compound scope type #1069

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

Merged
merged 7 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
133 changes: 133 additions & 0 deletions src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { maxBy, minBy } from "lodash";
import { Position, Range, TextEditor } from "vscode";
import { getScopeHandler } from ".";
import {
Direction,
OneOfScopeType,
} from "../../../typings/targetDescriptor.types";
import { OutOfRangeError } from "../targetSequenceUtils";
import type { TargetScope } from "./scope.types";
import { ScopeHandler } from "./scopeHandler.types";

export default class OneOfScopeHandler implements ScopeHandler {
private scopeHandlers: ScopeHandler[] = this.scopeType.scopeTypes.map(
(scopeType) => {
const handler = getScopeHandler(scopeType, this.languageId);
if (handler == null) {
throw new Error(`No available scope handler for '${scopeType.type}'`);
}
return handler;
},
);

public iterationScopeType: OneOfScopeType = {
type: "oneOf",
scopeTypes: this.scopeHandlers.map(
({ iterationScopeType }) => iterationScopeType,
),
};

constructor(
public readonly scopeType: OneOfScopeType,
private languageId: string,
) {}

getScopesTouchingPosition(
editor: TextEditor,
position: Position,
ancestorIndex?: number,
): TargetScope[] {
if (ancestorIndex !== 0) {
// FIXME: We could support this one, but it will be a bit of work.
throw new Error("`grand` not yet supported for compound scopes.");
}

return keepOnlyBottomLevelScopes(
this.scopeHandlers.flatMap((scopeHandler) =>
scopeHandler.getScopesTouchingPosition(editor, position, ancestorIndex),
),
);
}

/**
* We proceed as follows:
*
* 1. Get all scopes returned by
* {@link ScopeHandler.getScopesOverlappingRange} from each of
* {@link scopeHandlers}.
* 2. If any of these scopes has a {@link TargetScope.domain|domain} that
* terminates within {@link range}, return all such maximal scopes.
* 3. Otherwise, return a list containing just the minimal scope containing
* {@link range}.
*/
getScopesOverlappingRange(editor: TextEditor, range: Range): TargetScope[] {
const candidateScopes = this.scopeHandlers.flatMap((scopeHandler) =>
scopeHandler.getScopesOverlappingRange(editor, range),
);

const scopesTerminatingInRange = candidateScopes.filter(
({ domain }) => !domain.contains(range),
);

return scopesTerminatingInRange.length > 0
? keepOnlyTopLevelScopes(scopesTerminatingInRange)
: keepOnlyBottomLevelScopes(candidateScopes);
}

getScopeRelativeToPosition(
editor: TextEditor,
position: Position,
offset: number,
direction: Direction,
): TargetScope {
let currentPosition = position;
let currentScope: TargetScope;

if (this.scopeHandlers.length === 0) {
throw new OutOfRangeError();
}

for (let i = 0; i < offset; i++) {
const candidateScopes = this.scopeHandlers.map((scopeHandler) =>
scopeHandler.getScopeRelativeToPosition(
editor,
currentPosition,
1,
direction,
),
);

currentScope =
direction === "forward"
? minBy(candidateScopes, ({ domain: start }) => start)!
: maxBy(candidateScopes, ({ domain: end }) => end)!;

currentPosition =
direction === "forward"
? currentScope.domain.end
: currentScope.domain.start;
}

return currentScope!;
}
}

function keepOnlyTopLevelScopes(candidateScopes: TargetScope[]): TargetScope[] {
return candidateScopes.filter(
({ domain }) =>
!candidateScopes.some(({ domain: otherDomain }) =>
otherDomain.contains(domain),
),
);
}

function keepOnlyBottomLevelScopes(
candidateScopes: TargetScope[],
): TargetScope[] {
return candidateScopes.filter(
({ domain }) =>
!candidateScopes.some(({ domain: otherDomain }) =>
domain.contains(otherDomain),
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
LineScopeHandler,
TokenScopeHandler,
WordScopeHandler,
OneOfScopeHandler,
} from ".";
import type { ScopeType } from "../../../typings/targetDescriptor.types";
import type { ScopeHandler } from "./scopeHandler.types";
Expand Down Expand Up @@ -43,6 +44,8 @@ export default function getScopeHandler(
return new LineScopeHandler(scopeType, languageId);
case "document":
return new DocumentScopeHandler(scopeType, languageId);
case "oneOf":
return new OneOfScopeHandler(scopeType, languageId);
default:
return undefined;
}
Expand Down
2 changes: 2 additions & 0 deletions src/processTargets/modifiers/scopeHandlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ export * from "./TokenScopeHandler";
export { default as TokenScopeHandler } from "./TokenScopeHandler";
export * from "./DocumentScopeHandler";
export { default as DocumentScopeHandler } from "./DocumentScopeHandler";
export * from "./OneOfScopeHandler";
export { default as OneOfScopeHandler } from "./OneOfScopeHandler";
export * from "./getScopeHandler";
export { default as getScopeHandler } from "./getScopeHandler";
8 changes: 7 additions & 1 deletion src/typings/targetDescriptor.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,16 @@ export interface SurroundingPairScopeType {
requireStrongContainment?: boolean;
}

export interface OneOfScopeType {
type: "oneOf";
scopeTypes: ScopeType[];
}

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

export interface ContainingSurroundingPairModifier
extends ContainingScopeModifier {
Expand Down