Skip to content

Commit

Permalink
Add the scope visualizer
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Jul 17, 2023
1 parent b8532c3 commit 34ac721
Show file tree
Hide file tree
Showing 31 changed files with 1,427 additions and 5 deletions.
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()
49 changes: 49 additions & 0 deletions cursorless-talon/src/scope_visualizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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",
"content": "content",
}


@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)
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",
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { VscodeApi, getCursorlessApi } from "@cursorless/vscode-common";
import * as sinon from "sinon";
import { DecorationRenderOptions, WorkspaceConfiguration } from "vscode";
import { COLOR_CONFIG } from "./colorConfig";
import {
Fakes,
MockDecorationType,
SetDecorationsParameters,
} from "./scopeVisualizerTest.types";

export async function injectFakes(): Promise<Fakes> {
const { vscodeApi } = (await getCursorlessApi()).testHelpers!;

const dispose = sinon.fake<[number], void>();

let decorationIndex = 0;
const createTextEditorDecorationType = sinon.fake<
Parameters<VscodeApi["window"]["createTextEditorDecorationType"]>,
MockDecorationType
>((_options: DecorationRenderOptions) => {
const id = decorationIndex++;
return {
dispose() {
dispose(id);
},
id,
};
});

const setDecorations = sinon.fake<
SetDecorationsParameters,
ReturnType<VscodeApi["editor"]["setDecorations"]>
>();

const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG);

sinon.replace(
vscodeApi.window,
"createTextEditorDecorationType",
createTextEditorDecorationType as any,
);
sinon.replace(vscodeApi.editor, "setDecorations", setDecorations as any);
sinon.replace(
vscodeApi.workspace,
"getConfiguration",
sinon.fake.returns({
get: getConfigurationValue,
} as unknown as WorkspaceConfiguration),
);

return { setDecorations, createTextEditorDecorationType, dispose };
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { openNewEditor } from "@cursorless/vscode-common";
import * as vscode from "vscode";
import { checkAndResetFakes } from "./checkAndResetFakes";
import { injectFakes } from "./injectFakes";
import { ExpectedArgs } from "./scopeVisualizerTest.types";

/**
* Tests that the scope visualizer works with multiline content, by
* ensuring that the correct decorations are applied so that it looks
* like `./runBasicMultilineContentTest.png`.
*/
export async function runBasicMultilineContentTest() {
await openNewEditor(contents, {
languageId: "typescript",
});

const fakes = await injectFakes();

await vscode.commands.executeCommand(
"cursorless.showScopeVisualizer",
{
type: "namedFunction",
},
"content",
);

checkAndResetFakes(fakes, expectedArgs);
}

const contents = `
function helloWorld() {
}
`;

const expectedArgs: ExpectedArgs = {
decorationRenderOptions: [
{
backgroundColor: "#000001c0",
borderColor: "#010002c0 #010001c0 #010001c0 #010002c0",
borderStyle: "solid dashed dashed solid",
borderRadius: "2px 0px 0px 0px",
isWholeLine: false,
id: 0,
},
{
backgroundColor: "#000001c0",
borderColor: "#010001c0 #010001c0 #010001c0 #010001c0",
borderStyle: "none dashed none dashed",
borderRadius: "0px 0px 0px 0px",
isWholeLine: false,
id: 1,
},
{
backgroundColor: "#000001c0",
borderColor: "#010001c0 #010002c0 #010002c0 #010001c0",
borderStyle: "dashed solid solid dashed",
borderRadius: "0px 0px 2px 0px",
isWholeLine: false,
id: 2,
},
],
decorationRanges: [
{
decorationId: 0,
ranges: [
{ start: { line: 1, character: 0 }, end: { line: 1, character: 23 } },
],
},
{
decorationId: 1,
ranges: [
{ start: { line: 2, character: 0 }, end: { line: 2, character: 0 } },
],
},
{
decorationId: 2,
ranges: [
{ start: { line: 3, character: 0 }, end: { line: 3, character: 1 } },
],
},
],
disposedDecorationIds: [],
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 34ac721

Please sign in to comment.