Skip to content

Commit 77a3b55

Browse files
Add the scope visualizer (#1653)
- Depends on #1651 - Depends on #1652 - Depends on #1649 - Depends on #1644 ## Checklist - [x] Add image to doc page - [x] File issue to migrate all our vscode references to use new thin wrapper - [x] File issue about reworking removal highlight range to return generalised range, and not using line range for line target content range - [x] Investigate cheatsheet performance issue - [x] Migrate todos from #1523 - [x] 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) - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] I have not broken the cheatsheet - [x] Compute border colors from background color? --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 5106faa commit 77a3b55

39 files changed

+1527
-6
lines changed

cursorless-talon/src/cheatsheet/cheat_sheet.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .get_list import get_list, get_lists
88
from .sections.actions import get_actions
99
from .sections.compound_targets import get_compound_targets
10+
from .sections.get_scope_visualizer import get_scope_visualizer
1011
from .sections.modifiers import get_modifiers
1112
from .sections.scopes import get_scopes
1213
from .sections.special_marks import get_special_marks
@@ -102,6 +103,11 @@ def cursorless_cheat_sheet_get_json():
102103
"id": "scopes",
103104
"items": get_scopes(),
104105
},
106+
{
107+
"name": "Scope visualizer",
108+
"id": "scopeVisualizer",
109+
"items": get_scope_visualizer(),
110+
},
105111
{
106112
"name": "Modifiers",
107113
"id": "modifiers",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from ..get_list import get_list, get_raw_list, make_readable
2+
3+
4+
def get_scope_visualizer():
5+
show_scope_visualizer = list(get_raw_list("show_scope_visualizer").keys())[0]
6+
visualization_types = get_raw_list("visualization_type")
7+
8+
return [
9+
*get_list("hide_scope_visualizer", "command"),
10+
{
11+
"id": "show_scope_visualizer",
12+
"type": "command",
13+
"variations": [
14+
{
15+
"spokenForm": f"{show_scope_visualizer} <scope>",
16+
"description": "Visualize <scope>",
17+
},
18+
*[
19+
{
20+
"spokenForm": f"{show_scope_visualizer} <scope> {spoken_form}",
21+
"description": f"Visualize <scope> {make_readable(id).lower()} range",
22+
}
23+
for spoken_form, id in visualization_types.items()
24+
],
25+
],
26+
},
27+
]

cursorless-talon/src/cursorless.talon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ tag: user.cursorless
2020
user.cursorless_wrap(cursorless_wrap_action, cursorless_target, cursorless_wrapper)
2121

2222
{user.cursorless_homophone} settings: user.cursorless_show_settings_in_ide()
23+
24+
{user.cursorless_show_scope_visualizer} <user.cursorless_scope_type> [{user.cursorless_visualization_type}]:
25+
user.private_cursorless_show_scope_visualizer(cursorless_scope_type, cursorless_visualization_type or "content")
26+
{user.cursorless_hide_scope_visualizer}:
27+
user.private_cursorless_hide_scope_visualizer()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from talon import Module, app
2+
3+
from .csv_overrides import init_csv_and_watch_changes
4+
from .cursorless_command_server import run_rpc_command_no_wait
5+
6+
mod = Module()
7+
mod.list("cursorless_show_scope_visualizer", desc="Show scope visualizer")
8+
mod.list("cursorless_hide_scope_visualizer", desc="Hide scope visualizer")
9+
mod.list(
10+
"cursorless_visualization_type",
11+
desc='Cursorless visualization type, e.g. "removal" or "iteration"',
12+
)
13+
14+
# NOTE: Please do not change these dicts. Use the CSVs for customization.
15+
# See https://www.cursorless.org/docs/user/customization/
16+
visualization_types = {
17+
"removal": "removal",
18+
"iteration": "iteration",
19+
}
20+
21+
22+
@mod.action_class
23+
class Actions:
24+
def private_cursorless_show_scope_visualizer(
25+
scope_type: dict, visualization_type: str
26+
):
27+
"""Shows scope visualizer"""
28+
run_rpc_command_no_wait(
29+
"cursorless.showScopeVisualizer", scope_type, visualization_type
30+
)
31+
32+
def private_cursorless_hide_scope_visualizer():
33+
"""Hides scope visualizer"""
34+
run_rpc_command_no_wait("cursorless.hideScopeVisualizer")
35+
36+
37+
def on_ready():
38+
init_csv_and_watch_changes(
39+
"scope_visualizer",
40+
{
41+
"show_scope_visualizer": {"visualize": "showScopeVisualizer"},
42+
"hide_scope_visualizer": {"visualize nothing": "hideScopeVisualizer"},
43+
"visualization_type": visualization_types,
44+
},
45+
)
46+
47+
48+
app.register("ready", on_ready)

docs/user/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Note that if the mark is `"this"`, and you have multiple cursors, the modifiers
143143

144144
##### Syntactic scopes
145145

146-
For programming languages where Cursorless has rich parse tree support, we support modifiers that expand to the nearest containing function, class, etc. See [the source code](../../queries) for a list of supported languages. Some languages are still supported using our legacy implementation; those will be listed in [here](../../packages/cursorless-engine/src/languages/LegacyLanguageId.ts). Below is a list of supported scope types, keeping in mind that this table can sometimes lag behind the actual list. Your cheatsheet (say `"cursorless cheatsheet"` with VSCode focused) will have the most up-to-date list.
146+
For programming languages where Cursorless has rich parse tree support, we support modifiers that expand to the nearest containing function, class, etc. See [the source code](../../queries) for a list of supported languages. Some languages are still supported using our legacy implementation; those will be listed in [here](../../packages/cursorless-engine/src/languages/LegacyLanguageId.ts). Below is a list of supported scope types, keeping in mind that this table can sometimes lag behind the actual list. Your cheatsheet (say `"cursorless cheatsheet"` with VSCode focused) will have the most up-to-date list. It can also be helpful to use the [scope visualizer](./scope-visualizer.md) to visualize the scope types on your own code.
147147

148148
| Term | Syntactic element |
149149
| -------------- | --------------------------------------------------- |
148 KB
Loading

docs/user/images/visualize-funk.png

133 KB
Loading

docs/user/images/visualize-token.png

152 KB
Loading

docs/user/scope-visualizer.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Scope visualizer
2+
3+
The scope visualizer allows you to visualize Cursorless scopes on your code in real time. It is useful to understand how Cursorless scopes work, and is also useful for Cursorless contributors as they develop new scopes.
4+
5+
## Usage
6+
7+
To start the scope visualizer, say `"visualize <scope>"`, where `<scope>` is the name of the scope you want to visualize. For example, `"visualize funk"`. To stop the scope visualizer, say `"visualize nothing"`.
8+
9+
You can also visualize removal and iteration ranges for scopes by saying `"visualize <scope> removal"` and `"visualize <scope> iteration"`, respectively.
10+
11+
## Examples
12+
13+
### `"visualize funk"`
14+
15+
![visualize funk](images/visualize-funk.png)
16+
17+
### `"visualize token"`
18+
19+
![visualize token](images/visualize-token.png)
20+
21+
### `"visualize block removal"`
22+
23+
![visualize block removal](images/visualize-block-removal.png)

packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,40 @@
10301030
}
10311031
]
10321032
},
1033+
{
1034+
"name": "Scope visualizer",
1035+
"id": "scopeVisualizer",
1036+
"items": [
1037+
{
1038+
"id": "hideScopeVisualizer",
1039+
"type": "command",
1040+
"variations": [
1041+
{
1042+
"spokenForm": "visualize nothing",
1043+
"description": "Hide scope visualizer"
1044+
}
1045+
]
1046+
},
1047+
{
1048+
"id": "show_scope_visualizer",
1049+
"type": "command",
1050+
"variations": [
1051+
{
1052+
"spokenForm": "visualize <scope>",
1053+
"description": "Visualize <scope>"
1054+
},
1055+
{
1056+
"spokenForm": "visualize <scope> removal",
1057+
"description": "Visualize <scope> removal range"
1058+
},
1059+
{
1060+
"spokenForm": "visualize <scope> iteration",
1061+
"description": "Visualize <scope> iteration range"
1062+
}
1063+
]
1064+
}
1065+
]
1066+
},
10331067
{
10341068
"name": "Scopes",
10351069
"id": "scopes",
@@ -1374,6 +1408,16 @@
13741408
}
13751409
]
13761410
},
1411+
{
1412+
"id": "sentence",
1413+
"type": "scopeType",
1414+
"variations": [
1415+
{
1416+
"spokenForm": "sentence",
1417+
"description": "Sentence"
1418+
}
1419+
]
1420+
},
13771421
{
13781422
"id": "statement",
13791423
"type": "scopeType",

packages/common/src/cursorlessCommandIds.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export const cursorlessCommandIds = [
4343
"cursorless.showQuickPick",
4444
"cursorless.takeSnapshot",
4545
"cursorless.toggleDecorations",
46+
"cursorless.showScopeVisualizer",
47+
"cursorless.hideScopeVisualizer",
4648
] as const satisfies readonly `cursorless.${string}`[];
4749

4850
export type CursorlessCommandId = (typeof cursorlessCommandIds)[number];
@@ -104,4 +106,10 @@ export const cursorlessCommandDescriptions: Record<
104106
["cursorless.keyboard.modal.modeToggle"]: new HiddenCommand(
105107
"Toggle the cursorless modal mode",
106108
),
109+
["cursorless.showScopeVisualizer"]: new HiddenCommand(
110+
"Show the scope visualizer",
111+
),
112+
["cursorless.hideScopeVisualizer"]: new HiddenCommand(
113+
"Hide the scope visualizer",
114+
),
107115
};

