Skip to content

Commit baf7d7a

Browse files
committed
Scope support tree view
1 parent c03c303 commit baf7d7a

File tree

7 files changed

+383
-73
lines changed

7 files changed

+383
-73
lines changed

packages/common/src/types/command/PartialTargetDescriptor.types.ts

Lines changed: 86 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -75,88 +75,101 @@ export type PartialMark =
7575
| RangeMark
7676
| ExplicitMark;
7777

78+
export const simpleSurroundingPairNames = [
79+
"angleBrackets",
80+
"backtickQuotes",
81+
"curlyBrackets",
82+
"doubleQuotes",
83+
"escapedDoubleQuomes",
84+
"escapedParentheses",
85+
"escapedSquareBrackets",
86+
"escapedSingleQuotes",
87+
"parentheses",
88+
"singleQuotes",
89+
"squareBrackets",
90+
] as const;
91+
export const complexSurroundingPairNames = [
92+
"string",
93+
"any",
94+
"collectionBoundary",
95+
] as const;
96+
export const surroundingPairNames = [
97+
...simpleSurroundingPairNames,
98+
...complexSurroundingPairNames,
99+
];
78100
export type SimpleSurroundingPairName =
79-
| "angleBrackets"
80-
| "backtickQuotes"
81-
| "curlyBrackets"
82-
| "doubleQuotes"
83-
| "escapedDoubleQuotes"
84-
| "escapedParentheses"
85-
| "escapedSquareBrackets"
86-
| "escapedSingleQuotes"
87-
| "parentheses"
88-
| "singleQuotes"
89-
| "squareBrackets";
101+
(typeof simpleSurroundingPairNames)[number];
90102
export type ComplexSurroundingPairName =
91-
| "string"
92-
| "any"
93-
| "collectionBoundary";
103+
(typeof complexSurroundingPairNames)[number];
94104
export type SurroundingPairName =
95105
| SimpleSurroundingPairName
96106
| ComplexSurroundingPairName;
97107

98-
export type SimpleScopeTypeType =
99-
| "argumentOrParameter"
100-
| "anonymousFunction"
101-
| "attribute"
102-
| "branch"
103-
| "class"
104-
| "className"
105-
| "collectionItem"
106-
| "collectionKey"
107-
| "comment"
108-
| "functionCall"
109-
| "functionCallee"
110-
| "functionName"
111-
| "ifStatement"
112-
| "instance"
113-
| "list"
114-
| "map"
115-
| "name"
116-
| "namedFunction"
117-
| "regularExpression"
118-
| "statement"
119-
| "string"
120-
| "type"
121-
| "value"
122-
| "condition"
123-
| "section"
124-
| "sectionLevelOne"
125-
| "sectionLevelTwo"
126-
| "sectionLevelThree"
127-
| "sectionLevelFour"
128-
| "sectionLevelFive"
129-
| "sectionLevelSix"
130-
| "selector"
131-
| "switchStatementSubject"
132-
| "unit"
133-
| "xmlBothTags"
134-
| "xmlElement"
135-
| "xmlEndTag"
136-
| "xmlStartTag"
137-
| "notebookCell"
108+
export const simpleScopeTypeTypes = [
109+
"argumentOrParameter",
110+
"anonymousFunction",
111+
"attribute",
112+
"branch",
113+
"class",
114+
"className",
115+
"collectionItem",
116+
"collectionKey",
117+
"comment",
118+
"functionCall",
119+
"functionCallee",
120+
"functionName",
121+
"ifStatement",
122+
"instance",
123+
"list",
124+
"map",
125+
"name",
126+
"namedFunction",
127+
"regularExpression",
128+
"statement",
129+
"string",
130+
"type",
131+
"value",
132+
"condition",
133+
"section",
134+
"sectionLevelOne",
135+
"sectionLevelTwo",
136+
"sectionLevelThree",
137+
"sectionLevelFour",
138+
"sectionLevelFive",
139+
"sectionLevelSix",
140+
"selector",
141+
"switchStatementSubject",
142+
"unit",
143+
"xmlBothTags",
144+
"xmlElement",
145+
"xmlEndTag",
146+
"xmlStartTag",
138147
// Latex scope types
139-
| "part"
140-
| "chapter"
141-
| "subSection"
142-
| "subSubSection"
143-
| "namedParagraph"
144-
| "subParagraph"
145-
| "environment"
148+
"part",
149+
"chapter",
150+
"subSection",
151+
"subSubSection",
152+
"namedParagraph",
153+
"subParagraph",
154+
"environment",
146155
// Text based scopes
147-
| "character"
148-
| "word"
149-
| "token"
150-
| "identifier"
151-
| "line"
152-
| "sentence"
153-
| "paragraph"
154-
| "document"
155-
| "nonWhitespaceSequence"
156-
| "boundedNonWhitespaceSequence"
157-
| "url"
156+
"character",
157+
"word",
158+
"token",
159+
"identifier",
160+
"line",
161+
"sentence",
162+
"paragraph",
163+
"document",
164+
"nonWhitespaceSequence",
165+
"boundedNonWhitespaceSequence",
166+
"url",
167+
"notebookCell",
158168
// Talon
159-
| "command";
169+
"command",
170+
] as const;
171+
172+
export type SimpleScopeTypeType = (typeof simpleScopeTypeTypes)[number];
160173

