Skip to content

Commit 5ad9ac9

Browse files
authored
Add ScopeSupportChecker (#1652)
- Required by #1653 - Depends on #1651 ## 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 0684b21 commit 5ad9ac9

File tree

4 files changed

+158
-0
lines changed

4 files changed

+158
-0
lines changed

packages/common/src/util/itertools.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,18 @@ export function partition<T, U>(
5050
}
5151
return [first, second];
5252
}
53+
54+
/**
55+
* Returns `true` if the given iterable is empty, `false` otherwise
56+
*
57+
* From https://github.com/sindresorhus/is-empty-iterable/blob/12d3b4f966170d9d85a2067f5326668d5bb910a0/index.js
58+
* @param iterable The iterable to check
59+
* @returns `true` if the iterable is empty, `false` otherwise
60+
*/
61+
export function isEmptyIterable(iterable: Iterable<unknown>): boolean {
62+
for (const _ of iterable) {
63+
return false;
64+
}
65+
66+
return true;
67+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
Position,
3+
ScopeType,
4+
SimpleScopeTypeType,
5+
TextEditor,
6+
isEmptyIterable,
7+
} from "@cursorless/common";
8+
import { LegacyLanguageId } from "../languages/LegacyLanguageId";
9+
import { languageMatchers } from "../languages/getNodeMatcher";
10+
import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory";
11+
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
12+
import { ScopeSupport } from "../api/ScopeProvider";
13+
14+
/**
15+
* Determines the level of support for a given scope type in a given editor.
16+
* This is primarily determined by the language id of the editor, though some
17+
* scopes are supported in all languages.
18+
*/
19+
export class ScopeSupportChecker {
20+
constructor(private scopeHandlerFactory: ScopeHandlerFactory) {
21+
this.getScopeSupport = this.getScopeSupport.bind(this);
22+
this.getIterationScopeSupport = this.getIterationScopeSupport.bind(this);
23+
}
24+
25+
/**
26+
* Determine the level of support for {@link scopeType} in {@link editor}, as
27+
* determined by its language id.
28+
* @param editor The editor to check
29+
* @param scopeType The scope type to check
30+
* @returns The level of support for {@link scopeType} in {@link editor}
31+
*/
32+
getScopeSupport(editor: TextEditor, scopeType: ScopeType): ScopeSupport {
33+
const { languageId } = editor.document;
34+
const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId);
35+
36+
if (scopeHandler == null) {
37+
return getLegacyScopeSupport(languageId, scopeType);
38+
}
39+
40+
return editorContainsScope(editor, scopeHandler)
41+
? ScopeSupport.supportedAndPresentInEditor
42+
: ScopeSupport.supportedButNotPresentInEditor;
43+
}
44+
45+
/**
46+
* Determine the level of support for the iteration scope of {@link scopeType}
47+
* in {@link editor}, as determined by its language id.
48+
* @param editor The editor to check
49+
* @param scopeType The scope type to check
50+
* @returns The level of support for the iteration scope of {@link scopeType}
51+
* in {@link editor}
52+
*/
53+
getIterationScopeSupport(
54+
editor: TextEditor,
55+
scopeType: ScopeType,
56+
): ScopeSupport {
57+
const { languageId } = editor.document;
58+
const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId);
59+
60+
if (scopeHandler == null) {
61+
return getLegacyScopeSupport(languageId, scopeType);
62+
}
63+
64+
const iterationScopeHandler = this.scopeHandlerFactory.create(
65+
scopeHandler.iterationScopeType,
66+
languageId,
67+
);
68+
69+
if (iterationScopeHandler == null) {
70+
return ScopeSupport.unsupported;
71+
}
72+
73+
return editorContainsScope(editor, iterationScopeHandler)
74+
? ScopeSupport.supportedAndPresentInEditor
75+
: ScopeSupport.supportedButNotPresentInEditor;
76+
}
77+
}
78+
79+
function editorContainsScope(
80+
editor: TextEditor,
81+
scopeHandler: ScopeHandler,
82+
): boolean {
83+
return !isEmptyIterable(
84+
scopeHandler.generateScopes(editor, new Position(0, 0), "forward"),
85+
);
86+
}
87+
88+
function getLegacyScopeSupport(
89+
languageId: string,
90+
scopeType: ScopeType,
91+
): ScopeSupport {
92+
switch (scopeType.type) {
93+
case "boundedNonWhitespaceSequence":
94+
case "surroundingPair":
95+
return ScopeSupport.supportedLegacy;
96+
case "notebookCell":
97+
// FIXME: What to do here
98+
return ScopeSupport.unsupported;
99+
default:
100+
if (
101+
languageMatchers[languageId as LegacyLanguageId]?.[
102+
scopeType.type as SimpleScopeTypeType
103+
] != null
104+
) {
105+
return ScopeSupport.supportedLegacy;
106+
}
107+
108+
return ScopeSupport.unsupported;
109+
}
110+
}

