Skip to content

Commit c2baf48

Browse files
authored
keyboard: Have keyboard target follow selection if it moves (#2422)
- Fixes #2421 This is experimental, and currently guarded by a hidden setting. To activate this behaviour, add the following to your vscode `settings.json`: ```json "cursorless.experimental.keyboardTargetFollowsSelection": true, ``` Note that it is debounced, so the pink highlight will lag slightly. We could change that but I was worried it might slow down cursor movement ## 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 cbc85e8 commit c2baf48

File tree

4 files changed

+86
-2
lines changed

4 files changed

+86
-2
lines changed

packages/common/src/ide/normalized/NormalizedIDE.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class NormalizedIDE extends PassthroughIDEBase {
4747
"experimental.hatStability",
4848
),
4949
snippetsDir: getFixturePath("cursorless-snippets"),
50+
keyboardTargetFollowsSelection: false,
5051
});
5152
}
5253

packages/common/src/ide/types/Configuration.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { GetFieldType, Paths } from "./Paths";
66
export type CursorlessConfiguration = {
77
tokenHatSplittingMode: TokenHatSplittingMode;
88
wordSeparators: string[];
9-
experimental: { snippetsDir: string | undefined; hatStability: HatStability };
9+
experimental: {
10+
snippetsDir: string | undefined;
11+
hatStability: HatStability;
12+
keyboardTargetFollowsSelection: boolean;
13+
};
1014
decorationDebounceDelayMs: number;
1115
commandHistory: boolean;
1216
debug: boolean;
@@ -26,6 +30,7 @@ export const CONFIGURATION_DEFAULTS: CursorlessConfiguration = {
2630
experimental: {
2731
snippetsDir: undefined,
2832
hatStability: HatStability.balanced,
33+
keyboardTargetFollowsSelection: false,
2934
},
3035
commandHistory: false,
3136
debug: false,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Disposable } from "@cursorless/common";
2+
import { Debouncer } from "./core/Debouncer";
3+
import { StoredTargetMap } from "./core/StoredTargets";
4+
import { PlainTarget } from "./processTargets/targets";
5+
import { ide } from "./singletons/ide.singleton";
6+
7+
export class KeyboardTargetUpdater {
8+
private disposables: Disposable[] = [];
9+
private selectionWatcherDisposable: Disposable | undefined;
10+
private debouncer: Debouncer;
11+
12+
constructor(private storedTargets: StoredTargetMap) {
13+
this.debouncer = new Debouncer(() => this.updateKeyboardTarget());
14+
15+
this.disposables.push(
16+
ide().configuration.onDidChangeConfiguration(() => this.maybeActivate()),
17+
18+
this.debouncer,
19+
);
20+
21+
this.maybeActivate();
22+
}
23+
24+
maybeActivate(): void {
25+
const isActive = ide().configuration.getOwnConfiguration(
26+
"experimental.keyboardTargetFollowsSelection",
27+
);
28+
29+
if (isActive) {
30+
if (this.selectionWatcherDisposable == null) {
31+
this.selectionWatcherDisposable = ide().onDidChangeTextEditorSelection(
32+
this.debouncer.run,
33+
);
34+
}
35+
36+
return;
37+
}
38+
39+
if (this.selectionWatcherDisposable != null) {
40+
this.selectionWatcherDisposable.dispose();
41+
this.selectionWatcherDisposable = undefined;
42+
}
43+
}
44+
45+
private updateKeyboardTarget() {
46+
const activeEditor = ide().activeTextEditor;
47+
48+
if (activeEditor == null || this.storedTargets.get("keyboard") == null) {
49+
return;
50+
}
51+
52+
this.storedTargets.set(
53+
"keyboard",
54+
activeEditor.selections.map(
55+
(selection) =>
56+
new PlainTarget({
57+
contentRange: selection,
58+
editor: activeEditor,
59+
isReversed: selection.isReversed,
60+
}),
61+
),
62+
);
63+
}
64+
65+
dispose() {
66+
this.disposables.forEach((disposable) => disposable.dispose());
67+
this.selectionWatcherDisposable?.dispose();
68+
}
69+
}

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
3030
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
3131
import { injectIde } from "./singletons/ide.singleton";
3232
import { TreeSitter } from "./typings/TreeSitter";
33+
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
3334

3435
export async function createCursorlessEngine(
3536
treeSitter: TreeSitter,
@@ -57,6 +58,8 @@ export async function createCursorlessEngine(
5758

5859
const storedTargets = new StoredTargetMap();
5960

61+
const keyboardTargetUpdater = new KeyboardTargetUpdater(storedTargets);
62+
6063
const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);
6164
await languageDefinitions.init();
6265

@@ -66,7 +69,13 @@ export async function createCursorlessEngine(
6669
talonSpokenForms,
6770
);
6871

69-
ide.disposeOnExit(rangeUpdater, languageDefinitions, hatTokenMap, debug);
72+
ide.disposeOnExit(
73+
rangeUpdater,
74+
languageDefinitions,
75+
hatTokenMap,
76+
debug,
77+
keyboardTargetUpdater,
78+
);
7079

7180
const commandRunnerDecorators: CommandRunnerDecorator[] = [];
7281

0 commit comments

Comments
 (0)