Skip to content

Simplify default iteration scopes; file scopeHandler #1075

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
2 changes: 1 addition & 1 deletion src/actions/GenerateSnippet/GenerateSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Offsets } from "../../processTargets/modifiers/surroundingPair/types";
import isTesting from "../../testUtil/isTesting";
import { Target } from "../../typings/target.types";
import { Graph } from "../../typings/Types";
import { getDocumentRange } from "../../util/range";
import { getDocumentRange } from "../../util/rangeUtils";
import { selectionFromRange } from "../../util/selectionUtils";
import { Action, ActionReturnValue } from "../actions.types";
import { constructSnippetBody } from "./constructSnippetBody";
Expand Down
79 changes: 22 additions & 57 deletions src/processTargets/modifiers/EveryScopeStage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Range } from "vscode";
import { NoContainingScopeError } from "../../errors";
import type { Target } from "../../typings/target.types";
import type { EveryScopeModifier } from "../../typings/targetDescriptor.types";
import type { ProcessedTargetsContext } from "../../typings/Types";
import getModifierStage from "../getModifierStage";
import type { ModifierStage } from "../PipelineStages.types";
import getLegacyScopeStage from "./getLegacyScopeStage";
import { getPreferredScope, getRightScope } from "./getPreferredScope";
import getScopeHandler from "./scopeHandlers/getScopeHandler";
import { ScopeHandler } from "./scopeHandlers/scopeHandler.types";