packages/cursorless-engine/src/api/ScopeProvider.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ export interface ScopeProvider {
5353
callback: IterationScopeChangeEventCallback,
5454
config: IterationScopeRangeConfig,
5555
) => Disposable;
56+
57+
/**
58+
* Determine the level of support for {@link scopeType} in {@link editor}, as
59+
* determined by its language id.
60+
* @param editor The editor to check
61+
* @param scopeType The scope type to check
62+
* @returns The level of support for {@link scopeType} in {@link editor}
63+
*/
64+
getScopeSupport: (editor: TextEditor, scopeType: ScopeType) => ScopeSupport;
65+
66+
/**
67+
* Determine the level of support for the iteration scope of {@link scopeType}
68+
* in {@link editor}, as determined by its language id.
69+
* @param editor The editor to check
70+
* @param scopeType The scope type to check
71+
* @returns The level of support for the iteration scope of {@link scopeType}
72+
* in {@link editor}
73+
*/
74+
getIterationScopeSupport: (
75+
editor: TextEditor,
76+
scopeType: ScopeType,
77+
) => ScopeSupport;
5678
}
5779

5880
interface ScopeRangeConfigBase {
@@ -131,3 +153,10 @@ export interface IterationScopeRanges {
131153
targets?: TargetRanges[];
132154
}[];
133155
}
156+
157+
export enum ScopeSupport {
158+
supportedAndPresentInEditor,
159+
supportedButNotPresentInEditor,
160+
supportedLegacy,
161+
unsupported,
162+
}

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { StoredTargetMap, TestCaseRecorder, TreeSitter } from ".";
33
import { CursorlessEngine } from "./api/CursorlessEngineApi";
44
import { ScopeProvider } from "./api/ScopeProvider";
55
import { ScopeRangeProvider } from "./ScopeVisualizer/ScopeRangeProvider";
6+
import { ScopeSupportChecker } from "./ScopeVisualizer/ScopeSupportChecker";
67
import { Debug } from "./core/Debug";
78
import { HatTokenMapImpl } from "./core/HatTokenMapImpl";
89
import { Snippets } from "./core/Snippets";
@@ -101,12 +102,15 @@ function createScopeProvider(
101102
);
102103

103104
const rangeWatcher = new ScopeRangeWatcher(rangeProvider);
105+
const supportChecker = new ScopeSupportChecker(scopeHandlerFactory);
104106

105107
return {
106108
provideScopeRanges: rangeProvider.provideScopeRanges,
107109
provideIterationScopeRanges: rangeProvider.provideIterationScopeRanges,
108110
onDidChangeScopeRanges: rangeWatcher.onDidChangeScopeRanges,
109111
onDidChangeIterationScopeRanges:
110112
rangeWatcher.onDidChangeIterationScopeRanges,
113+
getScopeSupport: supportChecker.getScopeSupport,
114+
getIterationScopeSupport: supportChecker.getIterationScopeSupport,
111115
};
112116
}

0 commit comments

Comments
 (0)