Skip to content

Add the scope visualizer #1653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cursorless-talon/src/cheatsheet/cheat_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .get_list import get_list, get_lists
from .sections.actions import get_actions
from .sections.compound_targets import get_compound_targets
from .sections.get_scope_visualizer import get_scope_visualizer
from .sections.modifiers import get_modifiers
from .sections.scopes import get_scopes
from .sections.special_marks import get_special_marks
Expand Down Expand Up @@ -102,6 +103,11 @@ def cursorless_cheat_sheet_get_json():
"id": "scopes",
"items": get_scopes(),
},
{
"name": "Scope visualizer",
"id": "scopeVisualizer",
"items": get_scope_visualizer(),
},
{
"name": "Modifiers",
"id": "modifiers",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from ..get_list import get_list, get_raw_list, make_readable


def get_scope_visualizer():
show_scope_visualizer = list(get_raw_list("show_scope_visualizer").keys())[0]
visualization_types = get_raw_list("visualization_type")

return [
*get_list("hide_scope_visualizer", "command"),
{
"id": "show_scope_visualizer",
"type": "command",
"variations": [
{
"spokenForm": f"{show_scope_visualizer} <scope>",
"description": "Visualize <scope>",
},
*[
{
"spokenForm": f"{show_scope_visualizer} <scope> {spoken_form}",
"description": f"Visualize <scope> {make_readable(id).lower()} range",
}
for spoken_form, id in visualization_types.items()
],
],
},
]
5 changes: 5 additions & 0 deletions cursorless-talon/src/cursorless.talon
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ tag: user.cursorless
user.cursorless_wrap(cursorless_wrap_action, cursorless_target, cursorless_wrapper)

{user.cursorless_homophone} settings: user.cursorless_show_settings_in_ide()

{user.cursorless_show_scope_visualizer} <user.cursorless_scope_type> [{user.cursorless_visualization_type}]:
user.private_cursorless_show_scope_visualizer(cursorless_scope_type, cursorless_visualization_type or "content")
{user.cursorless_hide_scope_visualizer}:
user.private_cursorless_hide_scope_visualizer()
48 changes: 48 additions & 0 deletions cursorless-talon/src/scope_visualizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from talon import Module, app

from .csv_overrides import init_csv_and_watch_changes
from .cursorless_command_server import run_rpc_command_no_wait

mod = Module()
mod.list("cursorless_show_scope_visualizer", desc="Show scope visualizer")
mod.list("cursorless_hide_scope_visualizer", desc="Hide scope visualizer")
mod.list(
"cursorless_visualization_type",
desc='Cursorless visualization type, e.g. "removal" or "iteration"',
)

# NOTE: Please do not change these dicts. Use the CSVs for customization.
# See https://www.cursorless.org/docs/user/customization/
visualization_types = {
"removal": "removal",
"iteration": "iteration",
}


@mod.action_class
class Actions:
def private_cursorless_show_scope_visualizer(
scope_type: dict, visualization_type: str
):
"""Shows scope visualizer"""
run_rpc_command_no_wait(
"cursorless.showScopeVisualizer", scope_type, visualization_type
)

def private_cursorless_hide_scope_visualizer():
"""Hides scope visualizer"""
run_rpc_command_no_wait("cursorless.hideScopeVisualizer")


def on_ready():
init_csv_and_watch_changes(
"scope_visualizer",
{
"show_scope_visualizer": {"visualize": "showScopeVisualizer"},
"hide_scope_visualizer": {"visualize nothing": "hideScopeVisualizer"},
"visualization_type": visualization_types,
},
)


app.register("ready", on_ready)
2 changes: 1 addition & 1 deletion docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Note that if the mark is `"this"`, and you have multiple cursors, the modifiers

##### Syntactic scopes

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.
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.

| Term | Syntactic element |
| -------------- | --------------------------------------------------- |
Expand Down
Binary file added docs/user/images/visualize-block-removal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/user/images/visualize-funk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/user/images/visualize-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions docs/user/scope-visualizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Scope visualizer

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.

## Usage

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"`.

You can also visualize removal and iteration ranges for scopes by saying `"visualize <scope> removal"` and `"visualize <scope> iteration"`, respectively.

## Examples

### `"visualize funk"`

![visualize funk](images/visualize-funk.png)

### `"visualize token"`

![visualize token](images/visualize-token.png)

### `"visualize block removal"`

![visualize block removal](images/visualize-block-removal.png)
44 changes: 44 additions & 0 deletions packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,40 @@
}
]
},
{
"name": "Scope visualizer",
"id": "scopeVisualizer",
"items": [
{
"id": "hideScopeVisualizer",
"type": "command",
"variations": [
{
"spokenForm": "visualize nothing",
"description": "Hide scope visualizer"
}
]
},
{
"id": "show_scope_visualizer",
"type": "command",
"variations": [
{
"spokenForm": "visualize <scope>",
"description": "Visualize <scope>"
},
{
"spokenForm": "visualize <scope> removal",
"description": "Visualize <scope> removal range"
},
{
"spokenForm": "visualize <scope> iteration",
"description": "Visualize <scope> iteration range"
}
]
}
]
},
{
"name": "Scopes",
"id": "scopes",
Expand Down Expand Up @@ -1374,6 +1408,16 @@
}
]
},
{
"id": "sentence",
"type": "scopeType",
"variations": [
{
"spokenForm": "sentence",
"description": "Sentence"
}
]
},
{
"id": "statement",
"type": "scopeType",
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/cursorlessCommandIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const cursorlessCommandIds = [
"cursorless.showQuickPick",
"cursorless.takeSnapshot",
"cursorless.toggleDecorations",
"cursorless.showScopeVisualizer",
"cursorless.hideScopeVisualizer",
] as const satisfies readonly `cursorless.${string}`[];

export type CursorlessCommandId = (typeof cursorlessCommandIds)[number];
Expand Down Expand Up @@ -104,4 +106,10 @@ export const cursorlessCommandDescriptions: Record<
["cursorless.keyboard.modal.modeToggle"]: new HiddenCommand(
"Toggle the cursorless modal mode",
),
["cursorless.showScopeVisualizer"]: new HiddenCommand(
"Show the scope visualizer",
),
["cursorless.hideScopeVisualizer"]: new HiddenCommand(
"Hide the scope visualizer",
),
};
22 changes: 18 additions & 4 deletions packages/common/src/testUtil/toPlainObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import type {
} from "..";
import { FlashStyle, isLineRange } from "..";
import { Token } from "../types/Token";
import { Position } from "../types/Position";
import { Range } from "../types/Range";
import { Selection } from "../types/Selection";

