Skip to content

Commit 1973dba

Browse files
committed
Scope support tree view
1 parent 019cbf2 commit 1973dba

File tree

7 files changed

+347
-58
lines changed

7 files changed

+347
-58
lines changed

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

+61-58
Original file line numberDiff line numberDiff line change
@@ -95,66 +95,69 @@ export type SurroundingPairName =
9595
| SimpleSurroundingPairName
9696
| ComplexSurroundingPairName;
9797

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"
98+
export const simpleScopeTypeTypes = [
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",
137137
// Latex scope types
138-
| "part"
139-
| "chapter"
140-
| "subSection"
141-
| "subSubSection"
142-
| "namedParagraph"
143-
| "subParagraph"
144-
| "environment"
138+
"part",
139+
"chapter",
140+
"subSection",
141+
"subSubSection",
142+
"namedParagraph",
143+
"subParagraph",
144+
"environment",
145145
// Text based scopes
146-
| "character"
147-
| "word"
148-
| "token"
149-
| "identifier"
150-
| "line"
151-
| "sentence"
152-
| "paragraph"
153-
| "document"
154-
| "nonWhitespaceSequence"
155-
| "boundedNonWhitespaceSequence"
156-
| "url"
157-
| "notebookCell";
146+
"character",
147+
"word",
148+
"token",
149+
"identifier",
150+
"line",
151+
"sentence",
152+
"paragraph",
153+
"document",
154+
"nonWhitespaceSequence",
155+
"boundedNonWhitespaceSequence",
156+
"url",
157+
"notebookCell",
158+
] as const;
159+
160+
export type SimpleScopeTypeType = (typeof simpleScopeTypeTypes)[number];
158161

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

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

+18
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

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { runCommand } from "./runCommand";
1616
import { runIntegrationTests } from "./runIntegrationTests";
1717
import { injectIde } from "./singletons/ide.singleton";
1818
import { ScopeRangeWatcher } from "./ScopeVisualizer/ScopeRangeWatcher";
19+
import { ScopeSupportWatcher } from "./ScopeVisualizer/ScopeSupportWatcher";
1920

2021
export function createCursorlessEngine(
2122
treeSitter: TreeSitter,
@@ -103,6 +104,7 @@ function createScopeProvider(
103104

104105
const rangeWatcher = new ScopeRangeWatcher(rangeProvider);
105106
const supportChecker = new ScopeSupportChecker(scopeHandlerFactory);
107+
const supportWatcher = new ScopeSupportWatcher(supportChecker);
106108

107109
return {
108110
provideScopeRanges: rangeProvider.provideScopeRanges,
@@ -112,5 +114,6 @@ function createScopeProvider(
112114
rangeWatcher.onDidChangeIterationScopeRanges,
113115
getScopeSupport: supportChecker.getScopeSupport,
114116
getIterationScopeSupport: supportChecker.getIterationScopeSupport,
117+
onDidChangeScopeSupport: supportWatcher.onDidChangeScopeSupport,
115118
};
116119
}

packages/cursorless-vscode/package.json

+8
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@
7676
}
7777
},
7878
"contributes": {
79+
"views": {
80+
"explorer": [
81+
{
82+
"id": "cursorlessScopeSupport",
83+
"name": "Cursorless scope support"
84+
}
85+
]
86+
},
7987
"commands": [
8088
{
8189
"command": "cursorless.toggleDecorations",

0 commit comments

Comments
 (0)