Skip to content

Commit d967c54

Browse files
authored
Add ScopeRangeProvider (#1650)
- Required by #1653 - Depends on #1647 ## 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
1 parent e96c0c6 commit d967c54

9 files changed

+398
-22
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { TextEditor } from "@cursorless/common";
2+
import {
3+
IterationScopeRangeConfig,
4+
IterationScopeRanges,
5+
ScopeRangeConfig,
6+
ScopeRanges,
7+
} from "..";
8+
import { ModifierStageFactory } from "../processTargets/ModifierStageFactory";
9+
import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory";
10+
import { getIterationRange } from "./getIterationRange";
11+
import { getIterationScopeRanges } from "./getIterationScopeRanges";
12+
import { getScopeRanges } from "./getScopeRanges";
13+
14+
/**
15+
* Provides scope ranges for a given editor to use eg for visualizing scopes
16+
*/
17+
export class ScopeRangeProvider {
18+
constructor(
19+
private scopeHandlerFactory: ScopeHandlerFactory,
20+
private modifierStageFactory: ModifierStageFactory,
21+
) {
22+
this.provideScopeRanges = this.provideScopeRanges.bind(this);
23+
this.provideIterationScopeRanges =
24+
this.provideIterationScopeRanges.bind(this);
25+
}
26+
27+
provideScopeRanges(
28+
editor: TextEditor,
29+
{ scopeType, visibleOnly }: ScopeRangeConfig,
30+
): ScopeRanges[] {
31+
const scopeHandler = this.scopeHandlerFactory.create(
32+
scopeType,
33+
editor.document.languageId,
34+
);
35+
36+
if (scopeHandler == null) {
37+
return [];
38+
}
39+
40+
return getScopeRanges(
41+
editor,
42+
scopeHandler,
43+
getIterationRange(editor, scopeHandler, visibleOnly),
44+
);
45+
}
46+
47+
provideIterationScopeRanges(
48+
editor: TextEditor,
49+
{ scopeType, visibleOnly, includeNestedTargets }: IterationScopeRangeConfig,
50+
): IterationScopeRanges[] {
51+
const { languageId } = editor.document;
52+
const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId);
53+
54+
if (scopeHandler == null) {
55+
return [];
56+
}
57+
58+
const iterationScopeHandler = this.scopeHandlerFactory.create(
59+
scopeHandler.iterationScopeType,
60+
languageId,
61+
);
62+
63+
if (iterationScopeHandler == null) {
64+
return [];
65+
}
66+
67+
return getIterationScopeRanges(
68+
editor,
69+
iterationScopeHandler,
70+
this.modifierStageFactory.create({
71+
type: "everyScope",
72+
scopeType,
73+
}),
74+
getIterationRange(editor, scopeHandler, visibleOnly),
75+
includeNestedTargets,
76+
);
77+
}
78+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Range, TextEditor } from "@cursorless/common";
2+
import { last } from "lodash";
3+
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
4+
5+
/**
6+
* Get the range to iterate over for the given editor.
7+
*
8+
* - If {@link visibleOnly} is `false`, just return the full document range.
9+
* - Otherwise, we
10+
* 1. take the union of all visible ranges, then
11+
* 2. add 10 lines either side to make scrolling a bit smoother, and then
12+
* 3. expand to the largest ancestor of the start and end of the visible
13+
* range, so that we properly show nesting.
14+
* @param editor The editor to get the iteration range for
15+
* @param scopeHandler The scope handler to use
16+
* @param visibleOnly Whether to only iterate over visible ranges
17+
* @returns The range to iterate over
18+
*/
19+
export function getIterationRange(
20+
editor: TextEditor,
21+
scopeHandler: ScopeHandler,
22+
visibleOnly: boolean,
23+
): Range {
24+
if (!visibleOnly) {
25+
return editor.document.range;
26+
}
27+
28+
let visibleRange = editor.visibleRanges.reduce((acc, range) =>
29+
acc.union(range),
30+
);
31+
32+
visibleRange = editor.document.range.intersection(
33+
visibleRange.with(
34+
visibleRange.start.translate(-10),
35+
visibleRange.end.translate(10),
36+
),
37+
)!;
38+
39+
// Expand to largest ancestor of start of visible range FIXME: It's
40+
// possible that the removal range will be bigger than the domain range,
41+
// in which case we'll miss a scope if its removal range is visible but
42+
// its domain range is not. I don't think we care that much; they can
43+
// scroll, and we have the extra 10 lines on either side which might help.
44+
const expandedStart =
45+
last(
46+
Array.from(
47+
scopeHandler.generateScopes(editor, visibleRange.start, "forward", {
48+
containment: "required",
49+
}),
50+
),
51+
)?.domain ?? visibleRange;
52+
53+
// Expand to largest ancestor of end of visible range
54+
const expandedEnd =
55+
last(
56+
Array.from(
57+
scopeHandler.generateScopes(editor, visibleRange.end, "forward", {
58+
containment: "required",
59+
}),
60+
),
61+
)?.domain ?? visibleRange;
62+
63+
return expandedStart.union(expandedEnd);
64+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Range, TextEditor } from "@cursorless/common";
2+
import { map } from "itertools";
3+
import { IterationScopeRanges } from "..";
4+
import { ModifierStage } from "../processTargets/PipelineStages.types";
5+
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
6+
import { Target } from "../typings/target.types";
7+
import { getTargetRanges } from "./getTargetRanges";
8+
9+
/**
10+
* Returns a list of teration scope ranges of type {@link iterationScopeHandler}
11+
* within {@link iterationRange} in {@link editor}.
12+
* @param editor The editor to check
13+
* @param iterationScopeHandler The scope handler to use
14+
* @param everyStage An every stage for use in determining nested targets
15+
* @param iterationRange The range to iterate over
16+
* @param includeIterationNestedTargets Whether to include nested targets in the
17+
* iteration scope ranges
18+
* @returns A list of iteration scope ranges for the given editor
19+
*/
20+
export function getIterationScopeRanges(
21+
editor: TextEditor,
22+
iterationScopeHandler: ScopeHandler,
23+
everyStage: ModifierStage,
24+
iterationRange: Range,
25+
includeIterationNestedTargets: boolean,
26+
): IterationScopeRanges[] {
27+
return map(
28+
iterationScopeHandler.generateScopes(
29+
editor,
30+
iterationRange.start,
31+
"forward",
32+
{
33+
includeDescendantScopes: true,
34+
distalPosition: iterationRange.end,
35+
},
36+
),
37+
(scope) => {
38+
return {
39+
domain: scope.domain,
40+
ranges: scope.getTargets(false).map((target) => ({
41+
range: target.contentRange,
42+
targets: includeIterationNestedTargets
43+
? getEveryScopeLenient(everyStage, target).map(getTargetRanges)
44+
: undefined,
45+
})),
46+
};
47+
},
48+
);
49+
}
50+
51+
function getEveryScopeLenient(everyStage: ModifierStage, target: Target) {
52+
try {
53+
return everyStage.run(target);
54+
} catch (err) {
55+
if ((err as Error).name === "NoContainingScopeError") {
56+
return [];
57+
}
58+
59+
throw err;
60+
}
61+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Range, TextEditor } from "@cursorless/common";
2+
import { map } from "itertools";
3+
import { ScopeRanges } from "..";
4+
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
5+
import { getTargetRanges } from "./getTargetRanges";
6+
7+
/**
8+
* Returns a list of scope ranges of type {@link scopeHandler} within
9+
* {@link iterationRange} in {@link editor}.
10+
* @param editor The editor to check
11+
* @param scopeHandler The scope handler to use
12+
* @param iterationRange The range to iterate over
13+
* @returns A list of scope ranges for the given editor
14+
*/
15+
export function getScopeRanges(
16+
editor: TextEditor,
17+
scopeHandler: ScopeHandler,
18+
iterationRange: Range,
19+
): ScopeRanges[] {
20+
return map(
21+
scopeHandler.generateScopes(editor, iterationRange.start, "forward", {
22+
includeDescendantScopes: true,
23+
distalPosition: iterationRange.end,
24+
}),
25+
(scope) => ({
26+
domain: scope.domain,
27+
targets: scope.getTargets(false).map(getTargetRanges),
28+
}),
29+
);
30+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { toCharacterRange, toLineRange } from "@cursorless/common";
2+
import { Target } from "../typings/target.types";
3+
import { TargetRanges } from "../api/ScopeProvider";
4+
5+
export function getTargetRanges(target: Target): TargetRanges {
6+
return {
7+
contentRange: target.contentRange,
8+
removalHighlightRange: target.isLine
9+
? toLineRange(target.getRemovalHighlightRange())
10+
: toCharacterRange(target.getRemovalHighlightRange()),
11+
};
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Command, HatTokenMap, IDE } from "@cursorless/common";
2+
import { Snippets } from "../core/Snippets";
3+
import { StoredTargetMap } from "../core/StoredTargets";
4+
import { TestCaseRecorder } from "../testCaseRecorder/TestCaseRecorder";
5+
import { ScopeProvider } from "./ScopeProvider";
6+
7+
export interface CursorlessEngine {
8+
commandApi: CommandApi;
9+
scopeProvider: ScopeProvider;
10+
testCaseRecorder: TestCaseRecorder;
11+
storedTargets: StoredTargetMap;
12+
hatTokenMap: HatTokenMap;
13+
snippets: Snippets;
14+
injectIde: (ide: IDE | undefined) => void;
15+
runIntegrationTests: () => Promise<void>;
16+
}
17+
18+
export interface CommandApi {
19+
/**
20+
* Runs a command. This is the core of the Cursorless engine.
21+
* @param command The command to run
22+
*/
23+
runCommand(command: Command): Promise<unknown>;
24+
25+
/**
26+
* Designed to run commands that come directly from the user. Ensures that
27+
* the command args are of the correct shape.
28+
*/
29+
runCommandSafe(...args: unknown[]): Promise<unknown>;
30+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
GeneralizedRange,
3+
Range,
4+
ScopeType,
5+
TextEditor,
6+
} from "@cursorless/common";
7+
8+
export interface ScopeProvider {
9+
/**
10+
* Get the scope ranges for the given editor.
11+
* @param editor The editor
12+
* @param config The configuration for the scope ranges
13+
* @returns A list of scope ranges for the given editor
14+
*/
15+
provideScopeRanges: (
16+
editor: TextEditor,
17+
config: ScopeRangeConfig,
18+
) => ScopeRanges[];
19+
/**
20+
* Get the iteration scope ranges for the given editor.
21+
* @param editor The editor
22+
* @param config The configuration for the scope ranges
23+
* @returns A list of scope ranges for the given editor
24+
*/
25+
provideIterationScopeRanges: (
26+
editor: TextEditor,
27+
config: IterationScopeRangeConfig,
28+
) => IterationScopeRanges[];
29+
}
30+
31+
interface ScopeRangeConfigBase {
32+
/**
33+
* Whether to only include visible scopes
34+
*/
35+
visibleOnly: boolean;
36+
37+
/**
38+
* The scope type to use
39+
*/
40+
scopeType: ScopeType;
41+
}
42+
43+
export type ScopeRangeConfig = ScopeRangeConfigBase;
44+
45+
export interface IterationScopeRangeConfig extends ScopeRangeConfigBase {
46+
/**
47+
* Whether to include nested targets in each iteration scope range
48+
*/
49+
includeNestedTargets: boolean;
50+
}
51+
52+
/**
53+
* Contains the ranges that define a given scope, eg its {@link domain} and the
54+
* ranges for its {@link targets}.
55+
*/
56+
export interface ScopeRanges {
57+
domain: Range;
58+
targets: TargetRanges[];
59+
}
60+
61+
/**
62+
* Contains the ranges that define a given target, eg its {@link contentRange}
63+
* and the ranges for its {@link removalHighlightRange}.
64+
*/
65+
export interface TargetRanges {
66+
contentRange: Range;
67+
removalHighlightRange: GeneralizedRange;
68+
}
69+
70+
/**
71+
* Contains the ranges that define a given iteration scope, eg its
72+
* {@link domain}.
73+
*/
74+
export interface IterationScopeRanges {
75+
domain: Range;
76+
77+
/**
78+
* A list of ranges within within which iteration will happen. There is
79+
* almost always a single range here. There will be more than one if the
80+
* iteration scope handler returns a scope whose `getTargets` method returns
81+
* multiple targets. As of this writing, no scope handler returns multiple
82+
* targets.
83+
*/
84+
ranges: {
85+
/**
86+
* The range within which iteration will happen, ie the content range for
87+
* the target returned by the iteration scope handler.
88+
*/
89+
range: Range;
90+
91+
/**
92+
* The defining ranges for all targets within this iteration range.
93+
*/
94+
targets?: TargetRanges[];
95+
}[];
96+
}

0 commit comments

Comments
 (0)