Expand All @@ -19,39 +20,27 @@ import { ScopeHandler } from "./scopeHandlers/scopeHandler.types";
*
* 1. If target has an explicit range, just return all targets returned from
* {@link ScopeHandler.getScopesOverlappingRange}.
* 2. Otherwise, get the iteration scope for the start of the input target.
* 3. If two iteration scopes touch the start position, choose the preferred one
* if input target has empty content range, otherwise prefer the rightmost
* one, as that will have an overlap with the target input content range.
* 3. If the domain of the iteration scope doesn't contain the end of the input
* target, we error, because this situation shouldn't really happen, as
* targets without explicit range tend to be small.
* 4. Return all targets in the iteration scope
* 2. Otherwise, expand to the containing instance of
* {@link ScopeHandler.iterationScopeType}, and then return all targets
* returned from {@link ScopeHandler.getScopesOverlappingRange} when applied
* to the expanded target's {@link Target.contentRange}.
*/
export class EveryScopeStage implements ModifierStage {
constructor(private modifier: EveryScopeModifier) {}

run(context: ProcessedTargetsContext, target: Target): Target[] {
const scopeHandler = getScopeHandler(
this.modifier.scopeType,
target.editor.document.languageId
);
const { scopeType } = this.modifier;
const { editor, isReversed } = target;

const scopeHandler = getScopeHandler(scopeType, editor.document.languageId);

if (scopeHandler == null) {
return getLegacyScopeStage(this.modifier).run(context, target);
}

return target.hasExplicitRange
? this.handleExplicitRangeTarget(scopeHandler, target)
: this.handleNoExplicitRangeTarget(scopeHandler, target);
}

private handleExplicitRangeTarget(
scopeHandler: ScopeHandler,
target: Target
): Target[] {
const { editor, isReversed, contentRange: range } = target;
const { scopeType } = this.modifier;
const range = target.hasExplicitRange
? target.contentRange
: this.getDefaultIterationRange(context, scopeHandler, target);

const scopes = scopeHandler.getScopesOverlappingRange(editor, range);

Expand All @@ -62,41 +51,17 @@ export class EveryScopeStage implements ModifierStage {
return scopes.map((scope) => scope.getTarget(isReversed));
}

private handleNoExplicitRangeTarget(
getDefaultIterationRange(
context: ProcessedTargetsContext,
scopeHandler: ScopeHandler,
target: Target
): Target[] {
const {
editor,
isReversed,
contentRange: { start, end },
} = target;
const { scopeType } = this.modifier;

const startIterationScopes =
scopeHandler.getIterationScopesTouchingPosition(editor, start);

// If target is empty, use the preferred scope; otherwise use the rightmost
// scope, as that one will have non-empty intersection with input target
// content range
const startIterationScope = end.isEqual(start)
? getPreferredScope(startIterationScopes)
: getRightScope(startIterationScopes);

if (startIterationScope == null) {
throw new NoContainingScopeError(scopeType.type);
}

if (!startIterationScope.domain.contains(end)) {
// NB: This shouldn't really happen, because our weak scopes are
// generally no bigger than a token.
throw new Error(
"Canonical iteration scope domain must include entire input range"
);
}
): Range {
const containingIterationScopeModifier = getModifierStage({
type: "containingScope",
scopeType: scopeHandler.iterationScopeType,
});

return startIterationScope
.getScopes()
.map((scope) => scope.getTarget(isReversed));
return containingIterationScopeModifier.run(context, target)[0]
.contentRange;
}
}
3 changes: 0 additions & 3 deletions src/processTargets/modifiers/getLegacyScopeStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import ContainingSyntaxScopeStage, {
SimpleContainingScopeModifier,
SimpleEveryScopeModifier,
} from "./scopeTypeStages/ContainingSyntaxScopeStage";
import DocumentStage from "./scopeTypeStages/DocumentStage";
import NotebookCellStage from "./scopeTypeStages/NotebookCellStage";
import ParagraphStage from "./scopeTypeStages/ParagraphStage";
import {
Expand Down Expand Up @@ -40,8 +39,6 @@ export default function getLegacyScopeStage(
switch (modifier.scopeType.type) {
case "notebookCell":
return new NotebookCellStage(modifier);
case "document":
return new DocumentStage(modifier);
case "paragraph":
return new ParagraphStage(modifier);
case "nonWhitespaceSequence":
Expand Down
18 changes: 10 additions & 8 deletions src/processTargets/modifiers/getPreferredScope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Scope } from "./scopeHandlers/scope.types";
import { TargetScope } from "./scopeHandlers/scope.types";

/**
* Given a list of scopes, returns the preferred scope, or `undefined` if
Expand All @@ -7,7 +7,9 @@ import { Scope } from "./scopeHandlers/scope.types";
* @param scopes A list of scopes to choose from
* @returns A single preferred scope, or `undefined` if {@link scopes} is empty
*/
export function getPreferredScope<T extends Scope>(scopes: T[]): T | undefined {
export function getPreferredScope(
scopes: TargetScope[]
): TargetScope | undefined {
return getRightScope(scopes);
}

Expand All @@ -17,7 +19,7 @@ export function getPreferredScope<T extends Scope>(scopes: T[]): T | undefined {
* @param scopes A list of scopes to choose from
* @returns A single preferred scope, or `undefined` if {@link scopes} is empty
*/
export function getLeftScope<T extends Scope>(scopes: T[]): T | undefined {
export function getLeftScope(scopes: TargetScope[]): TargetScope | undefined {
return getScopeHelper(scopes, (scope1, scope2) =>
scope1.domain.start.isBefore(scope2.domain.start)
);
Expand All @@ -29,16 +31,16 @@ export function getLeftScope<T extends Scope>(scopes: T[]): T | undefined {
* @param scopes A list of scopes to choose from
* @returns A single preferred scope, or `undefined` if {@link scopes} is empty
*/
export function getRightScope<T extends Scope>(scopes: T[]): T | undefined {
export function getRightScope(scopes: TargetScope[]): TargetScope | undefined {
return getScopeHelper(scopes, (scope1, scope2) =>
scope1.domain.start.isAfter(scope2.domain.start)
);
}

function getScopeHelper<T extends Scope>(
scopes: T[],
isScope1Preferred: (scope1: Scope, scope2: Scope) => boolean
): T | undefined {
function getScopeHelper(
scopes: TargetScope[],
isScope1Preferred: (scope1: TargetScope, scope2: TargetScope) => boolean
): TargetScope | undefined {
if (scopes.length === 0) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class CharacterScopeHandler extends NestedScopeHandler {
public readonly scopeType = { type: "character" } as const;
public readonly iterationScopeType = { type: "token" } as const;

protected getScopesInIterationScope({
protected getScopesInSearchScope({
editor,
domain,
}: TargetScope): TargetScope[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Position, Range, TextEditor } from "vscode";
import { Direction, ScopeType } from "../../../typings/targetDescriptor.types";
import { getDocumentRange } from "../../../util/rangeUtils";
import { DocumentTarget } from "../../targets";
import { OutOfRangeError } from "../targetSequenceUtils";
import NotHierarchicalScopeError from "./NotHierarchicalScopeError";
import { TargetScope } from "./scope.types";
import { ScopeHandler } from "./scopeHandler.types";

export default class DocumentScopeHandler implements ScopeHandler {
public readonly scopeType = { type: "document" } as const;
public readonly iterationScopeType = { type: "document" } as const;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that iteration scope of document is document, so we get "every file" for free, if for whatever reason someone wants to do that 🤔


constructor(_scopeType: ScopeType, _languageId: string) {
// Empty
}

getScopesTouchingPosition(
editor: TextEditor,
_position: Position,
ancestorIndex: number = 0
): TargetScope[] {
if (ancestorIndex !== 0) {
throw new NotHierarchicalScopeError(this.scopeType);
}

return [getDocumentScope(editor)];
}

getScopesOverlappingRange(editor: TextEditor, _range: Range): TargetScope[] {
return [getDocumentScope(editor)];
}

getScopeRelativeToPosition(
_editor: TextEditor,
_position: Position,
_offset: number,
_direction: Direction
): TargetScope {
// NB: offset will always be greater than or equal to 1, so this will be an
// error
throw new OutOfRangeError();
}
}

function getDocumentScope(editor: TextEditor): TargetScope {
const contentRange = getDocumentRange(editor.document);

return {
editor,
domain: contentRange,
getTarget: (isReversed) =>
new DocumentTarget({
editor,
isReversed,
contentRange,
}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class IdentifierScopeHandler extends NestedScopeHandler {

private regex: RegExp = getMatcher(this.languageId).identifierMatcher;

protected getScopesInIterationScope({
protected getScopesInSearchScope({
editor,
domain,
}: TargetScope): TargetScope[] {
Expand Down
29 changes: 6 additions & 23 deletions src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { range } from "lodash";
import { Position, Range, TextEditor } from "vscode";
import { Direction } from "../../../typings/targetDescriptor.types";
import { getDocumentRange } from "../../../util/range";
import { Direction, ScopeType } from "../../../typings/targetDescriptor.types";
import { LineTarget } from "../../targets";
import { OutOfRangeError } from "../targetSequenceUtils";
import NotHierarchicalScopeError from "./NotHierarchicalScopeError";
import type { IterationScope, TargetScope } from "./scope.types";
import type { TargetScope } from "./scope.types";
import type { ScopeHandler } from "./scopeHandler.types";

export default class LineScopeHandler implements ScopeHandler {
public readonly scopeType = { type: "line" } as const;
public readonly iterationScopeType = { type: "document" } as const;

constructor(
public readonly scopeType: { type: "line" },
protected languageId: string
) {}
constructor(_scopeType: ScopeType, _languageId: string) {
// Empty
}

getScopesTouchingPosition(
editor: TextEditor,
Expand All @@ -37,22 +36,6 @@ export default class LineScopeHandler implements ScopeHandler {
);
}

getIterationScopesTouchingPosition(
editor: TextEditor,
_position: Position
): IterationScope[] {
return [
{
editor,
domain: getDocumentRange(editor.document),
getScopes: () =>
range(editor.document.lineCount).map((lineNumber) =>
lineNumberToScope(editor, lineNumber)
),
},
];
}

getScopeRelativeToPosition(
editor: TextEditor,
position: Position,
Expand Down
Loading