Skip to content

Commit b2ae87a

Browse files
committed
Add ScopeRangeWatcher
1 parent 17a384b commit b2ae87a

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Disposable } from "@cursorless/common";
2+
import { pull } from "lodash";
3+
import {
4+
IterationScopeChangeEventCallback,
5+
IterationScopeRangeConfig,
6+
ScopeChangeEventCallback,
7+
ScopeRangeConfig,
8+
} from "..";
9+
import { Debouncer } from "../core/Debouncer";
10+
import { ide } from "../singletons/ide.singleton";
11+
import { ScopeRangeProvider } from "./ScopeRangeProvider";
12+
13+
/**
14+
* Watches for changes to the scope ranges of visible editors and notifies
15+
* listeners when they change.
16+
*/
17+
export class ScopeRangeWatcher {
18+
private disposables: Disposable[] = [];
19+
private debouncer = new Debouncer(() => this.onChange());
20+
private listeners: (() => void)[] = [];
21+
22+
constructor(private scopeRangeProvider: ScopeRangeProvider) {
23+
this.disposables.push(
24+
// An Event which fires when the array of visible editors has changed.
25+
ide().onDidChangeVisibleTextEditors(this.debouncer.run),
26+
// An event that fires when a text document opens
27+
ide().onDidOpenTextDocument(this.debouncer.run),
28+
// An Event that fires when a text document closes
29+
ide().onDidCloseTextDocument(this.debouncer.run),
30+
// An event that is emitted when a text document is changed. This usually
31+
// happens when the contents changes but also when other things like the
32+
// dirty-state changes.
33+
ide().onDidChangeTextDocument(this.debouncer.run),
34+
ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run),
35+
this.debouncer,
36+
);
37+
38+
this.onDidChangeScopeRanges = this.onDidChangeScopeRanges.bind(this);
39+
this.onDidChangeIterationScopeRanges =
40+
this.onDidChangeIterationScopeRanges.bind(this);
41+
}
42+
43+
/**
44+
* Registers a callback to be run when the scope ranges change for any visible
45+
* editor. The callback will be run immediately once for each visible editor
46+
* with the current scope ranges.
47+
* @param callback The callback to run when the scope ranges change
48+
* @param config The configuration for the scope ranges
49+
* @returns A {@link Disposable} which will stop the callback from running
50+
*/
51+
onDidChangeScopeRanges(
52+
callback: ScopeChangeEventCallback,
53+
config: ScopeRangeConfig,
54+
): Disposable {
55+
const fn = () => {
56+
ide().visibleTextEditors.forEach((editor) => {
57+
callback(
58+
editor,
59+
this.scopeRangeProvider.provideScopeRanges(editor, config),
60+
);
61+
});
62+
};
63+
64+
this.listeners.push(fn);
65+
66+
fn();
67+
68+
return {
69+
dispose: () => {
70+
pull(this.listeners, fn);
71+
},
72+
};
73+
}
74+
75+
/**
76+
* Registers a callback to be run when the iteration scope ranges change for
77+
* any visible editor. The callback will be run immediately once for each
78+
* visible editor with the current iteration scope ranges.
79+
* @param callback The callback to run when the scope ranges change
80+
* @param config The configuration for the scope ranges
81+
* @returns A {@link Disposable} which will stop the callback from running
82+
*/
83+
onDidChangeIterationScopeRanges(
84+
callback: IterationScopeChangeEventCallback,
85+
config: IterationScopeRangeConfig,
86+
): Disposable {
87+
const fn = () => {
88+
ide().visibleTextEditors.forEach((editor) => {
89+
callback(
90+
editor,
91+
this.scopeRangeProvider.provideIterationScopeRanges(editor, config),
92+
);
93+
});
94+
};
95+
96+
this.listeners.push(fn);
97+
98+
fn();
99+
100+
return {
101+
dispose: () => {
102+
pull(this.listeners, fn);
103+
},
104+
};
105+
}
106+
107+
private onChange() {
108+
this.listeners.forEach((listener) => listener());
109+
}
110+
111+
dispose(): void {
112+
this.disposables.forEach(({ dispose }) => {
113+
try {
114+
dispose();
115+
} catch (e) {
116+
// do nothing; some of the VSCode disposables misbehave, and we don't
117+
// want that to prevent us from disposing the rest of the disposables
118+
}
119+
});
120+
}
121+
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Disposable,
23
GeneralizedRange,
34
Range,
45
ScopeType,
@@ -26,6 +27,32 @@ export interface ScopeProvider {
2627
editor: TextEditor,
2728
config: IterationScopeRangeConfig,
2829
) => IterationScopeRanges[];
30+
31+
/**
32+
* Registers a callback to be run when the scope ranges change for any visible
33+
* editor. The callback will be run immediately once for each visible editor
34+
* with the current scope ranges.
35+
* @param callback The callback to run when the scope ranges change
36+
* @param config The configuration for the scope ranges
37+
* @returns A {@link Disposable} which will stop the callback from running
38+
*/
39+
onDidChangeScopeRanges: (
40+
callback: ScopeChangeEventCallback,
41+
config: ScopeRangeConfig,
42+
) => Disposable;
43+
44+
/**
45+
* Registers a callback to be run when the iteration scope ranges change for
46+
* any visible editor. The callback will be run immediately once for each
47+
* visible editor with the current iteration scope ranges.
48+
* @param callback The callback to run when the scope ranges change
49+
* @param config The configuration for the scope ranges
50+
* @returns A {@link Disposable} which will stop the callback from running
51+
*/
52+
onDidChangeIterationScopeRanges: (
53+
callback: IterationScopeChangeEventCallback,
54+
config: IterationScopeRangeConfig,
55+
) => Disposable;
2956
}
3057

3158
interface ScopeRangeConfigBase {
@@ -49,6 +76,16 @@ export interface IterationScopeRangeConfig extends ScopeRangeConfigBase {
4976
includeNestedTargets: boolean;
5077
}
5178

79+
export type ScopeChangeEventCallback = (
80+
editor: TextEditor,
81+
scopeRanges: ScopeRanges[],
82+
) => void;
83+
84+
export type IterationScopeChangeEventCallback = (
85+
editor: TextEditor,
86+
scopeRanges: IterationScopeRanges[],
87+
) => void;
88+
5289
/**
5390
* Contains the ranges that define a given scope, eg its {@link domain} and the
5491
* ranges for its {@link targets}.

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandler
1414
import { runCommand } from "./runCommand";
1515
import { runIntegrationTests } from "./runIntegrationTests";
1616
import { injectIde } from "./singletons/ide.singleton";
17+
import { ScopeRangeWatcher } from "./ScopeVisualizer/ScopeRangeWatcher";
1718

1819
export function createCursorlessEngine(
1920
treeSitter: TreeSitter,
@@ -99,8 +100,13 @@ function createScopeProvider(
99100
),
100101
);
101102

103+
const rangeWatcher = new ScopeRangeWatcher(rangeProvider);
104+
102105
return {
103106
provideScopeRanges: rangeProvider.provideScopeRanges,
104107
provideIterationScopeRanges: rangeProvider.provideIterationScopeRanges,
108+
onDidChangeScopeRanges: rangeWatcher.onDidChangeScopeRanges,
109+
onDidChangeIterationScopeRanges:
110+
rangeWatcher.onDidChangeIterationScopeRanges,
105111
};
106112
}

0 commit comments

Comments
 (0)