161174
export interface SimpleScopeType {
162175
type: SimpleScopeTypeType;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
Disposable,
3+
ScopeType,
4+
SurroundingPairScopeType,
5+
simpleScopeTypeTypes,
6+
surroundingPairNames,
7+
} from "@cursorless/common";
8+
import { pull } from "lodash";
9+
import { ScopeSupport, ScopeSupportEventCallback } from "..";
10+
import { Debouncer } from "../core/Debouncer";
11+
import { ide } from "../singletons/ide.singleton";
12+
import { ScopeSupportChecker } from "./ScopeSupportChecker";
13+
14+
/**
15+
* Watches for changes to the scope support of the active editor and notifies
16+
* listeners when it changes. Watches support for all scopes at the same time.
17+
*/
18+
export class ScopeSupportWatcher {
19+
private disposables: Disposable[] = [];
20+
private debouncer = new Debouncer(() => this.onChange());
21+
private listeners: ScopeSupportEventCallback[] = [];
22+
23+
constructor(private scopeSupportChecker: ScopeSupportChecker) {
24+
this.disposables.push(
25+
// An event that fires when a text document opens
26+
ide().onDidOpenTextDocument(this.debouncer.run),
27+
// An Event that fires when a text document closes
28+
ide().onDidCloseTextDocument(this.debouncer.run),
29+
// An Event which fires when the active editor has changed. Note that the event also fires when the active editor changes to undefined.
30+
ide().onDidChangeActiveTextEditor(this.debouncer.run),
31+
// An event that is emitted when a text document is changed. This usually
32+
// happens when the contents changes but also when other things like the
33+
// dirty-state changes.
34+
ide().onDidChangeTextDocument(this.debouncer.run),
35+
this.debouncer,
36+
);
37+
38+
this.onDidChangeScopeSupport = this.onDidChangeScopeSupport.bind(this);
39+
}
40+
41+
/**
42+
* Registers a callback to be run when the scope ranges change for any visible
43+
* editor. The callback will be run immediately once for each visible editor
44+
* with the current scope ranges.
45+
* @param callback The callback to run when the scope ranges change
46+
* @param config The configuration for the scope ranges
47+
* @returns A {@link Disposable} which will stop the callback from running
48+
*/
49+
onDidChangeScopeSupport(callback: ScopeSupportEventCallback): Disposable {
50+
callback(this.getSupportLevels());
51+
52+
this.listeners.push(callback);
53+
54+
return {
55+
dispose: () => {
56+
pull(this.listeners, callback);
57+
},
58+
};
59+
}
60+
61+
private onChange() {
62+
if (this.listeners.length === 0) {
63+
// Don't bother if no one is listening
64+
return;
65+
}
66+
67+
const supportLevels = this.getSupportLevels();
68+
69+
this.listeners.forEach((listener) => listener(supportLevels));
70+
}
71+
72+
private getSupportLevels() {
73+
console.log("getSupportLevels");
74+
const activeTextEditor = ide().activeTextEditor;
75+
76+
const getScopeTypeSupport =
77+
activeTextEditor == null
78+
? () => ScopeSupport.unsupported
79+
: (scopeType: ScopeType) =>
80+
this.scopeSupportChecker.getScopeSupport(
81+
activeTextEditor,
82+
scopeType,
83+
);
84+
85+
const scopeTypes: ScopeType[] = [
86+
...simpleScopeTypeTypes
87+
// Ignore instance pseudo-scope for now
88+
.filter((scopeTypeType) => scopeTypeType !== "instance")
89+
.map((scopeTypeType) => ({
90+
type: scopeTypeType,
91+
})),
92+
93+
...surroundingPairNames.map(
94+
(surroundingPairName): SurroundingPairScopeType => ({
95+
type: "surroundingPair",
96+
delimiter: surroundingPairName,
97+
}),
98+
),
99+
100+
// TODO: Add custom regex scope types
101+
];
102+
103+
return scopeTypes.map((scopeType) => ({
104+
scopeType,
105+
support: getScopeTypeSupport(scopeType),
106+
}));
107+
}
108+
109+
dispose(): void {
110+
this.disposables.forEach(({ dispose }) => {
111+
try {
112+
dispose();
113+
} catch (e) {
114+
// do nothing; some of the VSCode disposables misbehave, and we don't
115+
// want that to prevent us from disposing the rest of the disposables
116+
}
117+
});
118+
}
119+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ export interface ScopeProvider {
7575
editor: TextEditor,
7676
scopeType: ScopeType,
7777
) => ScopeSupport;
78+
79+
/**
80+
* Registers a callback to be run when the scope support changes for the active
81+
* editor. The callback will be run immediately once with the current support
82+
* levels for the active editor.
83+
* @param callback The callback to run when the scope support changes
84+
* @returns A {@link Disposable} which will stop the callback from running
85+
*/
86+
onDidChangeScopeSupport: (callback: ScopeSupportEventCallback) => Disposable;
7887
}
7988