packages/common/src/testUtil/toPlainObject.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import type {
77
} from "..";
88
import { FlashStyle, isLineRange } from "..";
99
import { Token } from "../types/Token";
10-
import { Position } from "../types/Position";
11-
import { Range } from "../types/Range";
1210
import { Selection } from "../types/Selection";
1311

1412
export type PositionPlainObject = {
@@ -85,7 +83,23 @@ export type SerializedMarks = {
8583
[decoratedCharacter: string]: RangePlainObject;
8684
};
8785

88-
export function rangeToPlainObject(range: Range): RangePlainObject {
86+
/**
87+
* Simplified Position interface containing only what we need for serialization
88+
*/
89+
interface SimplePosition {
90+
line: number;
91+
character: number;
92+
}
93+
94+
/**
95+
* Simplified Range interface containing only what we need for serialization
96+
*/
97+
interface SimpleRange {
98+
start: SimplePosition;
99+
end: SimplePosition;
100+
}
101+
102+
export function rangeToPlainObject(range: SimpleRange): RangePlainObject {
89103
return {
90104
start: positionToPlainObject(range.start),
91105
end: positionToPlainObject(range.end),
@@ -104,7 +118,7 @@ export function selectionToPlainObject(
104118
export function positionToPlainObject({
105119
line,
106120
character,
107-
}: Position): PositionPlainObject {
121+
}: SimplePosition): PositionPlainObject {
108122
return { line, character };
109123
}
110124

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { assert } from "chai";
2+
import * as sinon from "sinon";
3+
import {
4+
createDecorationTypeCallToPlainObject,
5+
setDecorationsCallToPlainObject,
6+
} from "./spyCallsToPlainObject";
7+
import { Fakes, ExpectedArgs } from "./scopeVisualizerTest.types";
8+
9+
export function checkAndResetFakes(fakes: Fakes, expected: ExpectedArgs) {
10+
const actual = getSpyCallsAndResetFakes(fakes);
11+
assert.deepStrictEqual(actual, expected, JSON.stringify(actual));
12+
}
13+
14+
function getSpyCallsAndResetFakes({
15+
createTextEditorDecorationType,
16+
setDecorations,
17+
dispose,
18+
}: Fakes) {
19+
return {
20+
decorationRenderOptions: getAndResetFake(
21+
createTextEditorDecorationType,
22+
createDecorationTypeCallToPlainObject,
23+
),
24+
decorationRanges: getAndResetFake(
25+
setDecorations,
26+
setDecorationsCallToPlainObject,
27+
),
28+
disposedDecorationIds: getAndResetFake(dispose, ({ args: [id] }) => id),
29+
};
30+
}
31+
32+
function getAndResetFake<ArgList extends any[], Return, Expected>(
33+
spy: sinon.SinonSpy<ArgList, Return>,
34+
transform: (call: sinon.SinonSpyCall<ArgList, Return>) => Expected,
35+
) {
36+
const actual = spy.getCalls().map(transform);
37+
spy.resetHistory();
38+
return actual;
39+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ScopeVisualizerColorConfig } from "@cursorless/vscode-common";
2+
3+
/**
4+
* Fake color config to use for testing. We use an alpha of 50% and try to use
5+
* different rgb channels where possible to make it easier to see what happens
6+
* when we blend colors.
7+
*/
8+
export const COLOR_CONFIG: ScopeVisualizerColorConfig = {
9+
dark: {
10+
content: {
11+
background: "#00000180",
12+
borderPorous: "#00000280",
13+
borderSolid: "#00000380",
14+
},
15+
domain: {
16+
background: "#01000080",
17+
borderPorous: "#02000080",
18+
borderSolid: "#03000080",
19+
},
20+
iteration: {
21+
background: "#00000480",
22+
borderPorous: "#00000580",
23+
borderSolid: "#00000680",
24+
},
25+
removal: {
26+
background: "#00010080",
27+
borderPorous: "#00020080",
28+
borderSolid: "#00030080",
29+
},
30+
},
31+
light: {
32+
content: {
33+
background: "#00000180",
34+
borderPorous: "#00000280",
35+
borderSolid: "#00000380",
36+
},
37+
domain: {
38+
background: "#01000080",
39+
borderPorous: "#02000080",
40+
borderSolid: "#03000080",
41+
},
42+
iteration: {
43+
background: "#00000480",
44+
borderPorous: "#00000580",
45+
borderSolid: "#00000680",
46+
},
47+
removal: {
48+
background: "#00010080",
49+
borderPorous: "#00020080",
50+
borderSolid: "#00030080",
51+
},
52+
},
53+
};

0 commit comments

Comments
 (0)