Skip to content

Add ScopeRangeProvider #1650

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 2 commits into from
Jul 18, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { TextEditor } from "@cursorless/common";
import {
IterationScopeRangeConfig,
IterationScopeRanges,
ScopeRangeConfig,
ScopeRanges,
} from "..";
import { ModifierStageFactory } from "../processTargets/ModifierStageFactory";
import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory";
import { getIterationRange } from "./getIterationRange";
import { getIterationScopeRanges } from "./getIterationScopeRanges";
import { getScopeRanges } from "./getScopeRanges";

/**
* Provides scope ranges for a given editor to use eg for visualizing scopes
*/
export class ScopeRangeProvider {
constructor(
private scopeHandlerFactory: ScopeHandlerFactory,
private modifierStageFactory: ModifierStageFactory,
) {
this.provideScopeRanges = this.provideScopeRanges.bind(this);
this.provideIterationScopeRanges =
this.provideIterationScopeRanges.bind(this);
}

provideScopeRanges(
editor: TextEditor,
{ scopeType, visibleOnly }: ScopeRangeConfig,
): ScopeRanges[] {
const scopeHandler = this.scopeHandlerFactory.create(
scopeType,
editor.document.languageId,
);

if (scopeHandler == null) {
return [];
}

return getScopeRanges(
editor,
scopeHandler,
getIterationRange(editor, scopeHandler, visibleOnly),
);
}

provideIterationScopeRanges(
editor: TextEditor,
{ scopeType, visibleOnly, includeNestedTargets }: IterationScopeRangeConfig,
): IterationScopeRanges[] {
const { languageId } = editor.document;
const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId);

if (scopeHandler == null) {
return [];
}

const iterationScopeHandler = this.scopeHandlerFactory.create(
scopeHandler.iterationScopeType,
languageId,
);

if (iterationScopeHandler == null) {
return [];
}

return getIterationScopeRanges(
editor,
iterationScopeHandler,
this.modifierStageFactory.create({
type: "everyScope",
scopeType,
}),
getIterationRange(editor, scopeHandler, visibleOnly),
includeNestedTargets,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Range, TextEditor } from "@cursorless/common";
import { last } from "lodash";
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";

/**
* Get the range to iterate over for the given editor.
*
* - If {@link visibleOnly} is `false`, just return the full document range.
* - Otherwise, we
* 1. take the union of all visible ranges, then
* 2. add 10 lines either side to make scrolling a bit smoother, and then
* 3. expand to the largest ancestor of the start and end of the visible
* range, so that we properly show nesting.
* @param editor The editor to get the iteration range for
* @param scopeHandler The scope handler to use
* @param visibleOnly Whether to only iterate over visible ranges
* @returns The range to iterate over
*/
export function getIterationRange(
editor: TextEditor,
scopeHandler: ScopeHandler,
visibleOnly: boolean,
): Range {
if (!visibleOnly) {
return editor.document.range;
}

let visibleRange = editor.visibleRanges.reduce((acc, range) =>
acc.union(range),
);

visibleRange = editor.document.range.intersection(
visibleRange.with(
visibleRange.start.translate(-10),
visibleRange.end.translate(10),
),
)!;

// Expand to largest ancestor of start of visible range FIXME: It's
// possible that the removal range will be bigger than the domain range,
// in which case we'll miss a scope if its removal range is visible but
// its domain range is not. I don't think we care that much; they can
// scroll, and we have the extra 10 lines on either side which might help.
const expandedStart =
last(
Array.from(
scopeHandler.generateScopes(editor, visibleRange.start, "forward", {
containment: "required",
}),
),
)?.domain ?? visibleRange;

// Expand to largest ancestor of end of visible range
const expandedEnd =
last(
Array.from(
scopeHandler.generateScopes(editor, visibleRange.end, "forward", {
containment: "required",
}),
),
)?.domain ?? visibleRange;

return expandedStart.union(expandedEnd);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Range, TextEditor } from "@cursorless/common";
import { map } from "itertools";
import { IterationScopeRanges } from "..";
import { ModifierStage } from "../processTargets/PipelineStages.types";
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
import { Target } from "../typings/target.types";
import { getTargetRanges } from "./getTargetRanges";

/**
* Returns a list of teration scope ranges of type {@link iterationScopeHandler}
* within {@link iterationRange} in {@link editor}.
* @param editor The editor to check
* @param iterationScopeHandler The scope handler to use
* @param everyStage An every stage for use in determining nested targets
* @param iterationRange The range to iterate over
* @param includeIterationNestedTargets Whether to include nested targets in the
* iteration scope ranges
* @returns A list of iteration scope ranges for the given editor
*/
export function getIterationScopeRanges(
editor: TextEditor,
iterationScopeHandler: ScopeHandler,
everyStage: ModifierStage,
iterationRange: Range,
includeIterationNestedTargets: boolean,
): IterationScopeRanges[] {
return map(
iterationScopeHandler.generateScopes(
editor,
iterationRange.start,
"forward",
{
includeDescendantScopes: true,
distalPosition: iterationRange.end,
},
),
(scope) => {
return {
domain: scope.domain,
ranges: scope.getTargets(false).map((target) => ({
range: target.contentRange,
targets: includeIterationNestedTargets
? getEveryScopeLenient(everyStage, target).map(getTargetRanges)
: undefined,
})),
};
},
);
}

function getEveryScopeLenient(everyStage: ModifierStage, target: Target) {
try {
return everyStage.run(target);
} catch (err) {
if ((err as Error).name === "NoContainingScopeError") {
return [];
}

throw err;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Range, TextEditor } from "@cursorless/common";
import { map } from "itertools";
import { ScopeRanges } from "..";
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
import { getTargetRanges } from "./getTargetRanges";

/**
* Returns a list of scope ranges of type {@link scopeHandler} within
* {@link iterationRange} in {@link editor}.
* @param editor The editor to check
* @param scopeHandler The scope handler to use
* @param iterationRange The range to iterate over
* @returns A list of scope ranges for the given editor
*/
export function getScopeRanges(
editor: TextEditor,
scopeHandler: ScopeHandler,
iterationRange: Range,
): ScopeRanges[] {
return map(
scopeHandler.generateScopes(editor, iterationRange.start, "forward", {
includeDescendantScopes: true,
distalPosition: iterationRange.end,
}),
(scope) => ({
domain: scope.domain,
targets: scope.getTargets(false).map(getTargetRanges),
}),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { toCharacterRange, toLineRange } from "@cursorless/common";
import { Target } from "../typings/target.types";
import { TargetRanges } from "../api/ScopeProvider";

export function getTargetRanges(target: Target): TargetRanges {
return {
contentRange: target.contentRange,
removalHighlightRange: target.isLine
? toLineRange(target.getRemovalHighlightRange())
: toCharacterRange(target.getRemovalHighlightRange()),
};
}
30 changes: 30 additions & 0 deletions packages/cursorless-engine/src/api/CursorlessEngineApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Command, HatTokenMap, IDE } from "@cursorless/common";
import { Snippets } from "../core/Snippets";
import { StoredTargetMap } from "../core/StoredTargets";
import { TestCaseRecorder } from "../testCaseRecorder/TestCaseRecorder";
import { ScopeProvider } from "./ScopeProvider";

export interface CursorlessEngine {
commandApi: CommandApi;
scopeProvider: ScopeProvider;
testCaseRecorder: TestCaseRecorder;
storedTargets: StoredTargetMap;
hatTokenMap: HatTokenMap;
snippets: Snippets;
injectIde: (ide: IDE | undefined) => void;
runIntegrationTests: () => Promise<void>;
}

export interface CommandApi {
/**
* Runs a command. This is the core of the Cursorless engine.
* @param command The command to run
*/
runCommand(command: Command): Promise<unknown>;

/**
* Designed to run commands that come directly from the user. Ensures that
* the command args are of the correct shape.
*/
runCommandSafe(...args: unknown[]): Promise<unknown>;
}
96 changes: 96 additions & 0 deletions packages/cursorless-engine/src/api/ScopeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
GeneralizedRange,
Range,
ScopeType,
TextEditor,
} from "@cursorless/common";

export interface ScopeProvider {
/**
* Get the scope ranges for the given editor.
* @param editor The editor
* @param config The configuration for the scope ranges
* @returns A list of scope ranges for the given editor
*/
provideScopeRanges: (
editor: TextEditor,
config: ScopeRangeConfig,
) => ScopeRanges[];
/**
* Get the iteration scope ranges for the given editor.
* @param editor The editor
* @param config The configuration for the scope ranges
* @returns A list of scope ranges for the given editor
*/
provideIterationScopeRanges: (
editor: TextEditor,
config: IterationScopeRangeConfig,
) => IterationScopeRanges[];
}

interface ScopeRangeConfigBase {
/**
* Whether to only include visible scopes
*/
visibleOnly: boolean;

/**
* The scope type to use
*/
scopeType: ScopeType;
}

export type ScopeRangeConfig = ScopeRangeConfigBase;

export interface IterationScopeRangeConfig extends ScopeRangeConfigBase {
/**
* Whether to include nested targets in each iteration scope range
*/
includeNestedTargets: boolean;
}

/**
* Contains the ranges that define a given scope, eg its {@link domain} and the
* ranges for its {@link targets}.
*/
export interface ScopeRanges {
domain: Range;
targets: TargetRanges[];
}

/**
* Contains the ranges that define a given target, eg its {@link contentRange}
* and the ranges for its {@link removalHighlightRange}.
*/
export interface TargetRanges {
contentRange: Range;
removalHighlightRange: GeneralizedRange;
}

/**
* Contains the ranges that define a given iteration scope, eg its
* {@link domain}.
*/
export interface IterationScopeRanges {
domain: Range;

/**
* A list of ranges within within which iteration will happen. There is
* almost always a single range here. There will be more than one if the
* iteration scope handler returns a scope whose `getTargets` method returns
* multiple targets. As of this writing, no scope handler returns multiple
* targets.
*/
ranges: {
/**
* The range within which iteration will happen, ie the content range for
* the target returned by the iteration scope handler.
*/
range: Range;

/**
* The defining ranges for all targets within this iteration range.
*/
targets?: TargetRanges[];
}[];
}
Loading