8089
interface ScopeRangeConfigBase {
@@ -108,6 +117,15 @@ export type IterationScopeChangeEventCallback = (
108117
scopeRanges: IterationScopeRanges[],
109118
) => void;
110119

120+
export type ScopeSupportLevels = {
121+
scopeType: ScopeType;
122+
support: ScopeSupport;
123+
}[];
124+
125+
export type ScopeSupportEventCallback = (
126+
supportLevels: ScopeSupportLevels,
127+
) => void;
128+
111129
/**
112130
* Contains the ranges that define a given scope, eg its {@link domain} and the
113131
* ranges for its {@link targets}.

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { runCommand } from "./runCommand";
2222
import { runIntegrationTests } from "./runIntegrationTests";
2323
import { injectIde } from "./singletons/ide.singleton";
2424
import { ScopeRangeWatcher } from "./ScopeVisualizer/ScopeRangeWatcher";
25+
import { ScopeSupportWatcher } from "./ScopeVisualizer/ScopeSupportWatcher";
2526

2627
export function createCursorlessEngine(
2728
treeSitter: TreeSitter,
@@ -114,6 +115,7 @@ function createScopeProvider(
114115
rangeProvider,
115116
);
116117
const supportChecker = new ScopeSupportChecker(scopeHandlerFactory);
118+
const supportWatcher = new ScopeSupportWatcher(supportChecker);
117119

118120
return {
119121
provideScopeRanges: rangeProvider.provideScopeRanges,
@@ -123,5 +125,6 @@ function createScopeProvider(
123125
rangeWatcher.onDidChangeIterationScopeRanges,
124126
getScopeSupport: supportChecker.getScopeSupport,
125127
getIterationScopeSupport: supportChecker.getIterationScopeSupport,
128+
onDidChangeScopeSupport: supportWatcher.onDidChangeScopeSupport,
126129
};
127130
}

packages/cursorless-vscode/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
],
4747
"activationEvents": [
4848
"onLanguage",
49+
"onView:cursorlessScopeSupport",
4950
"onCommand:cursorless.command",
5051
"onCommand:cursorless.internal.updateCheatsheetDefaults",
5152
"onCommand:cursorless.keyboard.escape",
@@ -77,6 +78,14 @@
7778
}
7879
},
7980
"contributes": {
81+
"views": {
82+
"explorer": [
83+
{
84+
"id": "cursorlessScopeSupport",
85+
"name": "Cursorless scope support"
86+
}
87+
]
88+
},
8089
"commands": [
8190
{
8291
"command": "cursorless.toggleDecorations",

0 commit comments

Comments
 (0)