export type PositionPlainObject = {
Expand Down Expand Up @@ -85,7 +83,23 @@ export type SerializedMarks = {
[decoratedCharacter: string]: RangePlainObject;
};

export function rangeToPlainObject(range: Range): RangePlainObject {
/**
* Simplified Position interface containing only what we need for serialization
*/
interface SimplePosition {
line: number;
character: number;
}

/**
* Simplified Range interface containing only what we need for serialization
*/
interface SimpleRange {
start: SimplePosition;
end: SimplePosition;
}

export function rangeToPlainObject(range: SimpleRange): RangePlainObject {
return {
start: positionToPlainObject(range.start),
end: positionToPlainObject(range.end),
Expand All @@ -104,7 +118,7 @@ export function selectionToPlainObject(
export function positionToPlainObject({
line,
character,
}: Position): PositionPlainObject {
}: SimplePosition): PositionPlainObject {
return { line, character };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { assert } from "chai";
import * as sinon from "sinon";
import {
createDecorationTypeCallToPlainObject,
setDecorationsCallToPlainObject,
} from "./spyCallsToPlainObject";
import { Fakes, ExpectedArgs } from "./scopeVisualizerTest.types";

export function checkAndResetFakes(fakes: Fakes, expected: ExpectedArgs) {
const actual = getSpyCallsAndResetFakes(fakes);
assert.deepStrictEqual(actual, expected, JSON.stringify(actual));
}

function getSpyCallsAndResetFakes({
createTextEditorDecorationType,
setDecorations,
dispose,
}: Fakes) {
return {
decorationRenderOptions: getAndResetFake(
createTextEditorDecorationType,
createDecorationTypeCallToPlainObject,
),
decorationRanges: getAndResetFake(
setDecorations,
setDecorationsCallToPlainObject,
),
disposedDecorationIds: getAndResetFake(dispose, ({ args: [id] }) => id),
};
}

function getAndResetFake<ArgList extends any[], Return, Expected>(
spy: sinon.SinonSpy<ArgList, Return>,
transform: (call: sinon.SinonSpyCall<ArgList, Return>) => Expected,
) {
const actual = spy.getCalls().map(transform);
spy.resetHistory();
return actual;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ScopeVisualizerColorConfig } from "@cursorless/vscode-common";

/**
* Fake color config to use for testing. We use an alpha of 50% and try to use
* different rgb channels where possible to make it easier to see what happens
* when we blend colors.
*/
export const COLOR_CONFIG: ScopeVisualizerColorConfig = {
dark: {
content: {
background: "#00000180",
borderPorous: "#00000280",
borderSolid: "#00000380",
},
domain: {
background: "#01000080",
borderPorous: "#02000080",
borderSolid: "#03000080",
},
iteration: {
background: "#00000480",
borderPorous: "#00000580",
borderSolid: "#00000680",
},
removal: {
background: "#00010080",
borderPorous: "#00020080",
borderSolid: "#00030080",
},
},
light: {
content: {
background: "#00000180",
borderPorous: "#00000280",
borderSolid: "#00000380",
},
domain: {
background: "#01000080",
borderPorous: "#02000080",
borderSolid: "#03000080",
},
iteration: {
background: "#00000480",
borderPorous: "#00000580",
borderSolid: "#00000680",
},
removal: {
background: "#00010080",
borderPorous: "#00020080",
borderSolid: "#00030080",
},
},
};
Loading