From 63f7b24530abb24366c1888a0967c27eabc17f54 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 19 Jun 2023 14:25:42 +0100 Subject: [PATCH 01/61] Create the scope visualizer --- cursorless-talon/src/command.py | 9 + cursorless-talon/src/cursorless.talon | 7 + packages/common/src/cursorlessCommandIds.ts | 8 + packages/common/src/ide/PassthroughIDEBase.ts | 12 +- packages/common/src/ide/fake/FakeIDE.ts | 12 +- packages/common/src/ide/types/ide.types.ts | 15 ++ packages/common/src/index.ts | 1 + .../common/src/util/CompositeKeyDefaultMap.ts | 38 +++ .../cursorless-engine/src/ScopeVisualizer.ts | 201 +++++++++++++++ .../src/VisualizationType.ts | 4 + .../cursorless-engine/src/core/Debouncer.ts | 30 +++ .../src/core/HatAllocator.ts | 46 +--- .../cursorless-engine/src/cursorlessEngine.ts | 36 ++- .../scopeHandlers/BaseScopeHandler.ts | 1 + .../scopeHandlers/scopeHandler.types.ts | 2 + .../scopeHandlers/shouldYieldScope.ts | 12 +- packages/cursorless-vscode/package.json | 151 +++++++++++ packages/cursorless-vscode/src/extension.ts | 2 + .../VscodeScopeVisualizer.ts | 244 ++++++++++++++++++ .../VscodeScopeVisualizerRenderer.ts | 194 ++++++++++++++ .../getDecorationRanges/block.test.txt | 23 ++ .../generateDecorationsForCharacterRange.ts | 35 +++ .../generateDecorationsForLineRange.ts | 58 +++++ .../getDecorationRanges.test.ts | 93 +++++++ .../getDecorationRanges.types.ts | 32 +++ .../getDifferentiatedRanges.test.ts | 148 +++++++++++ .../getDifferentiatedRanges.ts | 80 ++++++ .../handleMultipleLines.ts | 207 +++++++++++++++ .../ide/vscode/VSCodeScopeVisualizer/index.ts | 1 + .../src/ide/vscode/VscodeIDE.ts | 17 +- .../cursorless-vscode/src/registerCommands.ts | 6 + pnpm-lock.yaml | 17 ++ 32 files changed, 1699 insertions(+), 43 deletions(-) create mode 100644 packages/common/src/util/CompositeKeyDefaultMap.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer.ts create mode 100644 packages/cursorless-engine/src/VisualizationType.ts create mode 100644 packages/cursorless-engine/src/core/Debouncer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/block.test.txt create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForCharacterRange.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForLineRange.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.test.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.types.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.test.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/handleMultipleLines.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index 3e7060e852..28e6fa4100 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -127,6 +127,15 @@ def cursorless_multiple_target_command_no_wait( ), ) + def private_cursorless_run_rpc_command_and_wait( + command_id: str, arg1: Any = NotSet, arg2: Any = NotSet, arg3: Any = NotSet + ): + """Execute command via rpc and wait for command to finish.""" + run_rpc_command_and_wait( + command_id, + *[x for x in [arg1, arg2, arg3] if x is not NotSet], + ) + def construct_cursorless_command_argument( action: str, targets: list[dict], args: list[Any] diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index a9a60a0cb5..7fdcfd1a4c 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -20,3 +20,10 @@ tag: user.cursorless user.cursorless_wrap(cursorless_wrap_action, cursorless_target, cursorless_wrapper) {user.cursorless_homophone} settings: user.cursorless_show_settings_in_ide() + +visualize : + user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "content") +visualize removal: + user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "removal") +visualize nothing: + user.private_cursorless_run_rpc_command_and_wait("cursorless.hideScopeVisualizer") diff --git a/packages/common/src/cursorlessCommandIds.ts b/packages/common/src/cursorlessCommandIds.ts index c369ca3c74..37770954f9 100644 --- a/packages/common/src/cursorlessCommandIds.ts +++ b/packages/common/src/cursorlessCommandIds.ts @@ -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]; @@ -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", + ), }; diff --git a/packages/common/src/ide/PassthroughIDEBase.ts b/packages/common/src/ide/PassthroughIDEBase.ts index 23dc1e034a..9584c69bdc 100644 --- a/packages/common/src/ide/PassthroughIDEBase.ts +++ b/packages/common/src/ide/PassthroughIDEBase.ts @@ -10,7 +10,13 @@ import { TextEditorVisibleRangesChangeEvent, } from "./types/events.types"; import { FlashDescriptor } from "./types/FlashDescriptor"; -import { Disposable, IDE, RunMode, WorkspaceFolder } from "./types/ide.types"; +import { + Disposable, + EditorScopeRanges, + IDE, + RunMode, + WorkspaceFolder, +} from "./types/ide.types"; import { Messages } from "./types/Messages"; import { QuickPickOptions } from "./types/QuickPickOptions"; import { State } from "./types/State"; @@ -30,6 +36,10 @@ export default class PassthroughIDEBase implements IDE { this.capabilities = original.capabilities; } + setScopeVisualizationRanges(scopeRanges: EditorScopeRanges[]): Promise { + return this.original.setScopeVisualizationRanges(scopeRanges); + } + flashRanges(flashDescriptors: FlashDescriptor[]): Promise { return this.original.flashRanges(flashDescriptors); } diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index 7bb1069fbe..1d4a5b57a6 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -1,21 +1,22 @@ -import type { EditableTextEditor, TextEditor } from "../.."; import { pull } from "lodash"; +import type { EditableTextEditor, TextEditor } from "../.."; import { GeneralizedRange } from "../../types/GeneralizedRange"; import { TextDocument } from "../../types/TextDocument"; import type { TextDocumentChangeEvent } from "../types/Events"; +import { FlashDescriptor } from "../types/FlashDescriptor"; +import { QuickPickOptions } from "../types/QuickPickOptions"; import { Event, TextEditorSelectionChangeEvent, TextEditorVisibleRangesChangeEvent, } from "../types/events.types"; -import { FlashDescriptor } from "../types/FlashDescriptor"; import type { Disposable, + EditorScopeRanges, IDE, RunMode, WorkspaceFolder, } from "../types/ide.types"; -import { QuickPickOptions } from "../types/QuickPickOptions"; import { FakeCapabilities } from "./FakeCapabilities"; import FakeClipboard from "./FakeClipboard"; import FakeConfiguration from "./FakeConfiguration"; @@ -46,6 +47,11 @@ export default class FakeIDE implements IDE { ): Promise { // empty } + async setScopeVisualizationRanges( + _scopeRanges: EditorScopeRanges[], + ): Promise { + // empty + } onDidOpenTextDocument: Event = dummyEvent; onDidCloseTextDocument: Event = dummyEvent; diff --git a/packages/common/src/ide/types/ide.types.ts b/packages/common/src/ide/types/ide.types.ts index c99e1e711d..dae5d5312e 100644 --- a/packages/common/src/ide/types/ide.types.ts +++ b/packages/common/src/ide/types/ide.types.ts @@ -1,6 +1,7 @@ import type { EditableTextEditor, InputBoxOptions, + ScopeType, TextDocument, TextEditor, } from "../.."; @@ -213,6 +214,20 @@ export interface IDE { editor: TextEditor, ranges: GeneralizedRange[], ): Promise; + + setScopeVisualizationRanges(scopeRanges: EditorScopeRanges[]): Promise; +} + +export interface EditorScopeRanges { + editor: TextEditor; + scopeRanges: ScopeRanges[]; +} + +export interface ScopeRanges { + scopeType: ScopeType; + domain: GeneralizedRange; + contentRanges?: GeneralizedRange[]; + removalRanges?: GeneralizedRange[]; } export interface WorkspaceFolder { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 9949e463f7..2f98a969b4 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -80,3 +80,4 @@ export * from "./extensionDependencies"; export * from "./getFakeCommandServerApi"; export * from "./types/TestCaseFixture"; export * from "./util/getEnvironmentVariableStrict"; +export * from "./util/CompositeKeyDefaultMap"; diff --git a/packages/common/src/util/CompositeKeyDefaultMap.ts b/packages/common/src/util/CompositeKeyDefaultMap.ts new file mode 100644 index 0000000000..24c600a38d --- /dev/null +++ b/packages/common/src/util/CompositeKeyDefaultMap.ts @@ -0,0 +1,38 @@ +/** + * A map that uses a composite key to store values. If a value is not found for + * a given key, the default value is returned. + */ +export class CompositeKeyDefaultMap { + private map = new Map(); + + constructor( + private getDefaultValue: (key: K) => V, + private hashFunction: (key: K) => unknown[], + ) {} + + hash(key: K): string { + return this.hashFunction(key).join("\u0000"); + } + + get(key: K): V { + const stringKey = this.hash(key); + const currentValue = this.map.get(stringKey); + + if (currentValue != null) { + return currentValue; + } + + const value = this.getDefaultValue(key); + this.map.set(stringKey, value); + + return value; + } + + entries(): IterableIterator<[string, V]> { + return this.map.entries(); + } + + values(): IterableIterator { + return this.map.values(); + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer.ts new file mode 100644 index 0000000000..87a12a10c1 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer.ts @@ -0,0 +1,201 @@ +import { + Disposable, + EditorScopeRanges, + Range, + ScopeRanges, + ScopeType, + TextEditor, + showError, + toCharacterRange, + toLineRange, +} from "@cursorless/common"; +import { last } from "lodash"; +import { VisualizationType } from "./VisualizationType"; +import { Debouncer } from "./core/Debouncer"; +import { ScopeHandlerFactory } from "./processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; +import { ScopeHandler } from "./processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { ide } from "./singletons/ide.singleton"; + +interface VisualizationInfo { + scopeType: ScopeType; + visualizationType: VisualizationType; +} + +export class ScopeVisualizer implements Disposable { + private shownErrorMessages = new Set(); + + async setScopeType(visualizationInfo: VisualizationInfo | undefined) { + this.visualizationInfo = visualizationInfo; + // Clear highlights becasue VSCode seems to behave strangely when + // changing the highlight type while highlights are active. Would probably + // be better to have this happen in VSCode-specific impl, but that's tricky + // because the VSCode impl doesn't know about the visualization type. + await this.clearHighlights(); + this.debouncer.run(); + } + + private disposables: Disposable[] = []; + private debouncer = new Debouncer(() => this.highlightScopes()); + private visualizationInfo: VisualizationInfo | undefined = undefined; + + constructor(private scopeHandlerFactory: ScopeHandlerFactory) { + this.disposables.push( + // An event that fires when a text document opens + ide().onDidOpenTextDocument(this.debouncer.run), + // An Event that fires when a text document closes + ide().onDidCloseTextDocument(this.debouncer.run), + // An Event which fires when the array of visible editors has changed. + ide().onDidChangeVisibleTextEditors(this.debouncer.run), + // An event that is emitted when a text document is changed. This usually happens when the contents changes but also when other things like the dirty-state changes. + ide().onDidChangeTextDocument(this.debouncer.run), + ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), + this.debouncer, + ); + + this.debouncer.run(); + } + + private async clearHighlights() { + await ide().setScopeVisualizationRanges( + ide().visibleTextEditors.map((editor) => ({ editor, scopeRanges: [] })), + ); + } + + private getErrorSetKey(scopeType: ScopeType, languageId: string) { + return `${JSON.stringify(scopeType)}:${languageId}`; + } + + private async highlightScopes() { + if (this.visualizationInfo == null) { + return; + } + + const editorScopeRanges: EditorScopeRanges[] = []; + + for (const editor of ide().visibleTextEditors) { + const { document } = editor; + + const { scopeType, visualizationType } = this.visualizationInfo!; + const scopeHandler = this.scopeHandlerFactory.create( + scopeType, + document.languageId, + ); + + if (scopeHandler == null) { + const errorSetKey = this.getErrorSetKey(scopeType, document.languageId); + if (!this.shownErrorMessages.has(errorSetKey)) { + this.shownErrorMessages.add(errorSetKey); + showError( + ide().messages, + "ScopeVisualizer.scopeTypeNotSupported", + `Scope type not supported for ${document.languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.`, + ); + } + editorScopeRanges.push({ editor, scopeRanges: [] }); + continue; + } + + const iterationRange = getIterationRange(editor, scopeHandler); + + const scopes = Array.from( + scopeHandler.generateScopes(editor, iterationRange.start, "forward", { + includeDescendantScopes: true, + distalPosition: iterationRange.end, + }) ?? [], + ); + + editorScopeRanges.push({ + editor, + scopeRanges: scopes.map((scope) => { + const targets = scope.getTargets(false); + const scopeRanges: ScopeRanges = { + scopeType, + domain: toCharacterRange(scope.domain), + }; + + switch (visualizationType) { + case VisualizationType.content: + scopeRanges.contentRanges = targets.map((target) => + toCharacterRange(target.contentRange), + ); + break; + case VisualizationType.removal: + scopeRanges.removalRanges = targets.map((target) => + target.isLine + ? toLineRange(target.getRemovalHighlightRange()) + : toCharacterRange(target.getRemovalHighlightRange()), + ); + break; + } + + return scopeRanges; + }), + }); + } + + await ide().setScopeVisualizationRanges(editorScopeRanges); + } + + dispose(): void { + this.disposables.forEach(({ dispose }) => { + try { + dispose(); + } catch (e) { + // do nothing + } + }); + + this.clearHighlights(); + } +} + +/** + * Get the range to iterate over for the given editor. We take the union of all + * visible ranges, add 10 lines either side to make scrolling a bit smoother, + * and then expand to the largest ancestor of the start and end of the visible + * range, so that we properly show nesting. + * @param editor The editor to get the iteration range for + * @param scopeHandler The scope handler to use + * @returns The range to iterate over + */ +function getIterationRange( + editor: TextEditor, + scopeHandler: ScopeHandler, +): Range { + let visibleRange = editor.visibleRanges.reduce((acc, range) => + acc.union(range), + ); + + visibleRange = editor.document.range.intersection( + visibleRange.with( + visibleRange.start.translate(-10), + visibleRange.end.translate(10), + ), + )!; + + // Expand to largest ancestor of start of visible range FIXME: It's + // possible that the removal range will be bigger than the domain range, + // in which case we'll miss a scope if its removal range is visible but + // its domain range is not. I don't think we care that much; they can + // scroll, and we have the extra 10 lines on either side which might help. + const expandedStart = + last( + Array.from( + scopeHandler.generateScopes(editor, visibleRange.start, "forward", { + containment: "required", + }), + ), + )?.domain ?? visibleRange; + + // Expand to largest ancestor of end of visible range + const expandedEnd = + last( + Array.from( + scopeHandler.generateScopes(editor, visibleRange.end, "forward", { + containment: "required", + }), + ), + )?.domain ?? visibleRange; + + return expandedStart.union(expandedEnd); +} diff --git a/packages/cursorless-engine/src/VisualizationType.ts b/packages/cursorless-engine/src/VisualizationType.ts new file mode 100644 index 0000000000..9744699c83 --- /dev/null +++ b/packages/cursorless-engine/src/VisualizationType.ts @@ -0,0 +1,4 @@ +export enum VisualizationType { + content = "content", + removal = "removal", +} diff --git a/packages/cursorless-engine/src/core/Debouncer.ts b/packages/cursorless-engine/src/core/Debouncer.ts new file mode 100644 index 0000000000..071387d98c --- /dev/null +++ b/packages/cursorless-engine/src/core/Debouncer.ts @@ -0,0 +1,30 @@ +import { ide } from "../singletons/ide.singleton"; + +export class Debouncer { + private timeoutHandle: NodeJS.Timeout | null = null; + + constructor(private callback: () => void) { + this.run = this.run.bind(this); + } + + run() { + if (this.timeoutHandle != null) { + clearTimeout(this.timeoutHandle); + } + + const decorationDebounceDelayMs = ide().configuration.getOwnConfiguration( + "decorationDebounceDelayMs", + ); + + this.timeoutHandle = setTimeout(() => { + this.callback(); + this.timeoutHandle = null; + }, decorationDebounceDelayMs); + } + + dispose() { + if (this.timeoutHandle != null) { + clearTimeout(this.timeoutHandle); + } + } +} diff --git a/packages/cursorless-engine/src/core/HatAllocator.ts b/packages/cursorless-engine/src/core/HatAllocator.ts index d43a7f2ead..a5ccef2384 100644 --- a/packages/cursorless-engine/src/core/HatAllocator.ts +++ b/packages/cursorless-engine/src/core/HatAllocator.ts @@ -2,6 +2,7 @@ import type { Disposable, Hats, TokenHat } from "@cursorless/common"; import { ide } from "../singletons/ide.singleton"; import tokenGraphemeSplitter from "../singletons/tokenGraphemeSplitter.singleton"; import { allocateHats } from "../util/allocateHats"; +import { Debouncer } from "./Debouncer"; import { IndividualHatMap } from "./IndividualHatMap"; interface Context { @@ -9,37 +10,37 @@ interface Context { } export class HatAllocator { - private timeoutHandle: NodeJS.Timeout | null = null; private disposables: Disposable[] = []; + private debouncer = new Debouncer(() => this.allocateHats()); constructor(private hats: Hats, private context: Context) { ide().disposeOnExit(this); - this.allocateHatsDebounced = this.allocateHatsDebounced.bind(this); - this.disposables.push( - this.hats.onDidChangeEnabledHatStyles(this.allocateHatsDebounced), - this.hats.onDidChangeIsEnabled(this.allocateHatsDebounced), + this.hats.onDidChangeEnabledHatStyles(this.debouncer.run), + this.hats.onDidChangeIsEnabled(this.debouncer.run), // An event that fires when a text document opens - ide().onDidOpenTextDocument(this.allocateHatsDebounced), + ide().onDidOpenTextDocument(this.debouncer.run), // An event that fires when a text document closes - ide().onDidCloseTextDocument(this.allocateHatsDebounced), + ide().onDidCloseTextDocument(this.debouncer.run), // An Event which fires when the active editor has changed. Note that the event also fires when the active editor changes to undefined. - ide().onDidChangeActiveTextEditor(this.allocateHatsDebounced), + ide().onDidChangeActiveTextEditor(this.debouncer.run), // An Event which fires when the array of visible editors has changed. - ide().onDidChangeVisibleTextEditors(this.allocateHatsDebounced), + ide().onDidChangeVisibleTextEditors(this.debouncer.run), // An event that is emitted when a text document is changed. This usually happens when the contents changes but also when other things like the dirty-state changes. - ide().onDidChangeTextDocument(this.allocateHatsDebounced), + ide().onDidChangeTextDocument(this.debouncer.run), // An Event which fires when the selection in an editor has changed. - ide().onDidChangeTextEditorSelection(this.allocateHatsDebounced), + ide().onDidChangeTextEditorSelection(this.debouncer.run), // An Event which fires when the visible ranges of an editor has changed. - ide().onDidChangeTextEditorVisibleRanges(this.allocateHatsDebounced), + ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), // Re-draw hats on grapheme splitting algorithm change in case they // changed their token hat splitting setting. tokenGraphemeSplitter().registerAlgorithmChangeListener( - this.allocateHatsDebounced, + this.debouncer.run, ), + + this.debouncer, ); } @@ -75,26 +76,7 @@ export class HatAllocator { ); } - allocateHatsDebounced() { - if (this.timeoutHandle != null) { - clearTimeout(this.timeoutHandle); - } - - const decorationDebounceDelayMs = ide().configuration.getOwnConfiguration( - "decorationDebounceDelayMs", - ); - - this.timeoutHandle = setTimeout(() => { - this.allocateHats(); - this.timeoutHandle = null; - }, decorationDebounceDelayMs); - } - dispose() { this.disposables.forEach(({ dispose }) => dispose()); - - if (this.timeoutHandle != null) { - clearTimeout(this.timeoutHandle); - } } } diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index ffcb7f7b3b..4bfeb2c6ea 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,14 +1,23 @@ -import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; +import { + Command, + CommandServerApi, + Hats, + IDE, + ScopeType, +} from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; +import { ScopeVisualizer as ScopeVisualizerImpl } from "./ScopeVisualizer"; +import { VisualizationType } from "./VisualizationType"; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; +import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; +import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; +import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; -import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; -import { runCommand } from "./runCommand"; export function createCursorlessEngine( treeSitter: TreeSitter, @@ -40,6 +49,10 @@ export function createCursorlessEngine( const languageDefinitions = new LanguageDefinitions(treeSitter); + const scopeVisualizer = new ScopeVisualizerImpl( + new ScopeHandlerFactoryImpl(languageDefinitions), + ); + return { commandApi: { runCommand(command: Command) { @@ -68,6 +81,17 @@ export function createCursorlessEngine( ); }, }, + scopeVisualizer: { + start(scopeType: ScopeType, visualizationType: string) { + scopeVisualizer.setScopeType({ + scopeType, + visualizationType: visualizationType as VisualizationType, + }); + }, + stop() { + scopeVisualizer.setScopeType(undefined); + }, + }, testCaseRecorder, storedTargets, hatTokenMap, @@ -92,8 +116,14 @@ export interface CommandApi { runCommandSafe(...args: unknown[]): Promise; } +export interface ScopeVisualizer { + start(scopeType: ScopeType, visualizationType: string): void; + stop(): void; +} + export interface CursorlessEngine { commandApi: CommandApi; + scopeVisualizer: ScopeVisualizer; testCaseRecorder: TestCaseRecorder; storedTargets: StoredTargetMap; hatTokenMap: HatTokenMapImpl; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts index 599fbbc113..a8d71ff9ec 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts @@ -14,6 +14,7 @@ const DEFAULT_REQUIREMENTS: Omit = containment: null, allowAdjacentScopes: false, skipAncestorScopes: false, + includeDescendantScopes: false, }; /** diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 9224802c99..815e7926b3 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -149,4 +149,6 @@ export interface ScopeIteratorRequirements { * @default false */ skipAncestorScopes: boolean; + + includeDescendantScopes: boolean; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/shouldYieldScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/shouldYieldScope.ts index 60d29d19ea..81c6dcac1a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/shouldYieldScope.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/shouldYieldScope.ts @@ -31,9 +31,17 @@ export function shouldYieldScope( checkRequirements(initialPosition, requirements, previousScope, scope) && // Note that we're using `currentPosition` instead of `initialPosition` // below, because we want to filter out scopes that are strictly contained - // by previous scopes. + // by previous scopes. However, if we want to include descendant scopes, + // then we do use the initial position (previousScope == null || - compareTargetScopes(direction, currentPosition, previousScope, scope) < 0) + compareTargetScopes( + direction, + requirements.includeDescendantScopes + ? initialPosition + : currentPosition, + previousScope, + scope, + ) < 0) ); } diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index 738d9f73f7..f778815c37 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -399,6 +399,154 @@ }, "additionalProperties": false }, + "cursorless.scopeVisualizer.colors.dark": { + "description": "Colors to use for scope visualizer with dark themes", + "type": "object", + "properties": { + "domain": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } + }, + "content": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } + }, + "removal": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } + } + }, + "default": { + "domain": { + "background": "#00e1ff18", + "borderSolid": "#ebdeec84", + "borderPorous": "rgba(235, 222, 236, 0.23)" + }, + "content": { + "background": "#ad00bc5b", + "borderSolid": "rgba(238, 0, 255, 0.47)", + "borderPorous": "rgba(235, 222, 236, 0.23)" + }, + "removal": { + "background": "#ff00002d", + "borderSolid": "rgba(255, 0, 0, 0.47)", + "borderPorous": "rgba(255, 0, 0, 0.29)" + } + } + }, + "cursorless.scopeVisualizer.colors.light": { + "description": "Colors to use for scope visualizer with light themes", + "type": "object", + "properties": { + "domain": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } + }, + "content": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } + }, + "removal": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } + } + }, + "default": { + "domain": { + "background": "#00e1ff18", + "borderSolid": "#ebdeec84", + "borderPorous": "rgba(235, 222, 236, 0.23)" + }, + "content": { + "background": "#ad00bc5b", + "borderSolid": "rgba(238, 0, 255, 0.47)", + "borderPorous": "rgba(235, 222, 236, 0.23)" + }, + "removal": { + "background": "#ff00002d", + "borderSolid": "rgba(255, 0, 0, 0.47)", + "borderPorous": "rgba(255, 0, 0, 0.29)" + } + } + }, "cursorless.hatEnablement.colors": { "description": "Which colors to enable", "type": "object", @@ -871,8 +1019,11 @@ "@cursorless/common": "workspace:*", "@cursorless/cursorless-engine": "workspace:*", "@cursorless/vscode-common": "workspace:*", + "@types/tinycolor2": "1.4.3", + "itertools": "^1.7.1", "lodash": "^4.17.21", "semver": "^7.3.9", + "tinycolor2": "1.6.0", "uuid": "^9.0.0", "vscode-uri": "^3.0.6" } diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 21ce44432f..2412228d60 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -63,6 +63,7 @@ export async function activate( testCaseRecorder, storedTargets, hatTokenMap, + scopeVisualizer, snippets, injectIde, runIntegrationTests, @@ -81,6 +82,7 @@ export async function activate( vscodeIDE, commandApi, testCaseRecorder, + scopeVisualizer, keyboardCommands, hats, ); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts new file mode 100644 index 0000000000..36ff8855eb --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -0,0 +1,244 @@ +import { GeneralizedRange, ScopeRanges } from "@cursorless/common"; +import * as vscode from "vscode"; +import { VscodeScopeVisualizerRenderer } from "./VscodeScopeVisualizerRenderer"; +import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; +import tinycolor = require("tinycolor2"); + +interface ThemeColors { + light: string; + dark: string; +} + +export interface RangeTypeColors { + background: ThemeColors; + borderSolid: ThemeColors; + borderPorous: ThemeColors; +} + +interface ScopeVisualizerThemeColorConfig { + domain: { + background: string; + borderSolid: string; + borderPorous: string; + }; + content: { + background: string; + borderSolid: string; + borderPorous: string; + }; + removal: { + background: string; + borderSolid: string; + borderPorous: string; + }; +} + +interface ScopeVisualizerColorConfig { + light: ScopeVisualizerThemeColorConfig; + dark: ScopeVisualizerThemeColorConfig; +} + +interface VscodeTextEditorScopeRanges { + editor: VscodeTextEditorImpl; + scopeRanges: ScopeRanges[]; +} + +export class VscodeScopeVisualizer { + private domainRenderer!: VscodeScopeVisualizerRenderer; + private contentRenderer!: VscodeScopeVisualizerRenderer; + private removalRenderer!: VscodeScopeVisualizerRenderer; + private domainContentOverlappingRenderer!: VscodeScopeVisualizerRenderer; + private domainRemovalOverlappingRenderer!: VscodeScopeVisualizerRenderer; + + constructor(extensionContext: vscode.ExtensionContext) { + this.computeColors(); + + extensionContext.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { + if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { + this.computeColors(); + } + }), + ); + } + + private computeColors() { + const config = vscode.workspace + .getConfiguration("cursorless.scopeVisualizer") + .get("colors")!; + + const domainColors = getColorsFromConfig(config, "domain"); + const contentColors = getColorsFromConfig(config, "content"); + const removalColors = getColorsFromConfig(config, "removal"); + + this.domainRenderer?.dispose(); + this.domainRenderer = new VscodeScopeVisualizerRenderer(domainColors); + this.contentRenderer?.dispose(); + this.contentRenderer = new VscodeScopeVisualizerRenderer(contentColors); + this.removalRenderer?.dispose(); + this.removalRenderer = new VscodeScopeVisualizerRenderer(removalColors); + + this.domainContentOverlappingRenderer?.dispose(); + this.domainContentOverlappingRenderer = new VscodeScopeVisualizerRenderer( + blendRangeTypeColors(domainColors, contentColors), + ); + this.domainRemovalOverlappingRenderer?.dispose(); + this.domainRemovalOverlappingRenderer = new VscodeScopeVisualizerRenderer( + blendRangeTypeColors(domainColors, removalColors), + ); + + this.drawScopes(); + } + + private editorScopeRanges: VscodeTextEditorScopeRanges[] = []; + + async setScopeVisualizationRanges( + editorScopeRanges: VscodeTextEditorScopeRanges[], + ) { + this.editorScopeRanges = editorScopeRanges; + this.drawScopes(); + } + + private drawScopes() { + this.editorScopeRanges.forEach(({ editor, scopeRanges }) => { + this.setScopeVisualizationRangesForEditor(editor, scopeRanges); + }); + } + + async setScopeVisualizationRangesForEditor( + editor: VscodeTextEditorImpl, + scopeRanges: ScopeRanges[], + ): Promise { + const domainRanges: GeneralizedRange[] = []; + const contentRanges: GeneralizedRange[] = []; + const removalRanges: GeneralizedRange[] = []; + const domainEqualsContentRanges: GeneralizedRange[] = []; + const domainEqualsRemovalRanges: GeneralizedRange[] = []; + + for (const scopeRange of scopeRanges) { + if ( + scopeRange.contentRanges?.length === 1 && + (scopeRange.removalRanges?.length ?? 0) === 0 && + isGeneralizedRangeEqual(scopeRange.contentRanges[0], scopeRange.domain) + ) { + domainEqualsContentRanges.push(scopeRange.domain); + continue; + } + + if ( + (scopeRange.contentRanges?.length ?? 0) === 0 && + scopeRange.removalRanges?.length === 1 && + isGeneralizedRangeEqual(scopeRange.removalRanges[0], scopeRange.domain) + ) { + domainEqualsRemovalRanges.push(scopeRange.domain); + continue; + } + + domainRanges.push(scopeRange.domain); + scopeRange.contentRanges?.forEach((range) => contentRanges.push(range)); + scopeRange.removalRanges?.forEach((range) => removalRanges.push(range)); + } + + this.domainRenderer.setRanges(editor, domainRanges); + this.contentRenderer.setRanges(editor, contentRanges); + this.removalRenderer.setRanges(editor, removalRanges); + this.domainContentOverlappingRenderer.setRanges( + editor, + domainEqualsContentRanges, + ); + this.domainRemovalOverlappingRenderer.setRanges( + editor, + domainEqualsRemovalRanges, + ); + } +} + +function getColorsFromConfig( + config: ScopeVisualizerColorConfig, + rangeType: "domain" | "content" | "removal", +): RangeTypeColors { + return { + background: { + light: config.light[rangeType].background, + dark: config.dark[rangeType].background, + }, + borderSolid: { + light: config.light[rangeType].borderSolid, + dark: config.dark[rangeType].borderSolid, + }, + borderPorous: { + light: config.light[rangeType].borderPorous, + dark: config.dark[rangeType].borderPorous, + }, + }; +} + +function isGeneralizedRangeEqual( + a: GeneralizedRange, + b: GeneralizedRange, +): boolean { + if (a.type === "character" && b.type === "character") { + return a.start.isEqual(b.start) && a.end.isEqual(b.end); + } + + if (a.type === "line" && b.type === "line") { + return a.start === b.start && a.end === b.end; + } + + return false; +} + +function blendRangeTypeColors( + baseColors: RangeTypeColors, + topColors: RangeTypeColors, +): RangeTypeColors { + return { + background: { + light: blendColors( + baseColors.background.light, + topColors.background.light, + ), + dark: blendColors(baseColors.background.dark, topColors.background.dark), + }, + borderSolid: { + light: blendColors( + baseColors.borderSolid.light, + topColors.borderSolid.light, + ), + dark: blendColors( + baseColors.borderSolid.dark, + topColors.borderSolid.dark, + ), + }, + borderPorous: { + light: blendColors( + baseColors.borderPorous.light, + topColors.borderPorous.light, + ), + dark: blendColors( + baseColors.borderPorous.dark, + topColors.borderPorous.dark, + ), + }, + }; +} + +function blendColors(base: string, top: string): string { + const baseRgba = tinycolor(base).toRgb(); + const topRgba = tinycolor(top).toRgb(); + const blendedAlpha = 1 - (1 - topRgba.a) * (1 - baseRgba.a); + + function interpolateChannel(channel: "r" | "g" | "b"): number { + return Math.round( + (topRgba[channel] * topRgba.a) / blendedAlpha + + (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha, + ); + } + + return tinycolor({ + r: interpolateChannel("r"), + g: interpolateChannel("g"), + b: interpolateChannel("b"), + a: blendedAlpha, + }).toHex8String(); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts new file mode 100644 index 0000000000..aeba18cb42 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts @@ -0,0 +1,194 @@ +import { + CharacterRange, + CompositeKeyDefaultMap, + GeneralizedRange, + LineRange, + Range, + isLineRange, + partition, +} from "@cursorless/common"; +import { toVscodeRange } from "@cursorless/vscode-common"; +import { chain, flatmap } from "itertools"; +import * as vscode from "vscode"; +import { + DecorationRangeBehavior, + DecorationRenderOptions, + TextEditorDecorationType, + window, +} from "vscode"; +import { RangeTypeColors } from "./VscodeScopeVisualizer"; +import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; +import { generateDecorationsForCharacterRange } from "./getDecorationRanges/generateDecorationsForCharacterRange"; +import { generateDecorationsForLineRange } from "./getDecorationRanges/generateDecorationsForLineRange"; +import { + BorderStyle, + DecorationStyle, + StyleParameters, + StyleParametersRanges, +} from "./getDecorationRanges/getDecorationRanges.types"; +import { getDifferentiatedRanges } from "./getDecorationRanges/getDifferentiatedRanges"; + +/** + * Manages VSCode decoration types for a highlight or flash style. + */ +export class VscodeScopeVisualizerRenderer { + private decorator: Decorator; + + constructor(colors: RangeTypeColors) { + this.decorator = new Decorator(colors); + } + + setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { + const [lineRanges, characterRanges] = partition( + ranges, + isLineRange, + ); + + const decoratedRanges = Array.from( + chain( + flatmap(characterRanges, ({ start, end }) => + generateDecorationsForCharacterRange(editor, new Range(start, end)), + ), + flatmap(lineRanges, ({ start, end }) => + generateDecorationsForLineRange(start, end), + ), + ), + ); + + this.decorator.setDecorations( + editor, + getDifferentiatedRanges(decoratedRanges, getBorderKey), + ); + } + + dispose() { + this.decorator.dispose(); + } +} + +function getBorderKey({ + top, + right, + left, + bottom, + isWholeLine, +}: DecorationStyle) { + return [top, right, left, bottom, isWholeLine ?? false]; +} + +class Decorator { + private decorationTypes: CompositeKeyDefaultMap< + StyleParameters, + TextEditorDecorationType + >; + + constructor(colors: RangeTypeColors) { + this.decorationTypes = new CompositeKeyDefaultMap( + ({ style }) => getDecorationStyle(colors, style), + ({ + style: { top, right, bottom, left, isWholeLine }, + differentiationIndex, + }) => [ + top, + right, + bottom, + left, + isWholeLine ?? false, + differentiationIndex, + ], + ); + } + + setDecorations( + editor: VscodeTextEditorImpl, + decoratedRanges: StyleParametersRanges[], + ) { + const untouchedDecorationTypes = new Set(this.decorationTypes.values()); + + decoratedRanges.forEach(({ styleParameters, ranges }) => { + const decorationType = this.decorationTypes.get(styleParameters); + + editor.vscodeEditor.setDecorations( + decorationType, + ranges.map(toVscodeRange), + ); + + untouchedDecorationTypes.delete(decorationType); + }); + + untouchedDecorationTypes.forEach((decorationType) => { + editor.vscodeEditor.setDecorations(decorationType, []); + }); + } + + dispose() { + Array.from(this.decorationTypes.values()).forEach((decorationType) => { + decorationType.dispose(); + }); + } +} + +function getDecorationStyle( + colors: RangeTypeColors, + borders: DecorationStyle, +): vscode.TextEditorDecorationType { + const options: DecorationRenderOptions = { + light: { + backgroundColor: colors.background.light, + borderColor: getBorderColor( + colors.borderSolid.light, + colors.borderPorous.light, + borders, + ), + }, + dark: { + backgroundColor: colors.background.dark, + borderColor: getBorderColor( + colors.borderSolid.dark, + colors.borderPorous.dark, + borders, + ), + }, + borderStyle: getBorderStyle(borders), + borderWidth: "1px", + borderRadius: getBorderRadius(borders), + rangeBehavior: DecorationRangeBehavior.ClosedClosed, + isWholeLine: borders.isWholeLine, + }; + + return window.createTextEditorDecorationType(options); +} + +function getBorderStyle(borders: DecorationStyle): string { + return [borders.top, borders.right, borders.bottom, borders.left].join(" "); +} + +function getBorderColor( + solidColor: string, + porousColor: string, + borders: DecorationStyle, +): string { + return [ + borders.top === BorderStyle.solid ? solidColor : porousColor, + borders.right === BorderStyle.solid ? solidColor : porousColor, + borders.bottom === BorderStyle.solid ? solidColor : porousColor, + borders.left === BorderStyle.solid ? solidColor : porousColor, + ].join(" "); +} + +function getBorderRadius(borders: DecorationStyle): string { + return [ + borders.top === BorderStyle.solid && borders.left === BorderStyle.solid + ? "2px" + : "0px", + borders.top === BorderStyle.solid && borders.right === BorderStyle.solid + ? "2px" + : "0px", + borders.bottom === BorderStyle.solid && borders.right === BorderStyle.solid + ? "2px" + : "0px", + borders.bottom === BorderStyle.solid && borders.left === BorderStyle.solid + ? "2px" + : "0px", + ].join(" "); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/block.test.txt b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/block.test.txt new file mode 100644 index 0000000000..246df004b2 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/block.test.txt @@ -0,0 +1,23 @@ + hello world +testing + + hello world +testing whatever + + hello world +testing whatever and another test + +hello world +testing whatever and another test + +hello there +another + +test +test + + testing + test + + another +test diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForCharacterRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForCharacterRange.ts new file mode 100644 index 0000000000..8714513fc8 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForCharacterRange.ts @@ -0,0 +1,35 @@ +import { Range, TextEditor } from "@cursorless/common"; +import { range } from "lodash"; +import { BorderStyle, DecoratedRange } from "./getDecorationRanges.types"; +import { handleMultipleLines } from "./handleMultipleLines"; + +export function* generateDecorationsForCharacterRange( + editor: TextEditor, + characterRange: Range, +): Iterable { + if (characterRange.isSingleLine) { + yield { + range: characterRange, + style: { + top: BorderStyle.solid, + right: BorderStyle.solid, + bottom: BorderStyle.solid, + left: BorderStyle.solid, + }, + }; + return; + } + + const { document } = editor; + const lineRanges = range( + characterRange.start.line, + characterRange.end.line + 1, + ).map((lineNumber) => document.lineAt(lineNumber).range); + lineRanges[0] = lineRanges[0].with(characterRange.start); + lineRanges[lineRanges.length - 1] = lineRanges[lineRanges.length - 1].with( + undefined, + characterRange.end, + ); + + yield* handleMultipleLines(lineRanges); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForLineRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForLineRange.ts new file mode 100644 index 0000000000..40a05ce9cf --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForLineRange.ts @@ -0,0 +1,58 @@ +import { Range } from "@cursorless/common"; +import { BorderStyle, DecoratedRange } from "./getDecorationRanges.types"; + +export function* generateDecorationsForLineRange( + startLine: number, + endLine: number, +): Iterable { + const lineCount = endLine - startLine + 1; + + if (lineCount === 1) { + yield { + range: new Range(startLine, 0, startLine, 0), + style: { + top: BorderStyle.solid, + right: BorderStyle.none, + bottom: BorderStyle.solid, + left: BorderStyle.none, + isWholeLine: true, + }, + }; + return; + } + + yield { + range: new Range(startLine, 0, startLine, 0), + style: { + top: BorderStyle.solid, + right: BorderStyle.none, + bottom: BorderStyle.none, + left: BorderStyle.none, + isWholeLine: true, + }, + }; + + if (lineCount > 2) { + yield { + range: new Range(startLine + 1, 0, endLine - 1, 0), + style: { + top: BorderStyle.none, + right: BorderStyle.none, + bottom: BorderStyle.none, + left: BorderStyle.none, + isWholeLine: true, + }, + }; + } + + yield { + range: new Range(endLine, 0, endLine, 0), + style: { + top: BorderStyle.none, + right: BorderStyle.none, + bottom: BorderStyle.solid, + left: BorderStyle.none, + isWholeLine: true, + }, + }; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.test.ts new file mode 100644 index 0000000000..6c3022b42d --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.test.ts @@ -0,0 +1,93 @@ +import assert = require("assert"); +import { getDecorationRanges } from "./getDecorationRanges"; +import { DecorationStyle, StyleParameters } from "./getDecorationRanges.types"; +import { + Range, + RangePlainObject, + rangeToPlainObject, +} from "@cursorless/common"; + +interface RangeDescription { + startLine?: number; + endLine?: number; + lineCharOffsets: [number, number][]; +} + +export interface ExpectedResult { + styleParameters: StyleParameters; + ranges: RangePlainObject[]; +} + +interface TestCase { + name: string; + ranges: RangeDescription[]; + expectedDecorations: ExpectedResult[]; +} + +const testCases: TestCase[] = [ + { + name: "should handle simple case", + ranges: [ + { + lineCharOffsets: [[0, 1]], + }, + ], + expectedDecorations: [ + { + styleParameters: { + style: { + top: true, + right: true, + bottom: true, + left: true, + }, + differentiationIndex: 0, + }, + ranges: [ + { + start: { + line: 0, + character: 0, + }, + end: { + line: 0, + character: 1, + }, + }, + ], + }, + ], + }, +]; + +suite("getDecorationRanges", function () { + for (const testCase of testCases) { + test(testCase.name, function () { + const actualDecorations = getDecorationRanges( + { + document: { + lineCount: testCase.ranges.length, + }, + } as any, + testCase.ranges.map( + ({ startLine, endLine, lineCharOffsets }) => + new Range( + startLine ?? 0, + lineCharOffsets[0][0], + endLine ?? 0, + lineCharOffsets[lineCharOffsets.length - 1][1], + ), + ), + ); + + assert.deepStrictEqual( + actualDecorations.map(({ styleParameters, ranges }) => ({ + styleParameters, + ranges: ranges.map(rangeToPlainObject), + })), + + testCase.expectedDecorations, + ); + }); + } +}); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.types.ts new file mode 100644 index 0000000000..1f667b7506 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.types.ts @@ -0,0 +1,32 @@ +import { Range } from "@cursorless/common"; + +export enum BorderStyle { + porous = "dashed", + solid = "solid", + none = "none", +} + +export interface DecorationStyle { + top: BorderStyle; + bottom: BorderStyle; + left: BorderStyle; + right: BorderStyle; + isWholeLine?: boolean; +} + +export interface StyledRange { + range: Range; + style: T; +} + +export type DecoratedRange = StyledRange; + +export interface StyleParameters { + style: T; + differentiationIndex: number; +} + +export interface StyleParametersRanges { + styleParameters: StyleParameters; + ranges: Range[]; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.test.ts new file mode 100644 index 0000000000..3b6624e8d9 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.test.ts @@ -0,0 +1,148 @@ +import assert = require("assert"); +import { StyleParameters } from "./getDecorationRanges.types"; +import { Position, Range } from "@cursorless/common"; +import { getDifferentiatedRanges } from "./getDifferentiatedRanges"; + +type Offsets = [number, number]; + +interface ExpectedResult { + styleParameters: StyleParameters; + ranges: Offsets[]; +} + +interface TestStyledRange { + range: Offsets; + style: number; +} + +interface TestCase { + name: string; + ranges: TestStyledRange[]; + expectedDecorations: ExpectedResult[]; +} + +const testCases: TestCase[] = [ + { + name: "should handle simple case", + ranges: [ + { + range: [0, 1], + style: 0, + }, + ], + expectedDecorations: [ + { + styleParameters: { + style: 0, + differentiationIndex: 0, + }, + ranges: [[0, 1]], + }, + ], + }, + + { + name: "should handle adjacent ranges", + ranges: [ + { + range: [0, 1], + style: 0, + }, + { + range: [1, 2], + style: 0, + }, + { + range: [2, 3], + style: 0, + }, + ], + expectedDecorations: [ + { + styleParameters: { + style: 0, + differentiationIndex: 0, + }, + ranges: [ + [0, 1], + [2, 3], + ], + }, + { + styleParameters: { + style: 0, + differentiationIndex: 1, + }, + ranges: [[1, 2]], + }, + ], + }, + + { + name: "should handle nested ranges", + ranges: [ + { + range: [0, 1], + style: 0, + }, + { + range: [2, 3], + style: 0, + }, + { + range: [0, 3], + style: 0, + }, + ], + expectedDecorations: [ + { + styleParameters: { + style: 0, + differentiationIndex: 0, + }, + ranges: [ + [0, 1], + [2, 3], + ], + }, + { + styleParameters: { + style: 0, + differentiationIndex: 1, + }, + ranges: [[0, 3]], + }, + ], + }, +]; + +suite("getDecorationRanges", function () { + for (const testCase of testCases) { + test(testCase.name, function () { + const actualDecorations = getDifferentiatedRanges( + testCase.ranges.map(({ range, style }) => ({ + range: toRange(range), + style, + })), + (style) => [style], + ).map(({ styleParameters, ranges }) => ({ + styleParameters, + ranges: ranges.map(fromRange), + })); + + assert.deepStrictEqual(actualDecorations, testCase.expectedDecorations); + }); + } +}); + +function fromRange(range: Range): Offsets { + return [fromPosition(range.start), fromPosition(range.end)]; +} + +function fromPosition(position: Position): number { + return position.character; +} + +function toRange([start, end]: Offsets): Range { + return new Range(0, start, 0, end); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.ts new file mode 100644 index 0000000000..19cb9049c5 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.ts @@ -0,0 +1,80 @@ +import { CompositeKeyDefaultMap, Range } from "@cursorless/common"; +import { groupBy } from "lodash"; +import { + StyleParametersRanges, + StyleParameters, + StyledRange, +} from "./getDecorationRanges.types"; +import { ifilter, take } from "itertools"; + +export function getDifferentiatedRanges( + decoratedRanges: StyledRange[], + getKeyList: (style: T) => unknown[], +): StyleParametersRanges[] { + const groups = groupBy(decoratedRanges, ({ style }) => + getKeyList(style).join("\u0000"), + ); + + const decorations: CompositeKeyDefaultMap< + StyleParameters, + StyleParametersRanges + > = new CompositeKeyDefaultMap( + (styleParameters) => ({ styleParameters, ranges: [] }), + ({ style, differentiationIndex }) => [ + ...getKeyList(style), + differentiationIndex, + ], + ); + + Object.entries(groups).forEach(([_, decoratedRanges]) => { + decoratedRanges.sort((a, b) => a.range.start.compareTo(b.range.start)); + // Generate extra decorations as necessary to ensure that there are no + // examples of the same decoration type touching each other. + let currentRanges: RangeInfo[] = []; + + for (const { range, style } of decoratedRanges) { + currentRanges = [ + ...currentRanges.filter( + ({ range: previousRange }) => + previousRange.intersection(range) != null, + ), + ]; + + const differentiationIndex = take( + 1, + ifilter( + irange(), + (i) => + !currentRanges.some( + ({ differentiationIndex }) => differentiationIndex === i, + ), + ), + )[0]; + + decorations + .get({ + style, + differentiationIndex, + }) + .ranges.push(range); + + currentRanges.push({ + range, + differentiationIndex, + }); + } + }); + + return Array.from(decorations.values()); +} + +function* irange(): Iterable { + for (let i = 0; ; i++) { + yield i; + } +} + +interface RangeInfo { + range: Range; + differentiationIndex: number; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/handleMultipleLines.ts new file mode 100644 index 0000000000..c991d356ba --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/handleMultipleLines.ts @@ -0,0 +1,207 @@ +import { Range } from "@cursorless/common"; +import { + BorderStyle, + DecorationStyle, + DecoratedRange, +} from "./getDecorationRanges.types"; +import { flatmap } from "itertools"; + +export function* handleMultipleLines( + lineRanges: Range[], +): Iterable { + yield* flatmap(generateLineGroupings(lineRanges), handleLine); +} + +export function* generateLineGroupings( + lineRanges: Range[], +): Iterable { + for (let i = 0; i < lineRanges.length; i++) { + const previousLine = i === 0 ? null : lineRanges[i - 1]; + const currentLine = lineRanges[i]; + const nextLine = i === lineRanges.length - 1 ? null : lineRanges[i + 1]; + yield { + lineNumber: currentLine.start.line, + + previousLine: + previousLine == null + ? null + : { + start: previousLine.start.character, + end: previousLine.end.character, + isFirst: i === 1, + isLast: false, + }, + + currentLine: { + start: currentLine.start.character, + end: currentLine.end.character, + isFirst: i === 0, + isLast: i === lineRanges.length - 1, + }, + + nextLine: + nextLine == null + ? null + : { + start: nextLine.start.character, + end: nextLine.end.character, + isFirst: false, + isLast: i === lineRanges.length - 2, + }, + }; + } +} + +interface LineGrouping { + lineNumber: number; + previousLine: Line | null; + currentLine: Line; + nextLine: Line | null; +} + +interface Line { + start: number; + end: number; + isFirst: boolean; + isLast: boolean; +} + +function* handleLine({ + lineNumber, + previousLine, + currentLine, + nextLine, +}: LineGrouping): Iterable { + const events: Event[] = [ + ...(previousLine == null + ? [] + : [ + { + offset: previousLine.start, + lineType: LineType.previous, + isStart: true, + }, + { + offset: previousLine.end, + lineType: LineType.previous, + isStart: false, + }, + ]), + { + offset: currentLine.end, + lineType: LineType.current, + isStart: false, + }, + ...(nextLine == null + ? [] + : [ + { + offset: nextLine.end, + lineType: LineType.next, + isStart: false, + }, + ]), + ]; + + events.sort((a, b) => { + if (a.offset === b.offset) { + if (a.lineType === LineType.current) { + return 1; + } + return a.isStart ? -1 : 1; + } + + return a.offset - b.offset; + }); + + const currentDecoration: DecorationStyle = { + top: + previousLine == null || previousLine.isFirst + ? BorderStyle.solid + : BorderStyle.none, + bottom: currentLine.isLast ? BorderStyle.solid : BorderStyle.none, + left: currentLine.isFirst ? BorderStyle.solid : BorderStyle.porous, + right: BorderStyle.none, + }; + + let currentOffset = currentLine.start; + let yieldedAnything = false; + let isDone = false; + + for (const { offset, lineType, isStart } of events) { + if (isDone) { + break; + } + if (offset > currentOffset) { + yield { + range: new Range(lineNumber, currentOffset, lineNumber, offset), + style: { + ...currentDecoration, + right: + offset === currentLine.end + ? currentLine.isLast + ? BorderStyle.solid + : BorderStyle.porous + : BorderStyle.none, + }, + }; + yieldedAnything = true; + currentDecoration.left = BorderStyle.none; + } + + switch (lineType) { + case LineType.previous: + if (isStart) { + currentDecoration.top = BorderStyle.none; + } else { + currentDecoration.top = BorderStyle.porous; + } + break; + case LineType.current: + if (!isStart) { + isDone = true; + } + break; + case LineType.next: + if (isStart) { + currentDecoration.bottom = BorderStyle.none; + } else { + currentDecoration.bottom = nextLine!.isLast + ? BorderStyle.solid + : BorderStyle.porous; + } + break; + } + + if (currentOffset < offset) { + currentOffset = offset; + } + } + + if (!yieldedAnything) { + yield { + range: new Range( + lineNumber, + currentLine.start, + lineNumber, + currentLine.end, + ), + style: { + ...currentDecoration, + right: currentLine.isLast ? BorderStyle.solid : BorderStyle.porous, + }, + }; + } +} + +interface Event { + offset: number; + lineType: LineType; + isStart: boolean; +} + +enum LineType { + previous = -1, + current = 0, + next = 1, +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts new file mode 100644 index 0000000000..1565cbc86a --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts @@ -0,0 +1 @@ +export * from "./VscodeScopeVisualizer"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts index 761bfb5fd9..a7d48a4504 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts @@ -1,6 +1,7 @@ import { Disposable, EditableTextEditor, + EditorScopeRanges, FlashDescriptor, GeneralizedRange, HighlightId, @@ -19,7 +20,7 @@ import { import { pull } from "lodash"; import { v4 as uuid } from "uuid"; import * as vscode from "vscode"; -import { ExtensionContext, window, workspace, WorkspaceFolder } from "vscode"; +import { ExtensionContext, WorkspaceFolder, window, workspace } from "vscode"; import { VscodeCapabilities } from "./VscodeCapabilities"; import VscodeClipboard from "./VscodeClipboard"; import VscodeConfiguration from "./VscodeConfiguration"; @@ -29,9 +30,10 @@ import VscodeGlobalState from "./VscodeGlobalState"; import VscodeHighlights, { HighlightStyle } from "./VscodeHighlights"; import VscodeMessages from "./VscodeMessages"; import { vscodeRunMode } from "./VscodeRunMode"; -import { vscodeShowQuickPick } from "./vscodeShowQuickPick"; +import { VscodeScopeVisualizer } from "./VSCodeScopeVisualizer"; import { VscodeTextDocumentImpl } from "./VscodeTextDocumentImpl"; import { VscodeTextEditorImpl } from "./VscodeTextEditorImpl"; +import { vscodeShowQuickPick } from "./vscodeShowQuickPick"; export class VscodeIDE implements IDE { readonly configuration: VscodeConfiguration; @@ -42,6 +44,7 @@ export class VscodeIDE implements IDE { private flashHandler: VscodeFlashHandler; private highlights: VscodeHighlights; private editorMap; + private scopeVisualizer: VscodeScopeVisualizer; constructor(private extensionContext: ExtensionContext) { this.configuration = new VscodeConfiguration(this); @@ -50,6 +53,7 @@ export class VscodeIDE implements IDE { this.clipboard = new VscodeClipboard(); this.highlights = new VscodeHighlights(extensionContext); this.flashHandler = new VscodeFlashHandler(this, this.highlights); + this.scopeVisualizer = new VscodeScopeVisualizer(extensionContext); this.capabilities = new VscodeCapabilities(); this.editorMap = new WeakMap(); } @@ -77,6 +81,15 @@ export class VscodeIDE implements IDE { ); } + setScopeVisualizationRanges(scopeRanges: EditorScopeRanges[]): Promise { + return this.scopeVisualizer.setScopeVisualizationRanges( + scopeRanges.map(({ editor, scopeRanges }) => ({ + editor: editor as VscodeTextEditorImpl, + scopeRanges, + })), + ); + } + flashRanges(flashDescriptors: FlashDescriptor[]): Promise { return this.flashHandler.flashRanges(flashDescriptors); } diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index bba8787b45..5c3397d067 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -5,6 +5,7 @@ import { } from "@cursorless/common"; import { CommandApi, + ScopeVisualizer, TestCaseRecorder, showCheatsheet, updateDefaults, @@ -20,6 +21,7 @@ export function registerCommands( vscodeIde: VscodeIDE, commandApi: CommandApi, testCaseRecorder: TestCaseRecorder, + scopeVisualizer: ScopeVisualizer, keyboardCommands: KeyboardCommands, hats: VscodeHats, ): void { @@ -56,6 +58,10 @@ export function registerCommands( ["cursorless.toggleDecorations"]: hats.toggle, ["cursorless.recomputeDecorationStyles"]: hats.recomputeDecorationStyles, + // Scope visualizer + ["cursorless.showScopeVisualizer"]: scopeVisualizer.start, + ["cursorless.hideScopeVisualizer"]: scopeVisualizer.stop, + // General keyboard commands ["cursorless.keyboard.escape"]: keyboardCommands.keyboardHandler.cancelActiveListener, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5f7d13a75..b4b8944f99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -381,12 +381,21 @@ importers: '@cursorless/vscode-common': specifier: workspace:* version: link:../vscode-common + '@types/tinycolor2': + specifier: 1.4.3 + version: 1.4.3 + itertools: + specifier: ^1.7.1 + version: 1.7.1 lodash: specifier: ^4.17.21 version: 4.17.21 semver: specifier: ^7.3.9 version: 7.4.0 + tinycolor2: + specifier: 1.6.0 + version: 1.6.0 uuid: specifier: ^9.0.0 version: 9.0.0 @@ -5668,6 +5677,10 @@ packages: /@types/stack-utils@2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + /@types/tinycolor2@1.4.3: + resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==} + dev: false + /@types/tough-cookie@4.0.2: resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} dev: true @@ -15548,6 +15561,10 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: false + /tinylogic@2.0.0: resolution: {integrity: sha512-dljTkiLLITtsjqBvTA1MRZQK/sGP4kI3UJKc3yA9fMzYbMF2RhcN04SeROVqJBIYYOoJMM8u0WDnhFwMSFQotw==} dev: true From cc4996e2e98e2b53a11e7a74d4903d86fee4d38b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 19 Jun 2023 15:06:33 +0100 Subject: [PATCH 02/61] Add ScopeVisualizer tests --- packages/common/src/ide/fake/FakeIDE.ts | 1 + packages/common/src/ide/spy/SpyIDE.ts | 27 ++- packages/common/src/testUtil/runTestSubset.ts | 2 +- packages/common/src/testUtil/toPlainObject.ts | 25 ++ .../cursorless-engine/src/ScopeVisualizer.ts | 2 +- .../cursorless-engine/src/cursorlessEngine.ts | 10 +- .../upgradeDecorations.ts | 1 + .../src/suite/scopeVisualizer.vscode.test.ts | 219 ++++++++++++++++++ 8 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index 1d4a5b57a6..d5aca980ad 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -47,6 +47,7 @@ export default class FakeIDE implements IDE { ): Promise { // empty } + async setScopeVisualizationRanges( _scopeRanges: EditorScopeRanges[], ): Promise { diff --git a/packages/common/src/ide/spy/SpyIDE.ts b/packages/common/src/ide/spy/SpyIDE.ts index 716b2113f4..0cbaf15abb 100644 --- a/packages/common/src/ide/spy/SpyIDE.ts +++ b/packages/common/src/ide/spy/SpyIDE.ts @@ -3,7 +3,12 @@ import { GeneralizedRange } from "../../types/GeneralizedRange"; import { TextEditor } from "../../types/TextEditor"; import PassthroughIDEBase from "../PassthroughIDEBase"; import { FlashDescriptor } from "../types/FlashDescriptor"; -import type { HighlightId, IDE } from "../types/ide.types"; +import type { + HighlightId, + IDE, + EditorScopeRanges, + ScopeRanges, +} from "../types/ide.types"; import SpyMessages, { Message } from "./SpyMessages"; interface Highlight { @@ -11,16 +16,22 @@ interface Highlight { ranges: GeneralizedRange[]; } +interface ScopeVisualization { + scopeRanges: ScopeRanges[]; +} + export interface SpyIDERecordedValues { messages?: Message[]; flashes?: FlashDescriptor[]; highlights?: Highlight[]; + scopeVisualizations?: ScopeVisualization[]; } export default class SpyIDE extends PassthroughIDEBase { messages: SpyMessages; private flashes: FlashDescriptor[] = []; private highlights: Highlight[] = []; + private scopeVisualizations: ScopeVisualization[] = []; constructor(original: IDE) { super(original); @@ -32,6 +43,10 @@ export default class SpyIDE extends PassthroughIDEBase { messages: this.messages.getSpyValues(), flashes: isFlashTest ? this.flashes : undefined, highlights: this.highlights.length === 0 ? undefined : this.highlights, + scopeVisualizations: + this.scopeVisualizations.length === 0 + ? undefined + : this.scopeVisualizations, }; return values(ret).every((value) => value == null) @@ -55,4 +70,14 @@ export default class SpyIDE extends PassthroughIDEBase { }); return super.setHighlightRanges(highlightId, editor, ranges); } + + async setScopeVisualizationRanges( + scopeRanges: EditorScopeRanges[], + ): Promise { + this.scopeVisualizations.push( + ...scopeRanges.map(({ scopeRanges }) => ({ + scopeRanges, + })), + ); + } } diff --git a/packages/common/src/testUtil/runTestSubset.ts b/packages/common/src/testUtil/runTestSubset.ts index feb220b211..f865174a0b 100644 --- a/packages/common/src/testUtil/runTestSubset.ts +++ b/packages/common/src/testUtil/runTestSubset.ts @@ -4,7 +4,7 @@ * configuration. * See https://mochajs.org/#-grep-regexp-g-regexp for supported syntax */ -export const TEST_SUBSET_GREP_STRING = "snippets"; +export const TEST_SUBSET_GREP_STRING = "visualizer"; /** * Determine whether we should run just the subset of the tests specified by diff --git a/packages/common/src/testUtil/toPlainObject.ts b/packages/common/src/testUtil/toPlainObject.ts index 3ab03b6ea3..c6d1c256dd 100644 --- a/packages/common/src/testUtil/toPlainObject.ts +++ b/packages/common/src/testUtil/toPlainObject.ts @@ -3,6 +3,7 @@ import type { GeneralizedRange, LineRange, Message, + ScopeType, SpyIDERecordedValues, } from ".."; import { FlashStyle, isLineRange } from ".."; @@ -39,10 +40,22 @@ interface PlainHighlight { ranges: GeneralizedRangePlainObject[]; } +interface PlainScopeRanges { + scopeType: ScopeType; + domain: GeneralizedRangePlainObject; + contentRanges?: GeneralizedRangePlainObject[]; + removalRanges?: GeneralizedRangePlainObject[]; +} + +export interface PlainScopeVisualization { + scopeRanges: PlainScopeRanges[]; +} + export interface PlainSpyIDERecordedValues { messages: Message[] | undefined; flashes: PlainFlashDescriptor[] | undefined; highlights: PlainHighlight[] | undefined; + scopeVisualizations: PlainScopeVisualization[] | undefined; } export type SelectionPlainObject = { @@ -150,5 +163,17 @@ export function spyIDERecordedValuesToPlainObject( generalizedRangeToPlainObject(range), ), })), + scopeVisualizations: input.scopeVisualizations?.map(({ scopeRanges }) => ({ + scopeRanges: scopeRanges.map((scopeRange) => ({ + scopeType: scopeRange.scopeType, + domain: generalizedRangeToPlainObject(scopeRange.domain), + contentRanges: scopeRange.contentRanges?.map((range) => + generalizedRangeToPlainObject(range), + ), + removalRanges: scopeRange.removalRanges?.map((range) => + generalizedRangeToPlainObject(range), + ), + })), + })), }; } diff --git a/packages/cursorless-engine/src/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer.ts index 87a12a10c1..ed44b99a6c 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer.ts @@ -31,7 +31,7 @@ export class ScopeVisualizer implements Disposable { // be better to have this happen in VSCode-specific impl, but that's tricky // because the VSCode impl doesn't know about the visualization type. await this.clearHighlights(); - this.debouncer.run(); + await this.highlightScopes(); } private disposables: Disposable[] = []; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 4bfeb2c6ea..94b4221795 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -6,7 +6,6 @@ import { ScopeType, } from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; -import { ScopeVisualizer as ScopeVisualizerImpl } from "./ScopeVisualizer"; import { VisualizationType } from "./VisualizationType"; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; @@ -18,6 +17,7 @@ import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandler import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; +import { ScopeVisualizer as ScopeVisualizerImpl } from "./ScopeVisualizer"; export function createCursorlessEngine( treeSitter: TreeSitter, @@ -83,13 +83,13 @@ export function createCursorlessEngine( }, scopeVisualizer: { start(scopeType: ScopeType, visualizationType: string) { - scopeVisualizer.setScopeType({ + return scopeVisualizer.setScopeType({ scopeType, visualizationType: visualizationType as VisualizationType, }); }, stop() { - scopeVisualizer.setScopeType(undefined); + return scopeVisualizer.setScopeType(undefined); }, }, testCaseRecorder, @@ -117,8 +117,8 @@ export interface CommandApi { } export interface ScopeVisualizer { - start(scopeType: ScopeType, visualizationType: string): void; - stop(): void; + start(scopeType: ScopeType, visualizationType: string): Promise; + stop(): Promise; } export interface CursorlessEngine { diff --git a/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts b/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts index 9028af461b..44ad6609a6 100644 --- a/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts +++ b/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts @@ -62,6 +62,7 @@ export const upgradeDecorations: FixtureTransformation = ( range: extractHighlightRange(flash), style: extractHighlightName(flash.name) as keyof typeof FlashStyle, })), + scopeVisualizations: undefined, }; return reorderFields(fixture as TestCaseFixture); diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts new file mode 100644 index 0000000000..ec1db8f68b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts @@ -0,0 +1,219 @@ +import { + PlainScopeVisualization, + SpyIDE, + omitByDeep, + spyIDERecordedValuesToPlainObject, +} from "@cursorless/common"; +import { openNewEditor } from "@cursorless/vscode-common"; +import { assert } from "chai"; +import asyncSafety from "../asyncSafety"; +import { endToEndTestSetup, sleepWithBackoff } from "../endToEndTestSetup"; +import * as vscode from "vscode"; +import { isUndefined } from "lodash"; + +suite("scope visualizer", async function () { + const { getSpy } = endToEndTestSetup(this); + + test( + "basic content", + asyncSafety(() => runContentTest(getSpy()!)), + ); + test( + "basic removal", + asyncSafety(() => runRemovalTest(getSpy()!)), + ); +}); + +const initialDocumentContents = ` +function helloWorld() { + +} +`; + +const updatedDocumentContents = ` +function helloWorld() { + function nestedFunction() { + + } +} +`; + +const expectedInitialContentVisualizations: PlainScopeVisualization[] = [ + { scopeRanges: [] }, + { + scopeRanges: [ + { + scopeType: { type: "namedFunction" }, + domain: { + type: "character", + start: { line: 1, character: 0 }, + end: { line: 3, character: 1 }, + }, + contentRanges: [ + { + type: "character", + start: { line: 1, character: 0 }, + end: { line: 3, character: 1 }, + }, + ], + }, + ], + }, +]; + +const expectedUpdatedContentVisualization: PlainScopeVisualization = { + scopeRanges: [ + { + contentRanges: [ + { + start: { + character: 0, + line: 1, + }, + end: { + character: 1, + line: 5, + }, + type: "character", + }, + ], + domain: { + start: { + character: 0, + line: 1, + }, + end: { + character: 1, + line: 5, + }, + type: "character", + }, + scopeType: { + type: "namedFunction", + }, + }, + { + contentRanges: [ + { + start: { + character: 2, + line: 2, + }, + end: { + character: 3, + line: 4, + }, + type: "character", + }, + ], + domain: { + start: { + character: 2, + line: 2, + }, + end: { + character: 3, + line: 4, + }, + type: "character", + }, + scopeType: { + type: "namedFunction", + }, + }, + ], +}; + +async function runContentTest(spyIde: SpyIDE) { + const editor = await openNewEditor(initialDocumentContents, { + languageId: "typescript", + }); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "namedFunction", + }, + "content", + ); + + const expectedVisualizations = [...expectedInitialContentVisualizations]; + checkVisualizations(spyIde, expectedVisualizations); + + await editor.edit((editBuilder) => { + editBuilder.replace( + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(2, 1)), + updatedDocumentContents, + ); + }); + await sleepWithBackoff(100); + + expectedVisualizations.push(expectedUpdatedContentVisualization); + checkVisualizations(spyIde, expectedVisualizations); + + await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); + + expectedVisualizations.push({ scopeRanges: [] }); + checkVisualizations(spyIde, expectedVisualizations); +} + +const expectedRemovalVisualizations: PlainScopeVisualization[] = [ + { scopeRanges: [] }, + { + scopeRanges: [ + { + scopeType: { type: "paragraph" }, + domain: { + type: "character", + start: { line: 1, character: 0 }, + end: { line: 1, character: 23 }, + }, + removalRanges: [{ type: "line", start: 1, end: 2 }], + }, + { + scopeType: { type: "paragraph" }, + domain: { + type: "character", + start: { line: 3, character: 0 }, + end: { line: 3, character: 1 }, + }, + removalRanges: [{ type: "line", start: 3, end: 4 }], + }, + ], + }, +]; + +async function runRemovalTest(spyIde: SpyIDE) { + await openNewEditor(initialDocumentContents, { + languageId: "typescript", + }); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "paragraph", + }, + "removal", + ); + + checkVisualizations(spyIde, expectedRemovalVisualizations); + + await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); +} + +function checkVisualizations( + spyIde: SpyIDE, + expectedVisualizations: PlainScopeVisualization[], +) { + const actualVisualizations = omitByDeep( + spyIDERecordedValuesToPlainObject(spyIde.getSpyValues(false)!) + .scopeVisualizations, + isUndefined, + ); + + assert.deepStrictEqual( + actualVisualizations, + expectedVisualizations, + JSON.stringify(actualVisualizations), + ); +} From 02b8c43a9f87e62407a3c8e9dc5b968c92f0c608 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 21 Jun 2023 15:53:43 +0100 Subject: [PATCH 03/61] Have separate `IdeScopeVisualizer` type --- packages/common/src/ide/types/ide.types.ts | 26 ++- .../src/CursorlessEngine.1.ts | 41 ++++ .../cursorless-engine/src/ScopeVisualizer.ts | 201 ------------------ .../ScopeVisualizer/EditorScopeVisualizer.ts | 107 ++++++++++ .../src/ScopeVisualizer/ScopeVisualizer.ts | 51 +++++ .../UnsupportedScopeTypeVisualizationError.ts | 9 + .../src/ScopeVisualizer/checkNonNull.ts | 9 + .../src/ScopeVisualizer/getIterationRange.ts | 50 +++++ .../src/ScopeVisualizer/getIterationScopes.ts | 40 ++++ .../src/ScopeVisualizer/getScopes.ts | 25 +++ .../src/ScopeVisualizer/getTargetRanges.ts | 14 ++ .../src/ScopeVisualizer/index.ts | 1 + .../cursorless-engine/src/cursorlessEngine.ts | 62 ++---- .../modifiers/EveryScopeStage.ts | 2 +- .../cursorless-engine/src/util/PerEditor.ts | 47 ++++ 15 files changed, 432 insertions(+), 253 deletions(-) create mode 100644 packages/cursorless-engine/src/CursorlessEngine.1.ts delete mode 100644 packages/cursorless-engine/src/ScopeVisualizer.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/index.ts create mode 100644 packages/cursorless-engine/src/util/PerEditor.ts diff --git a/packages/common/src/ide/types/ide.types.ts b/packages/common/src/ide/types/ide.types.ts index dae5d5312e..54f705d72e 100644 --- a/packages/common/src/ide/types/ide.types.ts +++ b/packages/common/src/ide/types/ide.types.ts @@ -1,7 +1,6 @@ import type { EditableTextEditor, InputBoxOptions, - ScopeType, TextDocument, TextEditor, } from "../.."; @@ -224,10 +223,29 @@ export interface EditorScopeRanges { } export interface ScopeRanges { - scopeType: ScopeType; domain: GeneralizedRange; - contentRanges?: GeneralizedRange[]; - removalRanges?: GeneralizedRange[]; + targets: TargetRanges[]; +} + +export interface TargetRanges { + contentRange: GeneralizedRange; + removalRange: GeneralizedRange; +} + +export interface IterationScopeRanges { + domain: GeneralizedRange; + ranges: { + range: GeneralizedRange; + targets?: TargetRanges[]; + }[]; +} + +export interface IdeScopeVisualizer { + setScopes( + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): Promise; } export interface WorkspaceFolder { diff --git a/packages/cursorless-engine/src/CursorlessEngine.1.ts b/packages/cursorless-engine/src/CursorlessEngine.1.ts new file mode 100644 index 0000000000..e22185ccae --- /dev/null +++ b/packages/cursorless-engine/src/CursorlessEngine.1.ts @@ -0,0 +1,41 @@ +import { Command, HatTokenMap, IDE, ScopeType } from "@cursorless/common"; +import { Snippets } from "./core/Snippets"; +import { StoredTargetMap } from "./core/StoredTargets"; +import { TestCaseRecorder } from "./testCaseRecorder/TestCaseRecorder"; + +export interface CursorlessEngine { + commandApi: CommandApi; + scopeVisualizer: ScopeVisualizer; + testCaseRecorder: TestCaseRecorder; + storedTargets: StoredTargetMap; + hatTokenMap: HatTokenMap; + snippets: Snippets; + injectIde: (ide: IDE | undefined) => void; + runIntegrationTests: () => Promise; +} + +export interface CommandApi { + /** + * Runs a command. This is the core of the Cursorless engine. + * @param command The command to run + */ + runCommand(command: Command): Promise; + + /** + * Designed to run commands that come directly from the user. Ensures that + * the command args are of the correct shape. + */ + runCommandSafe(...args: unknown[]): Promise; +} + +export interface ScopeVisualizer { + start(config: ScopeVisualizerConfig): void; + stop(): void; +} + +export interface ScopeVisualizerConfig { + scopeType: ScopeType; + includeScopes: boolean; + includeIterationScopes: boolean; + includeIterationNestedTargets: boolean; +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer.ts deleted file mode 100644 index ed44b99a6c..0000000000 --- a/packages/cursorless-engine/src/ScopeVisualizer.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { - Disposable, - EditorScopeRanges, - Range, - ScopeRanges, - ScopeType, - TextEditor, - showError, - toCharacterRange, - toLineRange, -} from "@cursorless/common"; -import { last } from "lodash"; -import { VisualizationType } from "./VisualizationType"; -import { Debouncer } from "./core/Debouncer"; -import { ScopeHandlerFactory } from "./processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; -import { ScopeHandler } from "./processTargets/modifiers/scopeHandlers/scopeHandler.types"; -import { ide } from "./singletons/ide.singleton"; - -interface VisualizationInfo { - scopeType: ScopeType; - visualizationType: VisualizationType; -} - -export class ScopeVisualizer implements Disposable { - private shownErrorMessages = new Set(); - - async setScopeType(visualizationInfo: VisualizationInfo | undefined) { - this.visualizationInfo = visualizationInfo; - // Clear highlights becasue VSCode seems to behave strangely when - // changing the highlight type while highlights are active. Would probably - // be better to have this happen in VSCode-specific impl, but that's tricky - // because the VSCode impl doesn't know about the visualization type. - await this.clearHighlights(); - await this.highlightScopes(); - } - - private disposables: Disposable[] = []; - private debouncer = new Debouncer(() => this.highlightScopes()); - private visualizationInfo: VisualizationInfo | undefined = undefined; - - constructor(private scopeHandlerFactory: ScopeHandlerFactory) { - this.disposables.push( - // An event that fires when a text document opens - ide().onDidOpenTextDocument(this.debouncer.run), - // An Event that fires when a text document closes - ide().onDidCloseTextDocument(this.debouncer.run), - // An Event which fires when the array of visible editors has changed. - ide().onDidChangeVisibleTextEditors(this.debouncer.run), - // An event that is emitted when a text document is changed. This usually happens when the contents changes but also when other things like the dirty-state changes. - ide().onDidChangeTextDocument(this.debouncer.run), - ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), - this.debouncer, - ); - - this.debouncer.run(); - } - - private async clearHighlights() { - await ide().setScopeVisualizationRanges( - ide().visibleTextEditors.map((editor) => ({ editor, scopeRanges: [] })), - ); - } - - private getErrorSetKey(scopeType: ScopeType, languageId: string) { - return `${JSON.stringify(scopeType)}:${languageId}`; - } - - private async highlightScopes() { - if (this.visualizationInfo == null) { - return; - } - - const editorScopeRanges: EditorScopeRanges[] = []; - - for (const editor of ide().visibleTextEditors) { - const { document } = editor; - - const { scopeType, visualizationType } = this.visualizationInfo!; - const scopeHandler = this.scopeHandlerFactory.create( - scopeType, - document.languageId, - ); - - if (scopeHandler == null) { - const errorSetKey = this.getErrorSetKey(scopeType, document.languageId); - if (!this.shownErrorMessages.has(errorSetKey)) { - this.shownErrorMessages.add(errorSetKey); - showError( - ide().messages, - "ScopeVisualizer.scopeTypeNotSupported", - `Scope type not supported for ${document.languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.`, - ); - } - editorScopeRanges.push({ editor, scopeRanges: [] }); - continue; - } - - const iterationRange = getIterationRange(editor, scopeHandler); - - const scopes = Array.from( - scopeHandler.generateScopes(editor, iterationRange.start, "forward", { - includeDescendantScopes: true, - distalPosition: iterationRange.end, - }) ?? [], - ); - - editorScopeRanges.push({ - editor, - scopeRanges: scopes.map((scope) => { - const targets = scope.getTargets(false); - const scopeRanges: ScopeRanges = { - scopeType, - domain: toCharacterRange(scope.domain), - }; - - switch (visualizationType) { - case VisualizationType.content: - scopeRanges.contentRanges = targets.map((target) => - toCharacterRange(target.contentRange), - ); - break; - case VisualizationType.removal: - scopeRanges.removalRanges = targets.map((target) => - target.isLine - ? toLineRange(target.getRemovalHighlightRange()) - : toCharacterRange(target.getRemovalHighlightRange()), - ); - break; - } - - return scopeRanges; - }), - }); - } - - await ide().setScopeVisualizationRanges(editorScopeRanges); - } - - dispose(): void { - this.disposables.forEach(({ dispose }) => { - try { - dispose(); - } catch (e) { - // do nothing - } - }); - - this.clearHighlights(); - } -} - -/** - * Get the range to iterate over for the given editor. We take the union of all - * visible ranges, add 10 lines either side to make scrolling a bit smoother, - * and then expand to the largest ancestor of the start and end of the visible - * range, so that we properly show nesting. - * @param editor The editor to get the iteration range for - * @param scopeHandler The scope handler to use - * @returns The range to iterate over - */ -function getIterationRange( - editor: TextEditor, - scopeHandler: ScopeHandler, -): Range { - let visibleRange = editor.visibleRanges.reduce((acc, range) => - acc.union(range), - ); - - visibleRange = editor.document.range.intersection( - visibleRange.with( - visibleRange.start.translate(-10), - visibleRange.end.translate(10), - ), - )!; - - // Expand to largest ancestor of start of visible range FIXME: It's - // possible that the removal range will be bigger than the domain range, - // in which case we'll miss a scope if its removal range is visible but - // its domain range is not. I don't think we care that much; they can - // scroll, and we have the extra 10 lines on either side which might help. - const expandedStart = - last( - Array.from( - scopeHandler.generateScopes(editor, visibleRange.start, "forward", { - containment: "required", - }), - ), - )?.domain ?? visibleRange; - - // Expand to largest ancestor of end of visible range - const expandedEnd = - last( - Array.from( - scopeHandler.generateScopes(editor, visibleRange.end, "forward", { - containment: "required", - }), - ), - )?.domain ?? visibleRange; - - return expandedStart.union(expandedEnd); -} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts new file mode 100644 index 0000000000..83a9d72a92 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts @@ -0,0 +1,107 @@ +import { + Disposable, + IdeScopeVisualizer, + IterationScopeRanges, + Range, + TextEditor, +} from "@cursorless/common"; +import { ScopeVisualizerConfig } from "../CursorlessEngine.1"; +import { Debouncer } from "../core/Debouncer"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { ide } from "../singletons/ide.singleton"; +import { UnsupportedScopeTypeVisualizationError } from "./UnsupportedScopeTypeVisualizationError"; +import { checkNonNull } from "./checkNonNull"; +import { getIterationRange } from "./getIterationRange"; +import { getIterationScopes } from "./getIterationScopes"; +import { getScopes } from "./getScopes"; + +export class EditorScopeVisualizer implements Disposable { + private disposables: Disposable[] = []; + private debouncer = new Debouncer(() => this.highlightScopes()); + + constructor( + private scopeHandlerFactory: ScopeHandlerFactory, + private modifierStageFactory: ModifierStageFactory, + private ideVisualizer: IdeScopeVisualizer, + private editor: TextEditor, + private config: ScopeVisualizerConfig, + ) { + this.disposables.push( + // An event that is emitted when a text document is changed. This usually + // happens when the contents changes but also when other things like the + // dirty-state changes. + ide().onDidChangeTextDocument(this.debouncer.run), + ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), + this.debouncer, + ); + + this.debouncer.run(); + } + + async highlightScopes() { + const { + document: { languageId }, + } = this.editor; + + const scopeHandler = checkNonNull( + this.scopeHandlerFactory.create(this.config.scopeType, languageId), + () => new UnsupportedScopeTypeVisualizationError(languageId), + ); + + const iterationRange = getIterationRange(this.editor, scopeHandler); + + this.ideVisualizer.setScopes( + this.editor, + this.config.includeScopes + ? getScopes(this.editor, scopeHandler, iterationRange) + : undefined, + this.config.includeIterationScopes + ? this.getIterationScopes(scopeHandler, iterationRange) + : undefined, + ); + } + + private getIterationScopes( + scopeHandler: ScopeHandler, + iterationRange: Range, + ): IterationScopeRanges[] | undefined { + const { editor } = this; + const { + document: { languageId }, + } = editor; + const { scopeType, includeIterationNestedTargets } = this.config; + + const iterationScopeHandler = checkNonNull( + this.scopeHandlerFactory.create( + scopeHandler.iterationScopeType, + languageId, + ), + () => new UnsupportedScopeTypeVisualizationError(languageId), + ); + + const everyStage = this.modifierStageFactory.create({ + type: "everyScope", + scopeType, + }); + + return getIterationScopes( + editor, + iterationScopeHandler, + everyStage, + iterationRange, + includeIterationNestedTargets, + ); + } + + dispose(): void { + this.disposables.forEach(({ dispose }) => { + try { + dispose(); + } catch (e) { + // do nothing + } + }); + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts new file mode 100644 index 0000000000..942fe2776d --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts @@ -0,0 +1,51 @@ +import { IdeScopeVisualizer, showError } from "@cursorless/common"; +import { ScopeVisualizerConfig } from "../CursorlessEngine.1"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; +import { ide } from "../singletons/ide.singleton"; +import { PerEditor } from "../util/PerEditor"; +import { EditorScopeVisualizer } from "./EditorScopeVisualizer"; + +export class ScopeVisualizer { + private scopeVisualizers: PerEditor | undefined; + + constructor( + private scopeHandlerFactory: ScopeHandlerFactory, + private modifierStageFactory: ModifierStageFactory, + private ideVisualizer: IdeScopeVisualizer, + ) {} + + start(config: ScopeVisualizerConfig) { + this.scopeVisualizers?.dispose(); + this.scopeVisualizers = new PerEditor((editor) => { + const visualizer = new EditorScopeVisualizer( + this.scopeHandlerFactory, + this.modifierStageFactory, + this.ideVisualizer, + editor, + config, + ); + + visualizer.highlightScopes().catch((err: Error) => { + if (err.name === "UnsupportedScopeTypeVisualizationError") { + if (editor.isActive) { + showError( + ide().messages, + "ScopeVisualizer.scopeTypeNotSupported", + err.message, + ); + } + return; + } + + showError(ide().messages, "ScopeVisualizer.exception", err.message); + }); + + return visualizer; + }); + } + + stop() { + this.scopeVisualizers?.dispose(); + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts b/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts new file mode 100644 index 0000000000..96d3b54219 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts @@ -0,0 +1,9 @@ + +export class UnsupportedScopeTypeVisualizationError extends Error { + constructor(languageId: string) { + super( + `Scope type not supported for ${languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.` + ); + this.name = "UnsupportedScopeTypeVisualizationError"; + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts b/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts new file mode 100644 index 0000000000..0d1e5c1746 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts @@ -0,0 +1,9 @@ +export function checkNonNull( + value: T | null | undefined, + errorMessage: () => Error): T { + if (value == null) { + throw errorMessage(); + } + + return value; +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts new file mode 100644 index 0000000000..94658e499c --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts @@ -0,0 +1,50 @@ +import { Range, TextEditor } from "@cursorless/common"; +import { last } from "lodash"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; + +/** + * Get the range to iterate over for the given editor. We take the union of all + * visible ranges, add 10 lines either side to make scrolling a bit smoother, + * and then expand to the largest ancestor of the start and end of the visible + * range, so that we properly show nesting. + * @param editor The editor to get the iteration range for + * @param scopeHandler The scope handler to use + * @returns The range to iterate over + */ +export function getIterationRange( + editor: TextEditor, + scopeHandler: ScopeHandler): Range { + let visibleRange = editor.visibleRanges.reduce((acc, range) => acc.union(range) + ); + + visibleRange = editor.document.range.intersection( + visibleRange.with( + visibleRange.start.translate(-10), + visibleRange.end.translate(10) + ) + )!; + + // Expand to largest ancestor of start of visible range FIXME: It's + // possible that the removal range will be bigger than the domain range, + // in which case we'll miss a scope if its removal range is visible but + // its domain range is not. I don't think we care that much; they can + // scroll, and we have the extra 10 lines on either side which might help. + const expandedStart = last( + Array.from( + scopeHandler.generateScopes(editor, visibleRange.start, "forward", { + containment: "required", + }) + ) + )?.domain ?? visibleRange; + + // Expand to largest ancestor of end of visible range + const expandedEnd = last( + Array.from( + scopeHandler.generateScopes(editor, visibleRange.end, "forward", { + containment: "required", + }) + ) + )?.domain ?? visibleRange; + + return expandedStart.union(expandedEnd); +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts new file mode 100644 index 0000000000..c10fb674b1 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts @@ -0,0 +1,40 @@ +import { + IterationScopeRanges, + Range, + TextEditor, + toCharacterRange +} from "@cursorless/common"; +import { map } from "itertools"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { getTargetRanges } from "./getTargetRanges"; +import { ModifierStage } from "../processTargets/PipelineStages.types"; + +export function getIterationScopes( + editor: TextEditor, + iterationScopeHandler: ScopeHandler, + everyStage: ModifierStage, + iterationRange: Range, + includeIterationNestedTargets: boolean): IterationScopeRanges[] { + return map( + iterationScopeHandler.generateScopes( + editor, + iterationRange.start, + "forward", + { + includeDescendantScopes: true, + distalPosition: iterationRange.end, + } + ), + (scope) => { + return { + domain: toCharacterRange(scope.domain), + ranges: scope.getTargets(false).map((target) => ({ + range: toCharacterRange(target.contentRange), + targets: includeIterationNestedTargets + ? everyStage.run(target).map(getTargetRanges) + : undefined, + })), + }; + } + ); +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts new file mode 100644 index 0000000000..32aca41e82 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts @@ -0,0 +1,25 @@ +import { + Range, + ScopeRanges, + TextEditor, + toCharacterRange +} from "@cursorless/common"; +import { map } from "itertools"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { getTargetRanges } from "./getTargetRanges"; + +export function getScopes( + editor: TextEditor, + scopeHandler: ScopeHandler, + iterationRange: Range): ScopeRanges[] { + return map( + scopeHandler.generateScopes(editor, iterationRange.start, "forward", { + includeDescendantScopes: true, + distalPosition: iterationRange.end, + }), + (scope) => ({ + domain: toCharacterRange(scope.domain), + targets: scope.getTargets(false).map(getTargetRanges), + }) + ); +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts new file mode 100644 index 0000000000..c226d17ca1 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts @@ -0,0 +1,14 @@ +import { + toCharacterRange, + toLineRange +} from "@cursorless/common"; +import { Target } from "../typings/target.types"; + +export function getTargetRanges(target: Target) { + return { + contentRange: toCharacterRange(target.contentRange), + removalRange: target.isLine + ? toLineRange(target.getRemovalHighlightRange()) + : toCharacterRange(target.getRemovalHighlightRange()), + }; +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/index.ts b/packages/cursorless-engine/src/ScopeVisualizer/index.ts new file mode 100644 index 0000000000..c16ea931c0 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/index.ts @@ -0,0 +1 @@ +export * from "./ScopeVisualizer"; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 94b4221795..b5bb030f21 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -3,26 +3,28 @@ import { CommandServerApi, Hats, IDE, - ScopeType, + IdeScopeVisualizer, } from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; -import { VisualizationType } from "./VisualizationType"; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; +import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; -import { ScopeVisualizer as ScopeVisualizerImpl } from "./ScopeVisualizer"; +import { ScopeVisualizer } from "./ScopeVisualizer"; +import { CursorlessEngine } from "./CursorlessEngine.1"; export function createCursorlessEngine( treeSitter: TreeSitter, ide: IDE, hats: Hats, + ideScopeVisualizer: IdeScopeVisualizer, commandServerApi: CommandServerApi | null, ): CursorlessEngine { injectIde(ide); @@ -49,9 +51,7 @@ export function createCursorlessEngine( const languageDefinitions = new LanguageDefinitions(treeSitter); - const scopeVisualizer = new ScopeVisualizerImpl( - new ScopeHandlerFactoryImpl(languageDefinitions), - ); + const scopeHandlerFactory = new ScopeHandlerFactoryImpl(languageDefinitions); return { commandApi: { @@ -81,17 +81,15 @@ export function createCursorlessEngine( ); }, }, - scopeVisualizer: { - start(scopeType: ScopeType, visualizationType: string) { - return scopeVisualizer.setScopeType({ - scopeType, - visualizationType: visualizationType as VisualizationType, - }); - }, - stop() { - return scopeVisualizer.setScopeType(undefined); - }, - }, + scopeVisualizer: new ScopeVisualizer( + scopeHandlerFactory, + new ModifierStageFactoryImpl( + languageDefinitions, + storedTargets, + scopeHandlerFactory, + ), + ideScopeVisualizer, + ), testCaseRecorder, storedTargets, hatTokenMap, @@ -101,33 +99,3 @@ export function createCursorlessEngine( runIntegrationTests(treeSitter, languageDefinitions), }; } - -export interface CommandApi { - /** - * Runs a command. This is the core of the Cursorless engine. - * @param command The command to run - */ - runCommand(command: Command): Promise; - - /** - * Designed to run commands that come directly from the user. Ensures that - * the command args are of the correct shape. - */ - runCommandSafe(...args: unknown[]): Promise; -} - -export interface ScopeVisualizer { - start(scopeType: ScopeType, visualizationType: string): Promise; - stop(): Promise; -} - -export interface CursorlessEngine { - commandApi: CommandApi; - scopeVisualizer: ScopeVisualizer; - testCaseRecorder: TestCaseRecorder; - storedTargets: StoredTargetMap; - hatTokenMap: HatTokenMapImpl; - snippets: Snippets; - injectIde: (ide: IDE | undefined) => void; - runIntegrationTests: () => Promise; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index 33424fa031..2d39de2cb0 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -121,7 +121,7 @@ export class EveryScopeStage implements ModifierStage { /** * Returns a list of all scopes that have nonempty overlap with {@link range}. */ -function getScopesOverlappingRange( +export function getScopesOverlappingRange( scopeHandler: ScopeHandler, editor: TextEditor, { start, end }: Range, diff --git a/packages/cursorless-engine/src/util/PerEditor.ts b/packages/cursorless-engine/src/util/PerEditor.ts new file mode 100644 index 0000000000..92da9807b0 --- /dev/null +++ b/packages/cursorless-engine/src/util/PerEditor.ts @@ -0,0 +1,47 @@ +import { Disposable, TextEditor } from "@cursorless/common"; +import { ide } from "../singletons/ide.singleton"; + +export class PerEditor { + private disposables: Disposable[] = []; + private editorHandlers: Map = new Map(); + + constructor(private makeEditorHandler: (editor: TextEditor) => Disposable) { + this.disposables.push( + // An event that fires when a text document opens + ide().onDidOpenTextDocument(this.handleChange), + // An Event that fires when a text document closes + ide().onDidCloseTextDocument(this.handleChange), + // An Event which fires when the array of visible editors has changed. + ide().onDidChangeVisibleTextEditors(this.handleChange), + ); + + this.handleChange(); + } + + private handleChange() { + const editors = ide().visibleTextEditors; + const editorIds = new Set(editors.map((editor) => editor.id)); + + for (const [editorId, editorHandler] of this.editorHandlers) { + if (!editorIds.has(editorId)) { + editorHandler.dispose(); + this.editorHandlers.delete(editorId); + } + } + + for (const editor of editors) { + if (!this.editorHandlers.has(editor.id)) { + this.editorHandlers.set(editor.id, this.makeEditorHandler(editor)); + } + } + } + + dispose() { + for (const disposable of this.disposables) { + disposable.dispose(); + } + for (const editorHandler of this.editorHandlers.values()) { + editorHandler.dispose(); + } + } +} From 97f85d709b3c0742b3edade1f3da622f79797be3 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:04:41 +0100 Subject: [PATCH 04/61] Some fixes; just add scope visualizer function to ide --- packages/common/src/ide/PassthroughIDEBase.ts | 15 ++++- packages/common/src/ide/fake/FakeIDE.ts | 7 ++- packages/common/src/ide/spy/SpyIDE.ts | 15 +++-- packages/common/src/ide/types/ide.types.ts | 14 ++--- packages/common/src/testUtil/toPlainObject.ts | 56 +++++++++++++------ .../ScopeVisualizer/EditorScopeVisualizer.ts | 4 +- .../src/ScopeVisualizer/ScopeVisualizer.ts | 4 +- .../cursorless-engine/src/cursorlessEngine.ts | 10 +--- 8 files changed, 71 insertions(+), 54 deletions(-) diff --git a/packages/common/src/ide/PassthroughIDEBase.ts b/packages/common/src/ide/PassthroughIDEBase.ts index 9584c69bdc..fcdc5b102e 100644 --- a/packages/common/src/ide/PassthroughIDEBase.ts +++ b/packages/common/src/ide/PassthroughIDEBase.ts @@ -12,9 +12,10 @@ import { import { FlashDescriptor } from "./types/FlashDescriptor"; import { Disposable, - EditorScopeRanges, IDE, + IterationScopeRanges, RunMode, + ScopeRanges, WorkspaceFolder, } from "./types/ide.types"; import { Messages } from "./types/Messages"; @@ -36,8 +37,16 @@ export default class PassthroughIDEBase implements IDE { this.capabilities = original.capabilities; } - setScopeVisualizationRanges(scopeRanges: EditorScopeRanges[]): Promise { - return this.original.setScopeVisualizationRanges(scopeRanges); + setScopeVisualizationRanges( + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): Promise { + return this.original.setScopeVisualizationRanges( + editor, + scopeRanges, + iterationScopeRanges, + ); } flashRanges(flashDescriptors: FlashDescriptor[]): Promise { diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index d5aca980ad..8b428758c7 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -12,9 +12,10 @@ import { } from "../types/events.types"; import type { Disposable, - EditorScopeRanges, IDE, + IterationScopeRanges, RunMode, + ScopeRanges, WorkspaceFolder, } from "../types/ide.types"; import { FakeCapabilities } from "./FakeCapabilities"; @@ -49,7 +50,9 @@ export default class FakeIDE implements IDE { } async setScopeVisualizationRanges( - _scopeRanges: EditorScopeRanges[], + _editor: TextEditor, + _scopeRanges: ScopeRanges[] | undefined, + _iterationScopeRanges: IterationScopeRanges[] | undefined, ): Promise { // empty } diff --git a/packages/common/src/ide/spy/SpyIDE.ts b/packages/common/src/ide/spy/SpyIDE.ts index 0cbaf15abb..127701bcfc 100644 --- a/packages/common/src/ide/spy/SpyIDE.ts +++ b/packages/common/src/ide/spy/SpyIDE.ts @@ -6,7 +6,7 @@ import { FlashDescriptor } from "../types/FlashDescriptor"; import type { HighlightId, IDE, - EditorScopeRanges, + IterationScopeRanges, ScopeRanges, } from "../types/ide.types"; import SpyMessages, { Message } from "./SpyMessages"; @@ -17,7 +17,8 @@ interface Highlight { } interface ScopeVisualization { - scopeRanges: ScopeRanges[]; + scopeRanges: ScopeRanges[] | undefined; + iterationScopeRanges: IterationScopeRanges[] | undefined; } export interface SpyIDERecordedValues { @@ -72,12 +73,10 @@ export default class SpyIDE extends PassthroughIDEBase { } async setScopeVisualizationRanges( - scopeRanges: EditorScopeRanges[], + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, ): Promise { - this.scopeVisualizations.push( - ...scopeRanges.map(({ scopeRanges }) => ({ - scopeRanges, - })), - ); + this.scopeVisualizations.push({ scopeRanges, iterationScopeRanges }); } } diff --git a/packages/common/src/ide/types/ide.types.ts b/packages/common/src/ide/types/ide.types.ts index 54f705d72e..7dd2d6fe81 100644 --- a/packages/common/src/ide/types/ide.types.ts +++ b/packages/common/src/ide/types/ide.types.ts @@ -214,7 +214,11 @@ export interface IDE { ranges: GeneralizedRange[], ): Promise; - setScopeVisualizationRanges(scopeRanges: EditorScopeRanges[]): Promise; + setScopeVisualizationRanges( + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): Promise; } export interface EditorScopeRanges { @@ -240,14 +244,6 @@ export interface IterationScopeRanges { }[]; } -export interface IdeScopeVisualizer { - setScopes( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise; -} - export interface WorkspaceFolder { uri: URI; name: string; diff --git a/packages/common/src/testUtil/toPlainObject.ts b/packages/common/src/testUtil/toPlainObject.ts index c6d1c256dd..34b8b50a82 100644 --- a/packages/common/src/testUtil/toPlainObject.ts +++ b/packages/common/src/testUtil/toPlainObject.ts @@ -3,8 +3,8 @@ import type { GeneralizedRange, LineRange, Message, - ScopeType, SpyIDERecordedValues, + TargetRanges, } from ".."; import { FlashStyle, isLineRange } from ".."; import { Token } from "../types/Token"; @@ -41,14 +41,26 @@ interface PlainHighlight { } interface PlainScopeRanges { - scopeType: ScopeType; domain: GeneralizedRangePlainObject; - contentRanges?: GeneralizedRangePlainObject[]; - removalRanges?: GeneralizedRangePlainObject[]; + targets: PlainTargetRanges[]; +} + +interface PlainIterationScopeRanges { + domain: GeneralizedRangePlainObject; + ranges: { + range: GeneralizedRangePlainObject; + targets: PlainTargetRanges[] | undefined; + }[]; +} + +interface PlainTargetRanges { + contentRange: GeneralizedRangePlainObject; + removalRange: GeneralizedRangePlainObject; } export interface PlainScopeVisualization { - scopeRanges: PlainScopeRanges[]; + scopeRanges: PlainScopeRanges[] | undefined; + iterationScopeRanges: PlainIterationScopeRanges[] | undefined; } export interface PlainSpyIDERecordedValues { @@ -163,17 +175,27 @@ export function spyIDERecordedValuesToPlainObject( generalizedRangeToPlainObject(range), ), })), - scopeVisualizations: input.scopeVisualizations?.map(({ scopeRanges }) => ({ - scopeRanges: scopeRanges.map((scopeRange) => ({ - scopeType: scopeRange.scopeType, - domain: generalizedRangeToPlainObject(scopeRange.domain), - contentRanges: scopeRange.contentRanges?.map((range) => - generalizedRangeToPlainObject(range), - ), - removalRanges: scopeRange.removalRanges?.map((range) => - generalizedRangeToPlainObject(range), - ), - })), - })), + scopeVisualizations: input.scopeVisualizations?.map( + ({ scopeRanges, iterationScopeRanges }) => ({ + scopeRanges: scopeRanges?.map((scopeRange) => ({ + domain: generalizedRangeToPlainObject(scopeRange.domain), + targets: scopeRange.targets?.map(targetRangesToPlainObject), + })), + iterationScopeRanges: iterationScopeRanges?.map((scopeRange) => ({ + domain: generalizedRangeToPlainObject(scopeRange.domain), + ranges: scopeRange.ranges.map(({ range, targets }) => ({ + range: generalizedRangeToPlainObject(range), + targets: targets?.map(targetRangesToPlainObject), + })), + })), + }), + ), + }; +} + +export function targetRangesToPlainObject(target: TargetRanges) { + return { + contentRange: generalizedRangeToPlainObject(target.contentRange), + removalRange: generalizedRangeToPlainObject(target.removalRange), }; } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts index 83a9d72a92..c8016fc569 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts @@ -1,6 +1,5 @@ import { Disposable, - IdeScopeVisualizer, IterationScopeRanges, Range, TextEditor, @@ -24,7 +23,6 @@ export class EditorScopeVisualizer implements Disposable { constructor( private scopeHandlerFactory: ScopeHandlerFactory, private modifierStageFactory: ModifierStageFactory, - private ideVisualizer: IdeScopeVisualizer, private editor: TextEditor, private config: ScopeVisualizerConfig, ) { @@ -52,7 +50,7 @@ export class EditorScopeVisualizer implements Disposable { const iterationRange = getIterationRange(this.editor, scopeHandler); - this.ideVisualizer.setScopes( + ide().setScopeVisualizationRanges( this.editor, this.config.includeScopes ? getScopes(this.editor, scopeHandler, iterationRange) diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts index 942fe2776d..bf7c870b1e 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts @@ -1,4 +1,4 @@ -import { IdeScopeVisualizer, showError } from "@cursorless/common"; +import { showError } from "@cursorless/common"; import { ScopeVisualizerConfig } from "../CursorlessEngine.1"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; @@ -12,7 +12,6 @@ export class ScopeVisualizer { constructor( private scopeHandlerFactory: ScopeHandlerFactory, private modifierStageFactory: ModifierStageFactory, - private ideVisualizer: IdeScopeVisualizer, ) {} start(config: ScopeVisualizerConfig) { @@ -21,7 +20,6 @@ export class ScopeVisualizer { const visualizer = new EditorScopeVisualizer( this.scopeHandlerFactory, this.modifierStageFactory, - this.ideVisualizer, editor, config, ); diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index b5bb030f21..f994a932d1 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,10 +1,4 @@ -import { - Command, - CommandServerApi, - Hats, - IDE, - IdeScopeVisualizer, -} from "@cursorless/common"; +import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; @@ -24,7 +18,6 @@ export function createCursorlessEngine( treeSitter: TreeSitter, ide: IDE, hats: Hats, - ideScopeVisualizer: IdeScopeVisualizer, commandServerApi: CommandServerApi | null, ): CursorlessEngine { injectIde(ide); @@ -88,7 +81,6 @@ export function createCursorlessEngine( storedTargets, scopeHandlerFactory, ), - ideScopeVisualizer, ), testCaseRecorder, storedTargets, From c7ef0d10729879bd83a5159f52ce089279b7d8b8 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:49:08 +0100 Subject: [PATCH 05/61] more stuff --- ...lessEngine.1.ts => CursorlessEngineApi.ts} | 0 .../ScopeVisualizer/EditorScopeVisualizer.ts | 2 +- .../src/ScopeVisualizer/ScopeVisualizer.ts | 2 +- .../cursorless-engine/src/cursorlessEngine.ts | 2 +- packages/cursorless-engine/src/index.ts | 1 + .../VSCodeScopeVisualizer/RangeTypeColors.ts | 10 ++ .../ScopeVisualizerColorConfig.ts | 22 +++ .../VscodeScopeVisualizer.ts | 146 ++---------------- .../VscodeScopeVisualizerRenderer.ts | 2 +- .../blendRangeTypeColors.ts | 55 +++++++ .../getColorsFromConfig.ts | 21 +++ .../isGeneralizedRangeEqual.ts | 15 ++ .../src/ide/vscode/VscodeIDE.ts | 18 ++- 13 files changed, 152 insertions(+), 144 deletions(-) rename packages/cursorless-engine/src/{CursorlessEngine.1.ts => CursorlessEngineApi.ts} (100%) create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts diff --git a/packages/cursorless-engine/src/CursorlessEngine.1.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts similarity index 100% rename from packages/cursorless-engine/src/CursorlessEngine.1.ts rename to packages/cursorless-engine/src/CursorlessEngineApi.ts diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts index c8016fc569..9a3afae6a3 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts @@ -4,7 +4,7 @@ import { Range, TextEditor, } from "@cursorless/common"; -import { ScopeVisualizerConfig } from "../CursorlessEngine.1"; +import { ScopeVisualizerConfig } from "../CursorlessEngineApi"; import { Debouncer } from "../core/Debouncer"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts index bf7c870b1e..b29f1dc588 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts @@ -1,5 +1,5 @@ import { showError } from "@cursorless/common"; -import { ScopeVisualizerConfig } from "../CursorlessEngine.1"; +import { ScopeVisualizerConfig } from "../CursorlessEngineApi"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { ide } from "../singletons/ide.singleton"; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index f994a932d1..a8be42cc96 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -12,7 +12,7 @@ import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; import { ScopeVisualizer } from "./ScopeVisualizer"; -import { CursorlessEngine } from "./CursorlessEngine.1"; +import { CursorlessEngine } from "./CursorlessEngineApi"; export function createCursorlessEngine( treeSitter: TreeSitter, diff --git a/packages/cursorless-engine/src/index.ts b/packages/cursorless-engine/src/index.ts index 2327870435..d011f0acfc 100644 --- a/packages/cursorless-engine/src/index.ts +++ b/packages/cursorless-engine/src/index.ts @@ -5,3 +5,4 @@ export * from "./testCaseRecorder/TestCaseRecorder"; export * from "./core/StoredTargets"; export * from "./typings/TreeSitter"; export * from "./cursorlessEngine"; +export * from "./CursorlessEngineApi"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts new file mode 100644 index 0000000000..5a11577454 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts @@ -0,0 +1,10 @@ + +export interface RangeTypeColors { + background: ThemeColors; + borderSolid: ThemeColors; + borderPorous: ThemeColors; +} +interface ThemeColors { + light: string; + dark: string; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts new file mode 100644 index 0000000000..af7d313aa6 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts @@ -0,0 +1,22 @@ + +export interface ScopeVisualizerColorConfig { + light: ScopeVisualizerThemeColorConfig; + dark: ScopeVisualizerThemeColorConfig; +} +interface ScopeVisualizerThemeColorConfig { + domain: { + background: string; + borderSolid: string; + borderPorous: string; + }; + content: { + background: string; + borderSolid: string; + borderPorous: string; + }; + removal: { + background: string; + borderSolid: string; + borderPorous: string; + }; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index 36ff8855eb..bb240e0605 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -1,47 +1,15 @@ -import { GeneralizedRange, ScopeRanges } from "@cursorless/common"; +import { + GeneralizedRange, + IterationScopeRanges, + ScopeRanges, +} from "@cursorless/common"; import * as vscode from "vscode"; -import { VscodeScopeVisualizerRenderer } from "./VscodeScopeVisualizerRenderer"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; -import tinycolor = require("tinycolor2"); - -interface ThemeColors { - light: string; - dark: string; -} - -export interface RangeTypeColors { - background: ThemeColors; - borderSolid: ThemeColors; - borderPorous: ThemeColors; -} - -interface ScopeVisualizerThemeColorConfig { - domain: { - background: string; - borderSolid: string; - borderPorous: string; - }; - content: { - background: string; - borderSolid: string; - borderPorous: string; - }; - removal: { - background: string; - borderSolid: string; - borderPorous: string; - }; -} - -interface ScopeVisualizerColorConfig { - light: ScopeVisualizerThemeColorConfig; - dark: ScopeVisualizerThemeColorConfig; -} - -interface VscodeTextEditorScopeRanges { - editor: VscodeTextEditorImpl; - scopeRanges: ScopeRanges[]; -} +import { VscodeScopeVisualizerRenderer } from "./VscodeScopeVisualizerRenderer"; +import { isGeneralizedRangeEqual } from "./isGeneralizedRangeEqual"; +import { blendRangeTypeColors } from "./blendRangeTypeColors"; +import { getColorsFromConfig } from "./getColorsFromConfig"; +import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; export class VscodeScopeVisualizer { private domainRenderer!: VscodeScopeVisualizerRenderer; @@ -93,7 +61,9 @@ export class VscodeScopeVisualizer { private editorScopeRanges: VscodeTextEditorScopeRanges[] = []; async setScopeVisualizationRanges( - editorScopeRanges: VscodeTextEditorScopeRanges[], + editor: VscodeTextEditorImpl, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, ) { this.editorScopeRanges = editorScopeRanges; this.drawScopes(); @@ -152,93 +122,3 @@ export class VscodeScopeVisualizer { ); } } - -function getColorsFromConfig( - config: ScopeVisualizerColorConfig, - rangeType: "domain" | "content" | "removal", -): RangeTypeColors { - return { - background: { - light: config.light[rangeType].background, - dark: config.dark[rangeType].background, - }, - borderSolid: { - light: config.light[rangeType].borderSolid, - dark: config.dark[rangeType].borderSolid, - }, - borderPorous: { - light: config.light[rangeType].borderPorous, - dark: config.dark[rangeType].borderPorous, - }, - }; -} - -function isGeneralizedRangeEqual( - a: GeneralizedRange, - b: GeneralizedRange, -): boolean { - if (a.type === "character" && b.type === "character") { - return a.start.isEqual(b.start) && a.end.isEqual(b.end); - } - - if (a.type === "line" && b.type === "line") { - return a.start === b.start && a.end === b.end; - } - - return false; -} - -function blendRangeTypeColors( - baseColors: RangeTypeColors, - topColors: RangeTypeColors, -): RangeTypeColors { - return { - background: { - light: blendColors( - baseColors.background.light, - topColors.background.light, - ), - dark: blendColors(baseColors.background.dark, topColors.background.dark), - }, - borderSolid: { - light: blendColors( - baseColors.borderSolid.light, - topColors.borderSolid.light, - ), - dark: blendColors( - baseColors.borderSolid.dark, - topColors.borderSolid.dark, - ), - }, - borderPorous: { - light: blendColors( - baseColors.borderPorous.light, - topColors.borderPorous.light, - ), - dark: blendColors( - baseColors.borderPorous.dark, - topColors.borderPorous.dark, - ), - }, - }; -} - -function blendColors(base: string, top: string): string { - const baseRgba = tinycolor(base).toRgb(); - const topRgba = tinycolor(top).toRgb(); - const blendedAlpha = 1 - (1 - topRgba.a) * (1 - baseRgba.a); - - function interpolateChannel(channel: "r" | "g" | "b"): number { - return Math.round( - (topRgba[channel] * topRgba.a) / blendedAlpha + - (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha, - ); - } - - return tinycolor({ - r: interpolateChannel("r"), - g: interpolateChannel("g"), - b: interpolateChannel("b"), - a: blendedAlpha, - }).toHex8String(); -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts index aeba18cb42..c5b16e0090 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts @@ -16,7 +16,7 @@ import { TextEditorDecorationType, window, } from "vscode"; -import { RangeTypeColors } from "./VscodeScopeVisualizer"; +import { RangeTypeColors } from "./RangeTypeColors"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { generateDecorationsForCharacterRange } from "./getDecorationRanges/generateDecorationsForCharacterRange"; import { generateDecorationsForLineRange } from "./getDecorationRanges/generateDecorationsForLineRange"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts new file mode 100644 index 0000000000..daae5a147a --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts @@ -0,0 +1,55 @@ +import tinycolor = require("tinycolor2"); +import { RangeTypeColors } from "./RangeTypeColors"; + +export function blendRangeTypeColors( + baseColors: RangeTypeColors, + topColors: RangeTypeColors): RangeTypeColors { + return { + background: { + light: blendColors( + baseColors.background.light, + topColors.background.light + ), + dark: blendColors(baseColors.background.dark, topColors.background.dark), + }, + borderSolid: { + light: blendColors( + baseColors.borderSolid.light, + topColors.borderSolid.light + ), + dark: blendColors( + baseColors.borderSolid.dark, + topColors.borderSolid.dark + ), + }, + borderPorous: { + light: blendColors( + baseColors.borderPorous.light, + topColors.borderPorous.light + ), + dark: blendColors( + baseColors.borderPorous.dark, + topColors.borderPorous.dark + ), + }, + }; +} +function blendColors(base: string, top: string): string { + const baseRgba = tinycolor(base).toRgb(); + const topRgba = tinycolor(top).toRgb(); + const blendedAlpha = 1 - (1 - topRgba.a) * (1 - baseRgba.a); + + function interpolateChannel(channel: "r" | "g" | "b"): number { + return Math.round( + (topRgba[channel] * topRgba.a) / blendedAlpha + + (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha + ); + } + + return tinycolor({ + r: interpolateChannel("r"), + g: interpolateChannel("g"), + b: interpolateChannel("b"), + a: blendedAlpha, + }).toHex8String(); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts new file mode 100644 index 0000000000..6c6b09567c --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts @@ -0,0 +1,21 @@ +import { RangeTypeColors } from "./RangeTypeColors"; +import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; + +export function getColorsFromConfig( + config: ScopeVisualizerColorConfig, + rangeType: "domain" | "content" | "removal"): RangeTypeColors { + return { + background: { + light: config.light[rangeType].background, + dark: config.dark[rangeType].background, + }, + borderSolid: { + light: config.light[rangeType].borderSolid, + dark: config.dark[rangeType].borderSolid, + }, + borderPorous: { + light: config.light[rangeType].borderPorous, + dark: config.dark[rangeType].borderPorous, + }, + }; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts new file mode 100644 index 0000000000..0b57a92b41 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts @@ -0,0 +1,15 @@ +import { GeneralizedRange } from "@cursorless/common"; + +export function isGeneralizedRangeEqual( + a: GeneralizedRange, + b: GeneralizedRange): boolean { + if (a.type === "character" && b.type === "character") { + return a.start.isEqual(b.start) && a.end.isEqual(b.end); + } + + if (a.type === "line" && b.type === "line") { + return a.start === b.start && a.end === b.end; + } + + return false; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts index a7d48a4504..0ebf5cb9f5 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts @@ -1,15 +1,16 @@ import { Disposable, EditableTextEditor, - EditorScopeRanges, FlashDescriptor, GeneralizedRange, HighlightId, IDE, InputBoxOptions, + IterationScopeRanges, OutdatedExtensionError, QuickPickOptions, RunMode, + ScopeRanges, TextDocumentChangeEvent, TextEditor, } from "@cursorless/common"; @@ -21,6 +22,7 @@ import { pull } from "lodash"; import { v4 as uuid } from "uuid"; import * as vscode from "vscode"; import { ExtensionContext, WorkspaceFolder, window, workspace } from "vscode"; +import { VscodeScopeVisualizer } from "./VSCodeScopeVisualizer"; import { VscodeCapabilities } from "./VscodeCapabilities"; import VscodeClipboard from "./VscodeClipboard"; import VscodeConfiguration from "./VscodeConfiguration"; @@ -30,7 +32,6 @@ import VscodeGlobalState from "./VscodeGlobalState"; import VscodeHighlights, { HighlightStyle } from "./VscodeHighlights"; import VscodeMessages from "./VscodeMessages"; import { vscodeRunMode } from "./VscodeRunMode"; -import { VscodeScopeVisualizer } from "./VSCodeScopeVisualizer"; import { VscodeTextDocumentImpl } from "./VscodeTextDocumentImpl"; import { VscodeTextEditorImpl } from "./VscodeTextEditorImpl"; import { vscodeShowQuickPick } from "./vscodeShowQuickPick"; @@ -81,12 +82,15 @@ export class VscodeIDE implements IDE { ); } - setScopeVisualizationRanges(scopeRanges: EditorScopeRanges[]): Promise { + setScopeVisualizationRanges( + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): Promise { return this.scopeVisualizer.setScopeVisualizationRanges( - scopeRanges.map(({ editor, scopeRanges }) => ({ - editor: editor as VscodeTextEditorImpl, - scopeRanges, - })), + editor as VscodeTextEditorImpl, + scopeRanges, + iterationScopeRanges, ); } From a5dc6c047a65924491a4c1bbaf6122013af7cb35 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 22 Jun 2023 16:21:53 +0100 Subject: [PATCH 06/61] More work towards simplified api --- packages/common/src/ide/types/ide.types.ts | 8 +++ .../src/CursorlessEngineApi.ts | 10 ++- .../ScopeVisualizer/EditorScopeVisualizer.ts | 10 +-- .../src/ScopeVisualizer/ScopeVisualizer.ts | 7 +- .../cursorless-engine/src/cursorlessEngine.ts | 7 +- packages/cursorless-vscode/src/extension.ts | 69 ++++++++++++++++++- .../VSCodeScopeVisualizer/RangeTypeColors.ts | 2 +- ...erer.ts => VscodeFancyRangeHighlighter.ts} | 2 +- .../VscodeScopeRenderer.ts | 62 +++++++++++++++++ .../VscodeScopeVisualizer.ts | 22 +++--- .../cursorless-vscode/src/registerCommands.ts | 3 +- 11 files changed, 172 insertions(+), 30 deletions(-) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{VscodeScopeVisualizerRenderer.ts => VscodeFancyRangeHighlighter.ts} (99%) create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts diff --git a/packages/common/src/ide/types/ide.types.ts b/packages/common/src/ide/types/ide.types.ts index 7dd2d6fe81..37cedbda54 100644 --- a/packages/common/src/ide/types/ide.types.ts +++ b/packages/common/src/ide/types/ide.types.ts @@ -244,6 +244,14 @@ export interface IterationScopeRanges { }[]; } +export interface IdeScopeVisualizer { + setScopes( + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): Promise; +} + export interface WorkspaceFolder { uri: URI; name: string; diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts index e22185ccae..9ca3e4be0e 100644 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/CursorlessEngineApi.ts @@ -1,4 +1,10 @@ -import { Command, HatTokenMap, IDE, ScopeType } from "@cursorless/common"; +import { + Command, + HatTokenMap, + IDE, + IdeScopeVisualizer, + ScopeType, +} from "@cursorless/common"; import { Snippets } from "./core/Snippets"; import { StoredTargetMap } from "./core/StoredTargets"; import { TestCaseRecorder } from "./testCaseRecorder/TestCaseRecorder"; @@ -29,7 +35,7 @@ export interface CommandApi { } export interface ScopeVisualizer { - start(config: ScopeVisualizerConfig): void; + start(ideScopeVisualizer: IdeScopeVisualizer): void; stop(): void; } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts index 9a3afae6a3..734d727fac 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts @@ -1,10 +1,10 @@ import { Disposable, + IdeScopeVisualizer, IterationScopeRanges, Range, TextEditor, } from "@cursorless/common"; -import { ScopeVisualizerConfig } from "../CursorlessEngineApi"; import { Debouncer } from "../core/Debouncer"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; @@ -23,8 +23,8 @@ export class EditorScopeVisualizer implements Disposable { constructor( private scopeHandlerFactory: ScopeHandlerFactory, private modifierStageFactory: ModifierStageFactory, + private ideVisualizer: IdeScopeVisualizer, private editor: TextEditor, - private config: ScopeVisualizerConfig, ) { this.disposables.push( // An event that is emitted when a text document is changed. This usually @@ -50,12 +50,12 @@ export class EditorScopeVisualizer implements Disposable { const iterationRange = getIterationRange(this.editor, scopeHandler); - ide().setScopeVisualizationRanges( + this.ideVisualizer.setScopes( this.editor, - this.config.includeScopes + this.ideVisualizer.config.includeScopes ? getScopes(this.editor, scopeHandler, iterationRange) : undefined, - this.config.includeIterationScopes + this.ideVisualizer.config.includeIterationScopes ? this.getIterationScopes(scopeHandler, iterationRange) : undefined, ); diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts index b29f1dc588..43f7262d57 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts @@ -1,5 +1,4 @@ -import { showError } from "@cursorless/common"; -import { ScopeVisualizerConfig } from "../CursorlessEngineApi"; +import { IdeScopeVisualizer, showError } from "@cursorless/common"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { ide } from "../singletons/ide.singleton"; @@ -14,14 +13,14 @@ export class ScopeVisualizer { private modifierStageFactory: ModifierStageFactory, ) {} - start(config: ScopeVisualizerConfig) { + start(ideScopeVisualizer: IdeScopeVisualizer) { this.scopeVisualizers?.dispose(); this.scopeVisualizers = new PerEditor((editor) => { const visualizer = new EditorScopeVisualizer( this.scopeHandlerFactory, this.modifierStageFactory, + ideScopeVisualizer, editor, - config, ); visualizer.highlightScopes().catch((err: Error) => { diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index a8be42cc96..14aea09f38 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,4 +1,9 @@ -import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; +import { + Command, + CommandServerApi, + Hats, + IDE, +} from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 2412228d60..1d6d955d38 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -4,10 +4,13 @@ import { isTesting, NormalizedIDE, Range, + ScopeType, TextDocument, } from "@cursorless/common"; import { createCursorlessEngine, + ScopeVisualizer, + ScopeVisualizerConfig, TreeSitter, } from "@cursorless/cursorless-engine"; import { @@ -26,6 +29,7 @@ import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; import { registerCommands } from "./registerCommands"; import { StatusBarItem } from "./StatusBarItem"; +import { VscodeScopeVisualizer } from "./ide/vscode/VSCodeScopeVisualizer"; /** * Extension entrypoint called by VSCode on Cursorless startup. @@ -40,7 +44,8 @@ export async function activate( ): Promise { const parseTreeApi = await getParseTreeApi(); - const { vscodeIDE, hats } = await createVscodeIde(context); + const { vscodeIDE, hats, vscodeScopeVisualizerFactory } = + await createVscodeIde(context); const normalizedIde = vscodeIDE.runMode === "production" @@ -63,7 +68,7 @@ export async function activate( testCaseRecorder, storedTargets, hatTokenMap, - scopeVisualizer, + scopeVisualizer: engineScopeVisualizer, snippets, injectIde, runIntegrationTests, @@ -74,6 +79,11 @@ export async function activate( commandServerApi, ); + const scopeVisualizer = new ScopeVisualizerImpl( + vscodeScopeVisualizerFactory, + engineScopeVisualizer, + ); + const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); const keyboardCommands = KeyboardCommands.create(context, statusBarItem); @@ -106,6 +116,59 @@ export async function activate( }; } +type VisualizationType = "content" | "removal" | "iteration" | "every"; + +type VscodeScopeVisualizerFactory = ( + scopeType: ScopeType, + visualizationType: VisualizationType, +) => VscodeScopeVisualizer; + +class ScopeVisualizerImpl { + private scopeVisualizer: VscodeScopeVisualizer | undefined; + + constructor( + private readonly vscodeScopeVisualizerFactory: VscodeScopeVisualizerFactory, + private engineScopeVisualizer: ScopeVisualizer, + ) {} + + start(scopeType: ScopeType, visualizationType: VisualizationType) { + this.stop(); + this.scopeVisualizer = this.vscodeScopeVisualizerFactory( + scopeType, + visualizationType, + ); + let config: ScopeVisualizerConfig; + switch (visualizationType) { + case "content": + case "removal": + config = { + scopeType, + includeScopes: true, + includeIterationScopes: false, + includeIterationNestedTargets: false, + }; + break; + + case "iteration": + case "every": + config = { + scopeType, + includeScopes: false, + includeIterationScopes: true, + includeIterationNestedTargets: visualizationType === "every", + }; + } + + this.engineScopeVisualizer.start(this.scopeVisualizer); + } + + stop() { + this.engineScopeVisualizer.stop(); + this.scopeVisualizer?.dispose(); + this.scopeVisualizer = undefined; + } +} + async function createVscodeIde(context: vscode.ExtensionContext) { const vscodeIDE = new VscodeIDE(context); @@ -118,7 +181,7 @@ async function createVscodeIde(context: vscode.ExtensionContext) { ); await hats.init(); - return { vscodeIDE, hats }; + return { vscodeIDE, hats, vscodeScopeVisualizerFactory }; } function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts index 5a11577454..633e5dea67 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts @@ -1,9 +1,9 @@ - export interface RangeTypeColors { background: ThemeColors; borderSolid: ThemeColors; borderPorous: ThemeColors; } + interface ThemeColors { light: string; dark: string; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts similarity index 99% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts index c5b16e0090..7b70a7e890 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizerRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts @@ -31,7 +31,7 @@ import { getDifferentiatedRanges } from "./getDecorationRanges/getDifferentiated /** * Manages VSCode decoration types for a highlight or flash style. */ -export class VscodeScopeVisualizerRenderer { +export class VscodeFancyRangeHighlighter { private decorator: Decorator; constructor(colors: RangeTypeColors) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts new file mode 100644 index 0000000000..4d441bf869 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts @@ -0,0 +1,62 @@ +import { Disposable, GeneralizedRange } from "@cursorless/common"; +import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; +import { RangeTypeColors } from "./RangeTypeColors"; +import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; +import { blendRangeTypeColors } from "./blendRangeTypeColors"; +import { isGeneralizedRangeEqual } from "./isGeneralizedRangeEqual"; + +interface RendererScope { + domain: GeneralizedRange; + nestedRanges: GeneralizedRange[]; +} + +export class VscodeScopeRenderer implements Disposable { + private domainHighlighter: VscodeFancyRangeHighlighter; + private nestedRangeHighlighter: VscodeFancyRangeHighlighter; + private domainEqualsNestedHighlighter: VscodeFancyRangeHighlighter; + + constructor( + domainColors: RangeTypeColors, + nestedRangeColors: RangeTypeColors, + ) { + this.domainHighlighter = new VscodeFancyRangeHighlighter(domainColors); + this.nestedRangeHighlighter = new VscodeFancyRangeHighlighter( + nestedRangeColors, + ); + this.domainEqualsNestedHighlighter = new VscodeFancyRangeHighlighter( + blendRangeTypeColors(domainColors, nestedRangeColors), + ); + } + + setScopes(editor: VscodeTextEditorImpl, scopes: RendererScope[]) { + const domainRanges: GeneralizedRange[] = []; + const nestedRanges: GeneralizedRange[] = []; + const domainEqualsNestedRanges: GeneralizedRange[] = []; + + for (const { domain, nestedRanges } of scopes) { + if ( + nestedRanges.length === 1 && + isGeneralizedRangeEqual(nestedRanges[0], domain) + ) { + domainEqualsNestedRanges.push(domain); + continue; + } + + domainRanges.push(domain); + nestedRanges.push(...nestedRanges); + } + + this.domainHighlighter.setRanges(editor, domainRanges); + this.nestedRangeHighlighter.setRanges(editor, nestedRanges); + this.domainEqualsNestedHighlighter.setRanges( + editor, + domainEqualsNestedRanges, + ); + } + + dispose(): void { + this.domainHighlighter.dispose(); + this.nestedRangeHighlighter.dispose(); + this.domainEqualsNestedHighlighter.dispose(); + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index bb240e0605..65df2ddb96 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -5,18 +5,18 @@ import { } from "@cursorless/common"; import * as vscode from "vscode"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; -import { VscodeScopeVisualizerRenderer } from "./VscodeScopeVisualizerRenderer"; +import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; import { isGeneralizedRangeEqual } from "./isGeneralizedRangeEqual"; import { blendRangeTypeColors } from "./blendRangeTypeColors"; import { getColorsFromConfig } from "./getColorsFromConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; export class VscodeScopeVisualizer { - private domainRenderer!: VscodeScopeVisualizerRenderer; - private contentRenderer!: VscodeScopeVisualizerRenderer; - private removalRenderer!: VscodeScopeVisualizerRenderer; - private domainContentOverlappingRenderer!: VscodeScopeVisualizerRenderer; - private domainRemovalOverlappingRenderer!: VscodeScopeVisualizerRenderer; + private domainRenderer!: VscodeFancyRangeHighlighter; + private contentRenderer!: VscodeFancyRangeHighlighter; + private removalRenderer!: VscodeFancyRangeHighlighter; + private domainContentOverlappingRenderer!: VscodeFancyRangeHighlighter; + private domainRemovalOverlappingRenderer!: VscodeFancyRangeHighlighter; constructor(extensionContext: vscode.ExtensionContext) { this.computeColors(); @@ -40,18 +40,18 @@ export class VscodeScopeVisualizer { const removalColors = getColorsFromConfig(config, "removal"); this.domainRenderer?.dispose(); - this.domainRenderer = new VscodeScopeVisualizerRenderer(domainColors); + this.domainRenderer = new VscodeFancyRangeHighlighter(domainColors); this.contentRenderer?.dispose(); - this.contentRenderer = new VscodeScopeVisualizerRenderer(contentColors); + this.contentRenderer = new VscodeFancyRangeHighlighter(contentColors); this.removalRenderer?.dispose(); - this.removalRenderer = new VscodeScopeVisualizerRenderer(removalColors); + this.removalRenderer = new VscodeFancyRangeHighlighter(removalColors); this.domainContentOverlappingRenderer?.dispose(); - this.domainContentOverlappingRenderer = new VscodeScopeVisualizerRenderer( + this.domainContentOverlappingRenderer = new VscodeFancyRangeHighlighter( blendRangeTypeColors(domainColors, contentColors), ); this.domainRemovalOverlappingRenderer?.dispose(); - this.domainRemovalOverlappingRenderer = new VscodeScopeVisualizerRenderer( + this.domainRemovalOverlappingRenderer = new VscodeFancyRangeHighlighter( blendRangeTypeColors(domainColors, removalColors), ); diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index 5c3397d067..d4fea616bc 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -5,7 +5,6 @@ import { } from "@cursorless/common"; import { CommandApi, - ScopeVisualizer, TestCaseRecorder, showCheatsheet, updateDefaults, @@ -21,7 +20,7 @@ export function registerCommands( vscodeIde: VscodeIDE, commandApi: CommandApi, testCaseRecorder: TestCaseRecorder, - scopeVisualizer: ScopeVisualizer, + scopeVisualizer: VscodeScopeVisualizer, keyboardCommands: KeyboardCommands, hats: VscodeHats, ): void { From 8f31ae02721f512f0d94a464f5d29974fc22e1bc Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 22 Jun 2023 17:46:21 +0100 Subject: [PATCH 07/61] Working version of rewrite --- cursorless-talon/src/cursorless.talon | 4 + packages/common/src/ide/PassthroughIDEBase.ts | 21 +- packages/common/src/ide/fake/FakeIDE.ts | 6 +- packages/common/src/ide/spy/SpyIDE.ts | 6 +- .../src/ide/types/IdeScopeVisualizer.ts | 37 ++++ packages/common/src/ide/types/ide.types.ts | 37 ---- packages/common/src/index.ts | 1 + .../src/CursorlessEngineApi.ts | 13 +- .../ScopeVisualizer/EditorScopeVisualizer.ts | 18 +- ...peVisualizer.ts => ScopeVisualizerImpl.ts} | 18 +- .../src/ScopeVisualizer/index.ts | 2 +- .../cursorless-engine/src/cursorlessEngine.ts | 4 +- .../cursorless-engine/src/util/PerEditor.ts | 10 +- packages/cursorless-vscode/package.json | 44 ++++ .../src/ScopeVisualizerImpl.ts | 37 ++++ packages/cursorless-vscode/src/extension.ts | 68 +----- .../src/getVisualizerConfig.ts | 28 +++ .../ScopeVisualizerColorConfig.ts | 29 +++ .../VscodeScopeRenderer.ts | 8 +- .../VscodeScopeVisualizer.ts | 208 ++++++++++-------- .../getColorsFromConfig.ts | 21 -- .../src/ide/vscode/VscodeIDE.ts | 17 -- .../cursorless-vscode/src/registerCommands.ts | 3 +- 23 files changed, 357 insertions(+), 283 deletions(-) create mode 100644 packages/common/src/ide/types/IdeScopeVisualizer.ts rename packages/cursorless-engine/src/ScopeVisualizer/{ScopeVisualizer.ts => ScopeVisualizerImpl.ts} (72%) create mode 100644 packages/cursorless-vscode/src/ScopeVisualizerImpl.ts create mode 100644 packages/cursorless-vscode/src/getVisualizerConfig.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index 7fdcfd1a4c..d5f77c21d8 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -25,5 +25,9 @@ visualize : user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "content") visualize removal: user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "removal") +visualize iteration: + user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "iteration") +visualize every : + user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "every") visualize nothing: user.private_cursorless_run_rpc_command_and_wait("cursorless.hideScopeVisualizer") diff --git a/packages/common/src/ide/PassthroughIDEBase.ts b/packages/common/src/ide/PassthroughIDEBase.ts index fcdc5b102e..23dc1e034a 100644 --- a/packages/common/src/ide/PassthroughIDEBase.ts +++ b/packages/common/src/ide/PassthroughIDEBase.ts @@ -10,14 +10,7 @@ import { TextEditorVisibleRangesChangeEvent, } from "./types/events.types"; import { FlashDescriptor } from "./types/FlashDescriptor"; -import { - Disposable, - IDE, - IterationScopeRanges, - RunMode, - ScopeRanges, - WorkspaceFolder, -} from "./types/ide.types"; +import { Disposable, IDE, RunMode, WorkspaceFolder } from "./types/ide.types"; import { Messages } from "./types/Messages"; import { QuickPickOptions } from "./types/QuickPickOptions"; import { State } from "./types/State"; @@ -37,18 +30,6 @@ export default class PassthroughIDEBase implements IDE { this.capabilities = original.capabilities; } - setScopeVisualizationRanges( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise { - return this.original.setScopeVisualizationRanges( - editor, - scopeRanges, - iterationScopeRanges, - ); - } - flashRanges(flashDescriptors: FlashDescriptor[]): Promise { return this.original.flashRanges(flashDescriptors); } diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index 8b428758c7..e752abe6ca 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -13,11 +13,13 @@ import { import type { Disposable, IDE, - IterationScopeRanges, RunMode, - ScopeRanges, WorkspaceFolder, } from "../types/ide.types"; +import type { + IterationScopeRanges, + ScopeRanges, +} from "../types/IdeScopeVisualizer"; import { FakeCapabilities } from "./FakeCapabilities"; import FakeClipboard from "./FakeClipboard"; import FakeConfiguration from "./FakeConfiguration"; diff --git a/packages/common/src/ide/spy/SpyIDE.ts b/packages/common/src/ide/spy/SpyIDE.ts index 127701bcfc..e5b4112fed 100644 --- a/packages/common/src/ide/spy/SpyIDE.ts +++ b/packages/common/src/ide/spy/SpyIDE.ts @@ -6,9 +6,11 @@ import { FlashDescriptor } from "../types/FlashDescriptor"; import type { HighlightId, IDE, - IterationScopeRanges, - ScopeRanges, } from "../types/ide.types"; +import type { + IterationScopeRanges, + ScopeRanges +} from "../types/IdeScopeVisualizer"; import SpyMessages, { Message } from "./SpyMessages"; interface Highlight { diff --git a/packages/common/src/ide/types/IdeScopeVisualizer.ts b/packages/common/src/ide/types/IdeScopeVisualizer.ts new file mode 100644 index 0000000000..23880bff77 --- /dev/null +++ b/packages/common/src/ide/types/IdeScopeVisualizer.ts @@ -0,0 +1,37 @@ +import type { ScopeType, TextEditor } from "../.."; +import { GeneralizedRange } from "../../types/GeneralizedRange"; + +export interface ScopeRenderer { + setScopes( + editor: TextEditor, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): Promise; + + visualizerConfig: ScopeVisualizerConfig; +} + +export interface ScopeRanges { + domain: GeneralizedRange; + targets: TargetRanges[]; +} + +export interface TargetRanges { + contentRange: GeneralizedRange; + removalRange: GeneralizedRange; +} + +export interface IterationScopeRanges { + domain: GeneralizedRange; + ranges: { + range: GeneralizedRange; + targets?: TargetRanges[]; + }[]; +} + +export interface ScopeVisualizerConfig { + scopeType: ScopeType; + includeScopes: boolean; + includeIterationScopes: boolean; + includeIterationNestedTargets: boolean; +} diff --git a/packages/common/src/ide/types/ide.types.ts b/packages/common/src/ide/types/ide.types.ts index 37cedbda54..c99e1e711d 100644 --- a/packages/common/src/ide/types/ide.types.ts +++ b/packages/common/src/ide/types/ide.types.ts @@ -213,43 +213,6 @@ export interface IDE { editor: TextEditor, ranges: GeneralizedRange[], ): Promise; - - setScopeVisualizationRanges( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise; -} - -export interface EditorScopeRanges { - editor: TextEditor; - scopeRanges: ScopeRanges[]; -} - -export interface ScopeRanges { - domain: GeneralizedRange; - targets: TargetRanges[]; -} - -export interface TargetRanges { - contentRange: GeneralizedRange; - removalRange: GeneralizedRange; -} - -export interface IterationScopeRanges { - domain: GeneralizedRange; - ranges: { - range: GeneralizedRange; - targets?: TargetRanges[]; - }[]; -} - -export interface IdeScopeVisualizer { - setScopes( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise; } export interface WorkspaceFolder { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 2f98a969b4..1a6817a74e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -18,6 +18,7 @@ export * from "./util/walkAsync"; export { Listener, Notifier } from "./util/Notifier"; export { TokenHatSplittingMode } from "./ide/types/Configuration"; export * from "./ide/types/ide.types"; +export * from "./ide/types/IdeScopeVisualizer"; export * from "./ide/types/Capabilities"; export * from "./ide/types/CommandId"; export * from "./ide/types/FlashDescriptor"; diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts index 9ca3e4be0e..b41a929284 100644 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/CursorlessEngineApi.ts @@ -2,8 +2,7 @@ import { Command, HatTokenMap, IDE, - IdeScopeVisualizer, - ScopeType, + ScopeRenderer, } from "@cursorless/common"; import { Snippets } from "./core/Snippets"; import { StoredTargetMap } from "./core/StoredTargets"; @@ -35,13 +34,7 @@ export interface CommandApi { } export interface ScopeVisualizer { - start(ideScopeVisualizer: IdeScopeVisualizer): void; + start(ideScopeVisualizer: ScopeRenderer): void; stop(): void; -} - -export interface ScopeVisualizerConfig { - scopeType: ScopeType; - includeScopes: boolean; - includeIterationScopes: boolean; - includeIterationNestedTargets: boolean; + refresh(): void; } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts index 734d727fac..7d144e7720 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts @@ -1,6 +1,6 @@ import { Disposable, - IdeScopeVisualizer, + ScopeRenderer, IterationScopeRanges, Range, TextEditor, @@ -23,7 +23,7 @@ export class EditorScopeVisualizer implements Disposable { constructor( private scopeHandlerFactory: ScopeHandlerFactory, private modifierStageFactory: ModifierStageFactory, - private ideVisualizer: IdeScopeVisualizer, + private renderer: ScopeRenderer, private editor: TextEditor, ) { this.disposables.push( @@ -44,18 +44,21 @@ export class EditorScopeVisualizer implements Disposable { } = this.editor; const scopeHandler = checkNonNull( - this.scopeHandlerFactory.create(this.config.scopeType, languageId), + this.scopeHandlerFactory.create( + this.renderer.visualizerConfig.scopeType, + languageId, + ), () => new UnsupportedScopeTypeVisualizationError(languageId), ); const iterationRange = getIterationRange(this.editor, scopeHandler); - this.ideVisualizer.setScopes( + this.renderer.setScopes( this.editor, - this.ideVisualizer.config.includeScopes + this.renderer.visualizerConfig.includeScopes ? getScopes(this.editor, scopeHandler, iterationRange) : undefined, - this.ideVisualizer.config.includeIterationScopes + this.renderer.visualizerConfig.includeIterationScopes ? this.getIterationScopes(scopeHandler, iterationRange) : undefined, ); @@ -69,7 +72,8 @@ export class EditorScopeVisualizer implements Disposable { const { document: { languageId }, } = editor; - const { scopeType, includeIterationNestedTargets } = this.config; + const { scopeType, includeIterationNestedTargets } = + this.renderer.visualizerConfig; const iterationScopeHandler = checkNonNull( this.scopeHandlerFactory.create( diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts similarity index 72% rename from packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts rename to packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts index 43f7262d57..51b21f1991 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts @@ -1,20 +1,21 @@ -import { IdeScopeVisualizer, showError } from "@cursorless/common"; +import { ScopeRenderer, showError } from "@cursorless/common"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { ide } from "../singletons/ide.singleton"; import { PerEditor } from "../util/PerEditor"; import { EditorScopeVisualizer } from "./EditorScopeVisualizer"; +import { ScopeVisualizer } from ".."; -export class ScopeVisualizer { - private scopeVisualizers: PerEditor | undefined; +export class ScopeVisualizerImpl implements ScopeVisualizer { + private scopeVisualizers: PerEditor | undefined; constructor( private scopeHandlerFactory: ScopeHandlerFactory, private modifierStageFactory: ModifierStageFactory, ) {} - start(ideScopeVisualizer: IdeScopeVisualizer) { - this.scopeVisualizers?.dispose(); + start(ideScopeVisualizer: ScopeRenderer) { + this.stop(); this.scopeVisualizers = new PerEditor((editor) => { const visualizer = new EditorScopeVisualizer( this.scopeHandlerFactory, @@ -42,7 +43,14 @@ export class ScopeVisualizer { }); } + refresh() { + for (const visualizer of this.scopeVisualizers?.values() || []) { + visualizer.highlightScopes(); + } + } + stop() { this.scopeVisualizers?.dispose(); + this.scopeVisualizers = undefined; } } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/index.ts b/packages/cursorless-engine/src/ScopeVisualizer/index.ts index c16ea931c0..2189e28dad 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/index.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/index.ts @@ -1 +1 @@ -export * from "./ScopeVisualizer"; +export * from "./ScopeVisualizerImpl"; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 14aea09f38..48ca72805e 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -16,7 +16,7 @@ import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandler import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; -import { ScopeVisualizer } from "./ScopeVisualizer"; +import { ScopeVisualizerImpl } from "./ScopeVisualizer"; import { CursorlessEngine } from "./CursorlessEngineApi"; export function createCursorlessEngine( @@ -79,7 +79,7 @@ export function createCursorlessEngine( ); }, }, - scopeVisualizer: new ScopeVisualizer( + scopeVisualizer: new ScopeVisualizerImpl( scopeHandlerFactory, new ModifierStageFactoryImpl( languageDefinitions, diff --git a/packages/cursorless-engine/src/util/PerEditor.ts b/packages/cursorless-engine/src/util/PerEditor.ts index 92da9807b0..f4877b9b65 100644 --- a/packages/cursorless-engine/src/util/PerEditor.ts +++ b/packages/cursorless-engine/src/util/PerEditor.ts @@ -1,11 +1,11 @@ import { Disposable, TextEditor } from "@cursorless/common"; import { ide } from "../singletons/ide.singleton"; -export class PerEditor { +export class PerEditor { private disposables: Disposable[] = []; - private editorHandlers: Map = new Map(); + private editorHandlers: Map = new Map(); - constructor(private makeEditorHandler: (editor: TextEditor) => Disposable) { + constructor(private makeEditorHandler: (editor: TextEditor) => T) { this.disposables.push( // An event that fires when a text document opens ide().onDidOpenTextDocument(this.handleChange), @@ -36,6 +36,10 @@ export class PerEditor { } } + values() { + return this.editorHandlers.values(); + } + dispose() { for (const disposable of this.disposables) { disposable.dispose(); diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index f778815c37..11b4be8ac6 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -453,6 +453,23 @@ "format": "color" } } + }, + "iteration": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } } }, "default": { @@ -470,6 +487,11 @@ "background": "#ff00002d", "borderSolid": "rgba(255, 0, 0, 0.47)", "borderPorous": "rgba(255, 0, 0, 0.29)" + }, + "iteration": { + "background": "#00ff002d", + "borderSolid": "rgba(255, 0, 0, 0.47)", + "borderPorous": "rgba(255, 0, 0, 0.29)" } } }, @@ -527,6 +549,23 @@ "format": "color" } } + }, + "iteration": { + "type": "object", + "properties": { + "background": { + "type": "string", + "format": "color" + }, + "borderSolid": { + "type": "string", + "format": "color" + }, + "borderPorous": { + "type": "string", + "format": "color" + } + } } }, "default": { @@ -544,6 +583,11 @@ "background": "#ff00002d", "borderSolid": "rgba(255, 0, 0, 0.47)", "borderPorous": "rgba(255, 0, 0, 0.29)" + }, + "iteration": { + "background": "#00ff002d", + "borderSolid": "rgba(255, 0, 0, 0.47)", + "borderPorous": "rgba(255, 0, 0, 0.29)" } } }, diff --git a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts new file mode 100644 index 0000000000..1cef2ca174 --- /dev/null +++ b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts @@ -0,0 +1,37 @@ +import { ScopeType } from "@cursorless/common"; +import { ScopeVisualizer } from "@cursorless/cursorless-engine"; +import { + createVscodeScopeVisualizer, + VscodeScopeVisualizer, +} from "./ide/vscode/VSCodeScopeVisualizer"; +import { VisualizationType } from "./getVisualizerConfig"; + +export interface ScopeVisualizerCommandApi { + start(scopeType: ScopeType, visualizationType: VisualizationType): void; + stop(): void; +} + +export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { + private scopeVisualizer: VscodeScopeVisualizer | undefined; + + constructor(private engineScopeVisualizer: ScopeVisualizer) { + this.start = this.start.bind(this); + this.stop = this.stop.bind(this); + } + + start(scopeType: ScopeType, visualizationType: VisualizationType) { + this.stop(); + this.scopeVisualizer = createVscodeScopeVisualizer( + scopeType, + visualizationType, + ); + + this.engineScopeVisualizer.start(this.scopeVisualizer); + } + + stop() { + this.engineScopeVisualizer.stop(); + this.scopeVisualizer?.dispose(); + this.scopeVisualizer = undefined; + } +} diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 1d6d955d38..023f5f10a5 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -4,13 +4,10 @@ import { isTesting, NormalizedIDE, Range, - ScopeType, TextDocument, } from "@cursorless/common"; import { createCursorlessEngine, - ScopeVisualizer, - ScopeVisualizerConfig, TreeSitter, } from "@cursorless/cursorless-engine"; import { @@ -29,7 +26,7 @@ import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; import { registerCommands } from "./registerCommands"; import { StatusBarItem } from "./StatusBarItem"; -import { VscodeScopeVisualizer } from "./ide/vscode/VSCodeScopeVisualizer"; +import { ScopeVisualizerImpl } from "./ScopeVisualizerImpl"; /** * Extension entrypoint called by VSCode on Cursorless startup. @@ -44,8 +41,7 @@ export async function activate( ): Promise { const parseTreeApi = await getParseTreeApi(); - const { vscodeIDE, hats, vscodeScopeVisualizerFactory } = - await createVscodeIde(context); + const { vscodeIDE, hats } = await createVscodeIde(context); const normalizedIde = vscodeIDE.runMode === "production" @@ -79,10 +75,7 @@ export async function activate( commandServerApi, ); - const scopeVisualizer = new ScopeVisualizerImpl( - vscodeScopeVisualizerFactory, - engineScopeVisualizer, - ); + const scopeVisualizer = new ScopeVisualizerImpl(engineScopeVisualizer); const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); const keyboardCommands = KeyboardCommands.create(context, statusBarItem); @@ -116,59 +109,6 @@ export async function activate( }; } -type VisualizationType = "content" | "removal" | "iteration" | "every"; - -type VscodeScopeVisualizerFactory = ( - scopeType: ScopeType, - visualizationType: VisualizationType, -) => VscodeScopeVisualizer; - -class ScopeVisualizerImpl { - private scopeVisualizer: VscodeScopeVisualizer | undefined; - - constructor( - private readonly vscodeScopeVisualizerFactory: VscodeScopeVisualizerFactory, - private engineScopeVisualizer: ScopeVisualizer, - ) {} - - start(scopeType: ScopeType, visualizationType: VisualizationType) { - this.stop(); - this.scopeVisualizer = this.vscodeScopeVisualizerFactory( - scopeType, - visualizationType, - ); - let config: ScopeVisualizerConfig; - switch (visualizationType) { - case "content": - case "removal": - config = { - scopeType, - includeScopes: true, - includeIterationScopes: false, - includeIterationNestedTargets: false, - }; - break; - - case "iteration": - case "every": - config = { - scopeType, - includeScopes: false, - includeIterationScopes: true, - includeIterationNestedTargets: visualizationType === "every", - }; - } - - this.engineScopeVisualizer.start(this.scopeVisualizer); - } - - stop() { - this.engineScopeVisualizer.stop(); - this.scopeVisualizer?.dispose(); - this.scopeVisualizer = undefined; - } -} - async function createVscodeIde(context: vscode.ExtensionContext) { const vscodeIDE = new VscodeIDE(context); @@ -181,7 +121,7 @@ async function createVscodeIde(context: vscode.ExtensionContext) { ); await hats.init(); - return { vscodeIDE, hats, vscodeScopeVisualizerFactory }; + return { vscodeIDE, hats }; } function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter { diff --git a/packages/cursorless-vscode/src/getVisualizerConfig.ts b/packages/cursorless-vscode/src/getVisualizerConfig.ts new file mode 100644 index 0000000000..da0e59db70 --- /dev/null +++ b/packages/cursorless-vscode/src/getVisualizerConfig.ts @@ -0,0 +1,28 @@ +import { ScopeType, ScopeVisualizerConfig } from "@cursorless/common"; + +export type VisualizationType = "content" | "removal" | "iteration" | "every"; + +export function getVisualizerConfig( + visualizationType: VisualizationType, + scopeType: ScopeType, +): ScopeVisualizerConfig { + switch (visualizationType) { + case "content": + case "removal": + return { + scopeType, + includeScopes: true, + includeIterationScopes: false, + includeIterationNestedTargets: false, + }; + + case "iteration": + case "every": + return { + scopeType, + includeScopes: false, + includeIterationScopes: true, + includeIterationNestedTargets: visualizationType === "every", + }; + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts index af7d313aa6..f4c01fc418 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts @@ -1,8 +1,32 @@ +import { RangeTypeColors } from "./RangeTypeColors"; + +export function getColorsFromConfig( + config: ScopeVisualizerColorConfig, + rangeType: ColorConfigKey, +): RangeTypeColors { + return { + background: { + light: config.light[rangeType].background, + dark: config.dark[rangeType].background, + }, + borderSolid: { + light: config.light[rangeType].borderSolid, + dark: config.dark[rangeType].borderSolid, + }, + borderPorous: { + light: config.light[rangeType].borderPorous, + dark: config.dark[rangeType].borderPorous, + }, + }; +} + +export type ColorConfigKey = keyof ScopeVisualizerThemeColorConfig; export interface ScopeVisualizerColorConfig { light: ScopeVisualizerThemeColorConfig; dark: ScopeVisualizerThemeColorConfig; } + interface ScopeVisualizerThemeColorConfig { domain: { background: string; @@ -19,4 +43,9 @@ interface ScopeVisualizerThemeColorConfig { borderSolid: string; borderPorous: string; }; + iteration: { + background: string; + borderSolid: string; + borderPorous: string; + }; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts index 4d441bf869..9d82a1e3aa 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts @@ -5,7 +5,7 @@ import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; import { blendRangeTypeColors } from "./blendRangeTypeColors"; import { isGeneralizedRangeEqual } from "./isGeneralizedRangeEqual"; -interface RendererScope { +export interface RendererScope { domain: GeneralizedRange; nestedRanges: GeneralizedRange[]; } @@ -30,7 +30,7 @@ export class VscodeScopeRenderer implements Disposable { setScopes(editor: VscodeTextEditorImpl, scopes: RendererScope[]) { const domainRanges: GeneralizedRange[] = []; - const nestedRanges: GeneralizedRange[] = []; + const allNestedRanges: GeneralizedRange[] = []; const domainEqualsNestedRanges: GeneralizedRange[] = []; for (const { domain, nestedRanges } of scopes) { @@ -43,11 +43,11 @@ export class VscodeScopeRenderer implements Disposable { } domainRanges.push(domain); - nestedRanges.push(...nestedRanges); + allNestedRanges.push(...nestedRanges); } this.domainHighlighter.setRanges(editor, domainRanges); - this.nestedRangeHighlighter.setRanges(editor, nestedRanges); + this.nestedRangeHighlighter.setRanges(editor, allNestedRanges); this.domainEqualsNestedHighlighter.setRanges( editor, domainEqualsNestedRanges, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index 65df2ddb96..fb86b508fe 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -1,124 +1,158 @@ import { - GeneralizedRange, + Disposable, + ScopeRenderer, IterationScopeRanges, ScopeRanges, + ScopeType, + ScopeVisualizerConfig, } from "@cursorless/common"; import * as vscode from "vscode"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; -import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; -import { isGeneralizedRangeEqual } from "./isGeneralizedRangeEqual"; -import { blendRangeTypeColors } from "./blendRangeTypeColors"; -import { getColorsFromConfig } from "./getColorsFromConfig"; +import { + ColorConfigKey, + getColorsFromConfig, +} from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; +import { RendererScope, VscodeScopeRenderer } from "./VscodeScopeRenderer"; +import { + VisualizationType, + getVisualizerConfig, +} from "../../../getVisualizerConfig"; -export class VscodeScopeVisualizer { - private domainRenderer!: VscodeFancyRangeHighlighter; - private contentRenderer!: VscodeFancyRangeHighlighter; - private removalRenderer!: VscodeFancyRangeHighlighter; - private domainContentOverlappingRenderer!: VscodeFancyRangeHighlighter; - private domainRemovalOverlappingRenderer!: VscodeFancyRangeHighlighter; +export abstract class VscodeScopeVisualizer implements ScopeRenderer { + private renderer!: VscodeScopeRenderer; + private disposables: Disposable[] = []; + visualizerConfig: ScopeVisualizerConfig; - constructor(extensionContext: vscode.ExtensionContext) { - this.computeColors(); + protected abstract getRendererScopes( + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ): RendererScope[]; + + constructor( + scopeType: ScopeType, + private visualizationType: VisualizationType, + private colorConfigKey: ColorConfigKey, + ) { + this.visualizerConfig = getVisualizerConfig(visualizationType, scopeType); - extensionContext.subscriptions.push( + this.disposables.push( vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { this.computeColors(); } }), ); + + this.computeColors(); } private computeColors() { - const config = vscode.workspace + const colorConfig = vscode.workspace .getConfiguration("cursorless.scopeVisualizer") .get("colors")!; - const domainColors = getColorsFromConfig(config, "domain"); - const contentColors = getColorsFromConfig(config, "content"); - const removalColors = getColorsFromConfig(config, "removal"); - - this.domainRenderer?.dispose(); - this.domainRenderer = new VscodeFancyRangeHighlighter(domainColors); - this.contentRenderer?.dispose(); - this.contentRenderer = new VscodeFancyRangeHighlighter(contentColors); - this.removalRenderer?.dispose(); - this.removalRenderer = new VscodeFancyRangeHighlighter(removalColors); - - this.domainContentOverlappingRenderer?.dispose(); - this.domainContentOverlappingRenderer = new VscodeFancyRangeHighlighter( - blendRangeTypeColors(domainColors, contentColors), + this.renderer = new VscodeScopeRenderer( + getColorsFromConfig(colorConfig, "domain"), + getColorsFromConfig(colorConfig, this.colorConfigKey), ); - this.domainRemovalOverlappingRenderer?.dispose(); - this.domainRemovalOverlappingRenderer = new VscodeFancyRangeHighlighter( - blendRangeTypeColors(domainColors, removalColors), + } + + async setScopes( + editor: VscodeTextEditorImpl, + scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ) { + this.renderer.setScopes( + editor, + this.getRendererScopes(scopeRanges, iterationScopeRanges), ); + } - this.drawScopes(); + dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + this.renderer.dispose(); } +} - private editorScopeRanges: VscodeTextEditorScopeRanges[] = []; +class VscodeScopeContentVisualizer extends VscodeScopeVisualizer { + constructor(scopeType: ScopeType, visualizationType: VisualizationType) { + super(scopeType, visualizationType, "content" as const); + } - async setScopeVisualizationRanges( - editor: VscodeTextEditorImpl, + protected getRendererScopes( + scopeRanges: ScopeRanges[] | undefined, + _iterationScopeRanges: IterationScopeRanges[] | undefined, + ) { + return scopeRanges!.map(({ domain, targets }) => ({ + domain, + nestedRanges: targets.map(({ contentRange }) => contentRange), + })); + } +} + +class VscodeScopeRemovalVisualizer extends VscodeScopeVisualizer { + constructor(scopeType: ScopeType, visualizationType: VisualizationType) { + super(scopeType, visualizationType, "removal" as const); + } + + protected getRendererScopes( scopeRanges: ScopeRanges[] | undefined, + _iterationScopeRanges: IterationScopeRanges[] | undefined, + ) { + return scopeRanges!.map(({ domain, targets }) => ({ + domain, + nestedRanges: targets.map(({ removalRange }) => removalRange), + })); + } +} + +class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { + constructor(scopeType: ScopeType, visualizationType: VisualizationType) { + super(scopeType, visualizationType, "iteration" as const); + } + + protected getRendererScopes( + _scopeRanges: ScopeRanges[] | undefined, iterationScopeRanges: IterationScopeRanges[] | undefined, ) { - this.editorScopeRanges = editorScopeRanges; - this.drawScopes(); + return iterationScopeRanges!.map(({ domain, ranges }) => ({ + domain, + nestedRanges: ranges.map(({ range }) => range), + })); } +} - private drawScopes() { - this.editorScopeRanges.forEach(({ editor, scopeRanges }) => { - this.setScopeVisualizationRangesForEditor(editor, scopeRanges); - }); +class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { + constructor(scopeType: ScopeType, visualizationType: VisualizationType) { + super(scopeType, visualizationType, "content" as const); } - async setScopeVisualizationRangesForEditor( - editor: VscodeTextEditorImpl, - scopeRanges: ScopeRanges[], - ): Promise { - const domainRanges: GeneralizedRange[] = []; - const contentRanges: GeneralizedRange[] = []; - const removalRanges: GeneralizedRange[] = []; - const domainEqualsContentRanges: GeneralizedRange[] = []; - const domainEqualsRemovalRanges: GeneralizedRange[] = []; - - for (const scopeRange of scopeRanges) { - if ( - scopeRange.contentRanges?.length === 1 && - (scopeRange.removalRanges?.length ?? 0) === 0 && - isGeneralizedRangeEqual(scopeRange.contentRanges[0], scopeRange.domain) - ) { - domainEqualsContentRanges.push(scopeRange.domain); - continue; - } - - if ( - (scopeRange.contentRanges?.length ?? 0) === 0 && - scopeRange.removalRanges?.length === 1 && - isGeneralizedRangeEqual(scopeRange.removalRanges[0], scopeRange.domain) - ) { - domainEqualsRemovalRanges.push(scopeRange.domain); - continue; - } - - domainRanges.push(scopeRange.domain); - scopeRange.contentRanges?.forEach((range) => contentRanges.push(range)); - scopeRange.removalRanges?.forEach((range) => removalRanges.push(range)); - } - - this.domainRenderer.setRanges(editor, domainRanges); - this.contentRenderer.setRanges(editor, contentRanges); - this.removalRenderer.setRanges(editor, removalRanges); - this.domainContentOverlappingRenderer.setRanges( - editor, - domainEqualsContentRanges, - ); - this.domainRemovalOverlappingRenderer.setRanges( - editor, - domainEqualsRemovalRanges, - ); + protected getRendererScopes( + _scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined, + ) { + return iterationScopeRanges!.map(({ domain, ranges }) => ({ + domain, + nestedRanges: ranges.flatMap(({ targets }) => + targets!.map(({ contentRange }) => contentRange), + ), + })); + } +} + +export function createVscodeScopeVisualizer( + scopeType: ScopeType, + visualizationType: VisualizationType, +) { + switch (visualizationType) { + case "content": + return new VscodeScopeContentVisualizer(scopeType, visualizationType); + case "removal": + return new VscodeScopeRemovalVisualizer(scopeType, visualizationType); + case "iteration": + return new VscodeScopeIterationVisualizer(scopeType, visualizationType); + case "every": + return new VscodeScopeEveryVisualizer(scopeType, visualizationType); } } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts deleted file mode 100644 index 6c6b09567c..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { RangeTypeColors } from "./RangeTypeColors"; -import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; - -export function getColorsFromConfig( - config: ScopeVisualizerColorConfig, - rangeType: "domain" | "content" | "removal"): RangeTypeColors { - return { - background: { - light: config.light[rangeType].background, - dark: config.dark[rangeType].background, - }, - borderSolid: { - light: config.light[rangeType].borderSolid, - dark: config.dark[rangeType].borderSolid, - }, - borderPorous: { - light: config.light[rangeType].borderPorous, - dark: config.dark[rangeType].borderPorous, - }, - }; -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts index 0ebf5cb9f5..0f87907420 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts @@ -6,11 +6,9 @@ import { HighlightId, IDE, InputBoxOptions, - IterationScopeRanges, OutdatedExtensionError, QuickPickOptions, RunMode, - ScopeRanges, TextDocumentChangeEvent, TextEditor, } from "@cursorless/common"; @@ -22,7 +20,6 @@ import { pull } from "lodash"; import { v4 as uuid } from "uuid"; import * as vscode from "vscode"; import { ExtensionContext, WorkspaceFolder, window, workspace } from "vscode"; -import { VscodeScopeVisualizer } from "./VSCodeScopeVisualizer"; import { VscodeCapabilities } from "./VscodeCapabilities"; import VscodeClipboard from "./VscodeClipboard"; import VscodeConfiguration from "./VscodeConfiguration"; @@ -45,7 +42,6 @@ export class VscodeIDE implements IDE { private flashHandler: VscodeFlashHandler; private highlights: VscodeHighlights; private editorMap; - private scopeVisualizer: VscodeScopeVisualizer; constructor(private extensionContext: ExtensionContext) { this.configuration = new VscodeConfiguration(this); @@ -54,7 +50,6 @@ export class VscodeIDE implements IDE { this.clipboard = new VscodeClipboard(); this.highlights = new VscodeHighlights(extensionContext); this.flashHandler = new VscodeFlashHandler(this, this.highlights); - this.scopeVisualizer = new VscodeScopeVisualizer(extensionContext); this.capabilities = new VscodeCapabilities(); this.editorMap = new WeakMap(); } @@ -82,18 +77,6 @@ export class VscodeIDE implements IDE { ); } - setScopeVisualizationRanges( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise { - return this.scopeVisualizer.setScopeVisualizationRanges( - editor as VscodeTextEditorImpl, - scopeRanges, - iterationScopeRanges, - ); - } - flashRanges(flashDescriptors: FlashDescriptor[]): Promise { return this.flashHandler.flashRanges(flashDescriptors); } diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index d4fea616bc..73c656c858 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -14,13 +14,14 @@ import { showDocumentation, showQuickPick } from "./commands"; import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { VscodeHats } from "./ide/vscode/hats/VscodeHats"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; +import { ScopeVisualizerCommandApi } from "./ScopeVisualizerImpl"; export function registerCommands( extensionContext: vscode.ExtensionContext, vscodeIde: VscodeIDE, commandApi: CommandApi, testCaseRecorder: TestCaseRecorder, - scopeVisualizer: VscodeScopeVisualizer, + scopeVisualizer: ScopeVisualizerCommandApi, keyboardCommands: KeyboardCommands, hats: VscodeHats, ): void { From 902b97bd64eb44dc749d32490b5d011b6ad1991d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:26:44 +0100 Subject: [PATCH 08/61] Cleanup --- .../src/ScopeVisualizerCommandApi.ts | 9 ++ .../src/ScopeVisualizerImpl.ts | 15 +-- .../src/getVisualizerConfig.ts | 28 ----- .../VscodeScopeContentVisualizer.ts | 30 +++++ .../VscodeScopeEveryVisualizer.ts | 32 +++++ .../VscodeScopeIterationVisualizer.ts | 30 +++++ .../VscodeScopeRemovalVisualizer.ts | 30 +++++ .../VscodeScopeVisualizer.ts | 115 +++--------------- .../createVscodeScopeVisualizer.ts | 22 ++++ .../ide/vscode/VSCodeScopeVisualizer/index.ts | 1 + .../weakenRangeTypeColors.ts | 30 +++++ .../cursorless-vscode/src/registerCommands.ts | 2 +- 12 files changed, 212 insertions(+), 132 deletions(-) create mode 100644 packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts delete mode 100644 packages/cursorless-vscode/src/getVisualizerConfig.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts diff --git a/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts b/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts new file mode 100644 index 0000000000..66a91a80fe --- /dev/null +++ b/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts @@ -0,0 +1,9 @@ +import { ScopeType } from "@cursorless/common"; + + +export interface ScopeVisualizerCommandApi { + start(scopeType: ScopeType, visualizationType: VisualizationType): void; + stop(): void; +} + +export type VisualizationType = "content" | "removal" | "iteration" | "every"; diff --git a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts index 1cef2ca174..26ece672ec 100644 --- a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts +++ b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts @@ -1,15 +1,13 @@ import { ScopeType } from "@cursorless/common"; import { ScopeVisualizer } from "@cursorless/cursorless-engine"; import { - createVscodeScopeVisualizer, VscodeScopeVisualizer, + createVscodeScopeVisualizer, } from "./ide/vscode/VSCodeScopeVisualizer"; -import { VisualizationType } from "./getVisualizerConfig"; - -export interface ScopeVisualizerCommandApi { - start(scopeType: ScopeType, visualizationType: VisualizationType): void; - stop(): void; -} +import { + ScopeVisualizerCommandApi, + VisualizationType, +} from "./ScopeVisualizerCommandApi"; export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { private scopeVisualizer: VscodeScopeVisualizer | undefined; @@ -25,6 +23,9 @@ export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { scopeType, visualizationType, ); + this.scopeVisualizer.onColorConfigChange(() => + this.engineScopeVisualizer.refresh(), + ); this.engineScopeVisualizer.start(this.scopeVisualizer); } diff --git a/packages/cursorless-vscode/src/getVisualizerConfig.ts b/packages/cursorless-vscode/src/getVisualizerConfig.ts deleted file mode 100644 index da0e59db70..0000000000 --- a/packages/cursorless-vscode/src/getVisualizerConfig.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ScopeType, ScopeVisualizerConfig } from "@cursorless/common"; - -export type VisualizationType = "content" | "removal" | "iteration" | "every"; - -export function getVisualizerConfig( - visualizationType: VisualizationType, - scopeType: ScopeType, -): ScopeVisualizerConfig { - switch (visualizationType) { - case "content": - case "removal": - return { - scopeType, - includeScopes: true, - includeIterationScopes: false, - includeIterationNestedTargets: false, - }; - - case "iteration": - case "every": - return { - scopeType, - includeScopes: false, - includeIterationScopes: true, - includeIterationNestedTargets: visualizationType === "every", - }; - } -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts new file mode 100644 index 0000000000..b40b663426 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts @@ -0,0 +1,30 @@ +import { + IterationScopeRanges, + ScopeRanges +} from "@cursorless/common"; +import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; +import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; +import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; + +export class VscodeScopeContentVisualizer extends VscodeScopeVisualizer { + readonly visualizerConfig = { + scopeType: this.scopeType, + includeScopes: true, + includeIterationScopes: false, + includeIterationNestedTargets: false, + }; + + protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { + return getColorsFromConfig(colorConfig, "content"); + } + + protected getRendererScopes( + scopeRanges: ScopeRanges[] | undefined, + _iterationScopeRanges: IterationScopeRanges[] | undefined + ) { + return scopeRanges!.map(({ domain, targets }) => ({ + domain, + nestedRanges: targets.map(({ contentRange }) => contentRange), + })); + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts new file mode 100644 index 0000000000..be85dff533 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts @@ -0,0 +1,32 @@ +import { + IterationScopeRanges, + ScopeRanges +} from "@cursorless/common"; +import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; +import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; +import { weakenRangeTypeColors } from "./weakenRangeTypeColors"; +import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; + +export class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { + readonly visualizerConfig = { + scopeType: this.scopeType, + includeScopes: false, + includeIterationScopes: true, + includeIterationNestedTargets: true, + }; + + protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { + return weakenRangeTypeColors(getColorsFromConfig(colorConfig, "content")); + } + + protected getRendererScopes( + _scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined + ) { + return iterationScopeRanges!.map(({ domain, ranges }) => ({ + domain, + nestedRanges: ranges.flatMap(({ targets }) => targets!.map(({ contentRange }) => contentRange) + ), + })); + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts new file mode 100644 index 0000000000..d2eb1c1ed9 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -0,0 +1,30 @@ +import { + IterationScopeRanges, + ScopeRanges +} from "@cursorless/common"; +import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; +import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; +import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; + +export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { + readonly visualizerConfig = { + scopeType: this.scopeType, + includeScopes: false, + includeIterationScopes: true, + includeIterationNestedTargets: false, + }; + + protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { + return getColorsFromConfig(colorConfig, "iteration"); + } + + protected getRendererScopes( + _scopeRanges: ScopeRanges[] | undefined, + iterationScopeRanges: IterationScopeRanges[] | undefined + ) { + return iterationScopeRanges!.map(({ domain, ranges }) => ({ + domain, + nestedRanges: ranges.map(({ range }) => range), + })); + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts new file mode 100644 index 0000000000..ec80451664 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts @@ -0,0 +1,30 @@ +import { + IterationScopeRanges, + ScopeRanges +} from "@cursorless/common"; +import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; +import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; +import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; + +export class VscodeScopeRemovalVisualizer extends VscodeScopeVisualizer { + readonly visualizerConfig = { + scopeType: this.scopeType, + includeScopes: true, + includeIterationScopes: false, + includeIterationNestedTargets: false, + }; + + protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { + return getColorsFromConfig(colorConfig, "removal"); + } + + protected getRendererScopes( + scopeRanges: ScopeRanges[] | undefined, + _iterationScopeRanges: IterationScopeRanges[] | undefined + ) { + return scopeRanges!.map(({ domain, targets }) => ({ + domain, + nestedRanges: targets.map(({ removalRange }) => removalRange), + })); + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index fb86b508fe..1f2694321f 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -5,41 +5,41 @@ import { ScopeRanges, ScopeType, ScopeVisualizerConfig, + Notifier, + Listener, } from "@cursorless/common"; import * as vscode from "vscode"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; -import { - ColorConfigKey, - getColorsFromConfig, -} from "./ScopeVisualizerColorConfig"; +import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; import { RendererScope, VscodeScopeRenderer } from "./VscodeScopeRenderer"; -import { - VisualizationType, - getVisualizerConfig, -} from "../../../getVisualizerConfig"; +import { RangeTypeColors } from "./RangeTypeColors"; +import { VisualizationType } from "../../../ScopeVisualizerCommandApi"; export abstract class VscodeScopeVisualizer implements ScopeRenderer { private renderer!: VscodeScopeRenderer; private disposables: Disposable[] = []; - visualizerConfig: ScopeVisualizerConfig; + abstract readonly visualizerConfig: ScopeVisualizerConfig; + private notifier: Notifier = new Notifier(); protected abstract getRendererScopes( scopeRanges: ScopeRanges[] | undefined, iterationScopeRanges: IterationScopeRanges[] | undefined, ): RendererScope[]; + protected abstract getNestedScopeColorConfig( + colorConfig: ScopeVisualizerColorConfig, + ): RangeTypeColors; + constructor( - scopeType: ScopeType, + protected scopeType: ScopeType, private visualizationType: VisualizationType, - private colorConfigKey: ColorConfigKey, ) { - this.visualizerConfig = getVisualizerConfig(visualizationType, scopeType); - this.disposables.push( vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { this.computeColors(); + this.notifier.notifyListeners(); } }), ); @@ -52,12 +52,17 @@ export abstract class VscodeScopeVisualizer implements ScopeRenderer { .getConfiguration("cursorless.scopeVisualizer") .get("colors")!; + this.renderer?.dispose(); this.renderer = new VscodeScopeRenderer( getColorsFromConfig(colorConfig, "domain"), - getColorsFromConfig(colorConfig, this.colorConfigKey), + this.getNestedScopeColorConfig(colorConfig), ); } + onColorConfigChange(listener: Listener) { + return this.notifier.registerListener(listener); + } + async setScopes( editor: VscodeTextEditorImpl, scopeRanges: ScopeRanges[] | undefined, @@ -74,85 +79,3 @@ export abstract class VscodeScopeVisualizer implements ScopeRenderer { this.renderer.dispose(); } } - -class VscodeScopeContentVisualizer extends VscodeScopeVisualizer { - constructor(scopeType: ScopeType, visualizationType: VisualizationType) { - super(scopeType, visualizationType, "content" as const); - } - - protected getRendererScopes( - scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return scopeRanges!.map(({ domain, targets }) => ({ - domain, - nestedRanges: targets.map(({ contentRange }) => contentRange), - })); - } -} - -class VscodeScopeRemovalVisualizer extends VscodeScopeVisualizer { - constructor(scopeType: ScopeType, visualizationType: VisualizationType) { - super(scopeType, visualizationType, "removal" as const); - } - - protected getRendererScopes( - scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return scopeRanges!.map(({ domain, targets }) => ({ - domain, - nestedRanges: targets.map(({ removalRange }) => removalRange), - })); - } -} - -class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { - constructor(scopeType: ScopeType, visualizationType: VisualizationType) { - super(scopeType, visualizationType, "iteration" as const); - } - - protected getRendererScopes( - _scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return iterationScopeRanges!.map(({ domain, ranges }) => ({ - domain, - nestedRanges: ranges.map(({ range }) => range), - })); - } -} - -class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { - constructor(scopeType: ScopeType, visualizationType: VisualizationType) { - super(scopeType, visualizationType, "content" as const); - } - - protected getRendererScopes( - _scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return iterationScopeRanges!.map(({ domain, ranges }) => ({ - domain, - nestedRanges: ranges.flatMap(({ targets }) => - targets!.map(({ contentRange }) => contentRange), - ), - })); - } -} - -export function createVscodeScopeVisualizer( - scopeType: ScopeType, - visualizationType: VisualizationType, -) { - switch (visualizationType) { - case "content": - return new VscodeScopeContentVisualizer(scopeType, visualizationType); - case "removal": - return new VscodeScopeRemovalVisualizer(scopeType, visualizationType); - case "iteration": - return new VscodeScopeIterationVisualizer(scopeType, visualizationType); - case "every": - return new VscodeScopeEveryVisualizer(scopeType, visualizationType); - } -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts new file mode 100644 index 0000000000..b71eef07d2 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts @@ -0,0 +1,22 @@ +import { ScopeType } from "@cursorless/common"; +import { VisualizationType } from "../../../ScopeVisualizerCommandApi"; +import { VscodeScopeContentVisualizer } from "./VscodeScopeContentVisualizer"; +import { VscodeScopeRemovalVisualizer } from "./VscodeScopeRemovalVisualizer"; +import { VscodeScopeIterationVisualizer } from "./VscodeScopeIterationVisualizer"; +import { VscodeScopeEveryVisualizer } from "./VscodeScopeEveryVisualizer"; + +export function createVscodeScopeVisualizer( + scopeType: ScopeType, + visualizationType: VisualizationType, +) { + switch (visualizationType) { + case "content": + return new VscodeScopeContentVisualizer(scopeType, visualizationType); + case "removal": + return new VscodeScopeRemovalVisualizer(scopeType, visualizationType); + case "iteration": + return new VscodeScopeIterationVisualizer(scopeType, visualizationType); + case "every": + return new VscodeScopeEveryVisualizer(scopeType, visualizationType); + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts index 1565cbc86a..be1bb9ef7e 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/index.ts @@ -1 +1,2 @@ +export * from "./createVscodeScopeVisualizer"; export * from "./VscodeScopeVisualizer"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts new file mode 100644 index 0000000000..b5fc60b8b1 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts @@ -0,0 +1,30 @@ +import tinycolor = require("tinycolor2"); +import { RangeTypeColors } from "./RangeTypeColors"; + +const BORDER_WEAKENING = 0.8; +const BACKGROUND_WEAKENING = 0.8; + +export function weakenRangeTypeColors( + colors: RangeTypeColors, +): RangeTypeColors { + return { + background: { + light: weakenColor(colors.background.light, BACKGROUND_WEAKENING), + dark: weakenColor(colors.background.dark, BACKGROUND_WEAKENING), + }, + borderSolid: { + light: weakenColor(colors.borderSolid.light, BORDER_WEAKENING), + dark: weakenColor(colors.borderSolid.dark, BORDER_WEAKENING), + }, + borderPorous: { + light: weakenColor(colors.borderPorous.light, BORDER_WEAKENING), + dark: weakenColor(colors.borderPorous.dark, BORDER_WEAKENING), + }, + }; +} + +function weakenColor(color: string, amount: number): string { + const parsed = tinycolor(color); + parsed.setAlpha(parsed.getAlpha() * amount); + return parsed.toHex8String(); +} diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index 73c656c858..21ade7d1f7 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -14,7 +14,7 @@ import { showDocumentation, showQuickPick } from "./commands"; import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { VscodeHats } from "./ide/vscode/hats/VscodeHats"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; -import { ScopeVisualizerCommandApi } from "./ScopeVisualizerImpl"; +import { ScopeVisualizerCommandApi } from "./ScopeVisualizerCommandApi"; export function registerCommands( extensionContext: vscode.ExtensionContext, From 670b6641183193377b63411a582cf1e6bb0f0bd7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 10:29:18 +0000 Subject: [PATCH 09/61] [pre-commit.ci lite] apply automatic fixes --- packages/common/src/ide/spy/SpyIDE.ts | 7 +--- .../src/CursorlessEngineApi.ts | 7 +--- .../UnsupportedScopeTypeVisualizationError.ts | 3 +- .../src/ScopeVisualizer/checkNonNull.ts | 3 +- .../src/ScopeVisualizer/getIterationRange.ts | 40 ++++++++++--------- .../src/ScopeVisualizer/getIterationScopes.ts | 9 +++-- .../src/ScopeVisualizer/getScopes.ts | 7 ++-- .../src/ScopeVisualizer/getTargetRanges.ts | 5 +-- .../cursorless-engine/src/cursorlessEngine.ts | 7 +--- .../src/ScopeVisualizerCommandApi.ts | 1 - .../VscodeScopeContentVisualizer.ts | 7 +--- .../VscodeScopeEveryVisualizer.ts | 10 ++--- .../VscodeScopeIterationVisualizer.ts | 7 +--- .../VscodeScopeRemovalVisualizer.ts | 7 +--- .../blendRangeTypeColors.ts | 15 +++---- .../isGeneralizedRangeEqual.ts | 3 +- 16 files changed, 59 insertions(+), 79 deletions(-) diff --git a/packages/common/src/ide/spy/SpyIDE.ts b/packages/common/src/ide/spy/SpyIDE.ts index e5b4112fed..6063e36d94 100644 --- a/packages/common/src/ide/spy/SpyIDE.ts +++ b/packages/common/src/ide/spy/SpyIDE.ts @@ -3,13 +3,10 @@ import { GeneralizedRange } from "../../types/GeneralizedRange"; import { TextEditor } from "../../types/TextEditor"; import PassthroughIDEBase from "../PassthroughIDEBase"; import { FlashDescriptor } from "../types/FlashDescriptor"; -import type { - HighlightId, - IDE, -} from "../types/ide.types"; +import type { HighlightId, IDE } from "../types/ide.types"; import type { IterationScopeRanges, - ScopeRanges + ScopeRanges, } from "../types/IdeScopeVisualizer"; import SpyMessages, { Message } from "./SpyMessages"; diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts index b41a929284..1fd4722c08 100644 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/CursorlessEngineApi.ts @@ -1,9 +1,4 @@ -import { - Command, - HatTokenMap, - IDE, - ScopeRenderer, -} from "@cursorless/common"; +import { Command, HatTokenMap, IDE, ScopeRenderer } from "@cursorless/common"; import { Snippets } from "./core/Snippets"; import { StoredTargetMap } from "./core/StoredTargets"; import { TestCaseRecorder } from "./testCaseRecorder/TestCaseRecorder"; diff --git a/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts b/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts index 96d3b54219..5aafe9a879 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts @@ -1,8 +1,7 @@ - export class UnsupportedScopeTypeVisualizationError extends Error { constructor(languageId: string) { super( - `Scope type not supported for ${languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.` + `Scope type not supported for ${languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.`, ); this.name = "UnsupportedScopeTypeVisualizationError"; } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts b/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts index 0d1e5c1746..c5b4fb2efc 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts @@ -1,6 +1,7 @@ export function checkNonNull( value: T | null | undefined, - errorMessage: () => Error): T { + errorMessage: () => Error, +): T { if (value == null) { throw errorMessage(); } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts index 94658e499c..d6a185884c 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts @@ -13,15 +13,17 @@ import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHan */ export function getIterationRange( editor: TextEditor, - scopeHandler: ScopeHandler): Range { - let visibleRange = editor.visibleRanges.reduce((acc, range) => acc.union(range) + scopeHandler: ScopeHandler, +): Range { + let visibleRange = editor.visibleRanges.reduce((acc, range) => + acc.union(range), ); visibleRange = editor.document.range.intersection( visibleRange.with( visibleRange.start.translate(-10), - visibleRange.end.translate(10) - ) + visibleRange.end.translate(10), + ), )!; // Expand to largest ancestor of start of visible range FIXME: It's @@ -29,22 +31,24 @@ export function getIterationRange( // in which case we'll miss a scope if its removal range is visible but // its domain range is not. I don't think we care that much; they can // scroll, and we have the extra 10 lines on either side which might help. - const expandedStart = last( - Array.from( - scopeHandler.generateScopes(editor, visibleRange.start, "forward", { - containment: "required", - }) - ) - )?.domain ?? visibleRange; + const expandedStart = + last( + Array.from( + scopeHandler.generateScopes(editor, visibleRange.start, "forward", { + containment: "required", + }), + ), + )?.domain ?? visibleRange; // Expand to largest ancestor of end of visible range - const expandedEnd = last( - Array.from( - scopeHandler.generateScopes(editor, visibleRange.end, "forward", { - containment: "required", - }) - ) - )?.domain ?? visibleRange; + const expandedEnd = + last( + Array.from( + scopeHandler.generateScopes(editor, visibleRange.end, "forward", { + containment: "required", + }), + ), + )?.domain ?? visibleRange; return expandedStart.union(expandedEnd); } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts index c10fb674b1..04c8d9b737 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts @@ -2,7 +2,7 @@ import { IterationScopeRanges, Range, TextEditor, - toCharacterRange + toCharacterRange, } from "@cursorless/common"; import { map } from "itertools"; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; @@ -14,7 +14,8 @@ export function getIterationScopes( iterationScopeHandler: ScopeHandler, everyStage: ModifierStage, iterationRange: Range, - includeIterationNestedTargets: boolean): IterationScopeRanges[] { + includeIterationNestedTargets: boolean, +): IterationScopeRanges[] { return map( iterationScopeHandler.generateScopes( editor, @@ -23,7 +24,7 @@ export function getIterationScopes( { includeDescendantScopes: true, distalPosition: iterationRange.end, - } + }, ), (scope) => { return { @@ -35,6 +36,6 @@ export function getIterationScopes( : undefined, })), }; - } + }, ); } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts index 32aca41e82..e39ca9830d 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts @@ -2,7 +2,7 @@ import { Range, ScopeRanges, TextEditor, - toCharacterRange + toCharacterRange, } from "@cursorless/common"; import { map } from "itertools"; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; @@ -11,7 +11,8 @@ import { getTargetRanges } from "./getTargetRanges"; export function getScopes( editor: TextEditor, scopeHandler: ScopeHandler, - iterationRange: Range): ScopeRanges[] { + iterationRange: Range, +): ScopeRanges[] { return map( scopeHandler.generateScopes(editor, iterationRange.start, "forward", { includeDescendantScopes: true, @@ -20,6 +21,6 @@ export function getScopes( (scope) => ({ domain: toCharacterRange(scope.domain), targets: scope.getTargets(false).map(getTargetRanges), - }) + }), ); } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts index c226d17ca1..01f4ca0df0 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts @@ -1,7 +1,4 @@ -import { - toCharacterRange, - toLineRange -} from "@cursorless/common"; +import { toCharacterRange, toLineRange } from "@cursorless/common"; import { Target } from "../typings/target.types"; export function getTargetRanges(target: Target) { diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 48ca72805e..fa9608aea2 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,9 +1,4 @@ -import { - Command, - CommandServerApi, - Hats, - IDE, -} from "@cursorless/common"; +import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; diff --git a/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts b/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts index 66a91a80fe..cb6681b1f2 100644 --- a/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts +++ b/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts @@ -1,6 +1,5 @@ import { ScopeType } from "@cursorless/common"; - export interface ScopeVisualizerCommandApi { start(scopeType: ScopeType, visualizationType: VisualizationType): void; stop(): void; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts index b40b663426..3e804f11e1 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts @@ -1,7 +1,4 @@ -import { - IterationScopeRanges, - ScopeRanges -} from "@cursorless/common"; +import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; @@ -20,7 +17,7 @@ export class VscodeScopeContentVisualizer extends VscodeScopeVisualizer { protected getRendererScopes( scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined + _iterationScopeRanges: IterationScopeRanges[] | undefined, ) { return scopeRanges!.map(({ domain, targets }) => ({ domain, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts index be85dff533..454a400902 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts @@ -1,7 +1,4 @@ -import { - IterationScopeRanges, - ScopeRanges -} from "@cursorless/common"; +import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; import { weakenRangeTypeColors } from "./weakenRangeTypeColors"; @@ -21,11 +18,12 @@ export class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { protected getRendererScopes( _scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined + iterationScopeRanges: IterationScopeRanges[] | undefined, ) { return iterationScopeRanges!.map(({ domain, ranges }) => ({ domain, - nestedRanges: ranges.flatMap(({ targets }) => targets!.map(({ contentRange }) => contentRange) + nestedRanges: ranges.flatMap(({ targets }) => + targets!.map(({ contentRange }) => contentRange), ), })); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts index d2eb1c1ed9..d8a214c687 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -1,7 +1,4 @@ -import { - IterationScopeRanges, - ScopeRanges -} from "@cursorless/common"; +import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; @@ -20,7 +17,7 @@ export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { protected getRendererScopes( _scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined + iterationScopeRanges: IterationScopeRanges[] | undefined, ) { return iterationScopeRanges!.map(({ domain, ranges }) => ({ domain, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts index ec80451664..7ae07339a9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts @@ -1,7 +1,4 @@ -import { - IterationScopeRanges, - ScopeRanges -} from "@cursorless/common"; +import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; @@ -20,7 +17,7 @@ export class VscodeScopeRemovalVisualizer extends VscodeScopeVisualizer { protected getRendererScopes( scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined + _iterationScopeRanges: IterationScopeRanges[] | undefined, ) { return scopeRanges!.map(({ domain, targets }) => ({ domain, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts index daae5a147a..5c4047b799 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts @@ -3,33 +3,34 @@ import { RangeTypeColors } from "./RangeTypeColors"; export function blendRangeTypeColors( baseColors: RangeTypeColors, - topColors: RangeTypeColors): RangeTypeColors { + topColors: RangeTypeColors, +): RangeTypeColors { return { background: { light: blendColors( baseColors.background.light, - topColors.background.light + topColors.background.light, ), dark: blendColors(baseColors.background.dark, topColors.background.dark), }, borderSolid: { light: blendColors( baseColors.borderSolid.light, - topColors.borderSolid.light + topColors.borderSolid.light, ), dark: blendColors( baseColors.borderSolid.dark, - topColors.borderSolid.dark + topColors.borderSolid.dark, ), }, borderPorous: { light: blendColors( baseColors.borderPorous.light, - topColors.borderPorous.light + topColors.borderPorous.light, ), dark: blendColors( baseColors.borderPorous.dark, - topColors.borderPorous.dark + topColors.borderPorous.dark, ), }, }; @@ -42,7 +43,7 @@ function blendColors(base: string, top: string): string { function interpolateChannel(channel: "r" | "g" | "b"): number { return Math.round( (topRgba[channel] * topRgba.a) / blendedAlpha + - (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha + (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha, ); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts index 0b57a92b41..2fbf926c90 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts @@ -2,7 +2,8 @@ import { GeneralizedRange } from "@cursorless/common"; export function isGeneralizedRangeEqual( a: GeneralizedRange, - b: GeneralizedRange): boolean { + b: GeneralizedRange, +): boolean { if (a.type === "character" && b.type === "character") { return a.start.isEqual(b.start) && a.end.isEqual(b.end); } From 73d5aefbfd45ed3aeb75bc1d416596f66f7e0d6f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:34:06 +0100 Subject: [PATCH 10/61] Revert unnecessary changes --- packages/common/src/ide/fake/FakeIDE.ts | 18 +++--------------- .../cursorless-engine/src/VisualizationType.ts | 4 ---- .../src/ide/vscode/VscodeIDE.ts | 4 ++-- 3 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 packages/cursorless-engine/src/VisualizationType.ts diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index e752abe6ca..7bb1069fbe 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -1,25 +1,21 @@ -import { pull } from "lodash"; import type { EditableTextEditor, TextEditor } from "../.."; +import { pull } from "lodash"; import { GeneralizedRange } from "../../types/GeneralizedRange"; import { TextDocument } from "../../types/TextDocument"; import type { TextDocumentChangeEvent } from "../types/Events"; -import { FlashDescriptor } from "../types/FlashDescriptor"; -import { QuickPickOptions } from "../types/QuickPickOptions"; import { Event, TextEditorSelectionChangeEvent, TextEditorVisibleRangesChangeEvent, } from "../types/events.types"; +import { FlashDescriptor } from "../types/FlashDescriptor"; import type { Disposable, IDE, RunMode, WorkspaceFolder, } from "../types/ide.types"; -import type { - IterationScopeRanges, - ScopeRanges, -} from "../types/IdeScopeVisualizer"; +import { QuickPickOptions } from "../types/QuickPickOptions"; import { FakeCapabilities } from "./FakeCapabilities"; import FakeClipboard from "./FakeClipboard"; import FakeConfiguration from "./FakeConfiguration"; @@ -51,14 +47,6 @@ export default class FakeIDE implements IDE { // empty } - async setScopeVisualizationRanges( - _editor: TextEditor, - _scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise { - // empty - } - onDidOpenTextDocument: Event = dummyEvent; onDidCloseTextDocument: Event = dummyEvent; onDidChangeActiveTextEditor: Event = dummyEvent; diff --git a/packages/cursorless-engine/src/VisualizationType.ts b/packages/cursorless-engine/src/VisualizationType.ts deleted file mode 100644 index 9744699c83..0000000000 --- a/packages/cursorless-engine/src/VisualizationType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum VisualizationType { - content = "content", - removal = "removal", -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts index 0f87907420..761bfb5fd9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeIDE.ts @@ -19,7 +19,7 @@ import { import { pull } from "lodash"; import { v4 as uuid } from "uuid"; import * as vscode from "vscode"; -import { ExtensionContext, WorkspaceFolder, window, workspace } from "vscode"; +import { ExtensionContext, window, workspace, WorkspaceFolder } from "vscode"; import { VscodeCapabilities } from "./VscodeCapabilities"; import VscodeClipboard from "./VscodeClipboard"; import VscodeConfiguration from "./VscodeConfiguration"; @@ -29,9 +29,9 @@ import VscodeGlobalState from "./VscodeGlobalState"; import VscodeHighlights, { HighlightStyle } from "./VscodeHighlights"; import VscodeMessages from "./VscodeMessages"; import { vscodeRunMode } from "./VscodeRunMode"; +import { vscodeShowQuickPick } from "./vscodeShowQuickPick"; import { VscodeTextDocumentImpl } from "./VscodeTextDocumentImpl"; import { VscodeTextEditorImpl } from "./VscodeTextEditorImpl"; -import { vscodeShowQuickPick } from "./vscodeShowQuickPick"; export class VscodeIDE implements IDE { readonly configuration: VscodeConfiguration; From 267b991b0b5ea7c85cc3e1cbb19183b2462029da Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:52:18 +0100 Subject: [PATCH 11/61] More cleanup --- .../src/ScopeVisualizerImpl.ts | 5 +- .../vscode/VSCodeScopeVisualizer/Decorator.ts | 130 +++++++++++++++++ .../VscodeFancyRangeHighlighter.ts | 134 +----------------- 3 files changed, 135 insertions(+), 134 deletions(-) create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts diff --git a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts index 26ece672ec..de1aa38543 100644 --- a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts +++ b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts @@ -23,11 +23,12 @@ export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { scopeType, visualizationType, ); + + this.engineScopeVisualizer.start(this.scopeVisualizer); + this.scopeVisualizer.onColorConfigChange(() => this.engineScopeVisualizer.refresh(), ); - - this.engineScopeVisualizer.start(this.scopeVisualizer); } stop() { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts new file mode 100644 index 0000000000..9a5e3e1787 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts @@ -0,0 +1,130 @@ +import { CompositeKeyDefaultMap } from "@cursorless/common"; +import { toVscodeRange } from "@cursorless/vscode-common"; +import { + DecorationRangeBehavior, + DecorationRenderOptions, + TextEditorDecorationType, + window, +} from "vscode"; +import { RangeTypeColors } from "./RangeTypeColors"; +import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; +import { + BorderStyle, + DecorationStyle, + StyleParameters, + StyleParametersRanges, +} from "./getDecorationRanges/getDecorationRanges.types"; + +export class Decorator { + private decorationTypes: CompositeKeyDefaultMap< + StyleParameters, + TextEditorDecorationType + >; + + constructor(colors: RangeTypeColors) { + this.decorationTypes = new CompositeKeyDefaultMap( + ({ style }) => getDecorationStyle(colors, style), + ({ + style: { top, right, bottom, left, isWholeLine }, + differentiationIndex, + }) => [ + top, + right, + bottom, + left, + isWholeLine ?? false, + differentiationIndex, + ], + ); + } + + setDecorations( + editor: VscodeTextEditorImpl, + decoratedRanges: StyleParametersRanges[], + ) { + const untouchedDecorationTypes = new Set(this.decorationTypes.values()); + + decoratedRanges.forEach(({ styleParameters, ranges }) => { + const decorationType = this.decorationTypes.get(styleParameters); + + editor.vscodeEditor.setDecorations( + decorationType, + ranges.map(toVscodeRange), + ); + + untouchedDecorationTypes.delete(decorationType); + }); + + untouchedDecorationTypes.forEach((decorationType) => { + editor.vscodeEditor.setDecorations(decorationType, []); + }); + } + + dispose() { + Array.from(this.decorationTypes.values()).forEach((decorationType) => { + decorationType.dispose(); + }); + } +} + +function getDecorationStyle( + colors: RangeTypeColors, + borders: DecorationStyle, +): TextEditorDecorationType { + const options: DecorationRenderOptions = { + light: { + backgroundColor: colors.background.light, + borderColor: getBorderColor( + colors.borderSolid.light, + colors.borderPorous.light, + borders, + ), + }, + dark: { + backgroundColor: colors.background.dark, + borderColor: getBorderColor( + colors.borderSolid.dark, + colors.borderPorous.dark, + borders, + ), + }, + borderStyle: getBorderStyle(borders), + borderWidth: "1px", + borderRadius: getBorderRadius(borders), + rangeBehavior: DecorationRangeBehavior.ClosedClosed, + isWholeLine: borders.isWholeLine, + }; + + return window.createTextEditorDecorationType(options); +} + +function getBorderStyle(borders: DecorationStyle): string { + return [borders.top, borders.right, borders.bottom, borders.left].join(" "); +} + +function getBorderColor( + solidColor: string, + porousColor: string, + borders: DecorationStyle, +): string { + return [ + borders.top === BorderStyle.solid ? solidColor : porousColor, + borders.right === BorderStyle.solid ? solidColor : porousColor, + borders.bottom === BorderStyle.solid ? solidColor : porousColor, + borders.left === BorderStyle.solid ? solidColor : porousColor, + ].join(" "); +} + +function getBorderRadius({ + top, + right, + bottom, + left, +}: DecorationStyle): string { + return [ + top === BorderStyle.solid && left === BorderStyle.solid ? "2px" : "0px", + top === BorderStyle.solid && right === BorderStyle.solid ? "2px" : "0px", + bottom === BorderStyle.solid && right === BorderStyle.solid ? "2px" : "0px", + bottom === BorderStyle.solid && left === BorderStyle.solid ? "2px" : "0px", + ].join(" "); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts index 7b70a7e890..4dbdd2a9a6 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts @@ -1,32 +1,19 @@ import { CharacterRange, - CompositeKeyDefaultMap, GeneralizedRange, LineRange, Range, isLineRange, partition, } from "@cursorless/common"; -import { toVscodeRange } from "@cursorless/vscode-common"; import { chain, flatmap } from "itertools"; -import * as vscode from "vscode"; -import { - DecorationRangeBehavior, - DecorationRenderOptions, - TextEditorDecorationType, - window, -} from "vscode"; import { RangeTypeColors } from "./RangeTypeColors"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { generateDecorationsForCharacterRange } from "./getDecorationRanges/generateDecorationsForCharacterRange"; import { generateDecorationsForLineRange } from "./getDecorationRanges/generateDecorationsForLineRange"; -import { - BorderStyle, - DecorationStyle, - StyleParameters, - StyleParametersRanges, -} from "./getDecorationRanges/getDecorationRanges.types"; +import { DecorationStyle } from "./getDecorationRanges/getDecorationRanges.types"; import { getDifferentiatedRanges } from "./getDecorationRanges/getDifferentiatedRanges"; +import { Decorator } from "./Decorator"; /** * Manages VSCode decoration types for a highlight or flash style. @@ -75,120 +62,3 @@ function getBorderKey({ }: DecorationStyle) { return [top, right, left, bottom, isWholeLine ?? false]; } - -class Decorator { - private decorationTypes: CompositeKeyDefaultMap< - StyleParameters, - TextEditorDecorationType - >; - - constructor(colors: RangeTypeColors) { - this.decorationTypes = new CompositeKeyDefaultMap( - ({ style }) => getDecorationStyle(colors, style), - ({ - style: { top, right, bottom, left, isWholeLine }, - differentiationIndex, - }) => [ - top, - right, - bottom, - left, - isWholeLine ?? false, - differentiationIndex, - ], - ); - } - - setDecorations( - editor: VscodeTextEditorImpl, - decoratedRanges: StyleParametersRanges[], - ) { - const untouchedDecorationTypes = new Set(this.decorationTypes.values()); - - decoratedRanges.forEach(({ styleParameters, ranges }) => { - const decorationType = this.decorationTypes.get(styleParameters); - - editor.vscodeEditor.setDecorations( - decorationType, - ranges.map(toVscodeRange), - ); - - untouchedDecorationTypes.delete(decorationType); - }); - - untouchedDecorationTypes.forEach((decorationType) => { - editor.vscodeEditor.setDecorations(decorationType, []); - }); - } - - dispose() { - Array.from(this.decorationTypes.values()).forEach((decorationType) => { - decorationType.dispose(); - }); - } -} - -function getDecorationStyle( - colors: RangeTypeColors, - borders: DecorationStyle, -): vscode.TextEditorDecorationType { - const options: DecorationRenderOptions = { - light: { - backgroundColor: colors.background.light, - borderColor: getBorderColor( - colors.borderSolid.light, - colors.borderPorous.light, - borders, - ), - }, - dark: { - backgroundColor: colors.background.dark, - borderColor: getBorderColor( - colors.borderSolid.dark, - colors.borderPorous.dark, - borders, - ), - }, - borderStyle: getBorderStyle(borders), - borderWidth: "1px", - borderRadius: getBorderRadius(borders), - rangeBehavior: DecorationRangeBehavior.ClosedClosed, - isWholeLine: borders.isWholeLine, - }; - - return window.createTextEditorDecorationType(options); -} - -function getBorderStyle(borders: DecorationStyle): string { - return [borders.top, borders.right, borders.bottom, borders.left].join(" "); -} - -function getBorderColor( - solidColor: string, - porousColor: string, - borders: DecorationStyle, -): string { - return [ - borders.top === BorderStyle.solid ? solidColor : porousColor, - borders.right === BorderStyle.solid ? solidColor : porousColor, - borders.bottom === BorderStyle.solid ? solidColor : porousColor, - borders.left === BorderStyle.solid ? solidColor : porousColor, - ].join(" "); -} - -function getBorderRadius(borders: DecorationStyle): string { - return [ - borders.top === BorderStyle.solid && borders.left === BorderStyle.solid - ? "2px" - : "0px", - borders.top === BorderStyle.solid && borders.right === BorderStyle.solid - ? "2px" - : "0px", - borders.bottom === BorderStyle.solid && borders.right === BorderStyle.solid - ? "2px" - : "0px", - borders.bottom === BorderStyle.solid && borders.left === BorderStyle.solid - ? "2px" - : "0px", - ].join(" "); -} From bb74e27ed2281b7e078e0eda9c6e75d0a85c1c3b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:56:16 +0100 Subject: [PATCH 12/61] More cleanup --- .../VscodeFancyRangeHighlighter.ts | 18 +++++++++--------- .../VscodeFancyRangeHighlighterRenderer.ts} | 8 ++++---- .../block.test.txt | 0 .../generateDecorationsForCharacterRange.ts | 0 .../generateDecorationsForLineRange.ts | 0 .../getDecorationRanges.test.ts | 0 .../getDecorationRanges.types.ts | 0 .../getDifferentiatedRanges.test.ts | 0 .../getDifferentiatedRanges.ts | 0 .../handleMultipleLines.ts | 0 .../VscodeFancyRangeHighlighter/index.ts | 1 + 11 files changed, 14 insertions(+), 13 deletions(-) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{ => VscodeFancyRangeHighlighter}/VscodeFancyRangeHighlighter.ts (64%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{Decorator.ts => VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts} (94%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/block.test.txt (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/generateDecorationsForCharacterRange.ts (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/generateDecorationsForLineRange.ts (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/getDecorationRanges.test.ts (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/getDecorationRanges.types.ts (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/getDifferentiatedRanges.test.ts (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/getDifferentiatedRanges.ts (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{getDecorationRanges => VscodeFancyRangeHighlighter}/handleMultipleLines.ts (100%) create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/index.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts similarity index 64% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 4dbdd2a9a6..c01386e33b 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -7,22 +7,22 @@ import { partition, } from "@cursorless/common"; import { chain, flatmap } from "itertools"; -import { RangeTypeColors } from "./RangeTypeColors"; -import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; -import { generateDecorationsForCharacterRange } from "./getDecorationRanges/generateDecorationsForCharacterRange"; -import { generateDecorationsForLineRange } from "./getDecorationRanges/generateDecorationsForLineRange"; -import { DecorationStyle } from "./getDecorationRanges/getDecorationRanges.types"; -import { getDifferentiatedRanges } from "./getDecorationRanges/getDifferentiatedRanges"; -import { Decorator } from "./Decorator"; +import { RangeTypeColors } from "../RangeTypeColors"; +import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; +import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; +import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; +import { DecorationStyle } from "./getDecorationRanges.types"; +import { getDifferentiatedRanges } from "./getDifferentiatedRanges"; +import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; /** * Manages VSCode decoration types for a highlight or flash style. */ export class VscodeFancyRangeHighlighter { - private decorator: Decorator; + private decorator: VscodeFancyRangeHighlighterRenderer; constructor(colors: RangeTypeColors) { - this.decorator = new Decorator(colors); + this.decorator = new VscodeFancyRangeHighlighterRenderer(colors); } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts similarity index 94% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index 9a5e3e1787..d6739adff6 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/Decorator.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -6,16 +6,16 @@ import { TextEditorDecorationType, window, } from "vscode"; -import { RangeTypeColors } from "./RangeTypeColors"; -import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; +import { RangeTypeColors } from "../RangeTypeColors"; +import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import { BorderStyle, DecorationStyle, StyleParameters, StyleParametersRanges, -} from "./getDecorationRanges/getDecorationRanges.types"; +} from "./getDecorationRanges.types"; -export class Decorator { +export class VscodeFancyRangeHighlighterRenderer { private decorationTypes: CompositeKeyDefaultMap< StyleParameters, TextEditorDecorationType diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/block.test.txt b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/block.test.txt similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/block.test.txt rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/block.test.txt diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForCharacterRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForCharacterRange.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForLineRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/generateDecorationsForLineRange.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.test.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDecorationRanges.types.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.test.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/getDifferentiatedRanges.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getDecorationRanges/handleMultipleLines.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/index.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/index.ts new file mode 100644 index 0000000000..bd61ccda88 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/index.ts @@ -0,0 +1 @@ +export * from "./VscodeFancyRangeHighlighter"; From cc4d5e2b18a7060207dd962e7825f3010b2502f8 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:18:31 +0100 Subject: [PATCH 13/61] Fix brightness issue with layered scopes of same type --- .../common/src/types/GeneralizedRange.test.ts | 184 ++++++++++++++++++ packages/common/src/types/GeneralizedRange.ts | 65 +++++++ .../src/ScopeVisualizer/getIterationScopes.ts | 15 +- .../VscodeFancyRangeHighlighter.ts | 51 +++-- .../VscodeFancyRangeHighlighterRenderer.ts | 6 + .../generateDifferentiatedRanges.ts | 83 ++++++++ .../getDecorationRanges.types.ts | 24 ++- .../getDifferentiatedRanges.test.ts | 4 +- .../getDifferentiatedRanges.ts | 80 -------- .../groupDifferentiatedRanges.ts | 33 ++++ .../VscodeScopeEveryVisualizer.ts | 35 +++- .../VscodeScopeRenderer.ts | 7 +- .../isGeneralizedRangeEqual.ts | 16 -- .../weakenRangeTypeColors.ts | 30 --- 14 files changed, 472 insertions(+), 161 deletions(-) create mode 100644 packages/common/src/types/GeneralizedRange.test.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts diff --git a/packages/common/src/types/GeneralizedRange.test.ts b/packages/common/src/types/GeneralizedRange.test.ts new file mode 100644 index 0000000000..d84ae36cf7 --- /dev/null +++ b/packages/common/src/types/GeneralizedRange.test.ts @@ -0,0 +1,184 @@ +import { Position, Range, TextEditor } from "./types"; +import { + CharacterRange, + GeneralizedRange, + LineRange, + generalizedRangeContains, + generalizedRangeTouches, + isGeneralizedRangeEqual, + toCharacterRange, + toLineRange, +} from "./GeneralizedRange"; + +describe("GeneralizedRange", () => { + const editor: TextEditor = { + document: { + uri: "", + fileName: "", + isUntitled: false, + getText: () => "", + lineCount: 0, + offsetAt: (pos: Position) => 0, + positionAt: (offset: number) => new Position(0, 0), + save: () => Promise.resolve(true), + eol: 0, + }, + selection: new Range(new Position(0, 0), new Position(0, 0)), + }; + + describe("toLineRange", () => { + it("converts a range to a line range", () => { + const range = new Range(new Position(1, 0), new Position(3, 5)); + const lineRange: LineRange = { type: "line", start: 1, end: 3 }; + expect(toLineRange(range)).toEqual(lineRange); + }); + }); + + describe("toCharacterRange", () => { + it("converts a range to a character range", () => { + const range = new Range(new Position(1, 0), new Position(3, 5)); + const charRange: CharacterRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + expect(toCharacterRange(range)).toEqual(charRange); + }); + }); + + describe("generalizedRangeContains", () => { + it("returns true if a contains b", () => { + const a: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + const b: GeneralizedRange = { + type: "character", + start: new Position(2, 0), + end: new Position(3, 0), + }; + expect(generalizedRangeContains(a, b)).toBe(true); + }); + + it("returns false if a does not contain b", () => { + const a: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + const b: GeneralizedRange = { + type: "character", + start: new Position(4, 0), + end: new Position(5, 0), + }; + expect(generalizedRangeContains(a, b)).toBe(false); + }); + + it("returns true if a contains b (line range)", () => { + const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; + const b: GeneralizedRange = { + type: "character", + start: new Position(2, 0), + end: new Position(3, 0), + }; + expect(generalizedRangeContains(a, b)).toBe(true); + }); + + it("returns false if a does not contain b (line range)", () => { + const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; + const b: GeneralizedRange = { + type: "character", + start: new Position(4, 0), + end: new Position(5, 0), + }; + expect(generalizedRangeContains(a, b)).toBe(false); + }); + }); + + describe("generalizedRangeTouches", () => { + it("returns true if a touches b", () => { + const a: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + const b: GeneralizedRange = { + type: "character", + start: new Position(3, 0), + end: new Position(4, 0), + }; + expect(generalizedRangeTouches(a, b)).toBe(true); + }); + + it("returns false if a does not touch b", () => { + const a: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + const b: GeneralizedRange = { + type: "character", + start: new Position(4, 0), + end: new Position(5, 0), + }; + expect(generalizedRangeTouches(a, b)).toBe(false); + }); + + it("returns true if a touches b (line range)", () => { + const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; + const b: GeneralizedRange = { + type: "character", + start: new Position(3, 0), + end: new Position(4, 0), + }; + expect(generalizedRangeTouches(a, b)).toBe(true); + }); + + it("returns false if a does not touch b (line range)", () => { + const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; + const b: GeneralizedRange = { + type: "character", + start: new Position(4, 0), + end: new Position(5, 0), + }; + expect(generalizedRangeTouches(a, b)).toBe(false); + }); + }); + + describe("isGeneralizedRangeEqual", () => { + it("returns true if a and b are equal (character range)", () => { + const a: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + const b: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + expect(isGeneralizedRangeEqual(a, b)).toBe(true); + }); + + it("returns true if a and b are equal (line range)", () => { + const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; + const b: GeneralizedRange = { type: "line", start: 1, end: 3 }; + expect(isGeneralizedRangeEqual(a, b)).toBe(true); + }); + + it("returns false if a and b are not equal", () => { + const a: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(3, 5), + }; + const b: GeneralizedRange = { + type: "character", + start: new Position(1, 0), + end: new Position(4, 0), + }; + expect(isGeneralizedRangeEqual(a, b)).toBe(false); + }); + }); +}); diff --git a/packages/common/src/types/GeneralizedRange.ts b/packages/common/src/types/GeneralizedRange.ts index ba1e8161a4..d538f86ab8 100644 --- a/packages/common/src/types/GeneralizedRange.ts +++ b/packages/common/src/types/GeneralizedRange.ts @@ -60,3 +60,68 @@ export function toLineRange(range: Range): LineRange { export function toCharacterRange({ start, end }: Range): CharacterRange { return { type: "character", start, end }; } + +export function isGeneralizedRangeEqual( + a: GeneralizedRange, + b: GeneralizedRange, +): boolean { + if (a.type === "character" && b.type === "character") { + return a.start.isEqual(b.start) && a.end.isEqual(b.end); + } + + if (a.type === "line" && b.type === "line") { + return a.start === b.start && a.end === b.end; + } + + return false; +} + +export function generalizedRangeContains( + a: GeneralizedRange, + b: GeneralizedRange, +): boolean { + if (a.type === "character") { + if (b.type === "character") { + // a.type === "character" && b.type === "character" + return a.start.isBeforeOrEqual(b.start) && a.end.isAfterOrEqual(b.end); + } + + // a.type === "character" && b.type === "line" + // Require that the line range is fully contained in the character range + // because otherwise it visually looks like the line range is not contained + return a.start.line < b.start && a.end.line > b.end; + } + + if (b.type === "line") { + // a.type === "line" && b.type === "line" + return a.start <= b.start && a.end >= b.end; + } + + // a.type === "line" && b.type === "character" + return a.start <= b.start.line && a.end >= b.end.line; +} + +export function generalizedRangeTouches( + a: GeneralizedRange, + b: GeneralizedRange, +): boolean { + if (a.type === "character") { + if (b.type === "character") { + // a.type === "character" && b.type === "character" + return a.start.isBeforeOrEqual(b.end) && a.end.isAfterOrEqual(b.start); + } + + // a.type === "character" && b.type === "line" + // Require that the line range is fully contained in the character range + // because otherwise it visually looks like the line range is not contained + return a.start.line <= b.end && a.end.line >= b.start; + } + + if (b.type === "line") { + // a.type === "line" && b.type === "line" + return a.start <= b.end && a.end >= b.start; + } + + // a.type === "line" && b.type === "character" + return a.start <= b.end.line && a.end >= b.start.line; +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts index 04c8d9b737..7e65380ac2 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts @@ -8,6 +8,7 @@ import { map } from "itertools"; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; import { getTargetRanges } from "./getTargetRanges"; import { ModifierStage } from "../processTargets/PipelineStages.types"; +import { Target } from "../typings/target.types"; export function getIterationScopes( editor: TextEditor, @@ -32,10 +33,22 @@ export function getIterationScopes( ranges: scope.getTargets(false).map((target) => ({ range: toCharacterRange(target.contentRange), targets: includeIterationNestedTargets - ? everyStage.run(target).map(getTargetRanges) + ? getEveryScopeLenient(everyStage, target).map(getTargetRanges) : undefined, })), }; }, ); } + +function getEveryScopeLenient(everyStage: ModifierStage, target: Target) { + try { + return everyStage.run(target); + } catch (err) { + if ((err as Error).name !== "NoContainingScopeError") { + throw err; + } + + return []; + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index c01386e33b..2c18d5f877 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -1,19 +1,13 @@ -import { - CharacterRange, - GeneralizedRange, - LineRange, - Range, - isLineRange, - partition, -} from "@cursorless/common"; -import { chain, flatmap } from "itertools"; -import { RangeTypeColors } from "../RangeTypeColors"; +import { GeneralizedRange, Range } from "@cursorless/common"; +import { flatmap } from "itertools"; import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; +import { RangeTypeColors } from "../RangeTypeColors"; +import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; +import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; import { DecorationStyle } from "./getDecorationRanges.types"; -import { getDifferentiatedRanges } from "./getDifferentiatedRanges"; -import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; +import { groupDifferentiatedRanges } from "./groupDifferentiatedRanges"; /** * Manages VSCode decoration types for a highlight or flash style. @@ -26,25 +20,30 @@ export class VscodeFancyRangeHighlighter { } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { - const [lineRanges, characterRanges] = partition( - ranges, - isLineRange, - ); + const decoratedRanges = flatmap( + generateDifferentiatedRanges(ranges), + function* ({ range, differentiationIndex }) { + const iterable = + range.type === "line" + ? generateDecorationsForLineRange(range.start, range.end) + : generateDecorationsForCharacterRange( + editor, + new Range(range.start, range.end), + ); - const decoratedRanges = Array.from( - chain( - flatmap(characterRanges, ({ start, end }) => - generateDecorationsForCharacterRange(editor, new Range(start, end)), - ), - flatmap(lineRanges, ({ start, end }) => - generateDecorationsForLineRange(start, end), - ), - ), + for (const { range, style } of iterable) { + yield { + range, + style, + differentiationIndex, + }; + } + }, ); this.decorator.setDecorations( editor, - getDifferentiatedRanges(decoratedRanges, getBorderKey), + groupDifferentiatedRanges(decoratedRanges, getBorderKey), ); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index d6739adff6..e536d89e8c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -44,6 +44,12 @@ export class VscodeFancyRangeHighlighterRenderer { ) { const untouchedDecorationTypes = new Set(this.decorationTypes.values()); + decoratedRanges.sort( + (a, b) => + a.styleParameters.differentiationIndex - + b.styleParameters.differentiationIndex, + ); + decoratedRanges.forEach(({ styleParameters, ranges }) => { const decorationType = this.decorationTypes.get(styleParameters); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts new file mode 100644 index 0000000000..bc53e3a5de --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts @@ -0,0 +1,83 @@ +import { + GeneralizedRange, + generalizedRangeContains, + generalizedRangeTouches, +} from "@cursorless/common"; + +import { max } from "lodash"; +import { DifferentiatedRange } from "./getDecorationRanges.types"; + +export function* generateDifferentiatedRanges( + ranges: GeneralizedRange[], +): Iterable { + ranges.sort(compareGeneralizedRangesByStart); + + let currentRanges: DifferentiatedRange[] = []; + + for (const range of ranges) { + currentRanges = [ + ...currentRanges.filter( + ({ range: previousRange }) => + generalizedRangeTouches(previousRange, range) != null, + ), + ]; + + const differentiatedRange = { + range, + differentiationIndex: getDifferentiationIndex(currentRanges, range), + } as DifferentiatedRange; + + yield differentiatedRange; + + currentRanges.push(differentiatedRange); + } +} + +function getDifferentiationIndex( + currentRanges: DifferentiatedRange[], + range: GeneralizedRange, +): number { + const maxContainingDifferentiationIndex = max( + currentRanges + .filter((r) => generalizedRangeContains(r.range, range)) + .map((r) => r.differentiationIndex), + ); + + if (maxContainingDifferentiationIndex != null) { + return maxContainingDifferentiationIndex + 1; + } + + for (let i = 0; ; i++) { + if ( + !currentRanges.some( + ({ differentiationIndex }) => differentiationIndex === i, + ) + ) { + return i; + } + } +} + +function compareGeneralizedRangesByStart( + a: GeneralizedRange, + b: GeneralizedRange, +): number { + if (a.type === "character") { + if (b.type === "character") { + // a.type === "character" && b.type === "character" + return a.start.compareTo(b.start); + } + + // a.type === "character" && b.type === "line" + // Line ranges are always sorted before character ranges + return a.start.line === b.start ? 1 : a.start.line - b.start; + } + + if (b.type === "line") { + // a.type === "line" && b.type === "line" + return a.start - b.start; + } + + // a.type === "line" && b.type === "character" + return b.start.line === a.start ? -1 : a.start - b.start.line; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts index 1f667b7506..a311aaa21c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts @@ -1,4 +1,4 @@ -import { Range } from "@cursorless/common"; +import { CharacterRange, LineRange, Range } from "@cursorless/common"; export enum BorderStyle { porous = "dashed", @@ -30,3 +30,25 @@ export interface StyleParametersRanges { styleParameters: StyleParameters; ranges: Range[]; } + +export interface DifferentiatedStyledRange { + range: Range; + style: T; + differentiationIndex: number; +} + +export interface DifferentiatedRangeBase { + differentiationIndex: number; +} + +export interface DifferentiatedCharacterRange extends DifferentiatedRangeBase { + range: CharacterRange; +} + +export interface DifferentiatedLineRange extends DifferentiatedRangeBase { + range: LineRange; +} + +export type DifferentiatedRange = + | DifferentiatedCharacterRange + | DifferentiatedLineRange; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts index 3b6624e8d9..c4932d7d53 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts @@ -1,7 +1,7 @@ import assert = require("assert"); import { StyleParameters } from "./getDecorationRanges.types"; import { Position, Range } from "@cursorless/common"; -import { getDifferentiatedRanges } from "./getDifferentiatedRanges"; +import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; type Offsets = [number, number]; @@ -119,7 +119,7 @@ const testCases: TestCase[] = [ suite("getDecorationRanges", function () { for (const testCase of testCases) { test(testCase.name, function () { - const actualDecorations = getDifferentiatedRanges( + const actualDecorations = generateDifferentiatedRanges( testCase.ranges.map(({ range, style }) => ({ range: toRange(range), style, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.ts deleted file mode 100644 index 19cb9049c5..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { CompositeKeyDefaultMap, Range } from "@cursorless/common"; -import { groupBy } from "lodash"; -import { - StyleParametersRanges, - StyleParameters, - StyledRange, -} from "./getDecorationRanges.types"; -import { ifilter, take } from "itertools"; - -export function getDifferentiatedRanges( - decoratedRanges: StyledRange[], - getKeyList: (style: T) => unknown[], -): StyleParametersRanges[] { - const groups = groupBy(decoratedRanges, ({ style }) => - getKeyList(style).join("\u0000"), - ); - - const decorations: CompositeKeyDefaultMap< - StyleParameters, - StyleParametersRanges - > = new CompositeKeyDefaultMap( - (styleParameters) => ({ styleParameters, ranges: [] }), - ({ style, differentiationIndex }) => [ - ...getKeyList(style), - differentiationIndex, - ], - ); - - Object.entries(groups).forEach(([_, decoratedRanges]) => { - decoratedRanges.sort((a, b) => a.range.start.compareTo(b.range.start)); - // Generate extra decorations as necessary to ensure that there are no - // examples of the same decoration type touching each other. - let currentRanges: RangeInfo[] = []; - - for (const { range, style } of decoratedRanges) { - currentRanges = [ - ...currentRanges.filter( - ({ range: previousRange }) => - previousRange.intersection(range) != null, - ), - ]; - - const differentiationIndex = take( - 1, - ifilter( - irange(), - (i) => - !currentRanges.some( - ({ differentiationIndex }) => differentiationIndex === i, - ), - ), - )[0]; - - decorations - .get({ - style, - differentiationIndex, - }) - .ranges.push(range); - - currentRanges.push({ - range, - differentiationIndex, - }); - } - }); - - return Array.from(decorations.values()); -} - -function* irange(): Iterable { - for (let i = 0; ; i++) { - yield i; - } -} - -interface RangeInfo { - range: Range; - differentiationIndex: number; -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts new file mode 100644 index 0000000000..1231dfa755 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts @@ -0,0 +1,33 @@ +import { CompositeKeyDefaultMap } from "@cursorless/common"; +import { + StyleParametersRanges, + StyleParameters, + DifferentiatedStyledRange, +} from "./getDecorationRanges.types"; + +export function groupDifferentiatedRanges( + decoratedRanges: Iterable>, + getKeyList: (style: T) => unknown[], +): StyleParametersRanges[] { + const decorations: CompositeKeyDefaultMap< + StyleParameters, + StyleParametersRanges + > = new CompositeKeyDefaultMap( + (styleParameters) => ({ styleParameters, ranges: [] }), + ({ style, differentiationIndex }) => [ + ...getKeyList(style), + differentiationIndex, + ], + ); + + for (const { range, style, differentiationIndex } of decoratedRanges) { + decorations + .get({ + style, + differentiationIndex, + }) + .ranges.push(range); + } + + return Array.from(decorations.values()); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts index 454a400902..187fa280a4 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts @@ -1,7 +1,10 @@ +import tinycolor = require("tinycolor2"); import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; -import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; -import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; -import { weakenRangeTypeColors } from "./weakenRangeTypeColors"; +import { RangeTypeColors } from "./RangeTypeColors"; +import { + ScopeVisualizerColorConfig, + getColorsFromConfig, +} from "./ScopeVisualizerColorConfig"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; export class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { @@ -28,3 +31,29 @@ export class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { })); } } + +const BORDER_WEAKENING = 0.8; +const BACKGROUND_WEAKENING = 0.2; + +function weakenRangeTypeColors(colors: RangeTypeColors): RangeTypeColors { + return { + background: { + light: weakenColor(colors.background.light, BACKGROUND_WEAKENING), + dark: weakenColor(colors.background.dark, BACKGROUND_WEAKENING), + }, + borderSolid: { + light: weakenColor(colors.borderSolid.light, BORDER_WEAKENING), + dark: weakenColor(colors.borderSolid.dark, BORDER_WEAKENING), + }, + borderPorous: { + light: weakenColor(colors.borderPorous.light, BORDER_WEAKENING), + dark: weakenColor(colors.borderPorous.dark, BORDER_WEAKENING), + }, + }; +} + +function weakenColor(color: string, amount: number): string { + const parsed = tinycolor(color); + parsed.setAlpha(parsed.getAlpha() * amount); + return parsed.toHex8String(); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts index 9d82a1e3aa..ffa893d880 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts @@ -1,9 +1,12 @@ -import { Disposable, GeneralizedRange } from "@cursorless/common"; +import { + Disposable, + GeneralizedRange, + isGeneralizedRangeEqual, +} from "@cursorless/common"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { RangeTypeColors } from "./RangeTypeColors"; import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; import { blendRangeTypeColors } from "./blendRangeTypeColors"; -import { isGeneralizedRangeEqual } from "./isGeneralizedRangeEqual"; export interface RendererScope { domain: GeneralizedRange; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts deleted file mode 100644 index 2fbf926c90..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/isGeneralizedRangeEqual.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GeneralizedRange } from "@cursorless/common"; - -export function isGeneralizedRangeEqual( - a: GeneralizedRange, - b: GeneralizedRange, -): boolean { - if (a.type === "character" && b.type === "character") { - return a.start.isEqual(b.start) && a.end.isEqual(b.end); - } - - if (a.type === "line" && b.type === "line") { - return a.start === b.start && a.end === b.end; - } - - return false; -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts deleted file mode 100644 index b5fc60b8b1..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/weakenRangeTypeColors.ts +++ /dev/null @@ -1,30 +0,0 @@ -import tinycolor = require("tinycolor2"); -import { RangeTypeColors } from "./RangeTypeColors"; - -const BORDER_WEAKENING = 0.8; -const BACKGROUND_WEAKENING = 0.8; - -export function weakenRangeTypeColors( - colors: RangeTypeColors, -): RangeTypeColors { - return { - background: { - light: weakenColor(colors.background.light, BACKGROUND_WEAKENING), - dark: weakenColor(colors.background.dark, BACKGROUND_WEAKENING), - }, - borderSolid: { - light: weakenColor(colors.borderSolid.light, BORDER_WEAKENING), - dark: weakenColor(colors.borderSolid.dark, BORDER_WEAKENING), - }, - borderPorous: { - light: weakenColor(colors.borderPorous.light, BORDER_WEAKENING), - dark: weakenColor(colors.borderPorous.dark, BORDER_WEAKENING), - }, - }; -} - -function weakenColor(color: string, amount: number): string { - const parsed = tinycolor(color); - parsed.setAlpha(parsed.getAlpha() * amount); - return parsed.toHex8String(); -} From 65164d75cc1966f2469e43461236c437cda9d8f5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:22:12 +0100 Subject: [PATCH 14/61] Update iteration default colors --- packages/cursorless-vscode/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index 11b4be8ac6..d23c76f379 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -489,9 +489,9 @@ "borderPorous": "rgba(255, 0, 0, 0.29)" }, "iteration": { - "background": "#00ff002d", - "borderSolid": "rgba(255, 0, 0, 0.47)", - "borderPorous": "rgba(255, 0, 0, 0.29)" + "background": "#00725f6c", + "borderSolid": "#00ffd577", + "borderPorous": "#00ffd525" } } }, @@ -585,9 +585,9 @@ "borderPorous": "rgba(255, 0, 0, 0.29)" }, "iteration": { - "background": "#00ff002d", - "borderSolid": "rgba(255, 0, 0, 0.47)", - "borderPorous": "rgba(255, 0, 0, 0.29)" + "background": "#00725f6c", + "borderSolid": "#00ffd577", + "borderPorous": "#00ffd525" } } }, From 956969a8ec9e70b20aeec104a449714c06cc68b6 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:27:25 +0100 Subject: [PATCH 15/61] Switch to hex-8 colors --- packages/cursorless-vscode/package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index d23c76f379..79d6b2c0f0 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -476,17 +476,17 @@ "domain": { "background": "#00e1ff18", "borderSolid": "#ebdeec84", - "borderPorous": "rgba(235, 222, 236, 0.23)" + "borderPorous": "#ebdeec3b" }, "content": { "background": "#ad00bc5b", - "borderSolid": "rgba(238, 0, 255, 0.47)", - "borderPorous": "rgba(235, 222, 236, 0.23)" + "borderSolid": "#ee00ff78", + "borderPorous": "#ebdeec3b" }, "removal": { "background": "#ff00002d", - "borderSolid": "rgba(255, 0, 0, 0.47)", - "borderPorous": "rgba(255, 0, 0, 0.29)" + "borderSolid": "#ff000078", + "borderPorous": "#ff00004a" }, "iteration": { "background": "#00725f6c", @@ -572,17 +572,17 @@ "domain": { "background": "#00e1ff18", "borderSolid": "#ebdeec84", - "borderPorous": "rgba(235, 222, 236, 0.23)" + "borderPorous": "#ebdeec3b" }, "content": { "background": "#ad00bc5b", - "borderSolid": "rgba(238, 0, 255, 0.47)", - "borderPorous": "rgba(235, 222, 236, 0.23)" + "borderSolid": "#ee00ff78", + "borderPorous": "#ebdeec3b" }, "removal": { "background": "#ff00002d", - "borderSolid": "rgba(255, 0, 0, 0.47)", - "borderPorous": "rgba(255, 0, 0, 0.29)" + "borderSolid": "#ff000078", + "borderPorous": "#ff00004a" }, "iteration": { "background": "#00725f6c", From 336630eaeab59870bc9cdd8d9111d92f81ba195b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:29:21 +0100 Subject: [PATCH 16/61] cleanup --- .../src/ScopeVisualizer/getIterationScopes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts index 7e65380ac2..ba37b22185 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts @@ -45,10 +45,10 @@ function getEveryScopeLenient(everyStage: ModifierStage, target: Target) { try { return everyStage.run(target); } catch (err) { - if ((err as Error).name !== "NoContainingScopeError") { - throw err; + if ((err as Error).name === "NoContainingScopeError") { + return []; } - return []; + throw err; } } From 97cdc5b24be68686b10a4031ebbf5737aebbd4c1 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:32:44 +0100 Subject: [PATCH 17/61] try to cleanup test --- .../common/src/types/GeneralizedRange.test.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/common/src/types/GeneralizedRange.test.ts b/packages/common/src/types/GeneralizedRange.test.ts index d84ae36cf7..5bffda7654 100644 --- a/packages/common/src/types/GeneralizedRange.test.ts +++ b/packages/common/src/types/GeneralizedRange.test.ts @@ -1,4 +1,3 @@ -import { Position, Range, TextEditor } from "./types"; import { CharacterRange, GeneralizedRange, @@ -9,23 +8,10 @@ import { toCharacterRange, toLineRange, } from "./GeneralizedRange"; +import { Position } from "./Position"; +import { Range } from "./Range"; describe("GeneralizedRange", () => { - const editor: TextEditor = { - document: { - uri: "", - fileName: "", - isUntitled: false, - getText: () => "", - lineCount: 0, - offsetAt: (pos: Position) => 0, - positionAt: (offset: number) => new Position(0, 0), - save: () => Promise.resolve(true), - eol: 0, - }, - selection: new Range(new Position(0, 0), new Position(0, 0)), - }; - describe("toLineRange", () => { it("converts a range to a line range", () => { const range = new Range(new Position(1, 0), new Position(3, 5)); From 800d7d21af3c380adf17965e4e461e03c05588c8 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:59:54 +0100 Subject: [PATCH 18/61] more cleanup --- .../VscodeFancyRangeHighlighter.ts | 13 +++++++------ .../VscodeFancyRangeHighlighterRenderer.ts | 2 +- .../getDecorationRanges.types.ts | 18 ++++++++++-------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 2c18d5f877..5c27672fba 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -13,15 +13,16 @@ import { groupDifferentiatedRanges } from "./groupDifferentiatedRanges"; * Manages VSCode decoration types for a highlight or flash style. */ export class VscodeFancyRangeHighlighter { - private decorator: VscodeFancyRangeHighlighterRenderer; + private renderer: VscodeFancyRangeHighlighterRenderer; constructor(colors: RangeTypeColors) { - this.decorator = new VscodeFancyRangeHighlighterRenderer(colors); + this.renderer = new VscodeFancyRangeHighlighterRenderer(colors); } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { const decoratedRanges = flatmap( generateDifferentiatedRanges(ranges), + function* ({ range, differentiationIndex }) { const iterable = range.type === "line" @@ -41,18 +42,18 @@ export class VscodeFancyRangeHighlighter { }, ); - this.decorator.setDecorations( + this.renderer.setRanges( editor, - groupDifferentiatedRanges(decoratedRanges, getBorderKey), + groupDifferentiatedRanges(decoratedRanges, getStyleKey), ); } dispose() { - this.decorator.dispose(); + this.renderer.dispose(); } } -function getBorderKey({ +function getStyleKey({ top, right, left, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index e536d89e8c..54cff1efb4 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -38,7 +38,7 @@ export class VscodeFancyRangeHighlighterRenderer { ); } - setDecorations( + setRanges( editor: VscodeTextEditorImpl, decoratedRanges: StyleParametersRanges[], ) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts index a311aaa21c..7b55531ca7 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts @@ -1,4 +1,9 @@ -import { CharacterRange, LineRange, Range } from "@cursorless/common"; +import { + CharacterRange, + GeneralizedRange, + LineRange, + Range, +} from "@cursorless/common"; export enum BorderStyle { porous = "dashed", @@ -37,18 +42,15 @@ export interface DifferentiatedStyledRange { differentiationIndex: number; } -export interface DifferentiatedRangeBase { +export interface DifferentiatedRange { + range: GeneralizedRange; differentiationIndex: number; } -export interface DifferentiatedCharacterRange extends DifferentiatedRangeBase { +export interface DifferentiatedCharacterRange extends DifferentiatedRange { range: CharacterRange; } -export interface DifferentiatedLineRange extends DifferentiatedRangeBase { +export interface DifferentiatedLineRange extends DifferentiatedRange { range: LineRange; } - -export type DifferentiatedRange = - | DifferentiatedCharacterRange - | DifferentiatedLineRange; From 94236efae56b7269eb6b6011032142b8c87eaddf Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:12:19 +0100 Subject: [PATCH 19/61] more cleanup --- .../VscodeFancyRangeHighlighter.ts | 12 +++++++---- .../VscodeFancyRangeHighlighterRenderer.ts | 14 ++++++------- .../getDecorationRanges.test.ts | 4 ++-- .../getDecorationRanges.types.ts | 17 +++++++-------- .../getDifferentiatedRanges.test.ts | 4 ++-- .../groupDifferentiatedRanges.ts | 21 +++++++------------ 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 5c27672fba..4c0a541a1e 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -6,7 +6,10 @@ import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlight import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; -import { DecorationStyle } from "./getDecorationRanges.types"; +import { + DecorationStyle, + DifferentiatedStyledRange, +} from "./getDecorationRanges.types"; import { groupDifferentiatedRanges } from "./groupDifferentiatedRanges"; /** @@ -20,7 +23,9 @@ export class VscodeFancyRangeHighlighter { } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { - const decoratedRanges = flatmap( + const decoratedRanges: Iterable< + DifferentiatedStyledRange + > = flatmap( generateDifferentiatedRanges(ranges), function* ({ range, differentiationIndex }) { @@ -35,8 +40,7 @@ export class VscodeFancyRangeHighlighter { for (const { range, style } of iterable) { yield { range, - style, - differentiationIndex, + differentiatedStyle: { style, differentiationIndex }, }; } }, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index 54cff1efb4..b6879fa82a 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -11,13 +11,13 @@ import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import { BorderStyle, DecorationStyle, - StyleParameters, - StyleParametersRanges, + DifferentiatedStyle, + DifferentiatedStyledRangeList, } from "./getDecorationRanges.types"; export class VscodeFancyRangeHighlighterRenderer { private decorationTypes: CompositeKeyDefaultMap< - StyleParameters, + DifferentiatedStyle, TextEditorDecorationType >; @@ -40,17 +40,17 @@ export class VscodeFancyRangeHighlighterRenderer { setRanges( editor: VscodeTextEditorImpl, - decoratedRanges: StyleParametersRanges[], + decoratedRanges: DifferentiatedStyledRangeList[], ) { const untouchedDecorationTypes = new Set(this.decorationTypes.values()); decoratedRanges.sort( (a, b) => - a.styleParameters.differentiationIndex - - b.styleParameters.differentiationIndex, + a.differentiatedStyles.differentiationIndex - + b.differentiatedStyles.differentiationIndex, ); - decoratedRanges.forEach(({ styleParameters, ranges }) => { + decoratedRanges.forEach(({ differentiatedStyles: styleParameters, ranges }) => { const decorationType = this.decorationTypes.get(styleParameters); editor.vscodeEditor.setDecorations( diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts index 6c3022b42d..ab7716f2e1 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts @@ -1,6 +1,6 @@ import assert = require("assert"); import { getDecorationRanges } from "./getDecorationRanges"; -import { DecorationStyle, StyleParameters } from "./getDecorationRanges.types"; +import { DecorationStyle, DifferentiatedStyle } from "./getDecorationRanges.types"; import { Range, RangePlainObject, @@ -14,7 +14,7 @@ interface RangeDescription { } export interface ExpectedResult { - styleParameters: StyleParameters; + styleParameters: DifferentiatedStyle; ranges: RangePlainObject[]; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts index 7b55531ca7..70153017f7 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts @@ -20,26 +20,25 @@ export interface DecorationStyle { } export interface StyledRange { - range: Range; style: T; + range: Range; } export type DecoratedRange = StyledRange; -export interface StyleParameters { +export interface DifferentiatedStyle { style: T; differentiationIndex: number; } -export interface StyleParametersRanges { - styleParameters: StyleParameters; - ranges: Range[]; -} - export interface DifferentiatedStyledRange { + differentiatedStyle: DifferentiatedStyle; range: Range; - style: T; - differentiationIndex: number; +} + +export interface DifferentiatedStyledRangeList { + differentiatedStyles: DifferentiatedStyle; + ranges: Range[]; } export interface DifferentiatedRange { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts index c4932d7d53..d357a1af8b 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts @@ -1,12 +1,12 @@ import assert = require("assert"); -import { StyleParameters } from "./getDecorationRanges.types"; +import { DifferentiatedStyle } from "./getDecorationRanges.types"; import { Position, Range } from "@cursorless/common"; import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; type Offsets = [number, number]; interface ExpectedResult { - styleParameters: StyleParameters; + styleParameters: DifferentiatedStyle; ranges: Offsets[]; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts index 1231dfa755..5e015cb23d 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts @@ -1,32 +1,27 @@ import { CompositeKeyDefaultMap } from "@cursorless/common"; import { - StyleParametersRanges, - StyleParameters, + DifferentiatedStyledRangeList, + DifferentiatedStyle, DifferentiatedStyledRange, } from "./getDecorationRanges.types"; export function groupDifferentiatedRanges( decoratedRanges: Iterable>, getKeyList: (style: T) => unknown[], -): StyleParametersRanges[] { +): DifferentiatedStyledRangeList[] { const decorations: CompositeKeyDefaultMap< - StyleParameters, - StyleParametersRanges + DifferentiatedStyle, + DifferentiatedStyledRangeList > = new CompositeKeyDefaultMap( - (styleParameters) => ({ styleParameters, ranges: [] }), + (differentiatedStyles) => ({ differentiatedStyles, ranges: [] }), ({ style, differentiationIndex }) => [ ...getKeyList(style), differentiationIndex, ], ); - for (const { range, style, differentiationIndex } of decoratedRanges) { - decorations - .get({ - style, - differentiationIndex, - }) - .ranges.push(range); + for (const { range, differentiatedStyle } of decoratedRanges) { + decorations.get(differentiatedStyle).ranges.push(range); } return Array.from(decorations.values()); From 84eeeb99984ea8773804eed40df409facbfbba4d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:21:04 +0100 Subject: [PATCH 20/61] more cleanup --- .../VscodeFancyRangeHighlighter.ts | 26 +++------------- .../VscodeFancyRangeHighlighterRenderer.ts | 22 +++++++------- .../generateDecorationsForCharacterRange.ts | 4 +-- .../generateDecorationsForLineRange.ts | 4 +-- .../generateDifferentiatedRanges.ts | 10 +++---- .../getDecorationRanges.test.ts | 5 +++- .../getDecorationRanges.types.ts | 30 +++++++++---------- ....ts => groupDifferentiatedStyledRanges.ts} | 23 +++++++------- .../handleMultipleLines.ts | 6 ++-- 9 files changed, 60 insertions(+), 70 deletions(-) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/{groupDifferentiatedRanges.ts => groupDifferentiatedStyledRanges.ts} (55%) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 4c0a541a1e..d0ae1f7c41 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -6,11 +6,8 @@ import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlight import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; -import { - DecorationStyle, - DifferentiatedStyledRange, -} from "./getDecorationRanges.types"; -import { groupDifferentiatedRanges } from "./groupDifferentiatedRanges"; +import { DifferentiatedStyledRange } from "./getDecorationRanges.types"; +import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRanges"; /** * Manages VSCode decoration types for a highlight or flash style. @@ -23,9 +20,7 @@ export class VscodeFancyRangeHighlighter { } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { - const decoratedRanges: Iterable< - DifferentiatedStyledRange - > = flatmap( + const decoratedRanges: Iterable = flatmap( generateDifferentiatedRanges(ranges), function* ({ range, differentiationIndex }) { @@ -46,23 +41,10 @@ export class VscodeFancyRangeHighlighter { }, ); - this.renderer.setRanges( - editor, - groupDifferentiatedRanges(decoratedRanges, getStyleKey), - ); + this.renderer.setRanges(editor, groupDifferentiatedStyledRanges(decoratedRanges)); } dispose() { this.renderer.dispose(); } } - -function getStyleKey({ - top, - right, - left, - bottom, - isWholeLine, -}: DecorationStyle) { - return [top, right, left, bottom, isWholeLine ?? false]; -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index b6879fa82a..90ab0c0e29 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -17,7 +17,7 @@ import { export class VscodeFancyRangeHighlighterRenderer { private decorationTypes: CompositeKeyDefaultMap< - DifferentiatedStyle, + DifferentiatedStyle, TextEditorDecorationType >; @@ -40,7 +40,7 @@ export class VscodeFancyRangeHighlighterRenderer { setRanges( editor: VscodeTextEditorImpl, - decoratedRanges: DifferentiatedStyledRangeList[], + decoratedRanges: DifferentiatedStyledRangeList[], ) { const untouchedDecorationTypes = new Set(this.decorationTypes.values()); @@ -50,16 +50,18 @@ export class VscodeFancyRangeHighlighterRenderer { b.differentiatedStyles.differentiationIndex, ); - decoratedRanges.forEach(({ differentiatedStyles: styleParameters, ranges }) => { - const decorationType = this.decorationTypes.get(styleParameters); + decoratedRanges.forEach( + ({ differentiatedStyles: styleParameters, ranges }) => { + const decorationType = this.decorationTypes.get(styleParameters); - editor.vscodeEditor.setDecorations( - decorationType, - ranges.map(toVscodeRange), - ); + editor.vscodeEditor.setDecorations( + decorationType, + ranges.map(toVscodeRange), + ); - untouchedDecorationTypes.delete(decorationType); - }); + untouchedDecorationTypes.delete(decorationType); + }, + ); untouchedDecorationTypes.forEach((decorationType) => { editor.vscodeEditor.setDecorations(decorationType, []); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts index 8714513fc8..cbb5cdedb4 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts @@ -1,12 +1,12 @@ import { Range, TextEditor } from "@cursorless/common"; import { range } from "lodash"; -import { BorderStyle, DecoratedRange } from "./getDecorationRanges.types"; +import { BorderStyle, StyledRange } from "./getDecorationRanges.types"; import { handleMultipleLines } from "./handleMultipleLines"; export function* generateDecorationsForCharacterRange( editor: TextEditor, characterRange: Range, -): Iterable { +): Iterable { if (characterRange.isSingleLine) { yield { range: characterRange, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts index 40a05ce9cf..a3c103490b 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts @@ -1,10 +1,10 @@ import { Range } from "@cursorless/common"; -import { BorderStyle, DecoratedRange } from "./getDecorationRanges.types"; +import { BorderStyle, StyledRange } from "./getDecorationRanges.types"; export function* generateDecorationsForLineRange( startLine: number, endLine: number, -): Iterable { +): Iterable { const lineCount = endLine - startLine + 1; if (lineCount === 1) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts index bc53e3a5de..d360834ebd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts @@ -5,14 +5,14 @@ import { } from "@cursorless/common"; import { max } from "lodash"; -import { DifferentiatedRange } from "./getDecorationRanges.types"; +import { DifferentiatedGeneralizedRange } from "./getDecorationRanges.types"; export function* generateDifferentiatedRanges( ranges: GeneralizedRange[], -): Iterable { +): Iterable { ranges.sort(compareGeneralizedRangesByStart); - let currentRanges: DifferentiatedRange[] = []; + let currentRanges: DifferentiatedGeneralizedRange[] = []; for (const range of ranges) { currentRanges = [ @@ -25,7 +25,7 @@ export function* generateDifferentiatedRanges( const differentiatedRange = { range, differentiationIndex: getDifferentiationIndex(currentRanges, range), - } as DifferentiatedRange; + } as DifferentiatedGeneralizedRange; yield differentiatedRange; @@ -34,7 +34,7 @@ export function* generateDifferentiatedRanges( } function getDifferentiationIndex( - currentRanges: DifferentiatedRange[], + currentRanges: DifferentiatedGeneralizedRange[], range: GeneralizedRange, ): number { const maxContainingDifferentiationIndex = max( diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts index ab7716f2e1..91e6a954f9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts @@ -1,6 +1,9 @@ import assert = require("assert"); import { getDecorationRanges } from "./getDecorationRanges"; -import { DecorationStyle, DifferentiatedStyle } from "./getDecorationRanges.types"; +import { + DecorationStyle, + DifferentiatedStyle, +} from "./getDecorationRanges.types"; import { Range, RangePlainObject, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts index 70153017f7..7fabdbf11b 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts @@ -19,37 +19,37 @@ export interface DecorationStyle { isWholeLine?: boolean; } -export interface StyledRange { - style: T; - range: Range; +export interface DifferentiatedStyle { + style: DecorationStyle; + differentiationIndex: number; } -export type DecoratedRange = StyledRange; - -export interface DifferentiatedStyle { - style: T; - differentiationIndex: number; +export interface StyledRange { + style: DecorationStyle; + range: Range; } -export interface DifferentiatedStyledRange { - differentiatedStyle: DifferentiatedStyle; +export interface DifferentiatedStyledRange { + differentiatedStyle: DifferentiatedStyle; range: Range; } -export interface DifferentiatedStyledRangeList { - differentiatedStyles: DifferentiatedStyle; +export interface DifferentiatedStyledRangeList { + differentiatedStyles: DifferentiatedStyle; ranges: Range[]; } -export interface DifferentiatedRange { +export interface DifferentiatedGeneralizedRange { range: GeneralizedRange; differentiationIndex: number; } -export interface DifferentiatedCharacterRange extends DifferentiatedRange { +export interface DifferentiatedCharacterRange + extends DifferentiatedGeneralizedRange { range: CharacterRange; } -export interface DifferentiatedLineRange extends DifferentiatedRange { +export interface DifferentiatedLineRange + extends DifferentiatedGeneralizedRange { range: LineRange; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts similarity index 55% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts index 5e015cb23d..0a5f543dfd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedRanges.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts @@ -5,19 +5,15 @@ import { DifferentiatedStyledRange, } from "./getDecorationRanges.types"; -export function groupDifferentiatedRanges( - decoratedRanges: Iterable>, - getKeyList: (style: T) => unknown[], -): DifferentiatedStyledRangeList[] { +export function groupDifferentiatedStyledRanges( + decoratedRanges: Iterable, +): DifferentiatedStyledRangeList[] { const decorations: CompositeKeyDefaultMap< - DifferentiatedStyle, - DifferentiatedStyledRangeList + DifferentiatedStyle, + DifferentiatedStyledRangeList > = new CompositeKeyDefaultMap( (differentiatedStyles) => ({ differentiatedStyles, ranges: [] }), - ({ style, differentiationIndex }) => [ - ...getKeyList(style), - differentiationIndex, - ], + getStyleKey, ); for (const { range, differentiatedStyle } of decoratedRanges) { @@ -26,3 +22,10 @@ export function groupDifferentiatedRanges( return Array.from(decorations.values()); } + +function getStyleKey({ + style: { top, right, left, bottom, isWholeLine }, + differentiationIndex, +}: DifferentiatedStyle) { + return [top, right, left, bottom, isWholeLine ?? false, differentiationIndex]; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts index c991d356ba..d0cd5645b9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts @@ -2,13 +2,13 @@ import { Range } from "@cursorless/common"; import { BorderStyle, DecorationStyle, - DecoratedRange, + StyledRange, } from "./getDecorationRanges.types"; import { flatmap } from "itertools"; export function* handleMultipleLines( lineRanges: Range[], -): Iterable { +): Iterable { yield* flatmap(generateLineGroupings(lineRanges), handleLine); } @@ -71,7 +71,7 @@ function* handleLine({ previousLine, currentLine, nextLine, -}: LineGrouping): Iterable { +}: LineGrouping): Iterable { const events: Event[] = [ ...(previousLine == null ? [] From 35efcb9ed0a70fc017fa7467e6e42b392cd71383 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:22:31 +0000 Subject: [PATCH 21/61] [pre-commit.ci lite] apply automatic fixes --- .../VscodeFancyRangeHighlighter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index d0ae1f7c41..029b055672 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -41,7 +41,10 @@ export class VscodeFancyRangeHighlighter { }, ); - this.renderer.setRanges(editor, groupDifferentiatedStyledRanges(decoratedRanges)); + this.renderer.setRanges( + editor, + groupDifferentiatedStyledRanges(decoratedRanges), + ); } dispose() { From f72b8462fc9cf371efc2e8cc62cc9095f68d96e9 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 25 Jun 2023 17:44:32 +0100 Subject: [PATCH 22/61] more cleanup --- .../common/src/types/GeneralizedRange.test.ts | 170 ------------------ .../VscodeScopeVisualizer.ts | 6 +- .../createVscodeScopeVisualizer.ts | 8 +- 3 files changed, 5 insertions(+), 179 deletions(-) delete mode 100644 packages/common/src/types/GeneralizedRange.test.ts diff --git a/packages/common/src/types/GeneralizedRange.test.ts b/packages/common/src/types/GeneralizedRange.test.ts deleted file mode 100644 index 5bffda7654..0000000000 --- a/packages/common/src/types/GeneralizedRange.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { - CharacterRange, - GeneralizedRange, - LineRange, - generalizedRangeContains, - generalizedRangeTouches, - isGeneralizedRangeEqual, - toCharacterRange, - toLineRange, -} from "./GeneralizedRange"; -import { Position } from "./Position"; -import { Range } from "./Range"; - -describe("GeneralizedRange", () => { - describe("toLineRange", () => { - it("converts a range to a line range", () => { - const range = new Range(new Position(1, 0), new Position(3, 5)); - const lineRange: LineRange = { type: "line", start: 1, end: 3 }; - expect(toLineRange(range)).toEqual(lineRange); - }); - }); - - describe("toCharacterRange", () => { - it("converts a range to a character range", () => { - const range = new Range(new Position(1, 0), new Position(3, 5)); - const charRange: CharacterRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - expect(toCharacterRange(range)).toEqual(charRange); - }); - }); - - describe("generalizedRangeContains", () => { - it("returns true if a contains b", () => { - const a: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - const b: GeneralizedRange = { - type: "character", - start: new Position(2, 0), - end: new Position(3, 0), - }; - expect(generalizedRangeContains(a, b)).toBe(true); - }); - - it("returns false if a does not contain b", () => { - const a: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - const b: GeneralizedRange = { - type: "character", - start: new Position(4, 0), - end: new Position(5, 0), - }; - expect(generalizedRangeContains(a, b)).toBe(false); - }); - - it("returns true if a contains b (line range)", () => { - const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; - const b: GeneralizedRange = { - type: "character", - start: new Position(2, 0), - end: new Position(3, 0), - }; - expect(generalizedRangeContains(a, b)).toBe(true); - }); - - it("returns false if a does not contain b (line range)", () => { - const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; - const b: GeneralizedRange = { - type: "character", - start: new Position(4, 0), - end: new Position(5, 0), - }; - expect(generalizedRangeContains(a, b)).toBe(false); - }); - }); - - describe("generalizedRangeTouches", () => { - it("returns true if a touches b", () => { - const a: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - const b: GeneralizedRange = { - type: "character", - start: new Position(3, 0), - end: new Position(4, 0), - }; - expect(generalizedRangeTouches(a, b)).toBe(true); - }); - - it("returns false if a does not touch b", () => { - const a: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - const b: GeneralizedRange = { - type: "character", - start: new Position(4, 0), - end: new Position(5, 0), - }; - expect(generalizedRangeTouches(a, b)).toBe(false); - }); - - it("returns true if a touches b (line range)", () => { - const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; - const b: GeneralizedRange = { - type: "character", - start: new Position(3, 0), - end: new Position(4, 0), - }; - expect(generalizedRangeTouches(a, b)).toBe(true); - }); - - it("returns false if a does not touch b (line range)", () => { - const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; - const b: GeneralizedRange = { - type: "character", - start: new Position(4, 0), - end: new Position(5, 0), - }; - expect(generalizedRangeTouches(a, b)).toBe(false); - }); - }); - - describe("isGeneralizedRangeEqual", () => { - it("returns true if a and b are equal (character range)", () => { - const a: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - const b: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - expect(isGeneralizedRangeEqual(a, b)).toBe(true); - }); - - it("returns true if a and b are equal (line range)", () => { - const a: GeneralizedRange = { type: "line", start: 1, end: 3 }; - const b: GeneralizedRange = { type: "line", start: 1, end: 3 }; - expect(isGeneralizedRangeEqual(a, b)).toBe(true); - }); - - it("returns false if a and b are not equal", () => { - const a: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(3, 5), - }; - const b: GeneralizedRange = { - type: "character", - start: new Position(1, 0), - end: new Position(4, 0), - }; - expect(isGeneralizedRangeEqual(a, b)).toBe(false); - }); - }); -}); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index 1f2694321f..636e9576f6 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -14,7 +14,6 @@ import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; import { RendererScope, VscodeScopeRenderer } from "./VscodeScopeRenderer"; import { RangeTypeColors } from "./RangeTypeColors"; -import { VisualizationType } from "../../../ScopeVisualizerCommandApi"; export abstract class VscodeScopeVisualizer implements ScopeRenderer { private renderer!: VscodeScopeRenderer; @@ -31,10 +30,7 @@ export abstract class VscodeScopeVisualizer implements ScopeRenderer { colorConfig: ScopeVisualizerColorConfig, ): RangeTypeColors; - constructor( - protected scopeType: ScopeType, - private visualizationType: VisualizationType, - ) { + constructor(protected scopeType: ScopeType) { this.disposables.push( vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts index b71eef07d2..371618d6cd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts @@ -11,12 +11,12 @@ export function createVscodeScopeVisualizer( ) { switch (visualizationType) { case "content": - return new VscodeScopeContentVisualizer(scopeType, visualizationType); + return new VscodeScopeContentVisualizer(scopeType); case "removal": - return new VscodeScopeRemovalVisualizer(scopeType, visualizationType); + return new VscodeScopeRemovalVisualizer(scopeType); case "iteration": - return new VscodeScopeIterationVisualizer(scopeType, visualizationType); + return new VscodeScopeIterationVisualizer(scopeType); case "every": - return new VscodeScopeEveryVisualizer(scopeType, visualizationType); + return new VscodeScopeEveryVisualizer(scopeType); } } From ebfc1bf7ae8040c16c1ace10b82591682d7519b4 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:16:12 +0100 Subject: [PATCH 23/61] Fix bug with changing language id --- .../src/ScopeVisualizer/EditorScopeVisualizer.ts | 4 ++++ packages/cursorless-engine/src/util/PerEditor.ts | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts index 7d144e7720..e44382929a 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts @@ -27,6 +27,10 @@ export class EditorScopeVisualizer implements Disposable { private editor: TextEditor, ) { this.disposables.push( + // An event that fires when a text document opens + ide().onDidOpenTextDocument(this.debouncer.run), + // An Event that fires when a text document closes + ide().onDidCloseTextDocument(this.debouncer.run), // An event that is emitted when a text document is changed. This usually // happens when the contents changes but also when other things like the // dirty-state changes. diff --git a/packages/cursorless-engine/src/util/PerEditor.ts b/packages/cursorless-engine/src/util/PerEditor.ts index f4877b9b65..9b33e26414 100644 --- a/packages/cursorless-engine/src/util/PerEditor.ts +++ b/packages/cursorless-engine/src/util/PerEditor.ts @@ -6,11 +6,9 @@ export class PerEditor { private editorHandlers: Map = new Map(); constructor(private makeEditorHandler: (editor: TextEditor) => T) { + this.handleChange = this.handleChange.bind(this); + this.disposables.push( - // An event that fires when a text document opens - ide().onDidOpenTextDocument(this.handleChange), - // An Event that fires when a text document closes - ide().onDidCloseTextDocument(this.handleChange), // An Event which fires when the array of visible editors has changed. ide().onDidChangeVisibleTextEditors(this.handleChange), ); From 44522466220abbdfbc14cd73d1c5ad329256db57 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:21:16 +0100 Subject: [PATCH 24/61] Simplification --- cursorless-talon/src/cursorless.talon | 2 - .../src/ide/types/IdeScopeVisualizer.ts | 21 ++++ packages/common/src/util/itertools.ts | 16 +++ packages/cursorless-engine/package.json | 2 +- .../src/CursorlessEngineApi.ts | 59 ++++++++- .../ScopeVisualizer/EditorScopeVisualizer.ts | 113 ------------------ .../src/ScopeVisualizer/ScopeRangeProvider.ts | 78 ++++++++++++ .../src/ScopeVisualizer/ScopeRangeWatcher.ts | 102 ++++++++++++++++ .../ScopeVisualizer/ScopeSupportChecker.ts | 92 ++++++++++++++ .../ScopeVisualizer/ScopeVisualizerImpl.ts | 56 --------- .../UnsupportedScopeTypeVisualizationError.ts | 8 -- .../src/ScopeVisualizer/getIterationRange.ts | 5 + .../src/ScopeVisualizer/index.ts | 2 +- .../cursorless-engine/src/cursorlessEngine.ts | 55 ++++++--- packages/cursorless-vscode/package.json | 2 +- .../src/ScopeVisualizerCommandApi.ts | 2 +- .../src/ScopeVisualizerImpl.ts | 16 +-- packages/cursorless-vscode/src/extension.ts | 7 +- .../VscodeScopeContentVisualizer.ts | 27 ----- .../VscodeScopeEveryVisualizer.ts | 59 --------- .../VscodeScopeIterationVisualizer.ts | 44 ++++--- .../VscodeScopeRemovalVisualizer.ts | 27 ----- .../VscodeScopeTargetVisualizer.ts | 54 +++++++++ .../VscodeScopeVisualizer.ts | 98 ++++++++------- .../createVscodeScopeVisualizer.ts | 20 ++-- pnpm-lock.yaml | 14 +-- 26 files changed, 573 insertions(+), 408 deletions(-) delete mode 100644 packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts delete mode 100644 packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts delete mode 100644 packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index d5f77c21d8..967789a498 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -27,7 +27,5 @@ visualize removal: user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "removal") visualize iteration: user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "iteration") -visualize every : - user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "every") visualize nothing: user.private_cursorless_run_rpc_command_and_wait("cursorless.hideScopeVisualizer") diff --git a/packages/common/src/ide/types/IdeScopeVisualizer.ts b/packages/common/src/ide/types/IdeScopeVisualizer.ts index 23880bff77..18323fd6dd 100644 --- a/packages/common/src/ide/types/IdeScopeVisualizer.ts +++ b/packages/common/src/ide/types/IdeScopeVisualizer.ts @@ -11,6 +11,27 @@ export interface ScopeRenderer { visualizerConfig: ScopeVisualizerConfig; } +interface ScopeRangeConfigBase { + visibleOnly: boolean; + scopeType: ScopeType; +} + +export type ScopeRangeConfig = ScopeRangeConfigBase; + +export interface IterationScopeRangeConfig extends ScopeRangeConfigBase { + includeIterationNestedTargets: boolean; +} + +export type ScopeChangeEventCallback = ( + editor: TextEditor, + scopeRanges: ScopeRanges[], +) => void; + +export type IterationScopeChangeEventCallback = ( + editor: TextEditor, + scopeRanges: IterationScopeRanges[], +) => void; + export interface ScopeRanges { domain: GeneralizedRange; targets: TargetRanges[]; diff --git a/packages/common/src/util/itertools.ts b/packages/common/src/util/itertools.ts index 5324763e23..398baf0689 100644 --- a/packages/common/src/util/itertools.ts +++ b/packages/common/src/util/itertools.ts @@ -50,3 +50,19 @@ export function partition( } return [first, second]; } + +/** + * Returns `true` if the given iterable is empty, `false` otherwise + * + * From https://github.com/sindresorhus/is-empty-iterable/blob/12d3b4f966170d9d85a2067f5326668d5bb910a0/index.js + * @param iterable The iterable to check + * @returns `true` if the iterable is empty, `false` otherwise + */ +export function isEmptyIterable(iterable: Iterable): boolean { + for (const _ of iterable) { + // eslint-disable-line no-unused-vars, no-unreachable-loop + return false; + } + + return true; +} diff --git a/packages/cursorless-engine/package.json b/packages/cursorless-engine/package.json index 198d1630c3..fe3cb6ab7b 100644 --- a/packages/cursorless-engine/package.json +++ b/packages/cursorless-engine/package.json @@ -15,7 +15,7 @@ "@cursorless/common": "workspace:*", "immer": "^9.0.15", "immutability-helper": "^3.1.1", - "itertools": "^1.7.1", + "itertools": "^2.1.1", "lodash": "^4.17.21", "node-html-parser": "^5.3.3", "zod": "3.21.4" diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts index 1fd4722c08..c181974f23 100644 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/CursorlessEngineApi.ts @@ -1,11 +1,24 @@ -import { Command, HatTokenMap, IDE, ScopeRenderer } from "@cursorless/common"; +import { + Command, + Disposable, + HatTokenMap, + IDE, + IterationScopeChangeEventCallback, + IterationScopeRangeConfig, + IterationScopeRanges, + ScopeChangeEventCallback, + ScopeRangeConfig, + ScopeRanges, + ScopeType, + TextEditor, +} from "@cursorless/common"; import { Snippets } from "./core/Snippets"; import { StoredTargetMap } from "./core/StoredTargets"; import { TestCaseRecorder } from "./testCaseRecorder/TestCaseRecorder"; export interface CursorlessEngine { commandApi: CommandApi; - scopeVisualizer: ScopeVisualizer; + scopeProvider: ScopeProvider; testCaseRecorder: TestCaseRecorder; storedTargets: StoredTargetMap; hatTokenMap: HatTokenMap; @@ -28,8 +41,42 @@ export interface CommandApi { runCommandSafe(...args: unknown[]): Promise; } -export interface ScopeVisualizer { - start(ideScopeVisualizer: ScopeRenderer): void; - stop(): void; - refresh(): void; +export interface ScopeProvider { + provideScopeRanges: ( + editor: TextEditor, + { scopeType, visibleOnly }: ScopeRangeConfig, + ) => ScopeRanges[]; + + provideIterationScopeRanges: ( + editor: TextEditor, + { + scopeType, + visibleOnly, + includeIterationNestedTargets, + }: IterationScopeRangeConfig, + ) => IterationScopeRanges[]; + + onDidChangeScopeRanges: ( + callback: ScopeChangeEventCallback, + config: ScopeRangeConfig, + ) => Disposable; + + onDidChangeIterationScopeRanges: ( + callback: IterationScopeChangeEventCallback, + config: IterationScopeRangeConfig, + ) => Disposable; + + getScopeSupport: (editor: TextEditor, scopeType: ScopeType) => ScopeSupport; + + getIterationScopeSupport: ( + editor: TextEditor, + scopeType: ScopeType, + ) => ScopeSupport; +} + +export enum ScopeSupport { + supportedAndPresentInEditor, + supportedButNotPresentInEditor, + supportedLegacy, + unsupported, } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts b/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts deleted file mode 100644 index e44382929a..0000000000 --- a/packages/cursorless-engine/src/ScopeVisualizer/EditorScopeVisualizer.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - Disposable, - ScopeRenderer, - IterationScopeRanges, - Range, - TextEditor, -} from "@cursorless/common"; -import { Debouncer } from "../core/Debouncer"; -import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; -import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; -import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; -import { ide } from "../singletons/ide.singleton"; -import { UnsupportedScopeTypeVisualizationError } from "./UnsupportedScopeTypeVisualizationError"; -import { checkNonNull } from "./checkNonNull"; -import { getIterationRange } from "./getIterationRange"; -import { getIterationScopes } from "./getIterationScopes"; -import { getScopes } from "./getScopes"; - -export class EditorScopeVisualizer implements Disposable { - private disposables: Disposable[] = []; - private debouncer = new Debouncer(() => this.highlightScopes()); - - constructor( - private scopeHandlerFactory: ScopeHandlerFactory, - private modifierStageFactory: ModifierStageFactory, - private renderer: ScopeRenderer, - private editor: TextEditor, - ) { - this.disposables.push( - // An event that fires when a text document opens - ide().onDidOpenTextDocument(this.debouncer.run), - // An Event that fires when a text document closes - ide().onDidCloseTextDocument(this.debouncer.run), - // An event that is emitted when a text document is changed. This usually - // happens when the contents changes but also when other things like the - // dirty-state changes. - ide().onDidChangeTextDocument(this.debouncer.run), - ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), - this.debouncer, - ); - - this.debouncer.run(); - } - - async highlightScopes() { - const { - document: { languageId }, - } = this.editor; - - const scopeHandler = checkNonNull( - this.scopeHandlerFactory.create( - this.renderer.visualizerConfig.scopeType, - languageId, - ), - () => new UnsupportedScopeTypeVisualizationError(languageId), - ); - - const iterationRange = getIterationRange(this.editor, scopeHandler); - - this.renderer.setScopes( - this.editor, - this.renderer.visualizerConfig.includeScopes - ? getScopes(this.editor, scopeHandler, iterationRange) - : undefined, - this.renderer.visualizerConfig.includeIterationScopes - ? this.getIterationScopes(scopeHandler, iterationRange) - : undefined, - ); - } - - private getIterationScopes( - scopeHandler: ScopeHandler, - iterationRange: Range, - ): IterationScopeRanges[] | undefined { - const { editor } = this; - const { - document: { languageId }, - } = editor; - const { scopeType, includeIterationNestedTargets } = - this.renderer.visualizerConfig; - - const iterationScopeHandler = checkNonNull( - this.scopeHandlerFactory.create( - scopeHandler.iterationScopeType, - languageId, - ), - () => new UnsupportedScopeTypeVisualizationError(languageId), - ); - - const everyStage = this.modifierStageFactory.create({ - type: "everyScope", - scopeType, - }); - - return getIterationScopes( - editor, - iterationScopeHandler, - everyStage, - iterationRange, - includeIterationNestedTargets, - ); - } - - dispose(): void { - this.disposables.forEach(({ dispose }) => { - try { - dispose(); - } catch (e) { - // do nothing - } - }); - } -} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts new file mode 100644 index 0000000000..389a44432e --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts @@ -0,0 +1,78 @@ +import { + IterationScopeRangeConfig, + IterationScopeRanges, + ScopeRangeConfig, + TextEditor, +} from "@cursorless/common"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; +import { getIterationRange } from "./getIterationRange"; +import { getIterationScopes } from "./getIterationScopes"; +import { getScopes } from "./getScopes"; + +export class ScopeRangeProvider { + constructor( + private scopeHandlerFactory: ScopeHandlerFactory, + private modifierStageFactory: ModifierStageFactory, + ) { + this.provideScopeRanges = this.provideScopeRanges.bind(this); + this.provideIterationScopeRanges = + this.provideIterationScopeRanges.bind(this); + } + + provideScopeRanges( + editor: TextEditor, + { scopeType, visibleOnly }: ScopeRangeConfig, + ) { + const scopeHandler = this.scopeHandlerFactory.create( + scopeType, + editor.document.languageId, + ); + + if (scopeHandler == null) { + return []; + } + + return getScopes( + editor, + scopeHandler, + getIterationRange(editor, scopeHandler, visibleOnly), + ); + } + + provideIterationScopeRanges( + editor: TextEditor, + { + scopeType, + visibleOnly, + includeIterationNestedTargets, + }: IterationScopeRangeConfig, + ): IterationScopeRanges[] { + const { languageId } = editor.document; + const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); + + if (scopeHandler == null) { + return []; + } + + const iterationScopeHandler = this.scopeHandlerFactory.create( + scopeHandler.iterationScopeType, + languageId, + ); + + if (iterationScopeHandler == null) { + return []; + } + + return getIterationScopes( + editor, + iterationScopeHandler, + this.modifierStageFactory.create({ + type: "everyScope", + scopeType, + }), + getIterationRange(editor, scopeHandler, visibleOnly), + includeIterationNestedTargets, + ); + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts new file mode 100644 index 0000000000..d1e2f33a39 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts @@ -0,0 +1,102 @@ +import { + Disposable, + IterationScopeChangeEventCallback, + IterationScopeRangeConfig, + ScopeChangeEventCallback, + ScopeRangeConfig, +} from "@cursorless/common"; +import { pull } from "lodash"; +import { Debouncer } from "../core/Debouncer"; +import { ide } from "../singletons/ide.singleton"; +import { ScopeRangeProvider } from "./ScopeRangeProvider"; + +type Listener = () => void; + +export class ScopeRangeWatcher { + private disposables: Disposable[] = []; + private debouncer = new Debouncer(() => this.onChange()); + private listeners: Listener[] = []; + + constructor(private scopeRangeProvider: ScopeRangeProvider) { + this.disposables.push( + // An Event which fires when the array of visible editors has changed. + ide().onDidChangeVisibleTextEditors(this.debouncer.run), + // An event that fires when a text document opens + ide().onDidOpenTextDocument(this.debouncer.run), + // An Event that fires when a text document closes + ide().onDidCloseTextDocument(this.debouncer.run), + // An event that is emitted when a text document is changed. This usually + // happens when the contents changes but also when other things like the + // dirty-state changes. + ide().onDidChangeTextDocument(this.debouncer.run), + ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), + this.debouncer, + ); + + this.onDidChangeScopeRanges = this.onDidChangeScopeRanges.bind(this); + this.onDidChangeIterationScopeRanges = + this.onDidChangeIterationScopeRanges.bind(this); + } + + onDidChangeScopeRanges( + callback: ScopeChangeEventCallback, + config: ScopeRangeConfig, + ): Disposable { + const fn = () => { + ide().visibleTextEditors.forEach((editor) => { + callback( + editor, + this.scopeRangeProvider.provideScopeRanges(editor, config), + ); + }); + }; + + this.listeners.push(fn); + + fn(); + + return { + dispose: () => { + pull(this.listeners, fn); + }, + }; + } + + onDidChangeIterationScopeRanges( + callback: IterationScopeChangeEventCallback, + config: IterationScopeRangeConfig, + ): Disposable { + const fn = () => { + ide().visibleTextEditors.forEach((editor) => { + callback( + editor, + this.scopeRangeProvider.provideIterationScopeRanges(editor, config), + ); + }); + }; + + this.listeners.push(fn); + + fn(); + + return { + dispose: () => { + pull(this.listeners, fn); + }, + }; + } + + private onChange() { + this.listeners.forEach((listener) => listener()); + } + + dispose(): void { + this.disposables.forEach(({ dispose }) => { + try { + dispose(); + } catch (e) { + // do nothing + } + }); + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts new file mode 100644 index 0000000000..7119a18422 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts @@ -0,0 +1,92 @@ +import { + Position, + ScopeType, + TextEditor, + isEmptyIterable, +} from "@cursorless/common"; +import { LegacyLanguageId } from "../languages/LegacyLanguageId"; +import { languageMatchers } from "../languages/getNodeMatcher"; +import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { ScopeSupport } from "../CursorlessEngineApi"; + +export class ScopeSupportChecker { + constructor(private scopeHandlerFactory: ScopeHandlerFactory) { + this.getScopeSupport = this.getScopeSupport.bind(this); + this.getIterationScopeSupport = this.getIterationScopeSupport.bind(this); + } + + getScopeSupport(editor: TextEditor, scopeType: ScopeType): ScopeSupport { + const { languageId } = editor.document; + const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); + + if (scopeHandler == null) { + return getLegacyScopeSupport(languageId, scopeType); + } + + return editorContainsScope(editor, scopeHandler) + ? ScopeSupport.supportedAndPresentInEditor + : ScopeSupport.supportedButNotPresentInEditor; + } + + getIterationScopeSupport( + editor: TextEditor, + scopeType: ScopeType, + ): ScopeSupport { + const { languageId } = editor.document; + const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); + + if (scopeHandler == null) { + return getLegacyScopeSupport(languageId, scopeType); + } + + const iterationScopeHandler = this.scopeHandlerFactory.create( + scopeHandler.iterationScopeType, + languageId, + ); + + if (iterationScopeHandler == null) { + return ScopeSupport.unsupported; + } + + return editorContainsScope(editor, iterationScopeHandler) + ? ScopeSupport.supportedAndPresentInEditor + : ScopeSupport.supportedButNotPresentInEditor; + } +} + +function editorContainsScope( + editor: TextEditor, + scopeHandler: ScopeHandler, +): boolean { + return !isEmptyIterable( + scopeHandler.generateScopes(editor, new Position(0, 0), "forward"), + ); +} + +function getLegacyScopeSupport( + languageId: string, + scopeType: ScopeType, +): ScopeSupport { + switch (scopeType.type) { + case "nonWhitespaceSequence": + case "boundedNonWhitespaceSequence": + case "url": + case "surroundingPair": + case "customRegex": + return ScopeSupport.supportedLegacy; + case "notebookCell": + case "oneOf": + // FIXME: What to do here + return ScopeSupport.unsupported; + default: + if ( + languageMatchers[languageId as LegacyLanguageId]?.[scopeType.type] != + null + ) { + return ScopeSupport.supportedLegacy; + } + + return ScopeSupport.unsupported; + } +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts deleted file mode 100644 index 51b21f1991..0000000000 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeVisualizerImpl.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ScopeRenderer, showError } from "@cursorless/common"; -import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; -import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; -import { ide } from "../singletons/ide.singleton"; -import { PerEditor } from "../util/PerEditor"; -import { EditorScopeVisualizer } from "./EditorScopeVisualizer"; -import { ScopeVisualizer } from ".."; - -export class ScopeVisualizerImpl implements ScopeVisualizer { - private scopeVisualizers: PerEditor | undefined; - - constructor( - private scopeHandlerFactory: ScopeHandlerFactory, - private modifierStageFactory: ModifierStageFactory, - ) {} - - start(ideScopeVisualizer: ScopeRenderer) { - this.stop(); - this.scopeVisualizers = new PerEditor((editor) => { - const visualizer = new EditorScopeVisualizer( - this.scopeHandlerFactory, - this.modifierStageFactory, - ideScopeVisualizer, - editor, - ); - - visualizer.highlightScopes().catch((err: Error) => { - if (err.name === "UnsupportedScopeTypeVisualizationError") { - if (editor.isActive) { - showError( - ide().messages, - "ScopeVisualizer.scopeTypeNotSupported", - err.message, - ); - } - return; - } - - showError(ide().messages, "ScopeVisualizer.exception", err.message); - }); - - return visualizer; - }); - } - - refresh() { - for (const visualizer of this.scopeVisualizers?.values() || []) { - visualizer.highlightScopes(); - } - } - - stop() { - this.scopeVisualizers?.dispose(); - this.scopeVisualizers = undefined; - } -} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts b/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts deleted file mode 100644 index 5aafe9a879..0000000000 --- a/packages/cursorless-engine/src/ScopeVisualizer/UnsupportedScopeTypeVisualizationError.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class UnsupportedScopeTypeVisualizationError extends Error { - constructor(languageId: string) { - super( - `Scope type not supported for ${languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.`, - ); - this.name = "UnsupportedScopeTypeVisualizationError"; - } -} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts index d6a185884c..e40fe2f6d8 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts @@ -14,7 +14,12 @@ import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHan export function getIterationRange( editor: TextEditor, scopeHandler: ScopeHandler, + visibleOnly: boolean, ): Range { + if (!visibleOnly) { + return editor.document.range; + } + let visibleRange = editor.visibleRanges.reduce((acc, range) => acc.union(range), ); diff --git a/packages/cursorless-engine/src/ScopeVisualizer/index.ts b/packages/cursorless-engine/src/ScopeVisualizer/index.ts index 2189e28dad..012d05f605 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/index.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/index.ts @@ -1 +1 @@ -export * from "./ScopeVisualizerImpl"; +export * from "./ScopeRangeWatcher"; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index fa9608aea2..b973d2dd07 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,18 +1,25 @@ import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; -import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; +import { + ScopeProvider, + StoredTargetMap, + TestCaseRecorder, + TreeSitter, +} from "."; +import { CursorlessEngine } from "./CursorlessEngineApi"; +import { ScopeRangeWatcher } from "./ScopeVisualizer"; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; -import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; -import { ScopeVisualizerImpl } from "./ScopeVisualizer"; -import { CursorlessEngine } from "./CursorlessEngineApi"; +import { ScopeRangeProvider } from "./ScopeVisualizer/ScopeRangeProvider"; +import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; +import { ScopeSupportChecker } from "./ScopeVisualizer/ScopeSupportChecker"; export function createCursorlessEngine( treeSitter: TreeSitter, @@ -44,8 +51,6 @@ export function createCursorlessEngine( const languageDefinitions = new LanguageDefinitions(treeSitter); - const scopeHandlerFactory = new ScopeHandlerFactoryImpl(languageDefinitions); - return { commandApi: { runCommand(command: Command) { @@ -74,14 +79,7 @@ export function createCursorlessEngine( ); }, }, - scopeVisualizer: new ScopeVisualizerImpl( - scopeHandlerFactory, - new ModifierStageFactoryImpl( - languageDefinitions, - storedTargets, - scopeHandlerFactory, - ), - ), + scopeProvider: createScopeProvider(languageDefinitions, storedTargets), testCaseRecorder, storedTargets, hatTokenMap, @@ -91,3 +89,32 @@ export function createCursorlessEngine( runIntegrationTests(treeSitter, languageDefinitions), }; } + +function createScopeProvider( + languageDefinitions: LanguageDefinitions, + storedTargets: StoredTargetMap, +): ScopeProvider { + const scopeHandlerFactory = new ScopeHandlerFactoryImpl(languageDefinitions); + + const rangeProvider = new ScopeRangeProvider( + scopeHandlerFactory, + new ModifierStageFactoryImpl( + languageDefinitions, + storedTargets, + scopeHandlerFactory, + ), + ); + + const rangeWatcher = new ScopeRangeWatcher(rangeProvider); + const supportChecker = new ScopeSupportChecker(scopeHandlerFactory); + + return { + provideScopeRanges: rangeProvider.provideScopeRanges, + provideIterationScopeRanges: rangeProvider.provideIterationScopeRanges, + onDidChangeScopeRanges: rangeWatcher.onDidChangeScopeRanges, + onDidChangeIterationScopeRanges: + rangeWatcher.onDidChangeIterationScopeRanges, + getScopeSupport: supportChecker.getScopeSupport, + getIterationScopeSupport: supportChecker.getIterationScopeSupport, + }; +} diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index 79d6b2c0f0..5679bdeb17 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -1064,7 +1064,7 @@ "@cursorless/cursorless-engine": "workspace:*", "@cursorless/vscode-common": "workspace:*", "@types/tinycolor2": "1.4.3", - "itertools": "^1.7.1", + "itertools": "^2.1.1", "lodash": "^4.17.21", "semver": "^7.3.9", "tinycolor2": "1.6.0", diff --git a/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts b/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts index cb6681b1f2..77dcc80c6a 100644 --- a/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts +++ b/packages/cursorless-vscode/src/ScopeVisualizerCommandApi.ts @@ -5,4 +5,4 @@ export interface ScopeVisualizerCommandApi { stop(): void; } -export type VisualizationType = "content" | "removal" | "iteration" | "every"; +export type VisualizationType = "content" | "removal" | "iteration"; diff --git a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts index de1aa38543..a4b2c5eb59 100644 --- a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts +++ b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts @@ -1,5 +1,4 @@ -import { ScopeType } from "@cursorless/common"; -import { ScopeVisualizer } from "@cursorless/cursorless-engine"; +import { IDE, ScopeType } from "@cursorless/common"; import { VscodeScopeVisualizer, createVscodeScopeVisualizer, @@ -8,11 +7,12 @@ import { ScopeVisualizerCommandApi, VisualizationType, } from "./ScopeVisualizerCommandApi"; +import { ScopeProvider } from "@cursorless/cursorless-engine"; export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { private scopeVisualizer: VscodeScopeVisualizer | undefined; - constructor(private engineScopeVisualizer: ScopeVisualizer) { + constructor(private ide: IDE, private scopeProvider: ScopeProvider) { this.start = this.start.bind(this); this.stop = this.stop.bind(this); } @@ -20,19 +20,15 @@ export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { start(scopeType: ScopeType, visualizationType: VisualizationType) { this.stop(); this.scopeVisualizer = createVscodeScopeVisualizer( + this.ide, + this.scopeProvider, scopeType, visualizationType, ); - - this.engineScopeVisualizer.start(this.scopeVisualizer); - - this.scopeVisualizer.onColorConfigChange(() => - this.engineScopeVisualizer.refresh(), - ); + this.scopeVisualizer.start(); } stop() { - this.engineScopeVisualizer.stop(); this.scopeVisualizer?.dispose(); this.scopeVisualizer = undefined; } diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 023f5f10a5..2a52bdb709 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -64,7 +64,7 @@ export async function activate( testCaseRecorder, storedTargets, hatTokenMap, - scopeVisualizer: engineScopeVisualizer, + scopeProvider, snippets, injectIde, runIntegrationTests, @@ -75,7 +75,10 @@ export async function activate( commandServerApi, ); - const scopeVisualizer = new ScopeVisualizerImpl(engineScopeVisualizer); + const scopeVisualizer = new ScopeVisualizerImpl( + normalizedIde ?? vscodeIDE, + scopeProvider, + ); const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); const keyboardCommands = KeyboardCommands.create(context, statusBarItem); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts deleted file mode 100644 index 3e804f11e1..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeContentVisualizer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; -import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; -import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; -import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; - -export class VscodeScopeContentVisualizer extends VscodeScopeVisualizer { - readonly visualizerConfig = { - scopeType: this.scopeType, - includeScopes: true, - includeIterationScopes: false, - includeIterationNestedTargets: false, - }; - - protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { - return getColorsFromConfig(colorConfig, "content"); - } - - protected getRendererScopes( - scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return scopeRanges!.map(({ domain, targets }) => ({ - domain, - nestedRanges: targets.map(({ contentRange }) => contentRange), - })); - } -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts deleted file mode 100644 index 187fa280a4..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeEveryVisualizer.ts +++ /dev/null @@ -1,59 +0,0 @@ -import tinycolor = require("tinycolor2"); -import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; -import { RangeTypeColors } from "./RangeTypeColors"; -import { - ScopeVisualizerColorConfig, - getColorsFromConfig, -} from "./ScopeVisualizerColorConfig"; -import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; - -export class VscodeScopeEveryVisualizer extends VscodeScopeVisualizer { - readonly visualizerConfig = { - scopeType: this.scopeType, - includeScopes: false, - includeIterationScopes: true, - includeIterationNestedTargets: true, - }; - - protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { - return weakenRangeTypeColors(getColorsFromConfig(colorConfig, "content")); - } - - protected getRendererScopes( - _scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return iterationScopeRanges!.map(({ domain, ranges }) => ({ - domain, - nestedRanges: ranges.flatMap(({ targets }) => - targets!.map(({ contentRange }) => contentRange), - ), - })); - } -} - -const BORDER_WEAKENING = 0.8; -const BACKGROUND_WEAKENING = 0.2; - -function weakenRangeTypeColors(colors: RangeTypeColors): RangeTypeColors { - return { - background: { - light: weakenColor(colors.background.light, BACKGROUND_WEAKENING), - dark: weakenColor(colors.background.dark, BACKGROUND_WEAKENING), - }, - borderSolid: { - light: weakenColor(colors.borderSolid.light, BORDER_WEAKENING), - dark: weakenColor(colors.borderSolid.dark, BORDER_WEAKENING), - }, - borderPorous: { - light: weakenColor(colors.borderPorous.light, BORDER_WEAKENING), - dark: weakenColor(colors.borderPorous.dark, BORDER_WEAKENING), - }, - }; -} - -function weakenColor(color: string, amount: number): string { - const parsed = tinycolor(color); - parsed.setAlpha(parsed.getAlpha() * amount); - return parsed.toHex8String(); -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts index d8a214c687..46da5563d7 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -1,27 +1,33 @@ -import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; -import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; -import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; +import { Disposable, TextEditor } from "@cursorless/common"; +import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; +import { ScopeSupport } from "@cursorless/cursorless-engine"; export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { - readonly visualizerConfig = { - scopeType: this.scopeType, - includeScopes: false, - includeIterationScopes: true, - includeIterationNestedTargets: false, - }; + protected getScopeSupport(editor: TextEditor): ScopeSupport { + return this.scopeProvider.getIterationScopeSupport(editor, this.scopeType); + } - protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { - return getColorsFromConfig(colorConfig, "iteration"); + protected registerListener(): Disposable { + return this.scopeProvider.onDidChangeIterationScopeRanges( + (editor, iterationScopeRanges) => { + this.renderer.setScopes( + editor as VscodeTextEditorImpl, + iterationScopeRanges!.map(({ domain, ranges }) => ({ + domain, + nestedRanges: ranges.map(({ range }) => range), + })), + ); + }, + { + scopeType: this.scopeType, + visibleOnly: true, + includeIterationNestedTargets: false, + }, + ); } - protected getRendererScopes( - _scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return iterationScopeRanges!.map(({ domain, ranges }) => ({ - domain, - nestedRanges: ranges.map(({ range }) => range), - })); + protected getNestedColorConfigKey() { + return "iteration" as const; } } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts deleted file mode 100644 index 7ae07339a9..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRemovalVisualizer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IterationScopeRanges, ScopeRanges } from "@cursorless/common"; -import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; -import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; -import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; - -export class VscodeScopeRemovalVisualizer extends VscodeScopeVisualizer { - readonly visualizerConfig = { - scopeType: this.scopeType, - includeScopes: true, - includeIterationScopes: false, - includeIterationNestedTargets: false, - }; - - protected getNestedScopeColorConfig(colorConfig: ScopeVisualizerColorConfig) { - return getColorsFromConfig(colorConfig, "removal"); - } - - protected getRendererScopes( - scopeRanges: ScopeRanges[] | undefined, - _iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - return scopeRanges!.map(({ domain, targets }) => ({ - domain, - nestedRanges: targets.map(({ removalRange }) => removalRange), - })); - } -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts new file mode 100644 index 0000000000..2c6deff185 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts @@ -0,0 +1,54 @@ +import { + Disposable, + GeneralizedRange, + TargetRanges, + TextEditor, +} from "@cursorless/common"; +import { ScopeSupport } from "@cursorless/cursorless-engine"; +import { VscodeScopeVisualizer } from "."; +import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; + +abstract class VscodeScopeTargetVisualizer extends VscodeScopeVisualizer { + protected abstract getTargetRange( + targetRanges: TargetRanges, + ): GeneralizedRange; + + protected getScopeSupport(editor: TextEditor): ScopeSupport { + return this.scopeProvider.getScopeSupport(editor, this.scopeType); + } + + protected registerListener(): Disposable { + return this.scopeProvider.onDidChangeScopeRanges( + (editor, scopeRanges) => { + this.renderer.setScopes( + editor as VscodeTextEditorImpl, + scopeRanges!.map(({ domain, targets }) => ({ + domain, + nestedRanges: targets.map((target) => this.getTargetRange(target)), + })), + ); + }, + { scopeType: this.scopeType, visibleOnly: true }, + ); + } +} + +export class VscodeScopeContentVisualizer extends VscodeScopeTargetVisualizer { + protected getTargetRange({ contentRange }: TargetRanges): GeneralizedRange { + return contentRange; + } + + protected getNestedColorConfigKey() { + return "content" as const; + } +} + +export class VscodeScopeRemovalVisualizer extends VscodeScopeTargetVisualizer { + protected getTargetRange({ removalRange }: TargetRanges): GeneralizedRange { + return removalRange; + } + + protected getNestedColorConfigKey() { + return "removal" as const; + } +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index 636e9576f6..ff706c5912 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -1,49 +1,69 @@ import { Disposable, - ScopeRenderer, - IterationScopeRanges, - ScopeRanges, + IDE, ScopeType, - ScopeVisualizerConfig, - Notifier, - Listener, + TextEditor, + showError, } from "@cursorless/common"; +import { ScopeProvider, ScopeSupport } from "@cursorless/cursorless-engine"; import * as vscode from "vscode"; -import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; -import { getColorsFromConfig } from "./ScopeVisualizerColorConfig"; -import { ScopeVisualizerColorConfig } from "./ScopeVisualizerColorConfig"; -import { RendererScope, VscodeScopeRenderer } from "./VscodeScopeRenderer"; -import { RangeTypeColors } from "./RangeTypeColors"; +import { + ColorConfigKey, + ScopeVisualizerColorConfig, + getColorsFromConfig, +} from "./ScopeVisualizerColorConfig"; +import { VscodeScopeRenderer } from "./VscodeScopeRenderer"; -export abstract class VscodeScopeVisualizer implements ScopeRenderer { - private renderer!: VscodeScopeRenderer; +export abstract class VscodeScopeVisualizer { + protected renderer!: VscodeScopeRenderer; + private scopeListenerDisposable!: Disposable; private disposables: Disposable[] = []; - abstract readonly visualizerConfig: ScopeVisualizerConfig; - private notifier: Notifier = new Notifier(); - - protected abstract getRendererScopes( - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): RendererScope[]; - protected abstract getNestedScopeColorConfig( - colorConfig: ScopeVisualizerColorConfig, - ): RangeTypeColors; + protected abstract registerListener(): Disposable; + protected abstract getNestedColorConfigKey(): ColorConfigKey; + protected abstract getScopeSupport(editor: TextEditor): ScopeSupport; - constructor(protected scopeType: ScopeType) { + constructor( + private ide: IDE, + protected scopeProvider: ScopeProvider, + protected scopeType: ScopeType, + ) { this.disposables.push( vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { - this.computeColors(); - this.notifier.notifyListeners(); + this.initialize(); } }), ); + } + + start() { + this.initialize(); + this.checkScopeSupport(); + } + + private checkScopeSupport() { + const editor = this.ide.activeTextEditor; - this.computeColors(); + if (editor == null) { + return; + } + + switch (this.getScopeSupport(editor)) { + case ScopeSupport.supportedAndPresentInEditor: + case ScopeSupport.supportedButNotPresentInEditor: + return; + case ScopeSupport.supportedLegacy: + case ScopeSupport.unsupported: + showError( + this.ide.messages, + "ScopeVisualizer.scopeTypeNotSupported", + `Scope type not supported for ${editor.document.languageId}, or only defined using legacy API which doesn't support visualization. See https://www.cursorless.org/docs/contributing/adding-a-new-language/ for more about how to upgrade your language.`, + ); + } } - private computeColors() { + private initialize() { const colorConfig = vscode.workspace .getConfiguration("cursorless.scopeVisualizer") .get("colors")!; @@ -51,27 +71,17 @@ export abstract class VscodeScopeVisualizer implements ScopeRenderer { this.renderer?.dispose(); this.renderer = new VscodeScopeRenderer( getColorsFromConfig(colorConfig, "domain"), - this.getNestedScopeColorConfig(colorConfig), + getColorsFromConfig(colorConfig, this.getNestedColorConfigKey()), ); - } - onColorConfigChange(listener: Listener) { - return this.notifier.registerListener(listener); - } - - async setScopes( - editor: VscodeTextEditorImpl, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ) { - this.renderer.setScopes( - editor, - this.getRendererScopes(scopeRanges, iterationScopeRanges), - ); + // Reregister to cause the renderer to be updated with the new colors + this.scopeListenerDisposable?.dispose(); + this.scopeListenerDisposable = this.registerListener(); } dispose() { this.disposables.forEach((disposable) => disposable.dispose()); - this.renderer.dispose(); + this.renderer?.dispose(); + this.scopeListenerDisposable?.dispose(); } } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts index 371618d6cd..6ec597e7cd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts @@ -1,22 +1,24 @@ -import { ScopeType } from "@cursorless/common"; +import { IDE, ScopeType } from "@cursorless/common"; +import { ScopeProvider } from "@cursorless/cursorless-engine"; import { VisualizationType } from "../../../ScopeVisualizerCommandApi"; -import { VscodeScopeContentVisualizer } from "./VscodeScopeContentVisualizer"; -import { VscodeScopeRemovalVisualizer } from "./VscodeScopeRemovalVisualizer"; import { VscodeScopeIterationVisualizer } from "./VscodeScopeIterationVisualizer"; -import { VscodeScopeEveryVisualizer } from "./VscodeScopeEveryVisualizer"; +import { + VscodeScopeContentVisualizer, + VscodeScopeRemovalVisualizer, +} from "./VscodeScopeTargetVisualizer"; export function createVscodeScopeVisualizer( + ide: IDE, + scopeProvider: ScopeProvider, scopeType: ScopeType, visualizationType: VisualizationType, ) { switch (visualizationType) { case "content": - return new VscodeScopeContentVisualizer(scopeType); + return new VscodeScopeContentVisualizer(ide, scopeProvider, scopeType); case "removal": - return new VscodeScopeRemovalVisualizer(scopeType); + return new VscodeScopeRemovalVisualizer(ide, scopeProvider, scopeType); case "iteration": - return new VscodeScopeIterationVisualizer(scopeType); - case "every": - return new VscodeScopeEveryVisualizer(scopeType); + return new VscodeScopeIterationVisualizer(ide, scopeProvider, scopeType); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4b8944f99..27352afd79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,8 +217,8 @@ importers: specifier: ^3.1.1 version: 3.1.1 itertools: - specifier: ^1.7.1 - version: 1.7.1 + specifier: ^2.1.1 + version: 2.1.1 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -385,8 +385,8 @@ importers: specifier: 1.4.3 version: 1.4.3 itertools: - specifier: ^1.7.1 - version: 1.7.1 + specifier: ^2.1.1 + version: 2.1.1 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -10828,10 +10828,8 @@ packages: html-escaper: 2.0.2 istanbul-lib-report: 3.0.0 - /itertools@1.7.1: - resolution: {integrity: sha512-0sC8t0HYOH0wb/mU5eLmp2g19yfhqho12Q6kCX6MGkNEEJQz97LIXzZ2bbIDyzBnQGcMixmcAtByzKjiaFkw8Q==} - dependencies: - '@babel/runtime': 7.21.0 + /itertools@2.1.1: + resolution: {integrity: sha512-T0icRZBQfWSwhdeBvJT3Sg1m3lBOv1RCD2m+vnY7F12sIInidVDLIn5Fbu1/1gAMN8XIjzkDP48ukF7mTRn/fw==} dev: false /jake@10.8.5: From 7b6f38f29d02bc15d4dda11ae9869cf95f528393 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 28 Jun 2023 12:03:16 +0100 Subject: [PATCH 25/61] cleanup --- .../common/src/ide/types/IdeScopeVisualizer.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/common/src/ide/types/IdeScopeVisualizer.ts b/packages/common/src/ide/types/IdeScopeVisualizer.ts index 18323fd6dd..8ba26f8415 100644 --- a/packages/common/src/ide/types/IdeScopeVisualizer.ts +++ b/packages/common/src/ide/types/IdeScopeVisualizer.ts @@ -1,16 +1,6 @@ import type { ScopeType, TextEditor } from "../.."; import { GeneralizedRange } from "../../types/GeneralizedRange"; -export interface ScopeRenderer { - setScopes( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise; - - visualizerConfig: ScopeVisualizerConfig; -} - interface ScopeRangeConfigBase { visibleOnly: boolean; scopeType: ScopeType; @@ -49,10 +39,3 @@ export interface IterationScopeRanges { targets?: TargetRanges[]; }[]; } - -export interface ScopeVisualizerConfig { - scopeType: ScopeType; - includeScopes: boolean; - includeIterationScopes: boolean; - includeIterationNestedTargets: boolean; -} From 32d819aaeb14a1bfbfb52d78d0e8144415757ef4 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 28 Jun 2023 12:07:55 +0100 Subject: [PATCH 26/61] more cleanup --- .../src/ScopeVisualizer/checkNonNull.ts | 10 ---- .../modifiers/EveryScopeStage.ts | 2 +- .../cursorless-engine/src/util/PerEditor.ts | 49 ------------------- 3 files changed, 1 insertion(+), 60 deletions(-) delete mode 100644 packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts delete mode 100644 packages/cursorless-engine/src/util/PerEditor.ts diff --git a/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts b/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts deleted file mode 100644 index c5b4fb2efc..0000000000 --- a/packages/cursorless-engine/src/ScopeVisualizer/checkNonNull.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function checkNonNull( - value: T | null | undefined, - errorMessage: () => Error, -): T { - if (value == null) { - throw errorMessage(); - } - - return value; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index 2d39de2cb0..33424fa031 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -121,7 +121,7 @@ export class EveryScopeStage implements ModifierStage { /** * Returns a list of all scopes that have nonempty overlap with {@link range}. */ -export function getScopesOverlappingRange( +function getScopesOverlappingRange( scopeHandler: ScopeHandler, editor: TextEditor, { start, end }: Range, diff --git a/packages/cursorless-engine/src/util/PerEditor.ts b/packages/cursorless-engine/src/util/PerEditor.ts deleted file mode 100644 index 9b33e26414..0000000000 --- a/packages/cursorless-engine/src/util/PerEditor.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Disposable, TextEditor } from "@cursorless/common"; -import { ide } from "../singletons/ide.singleton"; - -export class PerEditor { - private disposables: Disposable[] = []; - private editorHandlers: Map = new Map(); - - constructor(private makeEditorHandler: (editor: TextEditor) => T) { - this.handleChange = this.handleChange.bind(this); - - this.disposables.push( - // An Event which fires when the array of visible editors has changed. - ide().onDidChangeVisibleTextEditors(this.handleChange), - ); - - this.handleChange(); - } - - private handleChange() { - const editors = ide().visibleTextEditors; - const editorIds = new Set(editors.map((editor) => editor.id)); - - for (const [editorId, editorHandler] of this.editorHandlers) { - if (!editorIds.has(editorId)) { - editorHandler.dispose(); - this.editorHandlers.delete(editorId); - } - } - - for (const editor of editors) { - if (!this.editorHandlers.has(editor.id)) { - this.editorHandlers.set(editor.id, this.makeEditorHandler(editor)); - } - } - } - - values() { - return this.editorHandlers.values(); - } - - dispose() { - for (const disposable of this.disposables) { - disposable.dispose(); - } - for (const editorHandler of this.editorHandlers.values()) { - editorHandler.dispose(); - } - } -} From 51b6ba6c410a956ef61e5c193885e66a44a9e0b5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:37:27 +0100 Subject: [PATCH 27/61] Cleanup --- .../src/ide/types/IdeScopeVisualizer.ts | 2 +- .../src/CursorlessEngineApi.ts | 2 +- .../src/ScopeVisualizer/ScopeRangeProvider.ts | 2 +- .../src/ScopeVisualizerImpl.ts | 35 ------------------- packages/cursorless-vscode/src/extension.ts | 6 ++-- .../src/getScopeVisualizerCommandApi.ts | 35 +++++++++++++++++++ .../VscodeScopeIterationVisualizer.ts | 2 +- 7 files changed, 42 insertions(+), 42 deletions(-) delete mode 100644 packages/cursorless-vscode/src/ScopeVisualizerImpl.ts create mode 100644 packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts diff --git a/packages/common/src/ide/types/IdeScopeVisualizer.ts b/packages/common/src/ide/types/IdeScopeVisualizer.ts index 8ba26f8415..4f98915c2b 100644 --- a/packages/common/src/ide/types/IdeScopeVisualizer.ts +++ b/packages/common/src/ide/types/IdeScopeVisualizer.ts @@ -9,7 +9,7 @@ interface ScopeRangeConfigBase { export type ScopeRangeConfig = ScopeRangeConfigBase; export interface IterationScopeRangeConfig extends ScopeRangeConfigBase { - includeIterationNestedTargets: boolean; + includeNestedTargets: boolean; } export type ScopeChangeEventCallback = ( diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts index c181974f23..2fc22ea621 100644 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/CursorlessEngineApi.ts @@ -52,7 +52,7 @@ export interface ScopeProvider { { scopeType, visibleOnly, - includeIterationNestedTargets, + includeNestedTargets: includeIterationNestedTargets, }: IterationScopeRangeConfig, ) => IterationScopeRanges[]; diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts index 389a44432e..59843bf8a3 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts @@ -45,7 +45,7 @@ export class ScopeRangeProvider { { scopeType, visibleOnly, - includeIterationNestedTargets, + includeNestedTargets: includeIterationNestedTargets, }: IterationScopeRangeConfig, ): IterationScopeRanges[] { const { languageId } = editor.document; diff --git a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts b/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts deleted file mode 100644 index a4b2c5eb59..0000000000 --- a/packages/cursorless-vscode/src/ScopeVisualizerImpl.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IDE, ScopeType } from "@cursorless/common"; -import { - VscodeScopeVisualizer, - createVscodeScopeVisualizer, -} from "./ide/vscode/VSCodeScopeVisualizer"; -import { - ScopeVisualizerCommandApi, - VisualizationType, -} from "./ScopeVisualizerCommandApi"; -import { ScopeProvider } from "@cursorless/cursorless-engine"; - -export class ScopeVisualizerImpl implements ScopeVisualizerCommandApi { - private scopeVisualizer: VscodeScopeVisualizer | undefined; - - constructor(private ide: IDE, private scopeProvider: ScopeProvider) { - this.start = this.start.bind(this); - this.stop = this.stop.bind(this); - } - - start(scopeType: ScopeType, visualizationType: VisualizationType) { - this.stop(); - this.scopeVisualizer = createVscodeScopeVisualizer( - this.ide, - this.scopeProvider, - scopeType, - visualizationType, - ); - this.scopeVisualizer.start(); - } - - stop() { - this.scopeVisualizer?.dispose(); - this.scopeVisualizer = undefined; - } -} diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 2a52bdb709..f6fae5636a 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -26,7 +26,7 @@ import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; import { registerCommands } from "./registerCommands"; import { StatusBarItem } from "./StatusBarItem"; -import { ScopeVisualizerImpl } from "./ScopeVisualizerImpl"; +import { getScopeVisualizerCommandApi } from "./getScopeVisualizerCommandApi"; /** * Extension entrypoint called by VSCode on Cursorless startup. @@ -75,7 +75,7 @@ export async function activate( commandServerApi, ); - const scopeVisualizer = new ScopeVisualizerImpl( + const scopeVisualizerCommandApi = getScopeVisualizerCommandApi( normalizedIde ?? vscodeIDE, scopeProvider, ); @@ -88,7 +88,7 @@ export async function activate( vscodeIDE, commandApi, testCaseRecorder, - scopeVisualizer, + scopeVisualizerCommandApi, keyboardCommands, hats, ); diff --git a/packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts b/packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts new file mode 100644 index 0000000000..276e03a06a --- /dev/null +++ b/packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts @@ -0,0 +1,35 @@ +import { IDE, ScopeType } from "@cursorless/common"; +import { + VscodeScopeVisualizer, + createVscodeScopeVisualizer, +} from "./ide/vscode/VSCodeScopeVisualizer"; +import { + ScopeVisualizerCommandApi, + VisualizationType, +} from "./ScopeVisualizerCommandApi"; +import { ScopeProvider } from "@cursorless/cursorless-engine"; + +export function getScopeVisualizerCommandApi( + ide: IDE, + scopeProvider: ScopeProvider, +): ScopeVisualizerCommandApi { + let scopeVisualizer: VscodeScopeVisualizer | undefined; + + return { + start(scopeType: ScopeType, visualizationType: VisualizationType) { + scopeVisualizer?.dispose(); + scopeVisualizer = createVscodeScopeVisualizer( + ide, + scopeProvider, + scopeType, + visualizationType, + ); + scopeVisualizer.start(); + }, + + stop() { + scopeVisualizer?.dispose(); + scopeVisualizer = undefined; + }, + }; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts index 46da5563d7..f0f8efe3b0 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -22,7 +22,7 @@ export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { { scopeType: this.scopeType, visibleOnly: true, - includeIterationNestedTargets: false, + includeNestedTargets: false, }, ); } From b3698013433ced3fb53bcd75d2b4109649c83d7b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:40:15 +0100 Subject: [PATCH 28/61] more cleanup --- packages/cursorless-vscode/src/extension.ts | 34 ++++++++++++++---- .../src/getScopeVisualizerCommandApi.ts | 35 ------------------- 2 files changed, 27 insertions(+), 42 deletions(-) delete mode 100644 packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index f6fae5636a..5ee7362530 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -26,7 +26,7 @@ import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; import { registerCommands } from "./registerCommands"; import { StatusBarItem } from "./StatusBarItem"; -import { getScopeVisualizerCommandApi } from "./getScopeVisualizerCommandApi"; +import { createVscodeScopeVisualizer } from "./ide/vscode/VSCodeScopeVisualizer"; /** * Extension entrypoint called by VSCode on Cursorless startup. @@ -75,11 +75,6 @@ export async function activate( commandServerApi, ); - const scopeVisualizerCommandApi = getScopeVisualizerCommandApi( - normalizedIde ?? vscodeIDE, - scopeProvider, - ); - const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); const keyboardCommands = KeyboardCommands.create(context, statusBarItem); @@ -88,7 +83,7 @@ export async function activate( vscodeIDE, commandApi, testCaseRecorder, - scopeVisualizerCommandApi, + createScopeVisualizerCommandApi(normalizedIde ?? vscodeIDE, scopeProvider), keyboardCommands, hats, ); @@ -144,6 +139,31 @@ function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter { }; } +function createScopeVisualizerCommandApi( + ide: IDE, + scopeProvider: ScopeProvider, +): ScopeVisualizerCommandApi { + let scopeVisualizer: VscodeScopeVisualizer | undefined; + + return { + start(scopeType: ScopeType, visualizationType: VisualizationType) { + scopeVisualizer?.dispose(); + scopeVisualizer = createVscodeScopeVisualizer( + ide, + scopeProvider, + scopeType, + visualizationType, + ); + scopeVisualizer.start(); + }, + + stop() { + scopeVisualizer?.dispose(); + scopeVisualizer = undefined; + }, + }; +} + // this method is called when your extension is deactivated export function deactivate() { // do nothing diff --git a/packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts b/packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts deleted file mode 100644 index 276e03a06a..0000000000 --- a/packages/cursorless-vscode/src/getScopeVisualizerCommandApi.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IDE, ScopeType } from "@cursorless/common"; -import { - VscodeScopeVisualizer, - createVscodeScopeVisualizer, -} from "./ide/vscode/VSCodeScopeVisualizer"; -import { - ScopeVisualizerCommandApi, - VisualizationType, -} from "./ScopeVisualizerCommandApi"; -import { ScopeProvider } from "@cursorless/cursorless-engine"; - -export function getScopeVisualizerCommandApi( - ide: IDE, - scopeProvider: ScopeProvider, -): ScopeVisualizerCommandApi { - let scopeVisualizer: VscodeScopeVisualizer | undefined; - - return { - start(scopeType: ScopeType, visualizationType: VisualizationType) { - scopeVisualizer?.dispose(); - scopeVisualizer = createVscodeScopeVisualizer( - ide, - scopeProvider, - scopeType, - visualizationType, - ); - scopeVisualizer.start(); - }, - - stop() { - scopeVisualizer?.dispose(); - scopeVisualizer = undefined; - }, - }; -} From dcee4d0d676e91139dc6815c4097fff99cea6bd2 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:07:08 +0100 Subject: [PATCH 29/61] more cleanup --- .../src/CursorlessEngineApi.ts | 8 ++--- packages/cursorless-vscode/src/extension.ts | 12 ++++++- .../ScopeVisualizerColorConfig.ts | 34 ++++++------------- .../VscodeFancyRangeHighlighterRenderer.ts | 26 ++++++++------ .../VscodeScopeIterationVisualizer.ts | 2 +- .../VscodeScopeTargetVisualizer.ts | 4 +-- .../VscodeScopeVisualizer.ts | 6 ++-- .../blendRangeTypeColors.ts | 1 + 8 files changed, 46 insertions(+), 47 deletions(-) diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts index 2fc22ea621..0c70e1abcb 100644 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/CursorlessEngineApi.ts @@ -44,16 +44,12 @@ export interface CommandApi { export interface ScopeProvider { provideScopeRanges: ( editor: TextEditor, - { scopeType, visibleOnly }: ScopeRangeConfig, + config: ScopeRangeConfig, ) => ScopeRanges[]; provideIterationScopeRanges: ( editor: TextEditor, - { - scopeType, - visibleOnly, - includeNestedTargets: includeIterationNestedTargets, - }: IterationScopeRangeConfig, + config: IterationScopeRangeConfig, ) => IterationScopeRanges[]; onDidChangeScopeRanges: ( diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 5ee7362530..625bdcf4b0 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -1,13 +1,16 @@ import { FakeIDE, getFakeCommandServerApi, + IDE, isTesting, NormalizedIDE, Range, + ScopeType, TextDocument, } from "@cursorless/common"; import { createCursorlessEngine, + ScopeProvider, TreeSitter, } from "@cursorless/cursorless-engine"; import { @@ -26,7 +29,14 @@ import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { KeyboardCommands } from "./keyboard/KeyboardCommands"; import { registerCommands } from "./registerCommands"; import { StatusBarItem } from "./StatusBarItem"; -import { createVscodeScopeVisualizer } from "./ide/vscode/VSCodeScopeVisualizer"; +import { + createVscodeScopeVisualizer, + VscodeScopeVisualizer, +} from "./ide/vscode/VSCodeScopeVisualizer"; +import { + ScopeVisualizerCommandApi, + VisualizationType, +} from "./ScopeVisualizerCommandApi"; /** * Extension entrypoint called by VSCode on Cursorless startup. diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts index f4c01fc418..49be81da68 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts @@ -2,7 +2,7 @@ import { RangeTypeColors } from "./RangeTypeColors"; export function getColorsFromConfig( config: ScopeVisualizerColorConfig, - rangeType: ColorConfigKey, + rangeType: ScopeRangeType, ): RangeTypeColors { return { background: { @@ -20,32 +20,20 @@ export function getColorsFromConfig( }; } -export type ColorConfigKey = keyof ScopeVisualizerThemeColorConfig; +export type ScopeRangeType = "domain" | "content" | "removal" | "iteration"; export interface ScopeVisualizerColorConfig { light: ScopeVisualizerThemeColorConfig; dark: ScopeVisualizerThemeColorConfig; } -interface ScopeVisualizerThemeColorConfig { - domain: { - background: string; - borderSolid: string; - borderPorous: string; - }; - content: { - background: string; - borderSolid: string; - borderPorous: string; - }; - removal: { - background: string; - borderSolid: string; - borderPorous: string; - }; - iteration: { - background: string; - borderSolid: string; - borderPorous: string; - }; +type ScopeVisualizerThemeColorConfig = Record< + ScopeRangeType, + RangeTypeColorConfig +>; + +interface RangeTypeColorConfig { + background: string; + borderSolid: string; + borderPorous: string; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index 90ab0c0e29..d1ff750cba 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -15,6 +15,9 @@ import { DifferentiatedStyledRangeList, } from "./getDecorationRanges.types"; +const BORDER_WIDTH = "1px"; +const BORDER_RADIUS = "2px"; + export class VscodeFancyRangeHighlighterRenderer { private decorationTypes: CompositeKeyDefaultMap< DifferentiatedStyle, @@ -97,7 +100,7 @@ function getDecorationStyle( ), }, borderStyle: getBorderStyle(borders), - borderWidth: "1px", + borderWidth: BORDER_WIDTH, borderRadius: getBorderRadius(borders), rangeBehavior: DecorationRangeBehavior.ClosedClosed, isWholeLine: borders.isWholeLine, @@ -123,16 +126,17 @@ function getBorderColor( ].join(" "); } -function getBorderRadius({ - top, - right, - bottom, - left, -}: DecorationStyle): string { +function getBorderRadius(borders: DecorationStyle): string { return [ - top === BorderStyle.solid && left === BorderStyle.solid ? "2px" : "0px", - top === BorderStyle.solid && right === BorderStyle.solid ? "2px" : "0px", - bottom === BorderStyle.solid && right === BorderStyle.solid ? "2px" : "0px", - bottom === BorderStyle.solid && left === BorderStyle.solid ? "2px" : "0px", + getSingleCornerBorderRadius(borders.top, borders.left), + getSingleCornerBorderRadius(borders.top, borders.right), + getSingleCornerBorderRadius(borders.bottom, borders.right), + getSingleCornerBorderRadius(borders.bottom, borders.left), ].join(" "); } + +function getSingleCornerBorderRadius(side1: BorderStyle, side2: BorderStyle) { + return side1 === BorderStyle.solid && side2 === BorderStyle.solid + ? BORDER_RADIUS + : "0px"; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts index f0f8efe3b0..caf8a209dd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -27,7 +27,7 @@ export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { ); } - protected getNestedColorConfigKey() { + protected getNestedScopeRangeType() { return "iteration" as const; } } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts index 2c6deff185..75d73b837a 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts @@ -38,7 +38,7 @@ export class VscodeScopeContentVisualizer extends VscodeScopeTargetVisualizer { return contentRange; } - protected getNestedColorConfigKey() { + protected getNestedScopeRangeType() { return "content" as const; } } @@ -48,7 +48,7 @@ export class VscodeScopeRemovalVisualizer extends VscodeScopeTargetVisualizer { return removalRange; } - protected getNestedColorConfigKey() { + protected getNestedScopeRangeType() { return "removal" as const; } } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index ff706c5912..90fbb294f0 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -8,7 +8,7 @@ import { import { ScopeProvider, ScopeSupport } from "@cursorless/cursorless-engine"; import * as vscode from "vscode"; import { - ColorConfigKey, + ScopeRangeType, ScopeVisualizerColorConfig, getColorsFromConfig, } from "./ScopeVisualizerColorConfig"; @@ -20,7 +20,7 @@ export abstract class VscodeScopeVisualizer { private disposables: Disposable[] = []; protected abstract registerListener(): Disposable; - protected abstract getNestedColorConfigKey(): ColorConfigKey; + protected abstract getNestedScopeRangeType(): ScopeRangeType; protected abstract getScopeSupport(editor: TextEditor): ScopeSupport; constructor( @@ -71,7 +71,7 @@ export abstract class VscodeScopeVisualizer { this.renderer?.dispose(); this.renderer = new VscodeScopeRenderer( getColorsFromConfig(colorConfig, "domain"), - getColorsFromConfig(colorConfig, this.getNestedColorConfigKey()), + getColorsFromConfig(colorConfig, this.getNestedScopeRangeType()), ); // Reregister to cause the renderer to be updated with the new colors diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts index 5c4047b799..40dfa01a55 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts @@ -35,6 +35,7 @@ export function blendRangeTypeColors( }, }; } + function blendColors(base: string, top: string): string { const baseRgba = tinycolor(base).toRgb(); const topRgba = tinycolor(top).toRgb(); From cd5c9ac7195fa843f8dc44230a4b54db0886eff7 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:47:08 +0100 Subject: [PATCH 30/61] Add vscode api mocking support --- .../src/constructTestHelpers.ts | 8 +++--- .../cursorless-vscode/src/createVscodeApi.ts | 19 +++++++++++++ packages/cursorless-vscode/src/extension.ts | 20 ++++++++++---- .../VscodeFancyRangeHighlighter.ts | 5 ++-- .../VscodeFancyRangeHighlighterRenderer.ts | 14 +++++----- .../VscodeScopeRenderer.ts | 9 ++++++- .../VscodeScopeVisualizer.ts | 18 ++++++++----- .../createVscodeScopeVisualizer.ts | 23 +++++++++++++--- packages/cursorless-vscode/tsconfig.json | 7 ++++- packages/vscode-common/src/getExtensionApi.ts | 2 ++ packages/vscode-common/src/index.ts | 1 + packages/vscode-common/src/vscode.ts | 27 +++++++++++++++++++ 12 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 packages/cursorless-vscode/src/createVscodeApi.ts create mode 100644 packages/vscode-common/src/vscode.ts diff --git a/packages/cursorless-vscode/src/constructTestHelpers.ts b/packages/cursorless-vscode/src/constructTestHelpers.ts index 3d2941dd2a..631f4604a6 100644 --- a/packages/cursorless-vscode/src/constructTestHelpers.ts +++ b/packages/cursorless-vscode/src/constructTestHelpers.ts @@ -16,10 +16,10 @@ import { plainObjectToTarget, takeSnapshot, } from "@cursorless/cursorless-engine"; -import { TestHelpers } from "@cursorless/vscode-common"; -import * as vscode from "vscode"; +import { TestHelpers, Vscode } from "@cursorless/vscode-common"; import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { toVscodeEditor } from "./ide/vscode/toVscodeEditor"; +import type { TextEditor as VscodeTextEditor } from "vscode"; export function constructTestHelpers( commandServerApi: CommandServerApi | null, @@ -29,6 +29,7 @@ export function constructTestHelpers( normalizedIde: NormalizedIDE, injectIde: (ide: IDE) => void, runIntegrationTests: () => Promise, + vscode: Vscode, ): TestHelpers | undefined { return { commandServerApi: commandServerApi!, @@ -61,7 +62,7 @@ export function constructTestHelpers( }, setStoredTarget( - editor: vscode.TextEditor, + editor: VscodeTextEditor, key: StoredTargetKey, targets: TargetPlainObject[] | undefined, ): void { @@ -74,5 +75,6 @@ export function constructTestHelpers( }, hatTokenMap, runIntegrationTests, + vscode, }; } diff --git a/packages/cursorless-vscode/src/createVscodeApi.ts b/packages/cursorless-vscode/src/createVscodeApi.ts new file mode 100644 index 0000000000..27840d340c --- /dev/null +++ b/packages/cursorless-vscode/src/createVscodeApi.ts @@ -0,0 +1,19 @@ +import { workspace, window } from "vscode"; +import { Vscode } from "@cursorless/vscode-common"; + +export function createVscodeApi(): Vscode { + return { + workspace: { + onDidChangeConfiguration: workspace.onDidChangeConfiguration, + getConfiguration: workspace.getConfiguration, + }, + window: { + createTextEditorDecorationType: window.createTextEditorDecorationType, + }, + editor: { + setDecorations(editor, ...args) { + return editor.setDecorations(...args); + }, + }, + }; +} diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 625bdcf4b0..ce0195ab77 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -20,7 +20,6 @@ import { ParseTreeApi, toVscodeRange, } from "@cursorless/vscode-common"; -import * as vscode from "vscode"; import { constructTestHelpers } from "./constructTestHelpers"; import { FakeFontMeasurements } from "./ide/vscode/hats/FakeFontMeasurements"; import { FontMeasurementsImpl } from "./ide/vscode/hats/FontMeasurementsImpl"; @@ -37,6 +36,9 @@ import { ScopeVisualizerCommandApi, VisualizationType, } from "./ScopeVisualizerCommandApi"; +import { createVscodeApi } from "./createVscodeApi"; +import { Vscode } from "@cursorless/vscode-common"; +import { ExtensionContext, Location } from "vscode"; /** * Extension entrypoint called by VSCode on Cursorless startup. @@ -47,7 +49,7 @@ import { * - Creates an entrypoint for running commands {@link CommandRunner}. */ export async function activate( - context: vscode.ExtensionContext, + context: ExtensionContext, ): Promise { const parseTreeApi = await getParseTreeApi(); @@ -87,13 +89,18 @@ export async function activate( const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); const keyboardCommands = KeyboardCommands.create(context, statusBarItem); + const vscode = createVscodeApi(); registerCommands( context, vscodeIDE, commandApi, testCaseRecorder, - createScopeVisualizerCommandApi(normalizedIde ?? vscodeIDE, scopeProvider), + createScopeVisualizerCommandApi( + vscode, + normalizedIde ?? vscodeIDE, + scopeProvider, + ), keyboardCommands, hats, ); @@ -108,6 +115,7 @@ export async function activate( normalizedIde!, injectIde, runIntegrationTests, + vscode, ) : undefined, @@ -117,7 +125,7 @@ export async function activate( }; } -async function createVscodeIde(context: vscode.ExtensionContext) { +async function createVscodeIde(context: ExtensionContext) { const vscodeIDE = new VscodeIDE(context); const hats = new VscodeHats( @@ -136,7 +144,7 @@ function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter { return { getNodeAtLocation(document: TextDocument, range: Range) { return parseTreeApi.getNodeAtLocation( - new vscode.Location(document.uri, toVscodeRange(range)), + new Location(document.uri, toVscodeRange(range)), ); }, @@ -150,6 +158,7 @@ function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter { } function createScopeVisualizerCommandApi( + vscode: Vscode, ide: IDE, scopeProvider: ScopeProvider, ): ScopeVisualizerCommandApi { @@ -159,6 +168,7 @@ function createScopeVisualizerCommandApi( start(scopeType: ScopeType, visualizationType: VisualizationType) { scopeVisualizer?.dispose(); scopeVisualizer = createVscodeScopeVisualizer( + vscode, ide, scopeProvider, scopeType, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 029b055672..19582611b4 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -1,5 +1,6 @@ import { GeneralizedRange, Range } from "@cursorless/common"; import { flatmap } from "itertools"; +import { Vscode } from "@cursorless/vscode-common"; import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import { RangeTypeColors } from "../RangeTypeColors"; import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; @@ -15,8 +16,8 @@ import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRang export class VscodeFancyRangeHighlighter { private renderer: VscodeFancyRangeHighlighterRenderer; - constructor(colors: RangeTypeColors) { - this.renderer = new VscodeFancyRangeHighlighterRenderer(colors); + constructor(vscode: Vscode, colors: RangeTypeColors) { + this.renderer = new VscodeFancyRangeHighlighterRenderer(vscode, colors); } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index d1ff750cba..b31636751f 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -4,10 +4,10 @@ import { DecorationRangeBehavior, DecorationRenderOptions, TextEditorDecorationType, - window, } from "vscode"; -import { RangeTypeColors } from "../RangeTypeColors"; +import { Vscode } from "@cursorless/vscode-common"; import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; +import { RangeTypeColors } from "../RangeTypeColors"; import { BorderStyle, DecorationStyle, @@ -24,9 +24,9 @@ export class VscodeFancyRangeHighlighterRenderer { TextEditorDecorationType >; - constructor(colors: RangeTypeColors) { + constructor(private vscode: Vscode, colors: RangeTypeColors) { this.decorationTypes = new CompositeKeyDefaultMap( - ({ style }) => getDecorationStyle(colors, style), + ({ style }) => getDecorationStyle(this.vscode, colors, style), ({ style: { top, right, bottom, left, isWholeLine }, differentiationIndex, @@ -57,7 +57,8 @@ export class VscodeFancyRangeHighlighterRenderer { ({ differentiatedStyles: styleParameters, ranges }) => { const decorationType = this.decorationTypes.get(styleParameters); - editor.vscodeEditor.setDecorations( + this.vscode.editor.setDecorations( + editor.vscodeEditor, decorationType, ranges.map(toVscodeRange), ); @@ -79,6 +80,7 @@ export class VscodeFancyRangeHighlighterRenderer { } function getDecorationStyle( + vscode: Vscode, colors: RangeTypeColors, borders: DecorationStyle, ): TextEditorDecorationType { @@ -106,7 +108,7 @@ function getDecorationStyle( isWholeLine: borders.isWholeLine, }; - return window.createTextEditorDecorationType(options); + return vscode.window.createTextEditorDecorationType(options); } function getBorderStyle(borders: DecorationStyle): string { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts index ffa893d880..24fa1ceb1c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts @@ -3,6 +3,7 @@ import { GeneralizedRange, isGeneralizedRangeEqual, } from "@cursorless/common"; +import { Vscode } from "@cursorless/vscode-common"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { RangeTypeColors } from "./RangeTypeColors"; import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; @@ -19,14 +20,20 @@ export class VscodeScopeRenderer implements Disposable { private domainEqualsNestedHighlighter: VscodeFancyRangeHighlighter; constructor( + vscode: Vscode, domainColors: RangeTypeColors, nestedRangeColors: RangeTypeColors, ) { - this.domainHighlighter = new VscodeFancyRangeHighlighter(domainColors); + this.domainHighlighter = new VscodeFancyRangeHighlighter( + vscode, + domainColors, + ); this.nestedRangeHighlighter = new VscodeFancyRangeHighlighter( + vscode, nestedRangeColors, ); this.domainEqualsNestedHighlighter = new VscodeFancyRangeHighlighter( + vscode, blendRangeTypeColors(domainColors, nestedRangeColors), ); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index 90fbb294f0..cfb0439dbb 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -6,13 +6,13 @@ import { showError, } from "@cursorless/common"; import { ScopeProvider, ScopeSupport } from "@cursorless/cursorless-engine"; -import * as vscode from "vscode"; import { ScopeRangeType, ScopeVisualizerColorConfig, getColorsFromConfig, } from "./ScopeVisualizerColorConfig"; import { VscodeScopeRenderer } from "./VscodeScopeRenderer"; +import { Vscode } from "@cursorless/vscode-common"; export abstract class VscodeScopeVisualizer { protected renderer!: VscodeScopeRenderer; @@ -24,16 +24,19 @@ export abstract class VscodeScopeVisualizer { protected abstract getScopeSupport(editor: TextEditor): ScopeSupport; constructor( + private vscode: Vscode, private ide: IDE, protected scopeProvider: ScopeProvider, protected scopeType: ScopeType, ) { this.disposables.push( - vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { - if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { - this.initialize(); - } - }), + this.vscode.workspace.onDidChangeConfiguration( + ({ affectsConfiguration }) => { + if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { + this.initialize(); + } + }, + ), ); } @@ -64,12 +67,13 @@ export abstract class VscodeScopeVisualizer { } private initialize() { - const colorConfig = vscode.workspace + const colorConfig = this.vscode.workspace .getConfiguration("cursorless.scopeVisualizer") .get("colors")!; this.renderer?.dispose(); this.renderer = new VscodeScopeRenderer( + this.vscode, getColorsFromConfig(colorConfig, "domain"), getColorsFromConfig(colorConfig, this.getNestedScopeRangeType()), ); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts index 6ec597e7cd..c36ab06066 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts @@ -6,8 +6,10 @@ import { VscodeScopeContentVisualizer, VscodeScopeRemovalVisualizer, } from "./VscodeScopeTargetVisualizer"; +import { Vscode } from "@cursorless/vscode-common"; export function createVscodeScopeVisualizer( + vscode: Vscode, ide: IDE, scopeProvider: ScopeProvider, scopeType: ScopeType, @@ -15,10 +17,25 @@ export function createVscodeScopeVisualizer( ) { switch (visualizationType) { case "content": - return new VscodeScopeContentVisualizer(ide, scopeProvider, scopeType); + return new VscodeScopeContentVisualizer( + vscode, + ide, + scopeProvider, + scopeType, + ); case "removal": - return new VscodeScopeRemovalVisualizer(ide, scopeProvider, scopeType); + return new VscodeScopeRemovalVisualizer( + vscode, + ide, + scopeProvider, + scopeType, + ); case "iteration": - return new VscodeScopeIterationVisualizer(ide, scopeProvider, scopeType); + return new VscodeScopeIterationVisualizer( + vscode, + ide, + scopeProvider, + scopeType, + ); } } diff --git a/packages/cursorless-vscode/tsconfig.json b/packages/cursorless-vscode/tsconfig.json index 7564228e28..dcaf485d11 100644 --- a/packages/cursorless-vscode/tsconfig.json +++ b/packages/cursorless-vscode/tsconfig.json @@ -5,7 +5,12 @@ "outDir": "out", "rootDir": "src" }, - "include": ["src/**/*.ts", "src/**/*.json", "../../typings/**/*.d.ts"], + "include": [ + "src/**/*.ts", + "src/**/*.json", + "../../typings/**/*.d.ts", + "../vscode-common/src/vscode.ts" + ], "references": [ { "path": "../common" diff --git a/packages/vscode-common/src/getExtensionApi.ts b/packages/vscode-common/src/getExtensionApi.ts index 6c083ed85b..a0f2ee6142 100644 --- a/packages/vscode-common/src/getExtensionApi.ts +++ b/packages/vscode-common/src/getExtensionApi.ts @@ -13,6 +13,7 @@ import type { } from "@cursorless/common"; import * as vscode from "vscode"; import type { Language, SyntaxNode, Tree } from "web-tree-sitter"; +import { Vscode } from "./vscode"; export interface TestHelpers { ide: NormalizedIDE; @@ -42,6 +43,7 @@ export interface TestHelpers { ): Promise; runIntegrationTests(): Promise; + vscode: Vscode; } export interface CursorlessApi { diff --git a/packages/vscode-common/src/index.ts b/packages/vscode-common/src/index.ts index 15435d5cc6..64c2a64dec 100644 --- a/packages/vscode-common/src/index.ts +++ b/packages/vscode-common/src/index.ts @@ -3,3 +3,4 @@ export * from "./notebook"; export * from "./testUtil/openNewEditor"; export * from "./vscodeUtil"; export * from "./runCommand"; +export * from "./vscode"; diff --git a/packages/vscode-common/src/vscode.ts b/packages/vscode-common/src/vscode.ts new file mode 100644 index 0000000000..846444363a --- /dev/null +++ b/packages/vscode-common/src/vscode.ts @@ -0,0 +1,27 @@ +import { workspace, window, TextEditor } from "vscode"; + +/** + * Subset of VSCode api that we need to be able to mock for testing + */ +export interface Vscode { + workspace: { + onDidChangeConfiguration: typeof workspace.onDidChangeConfiguration; + getConfiguration: typeof workspace.getConfiguration; + }; + + window: { + createTextEditorDecorationType: typeof window.createTextEditorDecorationType; + }; + + /** + * Wrapper around editor api for easy mocking. Provides various + * {@link TextEditor} methods as static functions which take a text editor as + * their first argument. + */ + editor: { + setDecorations( + editor: TextEditor, + ...args: Parameters + ): ReturnType; + }; +} From 0ae8db31761c8ed8a9ddf379478462ef86d39344 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 5 Jul 2023 18:49:15 +0100 Subject: [PATCH 31/61] Initial test work --- packages/common/src/testUtil/toPlainObject.ts | 16 +- .../src/suite/scopeProvider.vscode.test.ts | 113 +++++++ .../src/suite/scopeVisualizer.vscode.test.ts | 294 +++++++++--------- .../VscodeScopeVisualizer.ts | 4 +- ...rColorConfig.ts => getColorsFromConfig.ts} | 22 +- packages/cursorless-vscode/tsconfig.json | 3 +- .../src/ScopeVisualizerColorConfig.ts | 17 + packages/vscode-common/src/index.ts | 1 + 8 files changed, 292 insertions(+), 178 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{ScopeVisualizerColorConfig.ts => getColorsFromConfig.ts} (55%) create mode 100644 packages/vscode-common/src/ScopeVisualizerColorConfig.ts diff --git a/packages/common/src/testUtil/toPlainObject.ts b/packages/common/src/testUtil/toPlainObject.ts index 34b8b50a82..47edc8ee65 100644 --- a/packages/common/src/testUtil/toPlainObject.ts +++ b/packages/common/src/testUtil/toPlainObject.ts @@ -8,8 +8,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 = { @@ -110,7 +108,17 @@ export type SerializedMarks = { [decoratedCharacter: string]: RangePlainObject; }; -export function rangeToPlainObject(range: Range): RangePlainObject { +interface SimplePosition { + line: number; + character: number; +} + +interface SimpleRange { + start: SimplePosition; + end: SimplePosition; +} + +export function rangeToPlainObject(range: SimpleRange): RangePlainObject { return { start: positionToPlainObject(range.start), end: positionToPlainObject(range.end), @@ -129,7 +137,7 @@ export function selectionToPlainObject( export function positionToPlainObject({ line, character, -}: Position): PositionPlainObject { +}: SimplePosition): PositionPlainObject { return { line, character }; } diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts new file mode 100644 index 0000000000..cefb7135fd --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts @@ -0,0 +1,113 @@ +import { + PlainScopeVisualization, + SpyIDE, + omitByDeep, + spyIDERecordedValuesToPlainObject, +} from "@cursorless/common"; +import { openNewEditor } from "@cursorless/vscode-common"; +import { assert } from "chai"; +import asyncSafety from "../asyncSafety"; +import { endToEndTestSetup, sleepWithBackoff } from "../endToEndTestSetup"; +import * as vscode from "vscode"; +import { isUndefined } from "lodash"; + +suite("scope visualizer", async function () { + const { getSpy } = endToEndTestSetup(this); + + test( + "basic content", + asyncSafety(() => runContentTest(getSpy()!)), + ); + test( + "basic removal", + asyncSafety(() => runRemovalTest(getSpy()!)), + ); +}); + +const initialDocumentContents = ` +function helloWorld() { + +} +`; + +const updatedDocumentContents = ` +function helloWorld() { + function nestedFunction() { + + } +} +`; + +const expectedInitialContentVisualizations: any[] = []; + +const expectedUpdatedContentVisualization = {}; + +async function runContentTest(spyIde: SpyIDE) { + const editor = await openNewEditor(initialDocumentContents, { + languageId: "typescript", + }); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "namedFunction", + }, + "content", + ); + + const expectedVisualizations = [...expectedInitialContentVisualizations]; + checkVisualizations(spyIde, expectedVisualizations); + + await editor.edit((editBuilder) => { + editBuilder.replace( + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(2, 1)), + updatedDocumentContents, + ); + }); + await sleepWithBackoff(100); + + expectedVisualizations.push(expectedUpdatedContentVisualization); + checkVisualizations(spyIde, expectedVisualizations); + + await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); + + expectedVisualizations.push({ scopeRanges: [] }); + checkVisualizations(spyIde, expectedVisualizations); +} + +const expectedRemovalVisualizations: any[] = []; + +async function runRemovalTest(spyIde: SpyIDE) { + await openNewEditor(initialDocumentContents, { + languageId: "typescript", + }); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "paragraph", + }, + "removal", + ); + + checkVisualizations(spyIde, expectedRemovalVisualizations); + + await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); +} + +function checkVisualizations( + spyIde: SpyIDE, + expectedVisualizations: PlainScopeVisualization[], +) { + const actualVisualizations = omitByDeep( + spyIDERecordedValuesToPlainObject(spyIde.getSpyValues(false)!) + .scopeVisualizations, + isUndefined, + ); + + assert.deepStrictEqual( + actualVisualizations, + expectedVisualizations, + JSON.stringify(actualVisualizations), + ); +} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts index ec1db8f68b..c8b44a847c 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts @@ -1,27 +1,32 @@ import { - PlainScopeVisualization, - SpyIDE, - omitByDeep, - spyIDERecordedValuesToPlainObject, -} from "@cursorless/common"; -import { openNewEditor } from "@cursorless/vscode-common"; + ScopeVisualizerColorConfig, + Vscode, + getCursorlessApi, + openNewEditor, +} from "@cursorless/vscode-common"; import { assert } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { + DecorationRenderOptions, + TextEditorDecorationType, + WorkspaceConfiguration, +} from "vscode"; import asyncSafety from "../asyncSafety"; import { endToEndTestSetup, sleepWithBackoff } from "../endToEndTestSetup"; -import * as vscode from "vscode"; -import { isUndefined } from "lodash"; +import { rangeToPlainObject } from "@cursorless/common"; suite("scope visualizer", async function () { - const { getSpy } = endToEndTestSetup(this); + endToEndTestSetup(this); test( "basic content", - asyncSafety(() => runContentTest(getSpy()!)), - ); - test( - "basic removal", - asyncSafety(() => runRemovalTest(getSpy()!)), + asyncSafety(() => runContentTest()), ); + // test( + // "basic removal", + // asyncSafety(() => runRemovalTest()), + // ); }); const initialDocumentContents = ` @@ -38,97 +43,94 @@ function helloWorld() { } `; -const expectedInitialContentVisualizations: PlainScopeVisualization[] = [ - { scopeRanges: [] }, - { - scopeRanges: [ - { - scopeType: { type: "namedFunction" }, - domain: { - type: "character", - start: { line: 1, character: 0 }, - end: { line: 3, character: 1 }, - }, - contentRanges: [ - { - type: "character", - start: { line: 1, character: 0 }, - end: { line: 3, character: 1 }, - }, - ], - }, - ], - }, -]; +const expectedInitialContentVisualizations: any[] = []; -const expectedUpdatedContentVisualization: PlainScopeVisualization = { - scopeRanges: [ - { - contentRanges: [ - { - start: { - character: 0, - line: 1, - }, - end: { - character: 1, - line: 5, - }, - type: "character", - }, - ], - domain: { - start: { - character: 0, - line: 1, - }, - end: { - character: 1, - line: 5, - }, - type: "character", - }, - scopeType: { - type: "namedFunction", - }, +const expectedUpdatedContentVisualization: any[] = []; + +const COLOR_CONFIG: ScopeVisualizerColorConfig = { + dark: { + content: { + background: "#000000", + borderPorous: "#000001", + borderSolid: "#000002", }, - { - contentRanges: [ - { - start: { - character: 2, - line: 2, - }, - end: { - character: 3, - line: 4, - }, - type: "character", - }, - ], - domain: { - start: { - character: 2, - line: 2, - }, - end: { - character: 3, - line: 4, - }, - type: "character", - }, - scopeType: { - type: "namedFunction", - }, + domain: { + background: "#000003", + borderPorous: "#000004", + borderSolid: "#000005", + }, + iteration: { + background: "#000006", + borderPorous: "#000007", + borderSolid: "#000008", }, - ], + removal: { + background: "#000009", + borderPorous: "#000010", + borderSolid: "#000011", + }, + }, + light: { + content: { + background: "#100000", + borderPorous: "#100001", + borderSolid: "#100002", + }, + domain: { + background: "#100003", + borderPorous: "#100004", + borderSolid: "#100005", + }, + iteration: { + background: "#100006", + borderPorous: "#100007", + borderSolid: "#100008", + }, + removal: { + background: "#100009", + borderPorous: "#100010", + borderSolid: "#100011", + }, + }, }; -async function runContentTest(spyIde: SpyIDE) { +async function runContentTest() { const editor = await openNewEditor(initialDocumentContents, { languageId: "typescript", }); + const { vscode: vscodeApi } = (await getCursorlessApi()).testHelpers!; + + let decorationIndex = 0; + const setDecorations = sinon.fake< + Parameters, + void + >(); + const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG); + const dispose = sinon.fake(); + const createTextEditorDecorationType = sinon.fake( + (_options: DecorationRenderOptions) => { + return { + dispose, + id: decorationIndex++, + } as unknown as TextEditorDecorationType; + }, + ); + + sinon.replace( + vscodeApi.window, + "createTextEditorDecorationType", + createTextEditorDecorationType, + ); + sinon.replace(vscodeApi.editor, "setDecorations", setDecorations); + sinon.replace( + vscodeApi.workspace, + "getConfiguration", + sinon.fake.returns({ + get: getConfigurationValue, + } as unknown as WorkspaceConfiguration), + ); + await vscode.commands.executeCommand( "cursorless.showScopeVisualizer", { @@ -137,8 +139,10 @@ async function runContentTest(spyIde: SpyIDE) { "content", ); - const expectedVisualizations = [...expectedInitialContentVisualizations]; - checkVisualizations(spyIde, expectedVisualizations); + checkAndResetDecorations( + setDecorations, + expectedInitialContentVisualizations, + ); await editor.edit((editBuilder) => { editBuilder.replace( @@ -148,72 +152,58 @@ async function runContentTest(spyIde: SpyIDE) { }); await sleepWithBackoff(100); - expectedVisualizations.push(expectedUpdatedContentVisualization); - checkVisualizations(spyIde, expectedVisualizations); + checkAndResetDecorations(setDecorations, expectedUpdatedContentVisualization); await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); - expectedVisualizations.push({ scopeRanges: [] }); - checkVisualizations(spyIde, expectedVisualizations); + checkAndResetDecorations(setDecorations, []); } -const expectedRemovalVisualizations: PlainScopeVisualization[] = [ - { scopeRanges: [] }, - { - scopeRanges: [ - { - scopeType: { type: "paragraph" }, - domain: { - type: "character", - start: { line: 1, character: 0 }, - end: { line: 1, character: 23 }, - }, - removalRanges: [{ type: "line", start: 1, end: 2 }], - }, - { - scopeType: { type: "paragraph" }, - domain: { - type: "character", - start: { line: 3, character: 0 }, - end: { line: 3, character: 1 }, - }, - removalRanges: [{ type: "line", start: 3, end: 4 }], - }, - ], - }, -]; +// const expectedRemovalVisualizations = []; -async function runRemovalTest(spyIde: SpyIDE) { - await openNewEditor(initialDocumentContents, { - languageId: "typescript", - }); +// async function runRemovalTest() { +// await openNewEditor(initialDocumentContents, { +// languageId: "typescript", +// }); - await vscode.commands.executeCommand( - "cursorless.showScopeVisualizer", - { - type: "paragraph", - }, - "removal", - ); +// await vscode.commands.executeCommand( +// "cursorless.showScopeVisualizer", +// { +// type: "paragraph", +// }, +// "removal", +// ); - checkVisualizations(spyIde, expectedRemovalVisualizations); +// checkDecorations(spyIde, expectedRemovalVisualizations); - await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); -} +// await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); +// } -function checkVisualizations( - spyIde: SpyIDE, - expectedVisualizations: PlainScopeVisualization[], +function checkAndResetDecorations( + setDecorations: sinon.SinonSpy< + Parameters, + void + >, + expectedDecorations: any[], ) { - const actualVisualizations = omitByDeep( - spyIDERecordedValuesToPlainObject(spyIde.getSpyValues(false)!) - .scopeVisualizations, - isUndefined, + const actualDecorations = setDecorations.args.map((args) => + setDecorationsArgsToPlainObject(...(args as [any, any, any])), ); - assert.deepStrictEqual( - actualVisualizations, - expectedVisualizations, - JSON.stringify(actualVisualizations), + actualDecorations, + expectedDecorations, + JSON.stringify(actualDecorations), ); + setDecorations.resetHistory(); +} + +function setDecorationsArgsToPlainObject( + _editor: vscode.TextEditor, + decorationType: { id: string }, + ranges: readonly vscode.Range[], +): any { + return { + decorationType: decorationType.id, + ranges: ranges.map(rangeToPlainObject), + }; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index cfb0439dbb..a6675e2bcd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -9,8 +9,8 @@ import { ScopeProvider, ScopeSupport } from "@cursorless/cursorless-engine"; import { ScopeRangeType, ScopeVisualizerColorConfig, - getColorsFromConfig, -} from "./ScopeVisualizerColorConfig"; +} from "@cursorless/vscode-common"; +import { getColorsFromConfig } from "./getColorsFromConfig"; import { VscodeScopeRenderer } from "./VscodeScopeRenderer"; import { Vscode } from "@cursorless/vscode-common"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts similarity index 55% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts index 49be81da68..b2e14ca3b2 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/ScopeVisualizerColorConfig.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts @@ -1,8 +1,10 @@ import { RangeTypeColors } from "./RangeTypeColors"; +import { ScopeVisualizerColorConfig, ScopeRangeType } from "../../../../../vscode-common/src/ScopeVisualizerColorConfig"; + export function getColorsFromConfig( config: ScopeVisualizerColorConfig, - rangeType: ScopeRangeType, + rangeType: ScopeRangeType ): RangeTypeColors { return { background: { @@ -19,21 +21,3 @@ export function getColorsFromConfig( }, }; } - -export type ScopeRangeType = "domain" | "content" | "removal" | "iteration"; - -export interface ScopeVisualizerColorConfig { - light: ScopeVisualizerThemeColorConfig; - dark: ScopeVisualizerThemeColorConfig; -} - -type ScopeVisualizerThemeColorConfig = Record< - ScopeRangeType, - RangeTypeColorConfig ->; - -interface RangeTypeColorConfig { - background: string; - borderSolid: string; - borderPorous: string; -} diff --git a/packages/cursorless-vscode/tsconfig.json b/packages/cursorless-vscode/tsconfig.json index dcaf485d11..e0807eeeb4 100644 --- a/packages/cursorless-vscode/tsconfig.json +++ b/packages/cursorless-vscode/tsconfig.json @@ -9,7 +9,8 @@ "src/**/*.ts", "src/**/*.json", "../../typings/**/*.d.ts", - "../vscode-common/src/vscode.ts" + "../vscode-common/src/vscode.ts", + "../vscode-common/src/ScopeVisualizerColorConfig.ts" ], "references": [ { diff --git a/packages/vscode-common/src/ScopeVisualizerColorConfig.ts b/packages/vscode-common/src/ScopeVisualizerColorConfig.ts new file mode 100644 index 0000000000..7910cf6a76 --- /dev/null +++ b/packages/vscode-common/src/ScopeVisualizerColorConfig.ts @@ -0,0 +1,17 @@ +export type ScopeRangeType = "domain" | "content" | "removal" | "iteration"; + +export interface ScopeVisualizerColorConfig { + light: ScopeVisualizerThemeColorConfig; + dark: ScopeVisualizerThemeColorConfig; +} + +type ScopeVisualizerThemeColorConfig = Record< + ScopeRangeType, + RangeTypeColorConfig +>; + +interface RangeTypeColorConfig { + background: string; + borderSolid: string; + borderPorous: string; +} diff --git a/packages/vscode-common/src/index.ts b/packages/vscode-common/src/index.ts index 64c2a64dec..8e5d7ec3d1 100644 --- a/packages/vscode-common/src/index.ts +++ b/packages/vscode-common/src/index.ts @@ -4,3 +4,4 @@ export * from "./testUtil/openNewEditor"; export * from "./vscodeUtil"; export * from "./runCommand"; export * from "./vscode"; +export * from "./ScopeVisualizerColorConfig"; From 95caa5ea476fd1a98e6f91b18089820916f5756f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 5 Jul 2023 21:03:46 +0100 Subject: [PATCH 32/61] Working basic content range test --- .../src/suite/scopeVisualizer.vscode.test.ts | 209 --------------- .../scopeVisualizer/checkAndResetFakes.ts | 34 +++ .../src/suite/scopeVisualizer/colorConfig.ts | 48 ++++ .../src/suite/scopeVisualizer/injectFakes.ts | 46 ++++ .../scopeVisualizer.vscode.test.ts | 250 ++++++++++++++++++ .../scopeVisualizerTest.types.ts | 41 +++ .../suite/scopeVisualizer/toPlainObject.ts | 28 ++ 7 files changed, 447 insertions(+), 209 deletions(-) delete mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts deleted file mode 100644 index c8b44a847c..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer.vscode.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { - ScopeVisualizerColorConfig, - Vscode, - getCursorlessApi, - openNewEditor, -} from "@cursorless/vscode-common"; -import { assert } from "chai"; -import * as sinon from "sinon"; -import * as vscode from "vscode"; -import { - DecorationRenderOptions, - TextEditorDecorationType, - WorkspaceConfiguration, -} from "vscode"; -import asyncSafety from "../asyncSafety"; -import { endToEndTestSetup, sleepWithBackoff } from "../endToEndTestSetup"; -import { rangeToPlainObject } from "@cursorless/common"; - -suite("scope visualizer", async function () { - endToEndTestSetup(this); - - test( - "basic content", - asyncSafety(() => runContentTest()), - ); - // test( - // "basic removal", - // asyncSafety(() => runRemovalTest()), - // ); -}); - -const initialDocumentContents = ` -function helloWorld() { - -} -`; - -const updatedDocumentContents = ` -function helloWorld() { - function nestedFunction() { - - } -} -`; - -const expectedInitialContentVisualizations: any[] = []; - -const expectedUpdatedContentVisualization: any[] = []; - -const COLOR_CONFIG: ScopeVisualizerColorConfig = { - dark: { - content: { - background: "#000000", - borderPorous: "#000001", - borderSolid: "#000002", - }, - domain: { - background: "#000003", - borderPorous: "#000004", - borderSolid: "#000005", - }, - iteration: { - background: "#000006", - borderPorous: "#000007", - borderSolid: "#000008", - }, - removal: { - background: "#000009", - borderPorous: "#000010", - borderSolid: "#000011", - }, - }, - light: { - content: { - background: "#100000", - borderPorous: "#100001", - borderSolid: "#100002", - }, - domain: { - background: "#100003", - borderPorous: "#100004", - borderSolid: "#100005", - }, - iteration: { - background: "#100006", - borderPorous: "#100007", - borderSolid: "#100008", - }, - removal: { - background: "#100009", - borderPorous: "#100010", - borderSolid: "#100011", - }, - }, -}; - -async function runContentTest() { - const editor = await openNewEditor(initialDocumentContents, { - languageId: "typescript", - }); - - const { vscode: vscodeApi } = (await getCursorlessApi()).testHelpers!; - - let decorationIndex = 0; - const setDecorations = sinon.fake< - Parameters, - void - >(); - const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG); - const dispose = sinon.fake(); - const createTextEditorDecorationType = sinon.fake( - (_options: DecorationRenderOptions) => { - return { - dispose, - id: decorationIndex++, - } as unknown as TextEditorDecorationType; - }, - ); - - sinon.replace( - vscodeApi.window, - "createTextEditorDecorationType", - createTextEditorDecorationType, - ); - sinon.replace(vscodeApi.editor, "setDecorations", setDecorations); - sinon.replace( - vscodeApi.workspace, - "getConfiguration", - sinon.fake.returns({ - get: getConfigurationValue, - } as unknown as WorkspaceConfiguration), - ); - - await vscode.commands.executeCommand( - "cursorless.showScopeVisualizer", - { - type: "namedFunction", - }, - "content", - ); - - checkAndResetDecorations( - setDecorations, - expectedInitialContentVisualizations, - ); - - await editor.edit((editBuilder) => { - editBuilder.replace( - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(2, 1)), - updatedDocumentContents, - ); - }); - await sleepWithBackoff(100); - - checkAndResetDecorations(setDecorations, expectedUpdatedContentVisualization); - - await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); - - checkAndResetDecorations(setDecorations, []); -} - -// const expectedRemovalVisualizations = []; - -// async function runRemovalTest() { -// await openNewEditor(initialDocumentContents, { -// languageId: "typescript", -// }); - -// await vscode.commands.executeCommand( -// "cursorless.showScopeVisualizer", -// { -// type: "paragraph", -// }, -// "removal", -// ); - -// checkDecorations(spyIde, expectedRemovalVisualizations); - -// await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); -// } - -function checkAndResetDecorations( - setDecorations: sinon.SinonSpy< - Parameters, - void - >, - expectedDecorations: any[], -) { - const actualDecorations = setDecorations.args.map((args) => - setDecorationsArgsToPlainObject(...(args as [any, any, any])), - ); - assert.deepStrictEqual( - actualDecorations, - expectedDecorations, - JSON.stringify(actualDecorations), - ); - setDecorations.resetHistory(); -} - -function setDecorationsArgsToPlainObject( - _editor: vscode.TextEditor, - decorationType: { id: string }, - ranges: readonly vscode.Range[], -): any { - return { - decorationType: decorationType.id, - ranges: ranges.map(rangeToPlainObject), - }; -} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts new file mode 100644 index 0000000000..e724d1b2b4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts @@ -0,0 +1,34 @@ +import { assert } from "chai"; +import * as sinon from "sinon"; +import { + decorationRenderOptionsToPlainObject, + setDecorationsArgsToPlainObject, +} from "./toPlainObject"; +import { Fakes, ExpectedArgs } from "./scopeVisualizerTest.types"; + +export function checkAndResetFakes( + { createTextEditorDecorationType, setDecorations, dispose }: Fakes, + expected: ExpectedArgs, +) { + const actual = { + decorationRenderOptions: getAndResetFake( + createTextEditorDecorationType, + decorationRenderOptionsToPlainObject, + ), + decorationRanges: getAndResetFake( + setDecorations, + setDecorationsArgsToPlainObject, + ), + disposedDecorationIds: getAndResetFake(dispose, (id) => id), + }; + assert.deepStrictEqual(actual, expected, JSON.stringify(actual)); +} + +function getAndResetFake( + spy: sinon.SinonSpy, + transform: (...arg: ArgList) => Expected, +) { + const actual = spy.args.map((args) => transform(...args)); + spy.resetHistory(); + return actual; +} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts new file mode 100644 index 0000000000..d485a97432 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts @@ -0,0 +1,48 @@ +import { ScopeVisualizerColorConfig } from "@cursorless/vscode-common"; + +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", + }, + }, +}; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts new file mode 100644 index 0000000000..aef31149f9 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts @@ -0,0 +1,46 @@ +import { Vscode } from "@cursorless/vscode-common"; +import * as sinon from "sinon"; +import { + DecorationRenderOptions, + TextEditorDecorationType, + WorkspaceConfiguration, +} from "vscode"; +import { SetDecorationsParameters } from "./scopeVisualizerTest.types"; +import { COLOR_CONFIG } from "./colorConfig"; + +export function injectFakes(vscodeApi: Vscode) { + let decorationIndex = 0; + const setDecorations = sinon.fake< + SetDecorationsParameters, + ReturnType + >(); + const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG); + const dispose = sinon.fake<[number], void>(); + const createTextEditorDecorationType = sinon.fake< + Parameters, + ReturnType + >((_options: DecorationRenderOptions) => { + const id = decorationIndex++; + return { + dispose() { + dispose(id); + }, + id, + } as unknown as TextEditorDecorationType; + }); + + sinon.replace( + vscodeApi.window, + "createTextEditorDecorationType", + createTextEditorDecorationType, + ); + 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 }; +} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts new file mode 100644 index 0000000000..1062a5887a --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts @@ -0,0 +1,250 @@ +import { getCursorlessApi, openNewEditor } from "@cursorless/vscode-common"; +import * as vscode from "vscode"; +import asyncSafety from "../../asyncSafety"; +import { endToEndTestSetup, sleepWithBackoff } from "../../endToEndTestSetup"; +import { injectFakes } from "./injectFakes"; +import { checkAndResetFakes } from "./checkAndResetFakes"; +import { ExpectedArgs } from "./scopeVisualizerTest.types"; + +suite("scope visualizer", async function () { + endToEndTestSetup(this); + + test( + "basic content", + asyncSafety(() => runContentTest()), + ); + // test( + // "basic removal", + // asyncSafety(() => runRemovalTest()), + // ); +}); + +const initialDocumentContents = ` +function helloWorld() { + +} +`; + +const updatedDocumentContents = ` +function helloWorld() { + function nestedFunction() { + + } +} +`; + +const expectedInitialArgs: ExpectedArgs = { + decorationRenderOptions: [ + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderRadius: "2px 0px 0px 0px", + borderStyle: "solid dashed dashed solid", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderRadius: "0px 0px 0px 0px", + borderStyle: "none dashed none dashed", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderRadius: "0px 0px 2px 0px", + borderStyle: "dashed solid solid dashed", + isWholeLine: false, + }, + ], + decorationRanges: [ + { + decorationType: 0, + ranges: [ + { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, + ], + }, + { + decorationType: 1, + ranges: [ + { start: { line: 2, character: 0 }, end: { line: 2, character: 0 } }, + ], + }, + { + decorationType: 2, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 1 } }, + ], + }, + ], + disposedDecorationIds: [], +}; +const expectedUpdatedArgs: ExpectedArgs = { + decorationRenderOptions: [ + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderStyle: "solid dashed none solid", + borderRadius: "2px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "none none dashed dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "dashed dashed dashed none", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "dashed none none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010002c0 #010001c0", + borderStyle: "dashed dashed solid none", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderStyle: "none solid solid dashed", + borderRadius: "0px 0px 2px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderStyle: "solid dashed dashed solid", + borderRadius: "2px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010001c0", + borderStyle: "solid dashed none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderStyle: "dashed solid solid dashed", + borderRadius: "0px 0px 2px 0px", + isWholeLine: false, + }, + ], + decorationRanges: [ + { + decorationType: 3, + ranges: [ + { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, + ], + }, + { + decorationType: 4, + ranges: [ + { start: { line: 2, character: 0 }, end: { line: 2, character: 23 } }, + ], + }, + { + decorationType: 5, + ranges: [ + { start: { line: 2, character: 23 }, end: { line: 2, character: 29 } }, + ], + }, + { + decorationType: 1, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, + ], + }, + { + decorationType: 6, + ranges: [ + { start: { line: 4, character: 0 }, end: { line: 4, character: 1 } }, + ], + }, + { + decorationType: 7, + ranges: [ + { start: { line: 4, character: 1 }, end: { line: 4, character: 3 } }, + ], + }, + { + decorationType: 8, + ranges: [ + { start: { line: 5, character: 0 }, end: { line: 5, character: 1 } }, + ], + }, + { + decorationType: 9, + ranges: [ + { start: { line: 2, character: 2 }, end: { line: 2, character: 29 } }, + ], + }, + { + decorationType: 10, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, + ], + }, + { + decorationType: 11, + ranges: [ + { start: { line: 4, character: 0 }, end: { line: 4, character: 3 } }, + ], + }, + ], + disposedDecorationIds: [], +}; +const expectedFinalArgs: ExpectedArgs = { + decorationRenderOptions: [], + decorationRanges: [], + disposedDecorationIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], +}; + +async function runContentTest() { + const editor = await openNewEditor(initialDocumentContents, { + languageId: "typescript", + }); + + const { vscode: vscodeApi } = (await getCursorlessApi()).testHelpers!; + + const fakes = injectFakes(vscodeApi); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "namedFunction", + }, + "content", + ); + + checkAndResetFakes(fakes, expectedInitialArgs); + + await editor.edit((editBuilder) => { + editBuilder.replace( + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(2, 1)), + updatedDocumentContents, + ); + }); + await sleepWithBackoff(100); + + checkAndResetFakes(fakes, expectedUpdatedArgs); + + await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); + + checkAndResetFakes(fakes, expectedFinalArgs); +} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts new file mode 100644 index 0000000000..586649952c --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts @@ -0,0 +1,41 @@ +import { RangePlainObject } from "@cursorless/common"; +import { Vscode } from "@cursorless/vscode-common"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; + +export type SetDecorationsParameters = [ + editor: vscode.TextEditor, + decorationType: { id: number }, + ranges: readonly vscode.Range[], +]; + +export interface Fakes { + setDecorations: sinon.SinonSpy< + SetDecorationsParameters, + ReturnType + >; + createTextEditorDecorationType: sinon.SinonSpy< + Parameters, + ReturnType + >; + dispose: sinon.SinonSpy<[number], void>; +} + +export interface ExpectedArgs { + decorationRenderOptions: DecorationRenderOptionsPlainObject[]; + decorationRanges: DecorationRangesPlainObject[]; + disposedDecorationIds: number[]; +} + +export interface DecorationRangesPlainObject { + decorationType: number; + ranges: RangePlainObject[]; +} + +export interface DecorationRenderOptionsPlainObject { + backgroundColor: string | undefined; + borderColor: string | undefined; + borderStyle: string | undefined; + borderRadius: string | undefined; + isWholeLine: boolean; +} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts new file mode 100644 index 0000000000..f378f080f9 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts @@ -0,0 +1,28 @@ +import { rangeToPlainObject } from "@cursorless/common"; +import { DecorationRenderOptions } from "vscode"; +import { + SetDecorationsParameters, + DecorationRangesPlainObject, + DecorationRenderOptionsPlainObject, +} from "./scopeVisualizerTest.types"; + +export function setDecorationsArgsToPlainObject( + ...[_editor, decorationType, ranges]: SetDecorationsParameters +): DecorationRangesPlainObject { + return { + decorationType: decorationType.id, + ranges: ranges.map(rangeToPlainObject), + }; +} + +export function decorationRenderOptionsToPlainObject( + options: DecorationRenderOptions, +): DecorationRenderOptionsPlainObject { + return { + backgroundColor: options.dark?.backgroundColor?.toString(), + borderColor: options.dark?.borderColor?.toString(), + borderStyle: options.borderStyle, + borderRadius: options.borderRadius, + isWholeLine: options.isWholeLine ?? false, + }; +} From 3e9e45985630a267c52e5f88be7909c1157544c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 20:05:33 +0000 Subject: [PATCH 33/61] [pre-commit.ci lite] apply automatic fixes --- .../vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts index b2e14ca3b2..9ef5c4589e 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts @@ -1,10 +1,12 @@ import { RangeTypeColors } from "./RangeTypeColors"; -import { ScopeVisualizerColorConfig, ScopeRangeType } from "../../../../../vscode-common/src/ScopeVisualizerColorConfig"; - +import { + ScopeVisualizerColorConfig, + ScopeRangeType, +} from "@cursorless/vscode-common/src/ScopeVisualizerColorConfig"; export function getColorsFromConfig( config: ScopeVisualizerColorConfig, - rangeType: ScopeRangeType + rangeType: ScopeRangeType, ): RangeTypeColors { return { background: { From b396efbf87e1b25c75d8714bc3e48afb9c713d51 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:37:30 +0100 Subject: [PATCH 34/61] Use entire vscode namespaces rather than individual functions --- packages/cursorless-vscode/src/createVscodeApi.ts | 9 ++------- packages/vscode-common/src/vscode.ts | 10 ++-------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/cursorless-vscode/src/createVscodeApi.ts b/packages/cursorless-vscode/src/createVscodeApi.ts index 27840d340c..587f12eba1 100644 --- a/packages/cursorless-vscode/src/createVscodeApi.ts +++ b/packages/cursorless-vscode/src/createVscodeApi.ts @@ -3,13 +3,8 @@ import { Vscode } from "@cursorless/vscode-common"; export function createVscodeApi(): Vscode { return { - workspace: { - onDidChangeConfiguration: workspace.onDidChangeConfiguration, - getConfiguration: workspace.getConfiguration, - }, - window: { - createTextEditorDecorationType: window.createTextEditorDecorationType, - }, + workspace, + window, editor: { setDecorations(editor, ...args) { return editor.setDecorations(...args); diff --git a/packages/vscode-common/src/vscode.ts b/packages/vscode-common/src/vscode.ts index 846444363a..916f44aa4d 100644 --- a/packages/vscode-common/src/vscode.ts +++ b/packages/vscode-common/src/vscode.ts @@ -4,14 +4,8 @@ import { workspace, window, TextEditor } from "vscode"; * Subset of VSCode api that we need to be able to mock for testing */ export interface Vscode { - workspace: { - onDidChangeConfiguration: typeof workspace.onDidChangeConfiguration; - getConfiguration: typeof workspace.getConfiguration; - }; - - window: { - createTextEditorDecorationType: typeof window.createTextEditorDecorationType; - }; + workspace: typeof workspace; + window: typeof window; /** * Wrapper around editor api for easy mocking. Provides various From 1143080e50acd5274524259d855376ada177d1ea Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:38:47 +0100 Subject: [PATCH 35/61] More cleanup --- .../scopeVisualizer/checkAndResetFakes.ts | 31 +++++++------ .../src/suite/scopeVisualizer/injectFakes.ts | 34 ++++++++------ .../scopeVisualizer.vscode.test.ts | 46 ++++++++++++------- .../scopeVisualizerTest.types.ts | 12 +++-- .../suite/scopeVisualizer/toPlainObject.ts | 21 ++++++--- 5 files changed, 90 insertions(+), 54 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts index e724d1b2b4..0171a97962 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts @@ -1,34 +1,39 @@ import { assert } from "chai"; import * as sinon from "sinon"; import { - decorationRenderOptionsToPlainObject, - setDecorationsArgsToPlainObject, + createDecorationTypeCallToPlainObject, + setDecorationsCallToPlainObject, } from "./toPlainObject"; import { Fakes, ExpectedArgs } from "./scopeVisualizerTest.types"; -export function checkAndResetFakes( - { createTextEditorDecorationType, setDecorations, dispose }: Fakes, - expected: ExpectedArgs, -) { - const actual = { +export function checkAndResetFakes(fakes: Fakes, expected: ExpectedArgs) { + const actual = getAndResetFakes(fakes); + assert.deepStrictEqual(actual, expected, JSON.stringify(actual)); +} + +export function getAndResetFakes({ + createTextEditorDecorationType, + setDecorations, + dispose, +}: Fakes) { + return { decorationRenderOptions: getAndResetFake( createTextEditorDecorationType, - decorationRenderOptionsToPlainObject, + createDecorationTypeCallToPlainObject, ), decorationRanges: getAndResetFake( setDecorations, - setDecorationsArgsToPlainObject, + setDecorationsCallToPlainObject, ), - disposedDecorationIds: getAndResetFake(dispose, (id) => id), + disposedDecorationIds: getAndResetFake(dispose, ({ args: [id] }) => id), }; - assert.deepStrictEqual(actual, expected, JSON.stringify(actual)); } function getAndResetFake( spy: sinon.SinonSpy, - transform: (...arg: ArgList) => Expected, + transform: (call: sinon.SinonSpyCall) => Expected, ) { - const actual = spy.args.map((args) => transform(...args)); + const actual = spy.getCalls().map(transform); spy.resetHistory(); return actual; } diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts index aef31149f9..ee204e1b82 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts @@ -1,24 +1,20 @@ import { Vscode } from "@cursorless/vscode-common"; import * as sinon from "sinon"; +import { DecorationRenderOptions, WorkspaceConfiguration } from "vscode"; import { - DecorationRenderOptions, - TextEditorDecorationType, - WorkspaceConfiguration, -} from "vscode"; -import { SetDecorationsParameters } from "./scopeVisualizerTest.types"; + Fakes, + MockDecorationType, + SetDecorationsParameters, +} from "./scopeVisualizerTest.types"; import { COLOR_CONFIG } from "./colorConfig"; -export function injectFakes(vscodeApi: Vscode) { - let decorationIndex = 0; - const setDecorations = sinon.fake< - SetDecorationsParameters, - ReturnType - >(); - const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG); +export function injectFakes(vscodeApi: Vscode): Fakes { const dispose = sinon.fake<[number], void>(); + + let decorationIndex = 0; const createTextEditorDecorationType = sinon.fake< Parameters, - ReturnType + MockDecorationType >((_options: DecorationRenderOptions) => { const id = decorationIndex++; return { @@ -26,13 +22,20 @@ export function injectFakes(vscodeApi: Vscode) { dispose(id); }, id, - } as unknown as TextEditorDecorationType; + }; }); + const setDecorations = sinon.fake< + SetDecorationsParameters, + ReturnType + >(); + + const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG); + sinon.replace( vscodeApi.window, "createTextEditorDecorationType", - createTextEditorDecorationType, + createTextEditorDecorationType as any, ); sinon.replace(vscodeApi.editor, "setDecorations", setDecorations as any); sinon.replace( @@ -42,5 +45,6 @@ export function injectFakes(vscodeApi: Vscode) { get: getConfigurationValue, } as unknown as WorkspaceConfiguration), ); + return { setDecorations, createTextEditorDecorationType, dispose }; } diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts index 1062a5887a..e7c419ba46 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts @@ -38,40 +38,43 @@ const expectedInitialArgs: ExpectedArgs = { { backgroundColor: "#000001c0", borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", - borderRadius: "2px 0px 0px 0px", borderStyle: "solid dashed dashed solid", + borderRadius: "2px 0px 0px 0px", isWholeLine: false, + id: 0, }, { backgroundColor: "#000001c0", borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderRadius: "0px 0px 0px 0px", borderStyle: "none dashed none dashed", + borderRadius: "0px 0px 0px 0px", isWholeLine: false, + id: 1, }, { backgroundColor: "#000001c0", borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", - borderRadius: "0px 0px 2px 0px", borderStyle: "dashed solid solid dashed", + borderRadius: "0px 0px 2px 0px", isWholeLine: false, + id: 2, }, ], decorationRanges: [ { - decorationType: 0, + decorationId: 0, ranges: [ { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, ], }, { - decorationType: 1, + decorationId: 1, ranges: [ { start: { line: 2, character: 0 }, end: { line: 2, character: 0 } }, ], }, { - decorationType: 2, + decorationId: 2, ranges: [ { start: { line: 3, character: 0 }, end: { line: 3, character: 1 } }, ], @@ -79,6 +82,7 @@ const expectedInitialArgs: ExpectedArgs = { ], disposedDecorationIds: [], }; + const expectedUpdatedArgs: ExpectedArgs = { decorationRenderOptions: [ { @@ -87,6 +91,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "solid dashed none solid", borderRadius: "2px 0px 0px 0px", isWholeLine: false, + id: 3, }, { backgroundColor: "#000001c0", @@ -94,6 +99,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "none none dashed dashed", borderRadius: "0px 0px 0px 0px", isWholeLine: false, + id: 4, }, { backgroundColor: "#000001c0", @@ -101,6 +107,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "dashed dashed dashed none", borderRadius: "0px 0px 0px 0px", isWholeLine: false, + id: 5, }, { backgroundColor: "#000001c0", @@ -108,6 +115,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "dashed none none dashed", borderRadius: "0px 0px 0px 0px", isWholeLine: false, + id: 6, }, { backgroundColor: "#000001c0", @@ -115,6 +123,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "dashed dashed solid none", borderRadius: "0px 0px 0px 0px", isWholeLine: false, + id: 7, }, { backgroundColor: "#000001c0", @@ -122,6 +131,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "none solid solid dashed", borderRadius: "0px 0px 2px 0px", isWholeLine: false, + id: 8, }, { backgroundColor: "#000001c0", @@ -129,6 +139,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "solid dashed dashed solid", borderRadius: "2px 0px 0px 0px", isWholeLine: false, + id: 9, }, { backgroundColor: "#000001c0", @@ -136,6 +147,7 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "solid dashed none dashed", borderRadius: "0px 0px 0px 0px", isWholeLine: false, + id: 10, }, { backgroundColor: "#000001c0", @@ -143,65 +155,66 @@ const expectedUpdatedArgs: ExpectedArgs = { borderStyle: "dashed solid solid dashed", borderRadius: "0px 0px 2px 0px", isWholeLine: false, + id: 11, }, ], decorationRanges: [ { - decorationType: 3, + decorationId: 3, ranges: [ { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, ], }, { - decorationType: 4, + decorationId: 4, ranges: [ { start: { line: 2, character: 0 }, end: { line: 2, character: 23 } }, ], }, { - decorationType: 5, + decorationId: 5, ranges: [ { start: { line: 2, character: 23 }, end: { line: 2, character: 29 } }, ], }, { - decorationType: 1, + decorationId: 1, ranges: [ { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, ], }, { - decorationType: 6, + decorationId: 6, ranges: [ { start: { line: 4, character: 0 }, end: { line: 4, character: 1 } }, ], }, { - decorationType: 7, + decorationId: 7, ranges: [ { start: { line: 4, character: 1 }, end: { line: 4, character: 3 } }, ], }, { - decorationType: 8, + decorationId: 8, ranges: [ { start: { line: 5, character: 0 }, end: { line: 5, character: 1 } }, ], }, { - decorationType: 9, + decorationId: 9, ranges: [ { start: { line: 2, character: 2 }, end: { line: 2, character: 29 } }, ], }, { - decorationType: 10, + decorationId: 10, ranges: [ { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, ], }, { - decorationType: 11, + decorationId: 11, ranges: [ { start: { line: 4, character: 0 }, end: { line: 4, character: 3 } }, ], @@ -209,6 +222,7 @@ const expectedUpdatedArgs: ExpectedArgs = { ], disposedDecorationIds: [], }; + const expectedFinalArgs: ExpectedArgs = { decorationRenderOptions: [], decorationRanges: [], diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts index 586649952c..f1bcc01bd9 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts @@ -3,9 +3,14 @@ import { Vscode } from "@cursorless/vscode-common"; import * as sinon from "sinon"; import * as vscode from "vscode"; +export interface MockDecorationType { + dispose(): void; + id: number; +} + export type SetDecorationsParameters = [ editor: vscode.TextEditor, - decorationType: { id: number }, + decorationType: MockDecorationType, ranges: readonly vscode.Range[], ]; @@ -16,7 +21,7 @@ export interface Fakes { >; createTextEditorDecorationType: sinon.SinonSpy< Parameters, - ReturnType + MockDecorationType >; dispose: sinon.SinonSpy<[number], void>; } @@ -28,7 +33,7 @@ export interface ExpectedArgs { } export interface DecorationRangesPlainObject { - decorationType: number; + decorationId: number; ranges: RangePlainObject[]; } @@ -38,4 +43,5 @@ export interface DecorationRenderOptionsPlainObject { borderStyle: string | undefined; borderRadius: string | undefined; isWholeLine: boolean; + id: number; } diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts index f378f080f9..e4b865c129 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts @@ -4,25 +4,32 @@ import { SetDecorationsParameters, DecorationRangesPlainObject, DecorationRenderOptionsPlainObject, + MockDecorationType, } from "./scopeVisualizerTest.types"; +import { SinonSpyCall } from "sinon"; -export function setDecorationsArgsToPlainObject( - ...[_editor, decorationType, ranges]: SetDecorationsParameters -): DecorationRangesPlainObject { +export function setDecorationsCallToPlainObject({ + args: [_editor, decorationType, ranges], +}: SinonSpyCall): DecorationRangesPlainObject { return { - decorationType: decorationType.id, + decorationId: decorationType.id, ranges: ranges.map(rangeToPlainObject), }; } -export function decorationRenderOptionsToPlainObject( - options: DecorationRenderOptions, -): DecorationRenderOptionsPlainObject { +export function createDecorationTypeCallToPlainObject({ + args: [options], + returnValue: decorationType, +}: SinonSpyCall< + [DecorationRenderOptions], + MockDecorationType +>): DecorationRenderOptionsPlainObject { return { backgroundColor: options.dark?.backgroundColor?.toString(), borderColor: options.dark?.borderColor?.toString(), borderStyle: options.borderStyle, borderRadius: options.borderRadius, isWholeLine: options.isWholeLine ?? false, + id: decorationType.id, }; } From 2fc3f1a7a49b3527e55d8431781b911ffc1dd55b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:07:10 +0100 Subject: [PATCH 36/61] cleanup tests --- .../src/suite/scopeVisualizer/injectFakes.ts | 8 +- .../runBasicMultilineContentTest.ts | 79 +++++ .../runNestedMultilineContentTest.ts | 179 ++++++++++++ .../suite/scopeVisualizer/runUpdateTest.ts | 231 +++++++++++++++ .../scopeVisualizer.vscode.test.ts | 273 ++---------------- 5 files changed, 511 insertions(+), 259 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts index ee204e1b82..a2a5d90edd 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts @@ -1,14 +1,16 @@ -import { Vscode } from "@cursorless/vscode-common"; +import { Vscode, 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"; -import { COLOR_CONFIG } from "./colorConfig"; -export function injectFakes(vscodeApi: Vscode): Fakes { +export async function injectFakes(): Promise { + const { vscode: vscodeApi } = (await getCursorlessApi()).testHelpers!; + const dispose = sinon.fake<[number], void>(); let decorationIndex = 0; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts new file mode 100644 index 0000000000..e3d21fd3c2 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts @@ -0,0 +1,79 @@ +import { openNewEditor } from "@cursorless/vscode-common"; +import * as vscode from "vscode"; +import { checkAndResetFakes } from "./checkAndResetFakes"; +import { injectFakes } from "./injectFakes"; +import { ExpectedArgs } from "./scopeVisualizerTest.types"; + +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: [], +}; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts new file mode 100644 index 0000000000..305b3b6304 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts @@ -0,0 +1,179 @@ +import { openNewEditor } from "@cursorless/vscode-common"; +import * as vscode from "vscode"; +import { injectFakes } from "./injectFakes"; +import { checkAndResetFakes } from "./checkAndResetFakes"; +import { ExpectedArgs } from "./scopeVisualizerTest.types"; + +export async function runNestedMultilineContentTest() { + await openNewEditor(contents, { + languageId: "typescript", + }); + + const fakes = await injectFakes(); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "namedFunction", + }, + "content", + ); + + checkAndResetFakes(fakes, expectedArgs); +} + +const contents = ` +function a() { + function b() { + + } +} +`; + +const expectedArgs: ExpectedArgs = { + decorationRenderOptions: [ + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderStyle: "solid dashed none solid", + borderRadius: "2px 0px 0px 0px", + isWholeLine: false, + id: 0, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "none none dashed dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 1, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "dashed dashed dashed none", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 2, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "none dashed none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 3, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "dashed none none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 4, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010002c0 #010001c0", + borderStyle: "dashed dashed solid none", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 5, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderStyle: "none solid solid dashed", + borderRadius: "0px 0px 2px 0px", + isWholeLine: false, + id: 6, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderStyle: "solid dashed dashed solid", + borderRadius: "2px 0px 0px 0px", + isWholeLine: false, + id: 7, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010001c0", + borderStyle: "solid dashed none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 8, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderStyle: "dashed solid solid dashed", + borderRadius: "0px 0px 2px 0px", + isWholeLine: false, + id: 9, + }, + ], + decorationRanges: [ + { + decorationId: 0, + ranges: [ + { start: { line: 1, character: 0 }, end: { line: 1, character: 14 } }, + ], + }, + { + decorationId: 1, + ranges: [ + { start: { line: 2, character: 0 }, end: { line: 2, character: 14 } }, + ], + }, + { + decorationId: 2, + ranges: [ + { start: { line: 2, character: 14 }, end: { line: 2, character: 16 } }, + ], + }, + { + decorationId: 3, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, + ], + }, + { + decorationId: 4, + ranges: [ + { start: { line: 4, character: 0 }, end: { line: 4, character: 1 } }, + ], + }, + { + decorationId: 5, + ranges: [ + { start: { line: 4, character: 1 }, end: { line: 4, character: 3 } }, + ], + }, + { + decorationId: 6, + ranges: [ + { start: { line: 5, character: 0 }, end: { line: 5, character: 1 } }, + ], + }, + { + decorationId: 7, + ranges: [ + { start: { line: 2, character: 2 }, end: { line: 2, character: 16 } }, + ], + }, + { + decorationId: 8, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, + ], + }, + { + decorationId: 9, + ranges: [ + { start: { line: 4, character: 0 }, end: { line: 4, character: 3 } }, + ], + }, + ], + disposedDecorationIds: [], +}; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts new file mode 100644 index 0000000000..f9f404bef3 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts @@ -0,0 +1,231 @@ +import { openNewEditor } from "@cursorless/vscode-common"; +import * as vscode from "vscode"; +import { sleepWithBackoff } from "../../endToEndTestSetup"; +import { injectFakes } from "./injectFakes"; +import { checkAndResetFakes } from "./checkAndResetFakes"; +import { ExpectedArgs } from "./scopeVisualizerTest.types"; + +export async function runUpdateTest() { + const editor = await openNewEditor("aaa", { + languageId: "typescript", + }); + + const fakes = await injectFakes(); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "token", + }, + "content", + ); + + checkAndResetFakes(fakes, expectedInitialArgs); + + await editor.edit((editBuilder) => { + editBuilder.insert(new vscode.Position(0, 3), " bbb"); + }); + await sleepWithBackoff(100); + + checkAndResetFakes(fakes, expectedUpdatedArgs); + + await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); + + checkAndResetFakes(fakes, expectedFinalArgs); +} + +const expectedInitialArgs: 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: [], +}; + +const expectedUpdatedArgs: ExpectedArgs = { + decorationRenderOptions: [ + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderStyle: "solid dashed none solid", + borderRadius: "2px 0px 0px 0px", + isWholeLine: false, + id: 3, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "none none dashed dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 4, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "dashed dashed dashed none", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 5, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", + borderStyle: "dashed none none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 6, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010001c0 #010002c0 #010001c0", + borderStyle: "dashed dashed solid none", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 7, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderStyle: "none solid solid dashed", + borderRadius: "0px 0px 2px 0px", + isWholeLine: false, + id: 8, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", + borderStyle: "solid dashed dashed solid", + borderRadius: "2px 0px 0px 0px", + isWholeLine: false, + id: 9, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010002c0 #010001c0 #010001c0 #010001c0", + borderStyle: "solid dashed none dashed", + borderRadius: "0px 0px 0px 0px", + isWholeLine: false, + id: 10, + }, + { + backgroundColor: "#000001c0", + borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", + borderStyle: "dashed solid solid dashed", + borderRadius: "0px 0px 2px 0px", + isWholeLine: false, + id: 11, + }, + ], + decorationRanges: [ + { + decorationId: 3, + ranges: [ + { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, + ], + }, + { + decorationId: 4, + ranges: [ + { start: { line: 2, character: 0 }, end: { line: 2, character: 23 } }, + ], + }, + { + decorationId: 5, + ranges: [ + { start: { line: 2, character: 23 }, end: { line: 2, character: 29 } }, + ], + }, + { + decorationId: 1, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, + ], + }, + { + decorationId: 6, + ranges: [ + { start: { line: 4, character: 0 }, end: { line: 4, character: 1 } }, + ], + }, + { + decorationId: 7, + ranges: [ + { start: { line: 4, character: 1 }, end: { line: 4, character: 3 } }, + ], + }, + { + decorationId: 8, + ranges: [ + { start: { line: 5, character: 0 }, end: { line: 5, character: 1 } }, + ], + }, + { + decorationId: 9, + ranges: [ + { start: { line: 2, character: 2 }, end: { line: 2, character: 29 } }, + ], + }, + { + decorationId: 10, + ranges: [ + { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, + ], + }, + { + decorationId: 11, + ranges: [ + { start: { line: 4, character: 0 }, end: { line: 4, character: 3 } }, + ], + }, + ], + disposedDecorationIds: [], +}; + +const expectedFinalArgs: ExpectedArgs = { + decorationRenderOptions: [], + decorationRanges: [], + disposedDecorationIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], +}; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts index e7c419ba46..2a02cf787e 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts @@ -1,264 +1,25 @@ -import { getCursorlessApi, openNewEditor } from "@cursorless/vscode-common"; -import * as vscode from "vscode"; +import { commands } from "vscode"; import asyncSafety from "../../asyncSafety"; -import { endToEndTestSetup, sleepWithBackoff } from "../../endToEndTestSetup"; -import { injectFakes } from "./injectFakes"; -import { checkAndResetFakes } from "./checkAndResetFakes"; -import { ExpectedArgs } from "./scopeVisualizerTest.types"; +import { endToEndTestSetup } from "../../endToEndTestSetup"; +import { runBasicMultilineContentTest } from "./runBasicMultilineContentTest"; +import { runNestedMultilineContentTest } from "./runNestedMultilineContentTest"; +import { runUpdateTest } from "./runUpdateTest"; suite("scope visualizer", async function () { endToEndTestSetup(this); + teardown(() => commands.executeCommand("cursorless.hideScopeVisualizer")); + test( - "basic content", - asyncSafety(() => runContentTest()), + "basic multiline content", + asyncSafety(() => runBasicMultilineContentTest()), ); - // test( - // "basic removal", - // asyncSafety(() => runRemovalTest()), - // ); -}); - -const initialDocumentContents = ` -function helloWorld() { - -} -`; - -const updatedDocumentContents = ` -function helloWorld() { - function nestedFunction() { - - } -} -`; - -const expectedInitialArgs: 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: [], -}; - -const expectedUpdatedArgs: ExpectedArgs = { - decorationRenderOptions: [ - { - backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", - borderStyle: "solid dashed none solid", - borderRadius: "2px 0px 0px 0px", - isWholeLine: false, - id: 3, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderStyle: "none none dashed dashed", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 4, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderStyle: "dashed dashed dashed none", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 5, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderStyle: "dashed none none dashed", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 6, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010002c0 #010001c0", - borderStyle: "dashed dashed solid none", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 7, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", - borderStyle: "none solid solid dashed", - borderRadius: "0px 0px 2px 0px", - isWholeLine: false, - id: 8, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", - borderStyle: "solid dashed dashed solid", - borderRadius: "2px 0px 0px 0px", - isWholeLine: false, - id: 9, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010001c0", - borderStyle: "solid dashed none dashed", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 10, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", - borderStyle: "dashed solid solid dashed", - borderRadius: "0px 0px 2px 0px", - isWholeLine: false, - id: 11, - }, - ], - decorationRanges: [ - { - decorationId: 3, - ranges: [ - { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, - ], - }, - { - decorationId: 4, - ranges: [ - { start: { line: 2, character: 0 }, end: { line: 2, character: 23 } }, - ], - }, - { - decorationId: 5, - ranges: [ - { start: { line: 2, character: 23 }, end: { line: 2, character: 29 } }, - ], - }, - { - decorationId: 1, - ranges: [ - { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, - ], - }, - { - decorationId: 6, - ranges: [ - { start: { line: 4, character: 0 }, end: { line: 4, character: 1 } }, - ], - }, - { - decorationId: 7, - ranges: [ - { start: { line: 4, character: 1 }, end: { line: 4, character: 3 } }, - ], - }, - { - decorationId: 8, - ranges: [ - { start: { line: 5, character: 0 }, end: { line: 5, character: 1 } }, - ], - }, - { - decorationId: 9, - ranges: [ - { start: { line: 2, character: 2 }, end: { line: 2, character: 29 } }, - ], - }, - { - decorationId: 10, - ranges: [ - { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, - ], - }, - { - decorationId: 11, - ranges: [ - { start: { line: 4, character: 0 }, end: { line: 4, character: 3 } }, - ], - }, - ], - disposedDecorationIds: [], -}; - -const expectedFinalArgs: ExpectedArgs = { - decorationRenderOptions: [], - decorationRanges: [], - disposedDecorationIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], -}; - -async function runContentTest() { - const editor = await openNewEditor(initialDocumentContents, { - languageId: "typescript", - }); - - const { vscode: vscodeApi } = (await getCursorlessApi()).testHelpers!; - - const fakes = injectFakes(vscodeApi); - - await vscode.commands.executeCommand( - "cursorless.showScopeVisualizer", - { - type: "namedFunction", - }, - "content", + test( + "basic nested multiline content", + asyncSafety(() => runNestedMultilineContentTest()), ); - - checkAndResetFakes(fakes, expectedInitialArgs); - - await editor.edit((editBuilder) => { - editBuilder.replace( - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(2, 1)), - updatedDocumentContents, - ); - }); - await sleepWithBackoff(100); - - checkAndResetFakes(fakes, expectedUpdatedArgs); - - await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); - - checkAndResetFakes(fakes, expectedFinalArgs); -} + test( + "update", + asyncSafety(() => runUpdateTest()), + ); +}); From a2d34460a44067c571f74107afbb560add1ee5f8 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:43:23 +0100 Subject: [PATCH 37/61] More testing --- .../scopeVisualizer/runBasicRemovalTest.ts | 72 +++++++ .../suite/scopeVisualizer/runUpdateTest.ts | 176 +----------------- .../scopeVisualizer.vscode.test.ts | 5 + .../generateDifferentiatedRanges.ts | 5 +- 4 files changed, 89 insertions(+), 169 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts new file mode 100644 index 0000000000..88cc727c0c --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts @@ -0,0 +1,72 @@ +import { openNewEditor } from "@cursorless/vscode-common"; +import * as vscode from "vscode"; +import { checkAndResetFakes } from "./checkAndResetFakes"; +import { injectFakes } from "./injectFakes"; +import { ExpectedArgs } from "./scopeVisualizerTest.types"; + +export async function runBasicRemovalTest() { + await openNewEditor("aaa bbb"); + + const fakes = await injectFakes(); + + await vscode.commands.executeCommand( + "cursorless.showScopeVisualizer", + { + type: "token", + }, + "removal", + ); + + checkAndResetFakes(fakes, expectedArgs); +} + +const expectedArgs: ExpectedArgs = { + decorationRenderOptions: [ + { + backgroundColor: "#01000080", + borderColor: "#03000080 #03000080 #03000080 #03000080", + borderStyle: "solid solid solid solid", + borderRadius: "2px 2px 2px 2px", + isWholeLine: false, + id: 0, + }, + { + backgroundColor: "#00010080", + borderColor: "#00030080 #00030080 #00030080 #00030080", + borderStyle: "solid solid solid solid", + borderRadius: "2px 2px 2px 2px", + isWholeLine: false, + id: 1, + }, + { + backgroundColor: "#00010080", + borderColor: "#00030080 #00030080 #00030080 #00030080", + borderStyle: "solid solid solid solid", + borderRadius: "2px 2px 2px 2px", + isWholeLine: false, + id: 2, + }, + ], + decorationRanges: [ + { + decorationId: 0, + ranges: [ + { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } }, + { start: { line: 0, character: 4 }, end: { line: 0, character: 7 } }, + ], + }, + { + decorationId: 1, + ranges: [ + { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } }, + ], + }, + { + decorationId: 2, + ranges: [ + { start: { line: 0, character: 3 }, end: { line: 0, character: 7 } }, + ], + }, + ], + disposedDecorationIds: [], +}; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts index f9f404bef3..01129a2fd5 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runUpdateTest.ts @@ -6,9 +6,7 @@ import { checkAndResetFakes } from "./checkAndResetFakes"; import { ExpectedArgs } from "./scopeVisualizerTest.types"; export async function runUpdateTest() { - const editor = await openNewEditor("aaa", { - languageId: "typescript", - }); + const editor = await openNewEditor("aaa"); const fakes = await injectFakes(); @@ -38,46 +36,18 @@ const expectedInitialArgs: ExpectedArgs = { decorationRenderOptions: [ { backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", - borderStyle: "solid dashed dashed solid", - borderRadius: "2px 0px 0px 0px", + borderColor: "#010002c0 #010002c0 #010002c0 #010002c0", + borderStyle: "solid solid solid solid", + borderRadius: "2px 2px 2px 2px", 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 } }, + { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } }, ], }, ], @@ -85,139 +55,13 @@ const expectedInitialArgs: ExpectedArgs = { }; const expectedUpdatedArgs: ExpectedArgs = { - decorationRenderOptions: [ - { - backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", - borderStyle: "solid dashed none solid", - borderRadius: "2px 0px 0px 0px", - isWholeLine: false, - id: 3, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderStyle: "none none dashed dashed", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 4, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderStyle: "dashed dashed dashed none", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 5, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010001c0 #010001c0", - borderStyle: "dashed none none dashed", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 6, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010001c0 #010002c0 #010001c0", - borderStyle: "dashed dashed solid none", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 7, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", - borderStyle: "none solid solid dashed", - borderRadius: "0px 0px 2px 0px", - isWholeLine: false, - id: 8, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010002c0", - borderStyle: "solid dashed dashed solid", - borderRadius: "2px 0px 0px 0px", - isWholeLine: false, - id: 9, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010002c0 #010001c0 #010001c0 #010001c0", - borderStyle: "solid dashed none dashed", - borderRadius: "0px 0px 0px 0px", - isWholeLine: false, - id: 10, - }, - { - backgroundColor: "#000001c0", - borderColor: "#010001c0 #010002c0 #010002c0 #010001c0", - borderStyle: "dashed solid solid dashed", - borderRadius: "0px 0px 2px 0px", - isWholeLine: false, - id: 11, - }, - ], + decorationRenderOptions: [], decorationRanges: [ { - decorationId: 3, - ranges: [ - { start: { line: 1, character: 0 }, end: { line: 1, character: 23 } }, - ], - }, - { - decorationId: 4, - ranges: [ - { start: { line: 2, character: 0 }, end: { line: 2, character: 23 } }, - ], - }, - { - decorationId: 5, - ranges: [ - { start: { line: 2, character: 23 }, end: { line: 2, character: 29 } }, - ], - }, - { - decorationId: 1, - ranges: [ - { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, - ], - }, - { - decorationId: 6, - ranges: [ - { start: { line: 4, character: 0 }, end: { line: 4, character: 1 } }, - ], - }, - { - decorationId: 7, - ranges: [ - { start: { line: 4, character: 1 }, end: { line: 4, character: 3 } }, - ], - }, - { - decorationId: 8, - ranges: [ - { start: { line: 5, character: 0 }, end: { line: 5, character: 1 } }, - ], - }, - { - decorationId: 9, - ranges: [ - { start: { line: 2, character: 2 }, end: { line: 2, character: 29 } }, - ], - }, - { - decorationId: 10, - ranges: [ - { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } }, - ], - }, - { - decorationId: 11, + decorationId: 0, ranges: [ - { start: { line: 4, character: 0 }, end: { line: 4, character: 3 } }, + { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } }, + { start: { line: 0, character: 4 }, end: { line: 0, character: 7 } }, ], }, ], @@ -227,5 +71,5 @@ const expectedUpdatedArgs: ExpectedArgs = { const expectedFinalArgs: ExpectedArgs = { decorationRenderOptions: [], decorationRanges: [], - disposedDecorationIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + disposedDecorationIds: [0], }; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts index 2a02cf787e..8a8165080a 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizer.vscode.test.ts @@ -2,6 +2,7 @@ import { commands } from "vscode"; import asyncSafety from "../../asyncSafety"; import { endToEndTestSetup } from "../../endToEndTestSetup"; import { runBasicMultilineContentTest } from "./runBasicMultilineContentTest"; +import { runBasicRemovalTest } from "./runBasicRemovalTest"; import { runNestedMultilineContentTest } from "./runNestedMultilineContentTest"; import { runUpdateTest } from "./runUpdateTest"; @@ -22,4 +23,8 @@ suite("scope visualizer", async function () { "update", asyncSafety(() => runUpdateTest()), ); + test( + "basic removal", + asyncSafety(() => runBasicRemovalTest()), + ); }); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts index d360834ebd..2e0a3b5802 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts @@ -16,9 +16,8 @@ export function* generateDifferentiatedRanges( for (const range of ranges) { currentRanges = [ - ...currentRanges.filter( - ({ range: previousRange }) => - generalizedRangeTouches(previousRange, range) != null, + ...currentRanges.filter(({ range: previousRange }) => + generalizedRangeTouches(previousRange, range), ), ]; From 9f914282f6f8b1228f3d062af0c0864957652e54 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:44:06 +0100 Subject: [PATCH 38/61] Remove provider test --- .../src/suite/scopeProvider.vscode.test.ts | 113 ------------------ 1 file changed, 113 deletions(-) delete mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts deleted file mode 100644 index cefb7135fd..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/scopeProvider.vscode.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - PlainScopeVisualization, - SpyIDE, - omitByDeep, - spyIDERecordedValuesToPlainObject, -} from "@cursorless/common"; -import { openNewEditor } from "@cursorless/vscode-common"; -import { assert } from "chai"; -import asyncSafety from "../asyncSafety"; -import { endToEndTestSetup, sleepWithBackoff } from "../endToEndTestSetup"; -import * as vscode from "vscode"; -import { isUndefined } from "lodash"; - -suite("scope visualizer", async function () { - const { getSpy } = endToEndTestSetup(this); - - test( - "basic content", - asyncSafety(() => runContentTest(getSpy()!)), - ); - test( - "basic removal", - asyncSafety(() => runRemovalTest(getSpy()!)), - ); -}); - -const initialDocumentContents = ` -function helloWorld() { - -} -`; - -const updatedDocumentContents = ` -function helloWorld() { - function nestedFunction() { - - } -} -`; - -const expectedInitialContentVisualizations: any[] = []; - -const expectedUpdatedContentVisualization = {}; - -async function runContentTest(spyIde: SpyIDE) { - const editor = await openNewEditor(initialDocumentContents, { - languageId: "typescript", - }); - - await vscode.commands.executeCommand( - "cursorless.showScopeVisualizer", - { - type: "namedFunction", - }, - "content", - ); - - const expectedVisualizations = [...expectedInitialContentVisualizations]; - checkVisualizations(spyIde, expectedVisualizations); - - await editor.edit((editBuilder) => { - editBuilder.replace( - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(2, 1)), - updatedDocumentContents, - ); - }); - await sleepWithBackoff(100); - - expectedVisualizations.push(expectedUpdatedContentVisualization); - checkVisualizations(spyIde, expectedVisualizations); - - await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); - - expectedVisualizations.push({ scopeRanges: [] }); - checkVisualizations(spyIde, expectedVisualizations); -} - -const expectedRemovalVisualizations: any[] = []; - -async function runRemovalTest(spyIde: SpyIDE) { - await openNewEditor(initialDocumentContents, { - languageId: "typescript", - }); - - await vscode.commands.executeCommand( - "cursorless.showScopeVisualizer", - { - type: "paragraph", - }, - "removal", - ); - - checkVisualizations(spyIde, expectedRemovalVisualizations); - - await vscode.commands.executeCommand("cursorless.hideScopeVisualizer"); -} - -function checkVisualizations( - spyIde: SpyIDE, - expectedVisualizations: PlainScopeVisualization[], -) { - const actualVisualizations = omitByDeep( - spyIDERecordedValuesToPlainObject(spyIde.getSpyValues(false)!) - .scopeVisualizations, - isUndefined, - ); - - assert.deepStrictEqual( - actualVisualizations, - expectedVisualizations, - JSON.stringify(actualVisualizations), - ); -} From 1b87b8c3b5708e29f8181530993d27fcaf82621a Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:59:17 +0100 Subject: [PATCH 39/61] Add light colors --- packages/cursorless-vscode/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index 5679bdeb17..9d33aa7ed9 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -490,7 +490,7 @@ }, "iteration": { "background": "#00725f6c", - "borderSolid": "#00ffd577", + "borderSolid": "#00ffd578", "borderPorous": "#00ffd525" } } @@ -571,13 +571,13 @@ "default": { "domain": { "background": "#00e1ff18", - "borderSolid": "#ebdeec84", - "borderPorous": "#ebdeec3b" + "borderSolid": "#19171984", + "borderPorous": "#1211123b" }, "content": { "background": "#ad00bc5b", "borderSolid": "#ee00ff78", - "borderPorous": "#ebdeec3b" + "borderPorous": "#ee00ff4e" }, "removal": { "background": "#ff00002d", @@ -586,7 +586,7 @@ }, "iteration": { "background": "#00725f6c", - "borderSolid": "#00ffd577", + "borderSolid": "#00ffd578", "borderPorous": "#00ffd525" } } From 853d6ca95db0550c4ff23b4339aaf191052d51d5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:01:10 +0100 Subject: [PATCH 40/61] Make spoken forms customizable --- cursorless-talon/src/cursorless.talon | 11 ++---- cursorless-talon/src/scope_visualizer.py | 49 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 cursorless-talon/src/scope_visualizer.py diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index 967789a498..2f01531a82 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -21,11 +21,6 @@ tag: user.cursorless {user.cursorless_homophone} settings: user.cursorless_show_settings_in_ide() -visualize : - user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "content") -visualize removal: - user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "removal") -visualize iteration: - user.private_cursorless_run_rpc_command_and_wait("cursorless.showScopeVisualizer", cursorless_scope_type, "iteration") -visualize nothing: - user.private_cursorless_run_rpc_command_and_wait("cursorless.hideScopeVisualizer") +{user.cursorless_show_scope_visualizer} [{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() diff --git a/cursorless-talon/src/scope_visualizer.py b/cursorless-talon/src/scope_visualizer.py new file mode 100644 index 0000000000..0c7ddda9c5 --- /dev/null +++ b/cursorless-talon/src/scope_visualizer.py @@ -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) From 0fdde7f420ce3ecc3f62b51d6c57b9c1fa6df76f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 17:03:50 +0000 Subject: [PATCH 41/61] [pre-commit.ci lite] apply automatic fixes --- cursorless-talon/src/cursorless.talon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon index 2f01531a82..615d57bb39 100644 --- a/cursorless-talon/src/cursorless.talon +++ b/cursorless-talon/src/cursorless.talon @@ -23,4 +23,5 @@ tag: user.cursorless {user.cursorless_show_scope_visualizer} [{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() +{user.cursorless_hide_scope_visualizer}: + user.private_cursorless_hide_scope_visualizer() From c9ef7f6ddc82a8928eb997ce3e409e60014078fe Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:05:09 +0100 Subject: [PATCH 42/61] Remove unused action --- cursorless-talon/src/command.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index 28e6fa4100..3e7060e852 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -127,15 +127,6 @@ def cursorless_multiple_target_command_no_wait( ), ) - def private_cursorless_run_rpc_command_and_wait( - command_id: str, arg1: Any = NotSet, arg2: Any = NotSet, arg3: Any = NotSet - ): - """Execute command via rpc and wait for command to finish.""" - run_rpc_command_and_wait( - command_id, - *[x for x in [arg1, arg2, arg3] if x is not NotSet], - ) - def construct_cursorless_command_argument( action: str, targets: list[dict], args: list[Any] From f0ca29f9592272009e35ee85c5ec96b26f392dcf Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:06:37 +0100 Subject: [PATCH 43/61] Reverts by changes --- packages/common/src/ide/spy/SpyIDE.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/common/src/ide/spy/SpyIDE.ts b/packages/common/src/ide/spy/SpyIDE.ts index 6063e36d94..716b2113f4 100644 --- a/packages/common/src/ide/spy/SpyIDE.ts +++ b/packages/common/src/ide/spy/SpyIDE.ts @@ -4,10 +4,6 @@ import { TextEditor } from "../../types/TextEditor"; import PassthroughIDEBase from "../PassthroughIDEBase"; import { FlashDescriptor } from "../types/FlashDescriptor"; import type { HighlightId, IDE } from "../types/ide.types"; -import type { - IterationScopeRanges, - ScopeRanges, -} from "../types/IdeScopeVisualizer"; import SpyMessages, { Message } from "./SpyMessages"; interface Highlight { @@ -15,23 +11,16 @@ interface Highlight { ranges: GeneralizedRange[]; } -interface ScopeVisualization { - scopeRanges: ScopeRanges[] | undefined; - iterationScopeRanges: IterationScopeRanges[] | undefined; -} - export interface SpyIDERecordedValues { messages?: Message[]; flashes?: FlashDescriptor[]; highlights?: Highlight[]; - scopeVisualizations?: ScopeVisualization[]; } export default class SpyIDE extends PassthroughIDEBase { messages: SpyMessages; private flashes: FlashDescriptor[] = []; private highlights: Highlight[] = []; - private scopeVisualizations: ScopeVisualization[] = []; constructor(original: IDE) { super(original); @@ -43,10 +32,6 @@ export default class SpyIDE extends PassthroughIDEBase { messages: this.messages.getSpyValues(), flashes: isFlashTest ? this.flashes : undefined, highlights: this.highlights.length === 0 ? undefined : this.highlights, - scopeVisualizations: - this.scopeVisualizations.length === 0 - ? undefined - : this.scopeVisualizations, }; return values(ret).every((value) => value == null) @@ -70,12 +55,4 @@ export default class SpyIDE extends PassthroughIDEBase { }); return super.setHighlightRanges(highlightId, editor, ranges); } - - async setScopeVisualizationRanges( - editor: TextEditor, - scopeRanges: ScopeRanges[] | undefined, - iterationScopeRanges: IterationScopeRanges[] | undefined, - ): Promise { - this.scopeVisualizations.push({ scopeRanges, iterationScopeRanges }); - } } From 1e0fa307e6e9ef56a0e6843de38a77283928e493 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:19:15 +0100 Subject: [PATCH 44/61] cleanup --- .../src/ide/types/IdeScopeVisualizer.ts | 41 ----- packages/common/src/index.ts | 1 - packages/common/src/testUtil/toPlainObject.ts | 47 ------ .../src/CursorlessEngineApi.ts | 78 --------- .../src/ScopeVisualizer/ScopeRangeProvider.ts | 4 +- .../src/ScopeVisualizer/ScopeRangeWatcher.ts | 6 +- .../ScopeVisualizer/ScopeSupportChecker.ts | 2 +- .../src/ScopeVisualizer/getIterationScopes.ts | 12 +- .../src/ScopeVisualizer/getScopes.ts | 8 +- .../src/api/CursorlessEngineApi.ts | 37 +++++ .../src/api/ScopeProvider.ts | 75 +++++++++ .../cursorless-engine/src/cursorlessEngine.ts | 16 +- packages/cursorless-engine/src/index.ts | 3 +- .../upgradeDecorations.ts | 1 - .../getDecorationRanges.test.ts | 96 ------------ .../getDifferentiatedRanges.test.ts | 148 ------------------ .../VscodeScopeTargetVisualizer.ts | 9 +- .../getColorsFromConfig.ts | 6 +- packages/cursorless-vscode/tsconfig.json | 8 +- 19 files changed, 138 insertions(+), 460 deletions(-) delete mode 100644 packages/common/src/ide/types/IdeScopeVisualizer.ts delete mode 100644 packages/cursorless-engine/src/CursorlessEngineApi.ts create mode 100644 packages/cursorless-engine/src/api/CursorlessEngineApi.ts create mode 100644 packages/cursorless-engine/src/api/ScopeProvider.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts diff --git a/packages/common/src/ide/types/IdeScopeVisualizer.ts b/packages/common/src/ide/types/IdeScopeVisualizer.ts deleted file mode 100644 index 4f98915c2b..0000000000 --- a/packages/common/src/ide/types/IdeScopeVisualizer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ScopeType, TextEditor } from "../.."; -import { GeneralizedRange } from "../../types/GeneralizedRange"; - -interface ScopeRangeConfigBase { - visibleOnly: boolean; - scopeType: ScopeType; -} - -export type ScopeRangeConfig = ScopeRangeConfigBase; - -export interface IterationScopeRangeConfig extends ScopeRangeConfigBase { - includeNestedTargets: boolean; -} - -export type ScopeChangeEventCallback = ( - editor: TextEditor, - scopeRanges: ScopeRanges[], -) => void; - -export type IterationScopeChangeEventCallback = ( - editor: TextEditor, - scopeRanges: IterationScopeRanges[], -) => void; - -export interface ScopeRanges { - domain: GeneralizedRange; - targets: TargetRanges[]; -} - -export interface TargetRanges { - contentRange: GeneralizedRange; - removalRange: GeneralizedRange; -} - -export interface IterationScopeRanges { - domain: GeneralizedRange; - ranges: { - range: GeneralizedRange; - targets?: TargetRanges[]; - }[]; -} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 1a6817a74e..2f98a969b4 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -18,7 +18,6 @@ export * from "./util/walkAsync"; export { Listener, Notifier } from "./util/Notifier"; export { TokenHatSplittingMode } from "./ide/types/Configuration"; export * from "./ide/types/ide.types"; -export * from "./ide/types/IdeScopeVisualizer"; export * from "./ide/types/Capabilities"; export * from "./ide/types/CommandId"; export * from "./ide/types/FlashDescriptor"; diff --git a/packages/common/src/testUtil/toPlainObject.ts b/packages/common/src/testUtil/toPlainObject.ts index 47edc8ee65..3445756c35 100644 --- a/packages/common/src/testUtil/toPlainObject.ts +++ b/packages/common/src/testUtil/toPlainObject.ts @@ -4,7 +4,6 @@ import type { LineRange, Message, SpyIDERecordedValues, - TargetRanges, } from ".."; import { FlashStyle, isLineRange } from ".."; import { Token } from "../types/Token"; @@ -38,34 +37,10 @@ interface PlainHighlight { ranges: GeneralizedRangePlainObject[]; } -interface PlainScopeRanges { - domain: GeneralizedRangePlainObject; - targets: PlainTargetRanges[]; -} - -interface PlainIterationScopeRanges { - domain: GeneralizedRangePlainObject; - ranges: { - range: GeneralizedRangePlainObject; - targets: PlainTargetRanges[] | undefined; - }[]; -} - -interface PlainTargetRanges { - contentRange: GeneralizedRangePlainObject; - removalRange: GeneralizedRangePlainObject; -} - -export interface PlainScopeVisualization { - scopeRanges: PlainScopeRanges[] | undefined; - iterationScopeRanges: PlainIterationScopeRanges[] | undefined; -} - export interface PlainSpyIDERecordedValues { messages: Message[] | undefined; flashes: PlainFlashDescriptor[] | undefined; highlights: PlainHighlight[] | undefined; - scopeVisualizations: PlainScopeVisualization[] | undefined; } export type SelectionPlainObject = { @@ -183,27 +158,5 @@ export function spyIDERecordedValuesToPlainObject( generalizedRangeToPlainObject(range), ), })), - scopeVisualizations: input.scopeVisualizations?.map( - ({ scopeRanges, iterationScopeRanges }) => ({ - scopeRanges: scopeRanges?.map((scopeRange) => ({ - domain: generalizedRangeToPlainObject(scopeRange.domain), - targets: scopeRange.targets?.map(targetRangesToPlainObject), - })), - iterationScopeRanges: iterationScopeRanges?.map((scopeRange) => ({ - domain: generalizedRangeToPlainObject(scopeRange.domain), - ranges: scopeRange.ranges.map(({ range, targets }) => ({ - range: generalizedRangeToPlainObject(range), - targets: targets?.map(targetRangesToPlainObject), - })), - })), - }), - ), - }; -} - -export function targetRangesToPlainObject(target: TargetRanges) { - return { - contentRange: generalizedRangeToPlainObject(target.contentRange), - removalRange: generalizedRangeToPlainObject(target.removalRange), }; } diff --git a/packages/cursorless-engine/src/CursorlessEngineApi.ts b/packages/cursorless-engine/src/CursorlessEngineApi.ts deleted file mode 100644 index 0c70e1abcb..0000000000 --- a/packages/cursorless-engine/src/CursorlessEngineApi.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Command, - Disposable, - HatTokenMap, - IDE, - IterationScopeChangeEventCallback, - IterationScopeRangeConfig, - IterationScopeRanges, - ScopeChangeEventCallback, - ScopeRangeConfig, - ScopeRanges, - ScopeType, - TextEditor, -} from "@cursorless/common"; -import { Snippets } from "./core/Snippets"; -import { StoredTargetMap } from "./core/StoredTargets"; -import { TestCaseRecorder } from "./testCaseRecorder/TestCaseRecorder"; - -export interface CursorlessEngine { - commandApi: CommandApi; - scopeProvider: ScopeProvider; - testCaseRecorder: TestCaseRecorder; - storedTargets: StoredTargetMap; - hatTokenMap: HatTokenMap; - snippets: Snippets; - injectIde: (ide: IDE | undefined) => void; - runIntegrationTests: () => Promise; -} - -export interface CommandApi { - /** - * Runs a command. This is the core of the Cursorless engine. - * @param command The command to run - */ - runCommand(command: Command): Promise; - - /** - * Designed to run commands that come directly from the user. Ensures that - * the command args are of the correct shape. - */ - runCommandSafe(...args: unknown[]): Promise; -} - -export interface ScopeProvider { - provideScopeRanges: ( - editor: TextEditor, - config: ScopeRangeConfig, - ) => ScopeRanges[]; - - provideIterationScopeRanges: ( - editor: TextEditor, - config: IterationScopeRangeConfig, - ) => IterationScopeRanges[]; - - onDidChangeScopeRanges: ( - callback: ScopeChangeEventCallback, - config: ScopeRangeConfig, - ) => Disposable; - - onDidChangeIterationScopeRanges: ( - callback: IterationScopeChangeEventCallback, - config: IterationScopeRangeConfig, - ) => Disposable; - - getScopeSupport: (editor: TextEditor, scopeType: ScopeType) => ScopeSupport; - - getIterationScopeSupport: ( - editor: TextEditor, - scopeType: ScopeType, - ) => ScopeSupport; -} - -export enum ScopeSupport { - supportedAndPresentInEditor, - supportedButNotPresentInEditor, - supportedLegacy, - unsupported, -} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts index 59843bf8a3..d6b693a045 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts @@ -1,9 +1,9 @@ +import { TextEditor } from "@cursorless/common"; import { IterationScopeRangeConfig, IterationScopeRanges, ScopeRangeConfig, - TextEditor, -} from "@cursorless/common"; +} from ".."; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { getIterationRange } from "./getIterationRange"; diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts index d1e2f33a39..f7964d7fef 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts @@ -1,11 +1,11 @@ +import { Disposable } from "@cursorless/common"; +import { pull } from "lodash"; import { - Disposable, IterationScopeChangeEventCallback, IterationScopeRangeConfig, ScopeChangeEventCallback, ScopeRangeConfig, -} from "@cursorless/common"; -import { pull } from "lodash"; +} from ".."; import { Debouncer } from "../core/Debouncer"; import { ide } from "../singletons/ide.singleton"; import { ScopeRangeProvider } from "./ScopeRangeProvider"; diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts index 7119a18422..9c982c2062 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts @@ -8,7 +8,7 @@ import { LegacyLanguageId } from "../languages/LegacyLanguageId"; import { languageMatchers } from "../languages/getNodeMatcher"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; -import { ScopeSupport } from "../CursorlessEngineApi"; +import { ScopeSupport } from "../api/CursorlessEngineApi"; export class ScopeSupportChecker { constructor(private scopeHandlerFactory: ScopeHandlerFactory) { diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts index ba37b22185..74fb954d42 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts @@ -1,14 +1,10 @@ -import { - IterationScopeRanges, - Range, - TextEditor, - toCharacterRange, -} from "@cursorless/common"; +import { Range, TextEditor, toCharacterRange } from "@cursorless/common"; import { map } from "itertools"; -import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; -import { getTargetRanges } from "./getTargetRanges"; +import { IterationScopeRanges } from ".."; import { ModifierStage } from "../processTargets/PipelineStages.types"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; import { Target } from "../typings/target.types"; +import { getTargetRanges } from "./getTargetRanges"; export function getIterationScopes( editor: TextEditor, diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts index e39ca9830d..f05ac1d986 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts @@ -1,10 +1,6 @@ -import { - Range, - ScopeRanges, - TextEditor, - toCharacterRange, -} from "@cursorless/common"; +import { Range, TextEditor, toCharacterRange } from "@cursorless/common"; import { map } from "itertools"; +import { ScopeRanges } from ".."; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; import { getTargetRanges } from "./getTargetRanges"; diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts new file mode 100644 index 0000000000..e66acd84a9 --- /dev/null +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -0,0 +1,37 @@ +import { Command, HatTokenMap, IDE } from "@cursorless/common"; +import { Snippets } from "../core/Snippets"; +import { StoredTargetMap } from "../core/StoredTargets"; +import { TestCaseRecorder } from "../testCaseRecorder/TestCaseRecorder"; +import { ScopeProvider } from "./ScopeProvider"; + +export interface CursorlessEngine { + commandApi: CommandApi; + scopeProvider: ScopeProvider; + testCaseRecorder: TestCaseRecorder; + storedTargets: StoredTargetMap; + hatTokenMap: HatTokenMap; + snippets: Snippets; + injectIde: (ide: IDE | undefined) => void; + runIntegrationTests: () => Promise; +} + +export interface CommandApi { + /** + * Runs a command. This is the core of the Cursorless engine. + * @param command The command to run + */ + runCommand(command: Command): Promise; + + /** + * Designed to run commands that come directly from the user. Ensures that + * the command args are of the correct shape. + */ + runCommandSafe(...args: unknown[]): Promise; +} + +export enum ScopeSupport { + supportedAndPresentInEditor, + supportedButNotPresentInEditor, + supportedLegacy, + unsupported, +} diff --git a/packages/cursorless-engine/src/api/ScopeProvider.ts b/packages/cursorless-engine/src/api/ScopeProvider.ts new file mode 100644 index 0000000000..b577f1e8f9 --- /dev/null +++ b/packages/cursorless-engine/src/api/ScopeProvider.ts @@ -0,0 +1,75 @@ +import { + Disposable, + GeneralizedRange, + ScopeType, + TextEditor, +} from "@cursorless/common"; +import { ScopeSupport } from "./CursorlessEngineApi"; + +export interface ScopeProvider { + provideScopeRanges: ( + editor: TextEditor, + config: ScopeRangeConfig, + ) => ScopeRanges[]; + + provideIterationScopeRanges: ( + editor: TextEditor, + config: IterationScopeRangeConfig, + ) => IterationScopeRanges[]; + + onDidChangeScopeRanges: ( + callback: ScopeChangeEventCallback, + config: ScopeRangeConfig, + ) => Disposable; + + onDidChangeIterationScopeRanges: ( + callback: IterationScopeChangeEventCallback, + config: IterationScopeRangeConfig, + ) => Disposable; + + getScopeSupport: (editor: TextEditor, scopeType: ScopeType) => ScopeSupport; + + getIterationScopeSupport: ( + editor: TextEditor, + scopeType: ScopeType, + ) => ScopeSupport; +} + +interface ScopeRangeConfigBase { + visibleOnly: boolean; + scopeType: ScopeType; +} + +export type ScopeRangeConfig = ScopeRangeConfigBase; + +export interface IterationScopeRangeConfig extends ScopeRangeConfigBase { + includeNestedTargets: boolean; +} + +export type ScopeChangeEventCallback = ( + editor: TextEditor, + scopeRanges: ScopeRanges[], +) => void; + +export type IterationScopeChangeEventCallback = ( + editor: TextEditor, + scopeRanges: IterationScopeRanges[], +) => void; + +export interface ScopeRanges { + domain: GeneralizedRange; + targets: TargetRanges[]; +} + +export interface TargetRanges { + contentRange: GeneralizedRange; + removalRange: GeneralizedRange; +} + +export interface IterationScopeRanges { + domain: GeneralizedRange; + ranges: { + range: GeneralizedRange; + targets?: TargetRanges[]; + }[]; +} diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index b973d2dd07..030ab27a7f 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,25 +1,21 @@ import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; -import { - ScopeProvider, - StoredTargetMap, - TestCaseRecorder, - TreeSitter, -} from "."; -import { CursorlessEngine } from "./CursorlessEngineApi"; +import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; +import { CursorlessEngine } from "./api/CursorlessEngineApi"; +import { ScopeProvider } from "./api/ScopeProvider"; import { ScopeRangeWatcher } from "./ScopeVisualizer"; +import { ScopeRangeProvider } from "./ScopeVisualizer/ScopeRangeProvider"; +import { ScopeSupportChecker } from "./ScopeVisualizer/ScopeSupportChecker"; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; +import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; -import { ScopeRangeProvider } from "./ScopeVisualizer/ScopeRangeProvider"; -import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; -import { ScopeSupportChecker } from "./ScopeVisualizer/ScopeSupportChecker"; export function createCursorlessEngine( treeSitter: TreeSitter, diff --git a/packages/cursorless-engine/src/index.ts b/packages/cursorless-engine/src/index.ts index d011f0acfc..45b5881824 100644 --- a/packages/cursorless-engine/src/index.ts +++ b/packages/cursorless-engine/src/index.ts @@ -5,4 +5,5 @@ export * from "./testCaseRecorder/TestCaseRecorder"; export * from "./core/StoredTargets"; export * from "./typings/TreeSitter"; export * from "./cursorlessEngine"; -export * from "./CursorlessEngineApi"; +export * from "./api/CursorlessEngineApi"; +export * from "./api/ScopeProvider"; diff --git a/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts b/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts index 44ad6609a6..9028af461b 100644 --- a/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts +++ b/packages/cursorless-engine/src/scripts/transformRecordedTests/upgradeDecorations.ts @@ -62,7 +62,6 @@ export const upgradeDecorations: FixtureTransformation = ( range: extractHighlightRange(flash), style: extractHighlightName(flash.name) as keyof typeof FlashStyle, })), - scopeVisualizations: undefined, }; return reorderFields(fixture as TestCaseFixture); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts deleted file mode 100644 index 91e6a954f9..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import assert = require("assert"); -import { getDecorationRanges } from "./getDecorationRanges"; -import { - DecorationStyle, - DifferentiatedStyle, -} from "./getDecorationRanges.types"; -import { - Range, - RangePlainObject, - rangeToPlainObject, -} from "@cursorless/common"; - -interface RangeDescription { - startLine?: number; - endLine?: number; - lineCharOffsets: [number, number][]; -} - -export interface ExpectedResult { - styleParameters: DifferentiatedStyle; - ranges: RangePlainObject[]; -} - -interface TestCase { - name: string; - ranges: RangeDescription[]; - expectedDecorations: ExpectedResult[]; -} - -const testCases: TestCase[] = [ - { - name: "should handle simple case", - ranges: [ - { - lineCharOffsets: [[0, 1]], - }, - ], - expectedDecorations: [ - { - styleParameters: { - style: { - top: true, - right: true, - bottom: true, - left: true, - }, - differentiationIndex: 0, - }, - ranges: [ - { - start: { - line: 0, - character: 0, - }, - end: { - line: 0, - character: 1, - }, - }, - ], - }, - ], - }, -]; - -suite("getDecorationRanges", function () { - for (const testCase of testCases) { - test(testCase.name, function () { - const actualDecorations = getDecorationRanges( - { - document: { - lineCount: testCase.ranges.length, - }, - } as any, - testCase.ranges.map( - ({ startLine, endLine, lineCharOffsets }) => - new Range( - startLine ?? 0, - lineCharOffsets[0][0], - endLine ?? 0, - lineCharOffsets[lineCharOffsets.length - 1][1], - ), - ), - ); - - assert.deepStrictEqual( - actualDecorations.map(({ styleParameters, ranges }) => ({ - styleParameters, - ranges: ranges.map(rangeToPlainObject), - })), - - testCase.expectedDecorations, - ); - }); - } -}); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts deleted file mode 100644 index d357a1af8b..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedRanges.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import assert = require("assert"); -import { DifferentiatedStyle } from "./getDecorationRanges.types"; -import { Position, Range } from "@cursorless/common"; -import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; - -type Offsets = [number, number]; - -interface ExpectedResult { - styleParameters: DifferentiatedStyle; - ranges: Offsets[]; -} - -interface TestStyledRange { - range: Offsets; - style: number; -} - -interface TestCase { - name: string; - ranges: TestStyledRange[]; - expectedDecorations: ExpectedResult[]; -} - -const testCases: TestCase[] = [ - { - name: "should handle simple case", - ranges: [ - { - range: [0, 1], - style: 0, - }, - ], - expectedDecorations: [ - { - styleParameters: { - style: 0, - differentiationIndex: 0, - }, - ranges: [[0, 1]], - }, - ], - }, - - { - name: "should handle adjacent ranges", - ranges: [ - { - range: [0, 1], - style: 0, - }, - { - range: [1, 2], - style: 0, - }, - { - range: [2, 3], - style: 0, - }, - ], - expectedDecorations: [ - { - styleParameters: { - style: 0, - differentiationIndex: 0, - }, - ranges: [ - [0, 1], - [2, 3], - ], - }, - { - styleParameters: { - style: 0, - differentiationIndex: 1, - }, - ranges: [[1, 2]], - }, - ], - }, - - { - name: "should handle nested ranges", - ranges: [ - { - range: [0, 1], - style: 0, - }, - { - range: [2, 3], - style: 0, - }, - { - range: [0, 3], - style: 0, - }, - ], - expectedDecorations: [ - { - styleParameters: { - style: 0, - differentiationIndex: 0, - }, - ranges: [ - [0, 1], - [2, 3], - ], - }, - { - styleParameters: { - style: 0, - differentiationIndex: 1, - }, - ranges: [[0, 3]], - }, - ], - }, -]; - -suite("getDecorationRanges", function () { - for (const testCase of testCases) { - test(testCase.name, function () { - const actualDecorations = generateDifferentiatedRanges( - testCase.ranges.map(({ range, style }) => ({ - range: toRange(range), - style, - })), - (style) => [style], - ).map(({ styleParameters, ranges }) => ({ - styleParameters, - ranges: ranges.map(fromRange), - })); - - assert.deepStrictEqual(actualDecorations, testCase.expectedDecorations); - }); - } -}); - -function fromRange(range: Range): Offsets { - return [fromPosition(range.start), fromPosition(range.end)]; -} - -function fromPosition(position: Position): number { - return position.character; -} - -function toRange([start, end]: Offsets): Range { - return new Range(0, start, 0, end); -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts index 75d73b837a..4459515048 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts @@ -1,10 +1,5 @@ -import { - Disposable, - GeneralizedRange, - TargetRanges, - TextEditor, -} from "@cursorless/common"; -import { ScopeSupport } from "@cursorless/cursorless-engine"; +import { Disposable, GeneralizedRange, TextEditor } from "@cursorless/common"; +import { ScopeSupport, TargetRanges } from "@cursorless/cursorless-engine"; import { VscodeScopeVisualizer } from "."; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts index 9ef5c4589e..862767b313 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/getColorsFromConfig.ts @@ -1,8 +1,8 @@ -import { RangeTypeColors } from "./RangeTypeColors"; import { - ScopeVisualizerColorConfig, ScopeRangeType, -} from "@cursorless/vscode-common/src/ScopeVisualizerColorConfig"; + ScopeVisualizerColorConfig, +} from "@cursorless/vscode-common"; +import { RangeTypeColors } from "./RangeTypeColors"; export function getColorsFromConfig( config: ScopeVisualizerColorConfig, diff --git a/packages/cursorless-vscode/tsconfig.json b/packages/cursorless-vscode/tsconfig.json index e0807eeeb4..7564228e28 100644 --- a/packages/cursorless-vscode/tsconfig.json +++ b/packages/cursorless-vscode/tsconfig.json @@ -5,13 +5,7 @@ "outDir": "out", "rootDir": "src" }, - "include": [ - "src/**/*.ts", - "src/**/*.json", - "../../typings/**/*.d.ts", - "../vscode-common/src/vscode.ts", - "../vscode-common/src/ScopeVisualizerColorConfig.ts" - ], + "include": ["src/**/*.ts", "src/**/*.json", "../../typings/**/*.d.ts"], "references": [ { "path": "../common" From 583eedc86c22485342ea23ec96d3e2ab754bee6a Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:20:39 +0100 Subject: [PATCH 45/61] Docs --- packages/common/src/testUtil/toPlainObject.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/common/src/testUtil/toPlainObject.ts b/packages/common/src/testUtil/toPlainObject.ts index 3445756c35..626378603d 100644 --- a/packages/common/src/testUtil/toPlainObject.ts +++ b/packages/common/src/testUtil/toPlainObject.ts @@ -83,11 +83,17 @@ export type SerializedMarks = { [decoratedCharacter: string]: 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; From 8139ac0ac7b30e4bba1edb9967e976ccf0c8072f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:55:36 +0100 Subject: [PATCH 46/61] Add tests --- .../types/generalizedRangeContains.test.ts | 163 ++++++++++++++++++ .../src/types/generalizedRangeTouches.test.ts | 139 +++++++++++++++ .../src/types/isGeneralizedRangeEqual.test.ts | 117 +++++++++++++ 3 files changed, 419 insertions(+) create mode 100644 packages/common/src/types/generalizedRangeContains.test.ts create mode 100644 packages/common/src/types/generalizedRangeTouches.test.ts create mode 100644 packages/common/src/types/isGeneralizedRangeEqual.test.ts diff --git a/packages/common/src/types/generalizedRangeContains.test.ts b/packages/common/src/types/generalizedRangeContains.test.ts new file mode 100644 index 0000000000..470d5d819f --- /dev/null +++ b/packages/common/src/types/generalizedRangeContains.test.ts @@ -0,0 +1,163 @@ +import assert = require("assert"); +import { generalizedRangeContains, Position } from ".."; + +suite("generalizedRangeContains", () => { + test("character", () => { + assert.strictEqual( + generalizedRangeContains( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + ), + true, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 1), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + ), + true, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 1), + }, + ), + false, + ); + }); + + test("line", () => { + assert.strictEqual( + generalizedRangeContains( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "line", + start: 0, + end: 0, + }, + ), + true, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "line", + start: 0, + end: 1, + }, + { + type: "line", + start: 0, + end: 0, + }, + ), + true, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "line", + start: 1, + end: 1, + }, + ), + false, + ); + }); + + test("mixed", () => { + assert.strictEqual( + generalizedRangeContains( + { + type: "line", + start: 0, + end: 1, + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(1, 1), + }, + ), + true, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(1, 0), + }, + ), + false, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "character", + start: new Position(0, 0), + end: new Position(2, 0), + }, + { + type: "line", + start: 1, + end: 1, + }, + ), + true, + ); + assert.strictEqual( + generalizedRangeContains( + { + type: "character", + start: new Position(0, 0), + end: new Position(1, 0), + }, + { + type: "line", + start: 1, + end: 1, + }, + ), + false, + ); + }); +}); diff --git a/packages/common/src/types/generalizedRangeTouches.test.ts b/packages/common/src/types/generalizedRangeTouches.test.ts new file mode 100644 index 0000000000..d5d595eab0 --- /dev/null +++ b/packages/common/src/types/generalizedRangeTouches.test.ts @@ -0,0 +1,139 @@ +import assert = require("assert"); +import { GeneralizedRange, generalizedRangeTouches, Position } from ".."; + +suite("generalizedRangeTouches", () => { + test("character", () => { + testRangePair( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + true, + ); + testRangePair( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 1), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + true, + ); + testRangePair( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 1), + }, + { + type: "character", + start: new Position(0, 1), + end: new Position(0, 2), + }, + true, + ); + testRangePair( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 1), + end: new Position(0, 1), + }, + false, + ); + }); + + test("line", () => { + testRangePair( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "line", + start: 0, + end: 0, + }, + true, + ); + testRangePair( + { + type: "line", + start: 0, + end: 1, + }, + { + type: "line", + start: 0, + end: 0, + }, + true, + ); + testRangePair( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "line", + start: 1, + end: 1, + }, + false, + ); + }); + + test("mixed", () => { + testRangePair( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(1, 1), + }, + true, + ); + testRangePair( + { + type: "line", + start: 0, + end: 0, + }, + { + type: "character", + start: new Position(1, 0), + end: new Position(1, 1), + }, + false, + ); + }); +}); + +function testRangePair( + a: GeneralizedRange, + b: GeneralizedRange, + expected: boolean, +) { + assert.strictEqual(generalizedRangeTouches(a, b), expected); + assert.strictEqual(generalizedRangeTouches(b, a), expected); +} diff --git a/packages/common/src/types/isGeneralizedRangeEqual.test.ts b/packages/common/src/types/isGeneralizedRangeEqual.test.ts new file mode 100644 index 0000000000..8efcd8de11 --- /dev/null +++ b/packages/common/src/types/isGeneralizedRangeEqual.test.ts @@ -0,0 +1,117 @@ +import assert = require("assert"); +import { isGeneralizedRangeEqual } from "./GeneralizedRange"; +import { Position } from "./Position"; + +suite("isGeneralizedRangeEqual", () => { + test("character", () => { + assert.strictEqual( + isGeneralizedRangeEqual( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + ), + true, + ); + assert.strictEqual( + isGeneralizedRangeEqual( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 1), + }, + ), + false, + ); + assert.strictEqual( + isGeneralizedRangeEqual( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 0), + end: new Position(1, 0), + }, + ), + false, + ); + assert.strictEqual( + isGeneralizedRangeEqual( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { + type: "character", + start: new Position(0, 1), + end: new Position(0, 0), + }, + ), + false, + ); + }); + + test("line", () => { + assert.strictEqual( + isGeneralizedRangeEqual( + { type: "line", start: 0, end: 0 }, + { type: "line", start: 0, end: 0 }, + ), + true, + ); + assert.strictEqual( + isGeneralizedRangeEqual( + { type: "line", start: 0, end: 0 }, + { type: "line", start: 0, end: 1 }, + ), + false, + ); + assert.strictEqual( + isGeneralizedRangeEqual( + { type: "line", start: 0, end: 0 }, + { type: "line", start: 1, end: 0 }, + ), + false, + ); + }); + + test("mixed", () => { + assert.strictEqual( + isGeneralizedRangeEqual( + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + { type: "line", start: 0, end: 0 }, + ), + false, + ); + assert.strictEqual( + isGeneralizedRangeEqual( + { type: "line", start: 0, end: 0 }, + { + type: "character", + start: new Position(0, 0), + end: new Position(0, 0), + }, + ), + false, + ); + }); +}); From c4c84196e4ded18f9a1ffa2aba47b8dbb45c41f4 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 18:57:16 +0100 Subject: [PATCH 47/61] Cleanup --- packages/common/src/types/GeneralizedRange.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/common/src/types/GeneralizedRange.ts b/packages/common/src/types/GeneralizedRange.ts index d538f86ab8..2bd446d5b4 100644 --- a/packages/common/src/types/GeneralizedRange.ts +++ b/packages/common/src/types/GeneralizedRange.ts @@ -112,8 +112,6 @@ export function generalizedRangeTouches( } // a.type === "character" && b.type === "line" - // Require that the line range is fully contained in the character range - // because otherwise it visually looks like the line range is not contained return a.start.line <= b.end && a.end.line >= b.start; } From a21fedc96099eb14eaa69df1c7774a0b2112adf5 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:03:53 +0100 Subject: [PATCH 48/61] Add ja stocks --- packages/common/src/types/GeneralizedRange.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/common/src/types/GeneralizedRange.ts b/packages/common/src/types/GeneralizedRange.ts index 2bd446d5b4..1161d7b01c 100644 --- a/packages/common/src/types/GeneralizedRange.ts +++ b/packages/common/src/types/GeneralizedRange.ts @@ -76,6 +76,20 @@ export function isGeneralizedRangeEqual( return false; } +/** + * Determines whether {@link a} contains {@link b}. This is true if {@link a} + * starts before or equal to the start of {@link b} and ends after or equal to + * the end of {@link b}. + * + * Note that if {@link a} is a {@link CharacterRange} and {@link b} is a + * {@link LineRange}, we require that the {@link LineRange} is fully contained + * in the {@link CharacterRange}, because otherwise it visually looks like the + * {@link LineRange} is not contained because the line range extends to the edge + * of the screen. + * @param a A generalized range + * @param b A generalized range + * @returns `true` if `a` contains `b`, `false` otherwise + */ export function generalizedRangeContains( a: GeneralizedRange, b: GeneralizedRange, @@ -101,6 +115,17 @@ export function generalizedRangeContains( return a.start <= b.start.line && a.end >= b.end.line; } +/** + * Determines whether {@link a} touches {@link b}. This is true if {@link a} + * has any intersection with {@link b}, even if the intersection is empty. + * + * In the case where one range is a {@link CharacterRange} and the other is a + * {@link LineRange}, we return `true` if they both include at least one line + * in common. + * @param a A generalized range + * @param b A generalized range + * @returns `true` if `a` touches `b`, `false` otherwise + */ export function generalizedRangeTouches( a: GeneralizedRange, b: GeneralizedRange, From 2b370ee1b092055607378ca0d068edce5727c4c2 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:07:05 +0100 Subject: [PATCH 49/61] River run test subset --- packages/common/src/testUtil/runTestSubset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/testUtil/runTestSubset.ts b/packages/common/src/testUtil/runTestSubset.ts index f865174a0b..feb220b211 100644 --- a/packages/common/src/testUtil/runTestSubset.ts +++ b/packages/common/src/testUtil/runTestSubset.ts @@ -4,7 +4,7 @@ * configuration. * See https://mochajs.org/#-grep-regexp-g-regexp for supported syntax */ -export const TEST_SUBSET_GREP_STRING = "visualizer"; +export const TEST_SUBSET_GREP_STRING = "snippets"; /** * Determine whether we should run just the subset of the tests specified by From 5869d760727ec5058650c2f81580fae2f7f66f30 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:49:00 +0100 Subject: [PATCH 50/61] Tweaks --- .../src/ScopeVisualizer/ScopeRangeProvider.ts | 22 ++++++------- .../src/ScopeVisualizer/ScopeRangeWatcher.ts | 27 +++++++++++++--- .../ScopeVisualizer/ScopeSupportChecker.ts | 32 +++++++++++++++---- .../src/ScopeVisualizer/getIterationRange.ts | 13 +++++--- ...onScopes.ts => getIterationScopeRanges.ts} | 13 +++++++- .../{getScopes.ts => getScopeRanges.ts} | 10 +++++- .../src/ScopeVisualizer/getTargetRanges.ts | 3 +- .../src/api/CursorlessEngineApi.ts | 7 ---- .../src/api/ScopeProvider.ts | 8 ++++- 9 files changed, 98 insertions(+), 37 deletions(-) rename packages/cursorless-engine/src/ScopeVisualizer/{getIterationScopes.ts => getIterationScopeRanges.ts} (71%) rename packages/cursorless-engine/src/ScopeVisualizer/{getScopes.ts => getScopeRanges.ts} (66%) diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts index d6b693a045..62f39039f9 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeProvider.ts @@ -3,13 +3,17 @@ import { IterationScopeRangeConfig, IterationScopeRanges, ScopeRangeConfig, + ScopeRanges, } from ".."; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { getIterationRange } from "./getIterationRange"; -import { getIterationScopes } from "./getIterationScopes"; -import { getScopes } from "./getScopes"; +import { getIterationScopeRanges } from "./getIterationScopeRanges"; +import { getScopeRanges } from "./getScopeRanges"; +/** + * Provides scope ranges for a given editor to use eg for visualizing scopes + */ export class ScopeRangeProvider { constructor( private scopeHandlerFactory: ScopeHandlerFactory, @@ -23,7 +27,7 @@ export class ScopeRangeProvider { provideScopeRanges( editor: TextEditor, { scopeType, visibleOnly }: ScopeRangeConfig, - ) { + ): ScopeRanges[] { const scopeHandler = this.scopeHandlerFactory.create( scopeType, editor.document.languageId, @@ -33,7 +37,7 @@ export class ScopeRangeProvider { return []; } - return getScopes( + return getScopeRanges( editor, scopeHandler, getIterationRange(editor, scopeHandler, visibleOnly), @@ -42,11 +46,7 @@ export class ScopeRangeProvider { provideIterationScopeRanges( editor: TextEditor, - { - scopeType, - visibleOnly, - includeNestedTargets: includeIterationNestedTargets, - }: IterationScopeRangeConfig, + { scopeType, visibleOnly, includeNestedTargets }: IterationScopeRangeConfig, ): IterationScopeRanges[] { const { languageId } = editor.document; const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); @@ -64,7 +64,7 @@ export class ScopeRangeProvider { return []; } - return getIterationScopes( + return getIterationScopeRanges( editor, iterationScopeHandler, this.modifierStageFactory.create({ @@ -72,7 +72,7 @@ export class ScopeRangeProvider { scopeType, }), getIterationRange(editor, scopeHandler, visibleOnly), - includeIterationNestedTargets, + includeNestedTargets, ); } } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts index f7964d7fef..c91b1df70f 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts @@ -10,12 +10,14 @@ import { Debouncer } from "../core/Debouncer"; import { ide } from "../singletons/ide.singleton"; import { ScopeRangeProvider } from "./ScopeRangeProvider"; -type Listener = () => void; - +/** + * Watches for changes to the scope ranges of visible editors and notifies + * listeners when they change. + */ export class ScopeRangeWatcher { private disposables: Disposable[] = []; private debouncer = new Debouncer(() => this.onChange()); - private listeners: Listener[] = []; + private listeners: (() => void)[] = []; constructor(private scopeRangeProvider: ScopeRangeProvider) { this.disposables.push( @@ -38,6 +40,14 @@ export class ScopeRangeWatcher { this.onDidChangeIterationScopeRanges.bind(this); } + /** + * Registers a callback to be run when the scope ranges change for any visible + * editor. The callback will be run immediately once for each visible editor + * with the current scope ranges. + * @param callback The callback to run when the scope ranges change + * @param config The configuration for the scope ranges + * @returns A {@link Disposable} which will stop the callback from running + */ onDidChangeScopeRanges( callback: ScopeChangeEventCallback, config: ScopeRangeConfig, @@ -62,6 +72,14 @@ export class ScopeRangeWatcher { }; } + /** + * Registers a callback to be run when the iteration scope ranges change for + * any visible editor. The callback will be run immediately once for each + * visible editor with the current iteration scope ranges. + * @param callback The callback to run when the scope ranges change + * @param config The configuration for the scope ranges + * @returns A {@link Disposable} which will stop the callback from running + */ onDidChangeIterationScopeRanges( callback: IterationScopeChangeEventCallback, config: IterationScopeRangeConfig, @@ -95,7 +113,8 @@ export class ScopeRangeWatcher { try { dispose(); } catch (e) { - // do nothing + // do nothing; some of the VSCode disposables misbehave, and we don't + // want that to prevent us from disposing the rest of the disposables } }); } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts index 9c982c2062..d9ee8f1664 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts @@ -1,6 +1,7 @@ import { Position, ScopeType, + SimpleScopeTypeType, TextEditor, isEmptyIterable, } from "@cursorless/common"; @@ -8,14 +9,26 @@ import { LegacyLanguageId } from "../languages/LegacyLanguageId"; import { languageMatchers } from "../languages/getNodeMatcher"; import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; -import { ScopeSupport } from "../api/CursorlessEngineApi"; +import { ScopeSupport } from "../api/ScopeProvider"; +/** + * Determines the level of support for a given scope type in a given editor. + * This is primarily determined by the language id of the editor, though some + * scopes are supported in all languages. + */ export class ScopeSupportChecker { constructor(private scopeHandlerFactory: ScopeHandlerFactory) { this.getScopeSupport = this.getScopeSupport.bind(this); this.getIterationScopeSupport = this.getIterationScopeSupport.bind(this); } + /** + * Determine the level of support for {@link scopeType} in {@link editor}, as + * determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for {@link scopeType} in {@link editor} + */ getScopeSupport(editor: TextEditor, scopeType: ScopeType): ScopeSupport { const { languageId } = editor.document; const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); @@ -29,6 +42,14 @@ export class ScopeSupportChecker { : ScopeSupport.supportedButNotPresentInEditor; } + /** + * Determine the level of support for the iteration scope of {@link scopeType} + * in {@link editor}, as determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for the iteration scope of {@link scopeType} + * in {@link editor} + */ getIterationScopeSupport( editor: TextEditor, scopeType: ScopeType, @@ -69,20 +90,17 @@ function getLegacyScopeSupport( scopeType: ScopeType, ): ScopeSupport { switch (scopeType.type) { - case "nonWhitespaceSequence": case "boundedNonWhitespaceSequence": - case "url": case "surroundingPair": - case "customRegex": return ScopeSupport.supportedLegacy; case "notebookCell": - case "oneOf": // FIXME: What to do here return ScopeSupport.unsupported; default: if ( - languageMatchers[languageId as LegacyLanguageId]?.[scopeType.type] != - null + languageMatchers[languageId as LegacyLanguageId]?.[ + scopeType.type as SimpleScopeTypeType + ] != null ) { return ScopeSupport.supportedLegacy; } diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts index e40fe2f6d8..38ccc0215c 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationRange.ts @@ -3,12 +3,17 @@ import { last } from "lodash"; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; /** - * Get the range to iterate over for the given editor. We take the union of all - * visible ranges, add 10 lines either side to make scrolling a bit smoother, - * and then expand to the largest ancestor of the start and end of the visible - * range, so that we properly show nesting. + * Get the range to iterate over for the given editor. + * + * - If {@link visibleOnly} is `false`, just return the full document range. + * - Otherwise, we + * 1. take the union of all visible ranges, then + * 2. add 10 lines either side to make scrolling a bit smoother, and then + * 3. expand to the largest ancestor of the start and end of the visible + * range, so that we properly show nesting. * @param editor The editor to get the iteration range for * @param scopeHandler The scope handler to use + * @param visibleOnly Whether to only iterate over visible ranges * @returns The range to iterate over */ export function getIterationRange( diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts similarity index 71% rename from packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts rename to packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts index 74fb954d42..b1e860d753 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts @@ -6,7 +6,18 @@ import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHan import { Target } from "../typings/target.types"; import { getTargetRanges } from "./getTargetRanges"; -export function getIterationScopes( +/** + * Returns a list of teration scope ranges of type {@link iterationScopeHandler} + * within {@link iterationRange} in {@link editor}. + * @param editor The editor to check + * @param iterationScopeHandler The scope handler to use + * @param everyStage An every stage for use in determining nested targets + * @param iterationRange The range to iterate over + * @param includeIterationNestedTargets Whether to include nested targets in the + * iteration scope ranges + * @returns A list of iteration scope ranges for the given editor + */ +export function getIterationScopeRanges( editor: TextEditor, iterationScopeHandler: ScopeHandler, everyStage: ModifierStage, diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts b/packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts similarity index 66% rename from packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts rename to packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts index f05ac1d986..507a60eabf 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getScopes.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts @@ -4,7 +4,15 @@ import { ScopeRanges } from ".."; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; import { getTargetRanges } from "./getTargetRanges"; -export function getScopes( +/** + * Returns a list of scope ranges of type {@link scopeHandler} within + * {@link iterationRange} in {@link editor}. + * @param editor The editor to check + * @param scopeHandler The scope handler to use + * @param iterationRange The range to iterate over + * @returns A list of scope ranges for the given editor + */ +export function getScopeRanges( editor: TextEditor, scopeHandler: ScopeHandler, iterationRange: Range, diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts index 01f4ca0df0..eb3072561f 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts @@ -1,7 +1,8 @@ import { toCharacterRange, toLineRange } from "@cursorless/common"; import { Target } from "../typings/target.types"; +import { TargetRanges } from "../api/ScopeProvider"; -export function getTargetRanges(target: Target) { +export function getTargetRanges(target: Target): TargetRanges { return { contentRange: toCharacterRange(target.contentRange), removalRange: target.isLine diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index e66acd84a9..4dd323bf27 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -28,10 +28,3 @@ export interface CommandApi { */ runCommandSafe(...args: unknown[]): Promise; } - -export enum ScopeSupport { - supportedAndPresentInEditor, - supportedButNotPresentInEditor, - supportedLegacy, - unsupported, -} diff --git a/packages/cursorless-engine/src/api/ScopeProvider.ts b/packages/cursorless-engine/src/api/ScopeProvider.ts index b577f1e8f9..7d37e6c92a 100644 --- a/packages/cursorless-engine/src/api/ScopeProvider.ts +++ b/packages/cursorless-engine/src/api/ScopeProvider.ts @@ -4,7 +4,6 @@ import { ScopeType, TextEditor, } from "@cursorless/common"; -import { ScopeSupport } from "./CursorlessEngineApi"; export interface ScopeProvider { provideScopeRanges: ( @@ -73,3 +72,10 @@ export interface IterationScopeRanges { targets?: TargetRanges[]; }[]; } + +export enum ScopeSupport { + supportedAndPresentInEditor, + supportedButNotPresentInEditor, + supportedLegacy, + unsupported, +} From a13fe00c390003bfa5ea28bb3742984c46331d02 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 13 Jul 2023 20:06:10 +0100 Subject: [PATCH 51/61] Only use generalized ranges where necessary --- .../ScopeVisualizer/getIterationScopeRanges.ts | 6 +++--- .../src/ScopeVisualizer/getScopeRanges.ts | 4 ++-- .../src/ScopeVisualizer/getTargetRanges.ts | 4 ++-- .../cursorless-engine/src/api/ScopeProvider.ts | 11 ++++++----- .../VscodeScopeIterationVisualizer.ts | 6 +++--- .../VscodeScopeTargetVisualizer.ts | 17 ++++++++++++----- 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts index b1e860d753..feb42bffe9 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getIterationScopeRanges.ts @@ -1,4 +1,4 @@ -import { Range, TextEditor, toCharacterRange } from "@cursorless/common"; +import { Range, TextEditor } from "@cursorless/common"; import { map } from "itertools"; import { IterationScopeRanges } from ".."; import { ModifierStage } from "../processTargets/PipelineStages.types"; @@ -36,9 +36,9 @@ export function getIterationScopeRanges( ), (scope) => { return { - domain: toCharacterRange(scope.domain), + domain: scope.domain, ranges: scope.getTargets(false).map((target) => ({ - range: toCharacterRange(target.contentRange), + range: target.contentRange, targets: includeIterationNestedTargets ? getEveryScopeLenient(everyStage, target).map(getTargetRanges) : undefined, diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts b/packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts index 507a60eabf..56e47dde50 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getScopeRanges.ts @@ -1,4 +1,4 @@ -import { Range, TextEditor, toCharacterRange } from "@cursorless/common"; +import { Range, TextEditor } from "@cursorless/common"; import { map } from "itertools"; import { ScopeRanges } from ".."; import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; @@ -23,7 +23,7 @@ export function getScopeRanges( distalPosition: iterationRange.end, }), (scope) => ({ - domain: toCharacterRange(scope.domain), + domain: scope.domain, targets: scope.getTargets(false).map(getTargetRanges), }), ); diff --git a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts index eb3072561f..5fd843310c 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/getTargetRanges.ts @@ -4,8 +4,8 @@ import { TargetRanges } from "../api/ScopeProvider"; export function getTargetRanges(target: Target): TargetRanges { return { - contentRange: toCharacterRange(target.contentRange), - removalRange: target.isLine + contentRange: target.contentRange, + removalHighlightRange: target.isLine ? toLineRange(target.getRemovalHighlightRange()) : toCharacterRange(target.getRemovalHighlightRange()), }; diff --git a/packages/cursorless-engine/src/api/ScopeProvider.ts b/packages/cursorless-engine/src/api/ScopeProvider.ts index 7d37e6c92a..a76e2d495d 100644 --- a/packages/cursorless-engine/src/api/ScopeProvider.ts +++ b/packages/cursorless-engine/src/api/ScopeProvider.ts @@ -1,6 +1,7 @@ import { Disposable, GeneralizedRange, + Range, ScopeType, TextEditor, } from "@cursorless/common"; @@ -56,19 +57,19 @@ export type IterationScopeChangeEventCallback = ( ) => void; export interface ScopeRanges { - domain: GeneralizedRange; + domain: Range; targets: TargetRanges[]; } export interface TargetRanges { - contentRange: GeneralizedRange; - removalRange: GeneralizedRange; + contentRange: Range; + removalHighlightRange: GeneralizedRange; } export interface IterationScopeRanges { - domain: GeneralizedRange; + domain: Range; ranges: { - range: GeneralizedRange; + range: Range; targets?: TargetRanges[]; }[]; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts index caf8a209dd..1610f785ee 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -1,4 +1,4 @@ -import { Disposable, TextEditor } from "@cursorless/common"; +import { Disposable, TextEditor, toCharacterRange } from "@cursorless/common"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; import { ScopeSupport } from "@cursorless/cursorless-engine"; @@ -14,8 +14,8 @@ export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { this.renderer.setScopes( editor as VscodeTextEditorImpl, iterationScopeRanges!.map(({ domain, ranges }) => ({ - domain, - nestedRanges: ranges.map(({ range }) => range), + domain: toCharacterRange(domain), + nestedRanges: ranges.map(({ range }) => toCharacterRange(range)), })), ); }, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts index 4459515048..c22782a505 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts @@ -1,4 +1,9 @@ -import { Disposable, GeneralizedRange, TextEditor } from "@cursorless/common"; +import { + Disposable, + GeneralizedRange, + TextEditor, + toCharacterRange, +} from "@cursorless/common"; import { ScopeSupport, TargetRanges } from "@cursorless/cursorless-engine"; import { VscodeScopeVisualizer } from "."; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; @@ -18,7 +23,7 @@ abstract class VscodeScopeTargetVisualizer extends VscodeScopeVisualizer { this.renderer.setScopes( editor as VscodeTextEditorImpl, scopeRanges!.map(({ domain, targets }) => ({ - domain, + domain: toCharacterRange(domain), nestedRanges: targets.map((target) => this.getTargetRange(target)), })), ); @@ -30,7 +35,7 @@ abstract class VscodeScopeTargetVisualizer extends VscodeScopeVisualizer { export class VscodeScopeContentVisualizer extends VscodeScopeTargetVisualizer { protected getTargetRange({ contentRange }: TargetRanges): GeneralizedRange { - return contentRange; + return toCharacterRange(contentRange); } protected getNestedScopeRangeType() { @@ -39,8 +44,10 @@ export class VscodeScopeContentVisualizer extends VscodeScopeTargetVisualizer { } export class VscodeScopeRemovalVisualizer extends VscodeScopeTargetVisualizer { - protected getTargetRange({ removalRange }: TargetRanges): GeneralizedRange { - return removalRange; + protected getTargetRange({ + removalHighlightRange, + }: TargetRanges): GeneralizedRange { + return removalHighlightRange; } protected getNestedScopeRangeType() { From 7b8e38b8583e87a2f74c1113ed977bcc0ff667aa Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:59:42 +0100 Subject: [PATCH 52/61] cleanup --- .../src/ScopeVisualizer/index.ts | 1 - .../src/api/ScopeProvider.ts | 82 ++++++++++++++++++- .../cursorless-engine/src/core/Debouncer.ts | 9 +- .../cursorless-engine/src/cursorlessEngine.ts | 2 +- .../scopeHandlers/scopeHandler.types.ts | 11 +++ .../VscodeFancyRangeHighlighter.ts | 21 ++++- .../VscodeFancyRangeHighlighterRenderer.ts | 4 + .../block.test.txt | 23 ------ .../VscodeScopeIterationVisualizer.ts | 2 +- .../VscodeScopeTargetVisualizer.ts | 2 +- 10 files changed, 127 insertions(+), 30 deletions(-) delete mode 100644 packages/cursorless-engine/src/ScopeVisualizer/index.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/block.test.txt diff --git a/packages/cursorless-engine/src/ScopeVisualizer/index.ts b/packages/cursorless-engine/src/ScopeVisualizer/index.ts deleted file mode 100644 index 012d05f605..0000000000 --- a/packages/cursorless-engine/src/ScopeVisualizer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ScopeRangeWatcher"; diff --git a/packages/cursorless-engine/src/api/ScopeProvider.ts b/packages/cursorless-engine/src/api/ScopeProvider.ts index a76e2d495d..40d4bd6916 100644 --- a/packages/cursorless-engine/src/api/ScopeProvider.ts +++ b/packages/cursorless-engine/src/api/ScopeProvider.ts @@ -7,28 +7,70 @@ import { } from "@cursorless/common"; export interface ScopeProvider { + /** + * Get the scope ranges for the given editor. + * @param editor The editor + * @param config The configuration for the scope ranges + * @returns A list of scope ranges for the given editor + */ provideScopeRanges: ( editor: TextEditor, config: ScopeRangeConfig, ) => ScopeRanges[]; - + /** + * Get the iteration scope ranges for the given editor. + * @param editor The editor + * @param config The configuration for the scope ranges + * @returns A list of scope ranges for the given editor + */ provideIterationScopeRanges: ( editor: TextEditor, config: IterationScopeRangeConfig, ) => IterationScopeRanges[]; + /** + * Registers a callback to be run when the scope ranges change for any visible + * editor. The callback will be run immediately once for each visible editor + * with the current scope ranges. + * @param callback The callback to run when the scope ranges change + * @param config The configuration for the scope ranges + * @returns A {@link Disposable} which will stop the callback from running + */ onDidChangeScopeRanges: ( callback: ScopeChangeEventCallback, config: ScopeRangeConfig, ) => Disposable; + /** + * Registers a callback to be run when the iteration scope ranges change for + * any visible editor. The callback will be run immediately once for each + * visible editor with the current iteration scope ranges. + * @param callback The callback to run when the scope ranges change + * @param config The configuration for the scope ranges + * @returns A {@link Disposable} which will stop the callback from running + */ onDidChangeIterationScopeRanges: ( callback: IterationScopeChangeEventCallback, config: IterationScopeRangeConfig, ) => Disposable; + /** + * Determine the level of support for {@link scopeType} in {@link editor}, as + * determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for {@link scopeType} in {@link editor} + */ getScopeSupport: (editor: TextEditor, scopeType: ScopeType) => ScopeSupport; + /** + * Determine the level of support for the iteration scope of {@link scopeType} + * in {@link editor}, as determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for the iteration scope of {@link scopeType} + * in {@link editor} + */ getIterationScopeSupport: ( editor: TextEditor, scopeType: ScopeType, @@ -36,13 +78,23 @@ export interface ScopeProvider { } interface ScopeRangeConfigBase { + /** + * Whether to only include visible scopes + */ visibleOnly: boolean; + + /** + * The scope type to use + */ scopeType: ScopeType; } export type ScopeRangeConfig = ScopeRangeConfigBase; export interface IterationScopeRangeConfig extends ScopeRangeConfigBase { + /** + * Whether to include nested targets in each iteration scope range + */ includeNestedTargets: boolean; } @@ -56,20 +108,48 @@ export type IterationScopeChangeEventCallback = ( scopeRanges: IterationScopeRanges[], ) => void; +/** + * Contains the ranges that define a given scope, eg its {@link domain} and the + * ranges for its {@link targets}. + */ export interface ScopeRanges { domain: Range; targets: TargetRanges[]; } +/** + * Contains the ranges that define a given target, eg its {@link contentRange} + * and the ranges for its {@link removalHighlightRange}. + */ export interface TargetRanges { contentRange: Range; removalHighlightRange: GeneralizedRange; } +/** + * Contains the ranges that define a given iteration scope, eg its + * {@link domain}. + */ export interface IterationScopeRanges { domain: Range; + + /** + * A list of ranges within within which iteration will happen. There is + * almost always a single range here. There will be more than one if the + * iteration scope handler returns a scope whose `getTargets` method returns + * multiple targets. As of this writing, no scope handler returns multiple + * targets. + */ ranges: { + /** + * The range within which iteration will happen, ie the content range for + * the target returned by the iteration scope handler. + */ range: Range; + + /** + * The defining ranges for all targets within this iteration range. + */ targets?: TargetRanges[]; }[]; } diff --git a/packages/cursorless-engine/src/core/Debouncer.ts b/packages/cursorless-engine/src/core/Debouncer.ts index 071387d98c..48c498839c 100644 --- a/packages/cursorless-engine/src/core/Debouncer.ts +++ b/packages/cursorless-engine/src/core/Debouncer.ts @@ -1,9 +1,16 @@ import { ide } from "../singletons/ide.singleton"; +/** + * Debounces a callback. Uses the `decorationDebounceDelayMs` configuration + * value to determine the debounce delay. + */ export class Debouncer { private timeoutHandle: NodeJS.Timeout | null = null; - constructor(private callback: () => void) { + constructor( + /** The callback to debounce */ + private callback: () => void, + ) { this.run = this.run.bind(this); } diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 030ab27a7f..e1265749bb 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -2,7 +2,6 @@ import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common"; import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; import { CursorlessEngine } from "./api/CursorlessEngineApi"; import { ScopeProvider } from "./api/ScopeProvider"; -import { ScopeRangeWatcher } from "./ScopeVisualizer"; import { ScopeRangeProvider } from "./ScopeVisualizer/ScopeRangeProvider"; import { ScopeSupportChecker } from "./ScopeVisualizer/ScopeSupportChecker"; import { Debug } from "./core/Debug"; @@ -16,6 +15,7 @@ import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandler import { runCommand } from "./runCommand"; import { runIntegrationTests } from "./runIntegrationTests"; import { injectIde } from "./singletons/ide.singleton"; +import { ScopeRangeWatcher } from "./ScopeVisualizer/ScopeRangeWatcher"; export function createCursorlessEngine( treeSitter: TreeSitter, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 815e7926b3..730adf60e4 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -150,5 +150,16 @@ export interface ScopeIteratorRequirements { */ skipAncestorScopes: boolean; + /** + * Indicates whether the ScopeHandler should yield a scope if it is a + * descendant of any scope that has been previously yielded. + * + * - `true` means that descendant scopes of any previously yielded scope will + * be yielded. + * - `false` means that descendant scopes of any previously yielded scope will + * not be yielded. + * + * @default false + */ includeDescendantScopes: boolean; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 19582611b4..9abc54267d 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -11,7 +11,15 @@ import { DifferentiatedStyledRange } from "./getDecorationRanges.types"; import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRanges"; /** - * Manages VSCode decoration types for a highlight or flash style. + * A class for highlighting ranges in a VSCode editor, which does the following: + * + * - Uses a combination of solid lines and dotted lines to make it easier to + * visualize multi-line ranges, while still making directly adjacent ranges + * visually distinct. + * - Works around a bug in VSCode where decorations that are touching get merged + * together. + * - Ensures that nested ranges are rendered after their parents, so that they + * look properly nested. */ export class VscodeFancyRangeHighlighter { private renderer: VscodeFancyRangeHighlighterRenderer; @@ -22,8 +30,17 @@ export class VscodeFancyRangeHighlighter { setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { const decoratedRanges: Iterable = flatmap( + // We first generate a list of differentiated ranges, which are ranges + // where any ranges that are touching have different differentiation + // indices. This is used to ensure that ranges that are touching are + // rendered with different TextEditorDecorationTypes, so that they don't + // get merged together by VSCode. generateDifferentiatedRanges(ranges), + // Then, we generate the actual decorations for each differentiated range. + // A single range will be split into multiple decorations if it spans + // multiple lines, so that we can eg use dashed lines to end lines that + // are part of the same range. function* ({ range, differentiationIndex }) { const iterable = range.type === "line" @@ -44,6 +61,8 @@ export class VscodeFancyRangeHighlighter { this.renderer.setRanges( editor, + // Group the decorations so that we have a list of ranges for each + // differentiated style groupDifferentiatedStyledRanges(decoratedRanges), ); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index b31636751f..2668e13744 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -18,6 +18,10 @@ import { const BORDER_WIDTH = "1px"; const BORDER_RADIUS = "2px"; +/** + * Handles the actual rendering of decorations for + * {@link VscodeFancyRangeHighlighter}. + */ export class VscodeFancyRangeHighlighterRenderer { private decorationTypes: CompositeKeyDefaultMap< DifferentiatedStyle, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/block.test.txt b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/block.test.txt deleted file mode 100644 index 246df004b2..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/block.test.txt +++ /dev/null @@ -1,23 +0,0 @@ - hello world -testing - - hello world -testing whatever - - hello world -testing whatever and another test - -hello world -testing whatever and another test - -hello there -another - -test -test - - testing - test - - another -test diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts index 1610f785ee..b2f5f5dd58 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts @@ -13,7 +13,7 @@ export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { (editor, iterationScopeRanges) => { this.renderer.setScopes( editor as VscodeTextEditorImpl, - iterationScopeRanges!.map(({ domain, ranges }) => ({ + iterationScopeRanges.map(({ domain, ranges }) => ({ domain: toCharacterRange(domain), nestedRanges: ranges.map(({ range }) => toCharacterRange(range)), })), diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts index c22782a505..6303d6ee3c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeTargetVisualizer.ts @@ -22,7 +22,7 @@ abstract class VscodeScopeTargetVisualizer extends VscodeScopeVisualizer { (editor, scopeRanges) => { this.renderer.setScopes( editor as VscodeTextEditorImpl, - scopeRanges!.map(({ domain, targets }) => ({ + scopeRanges.map(({ domain, targets }) => ({ domain: toCharacterRange(domain), nestedRanges: targets.map((target) => this.getTargetRange(target)), })), From bc4c816ab664883cd7d05df8cffa3769bc72ac35 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 14 Jul 2023 14:21:11 +0100 Subject: [PATCH 53/61] Convert vscodeApi to singleton --- .../src/suite/scopeVisualizer/injectFakes.ts | 8 +++---- .../scopeVisualizerTest.types.ts | 6 ++--- .../src/constructTestHelpers.ts | 8 +++---- .../cursorless-vscode/src/createVscodeApi.ts | 14 ----------- packages/cursorless-vscode/src/extension.ts | 12 +--------- .../VscodeFancyRangeHighlighter.ts | 5 ++-- .../VscodeFancyRangeHighlighterRenderer.ts | 11 ++++----- .../VscodeScopeRenderer.ts | 9 +------- .../VscodeScopeVisualizer.ts | 10 ++++---- .../createVscodeScopeVisualizer.ts | 23 +++---------------- packages/cursorless-vscode/src/vscodeApi.ts | 19 +++++++++++++++ .../src/{vscode.ts => VscodeApi.ts} | 2 +- packages/vscode-common/src/getExtensionApi.ts | 4 ++-- packages/vscode-common/src/index.ts | 2 +- 14 files changed, 50 insertions(+), 83 deletions(-) delete mode 100644 packages/cursorless-vscode/src/createVscodeApi.ts create mode 100644 packages/cursorless-vscode/src/vscodeApi.ts rename packages/vscode-common/src/{vscode.ts => VscodeApi.ts} (94%) diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts index a2a5d90edd..5b8d478a32 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts @@ -1,4 +1,4 @@ -import { Vscode, getCursorlessApi } from "@cursorless/vscode-common"; +import { VscodeApi, getCursorlessApi } from "@cursorless/vscode-common"; import * as sinon from "sinon"; import { DecorationRenderOptions, WorkspaceConfiguration } from "vscode"; import { COLOR_CONFIG } from "./colorConfig"; @@ -9,13 +9,13 @@ import { } from "./scopeVisualizerTest.types"; export async function injectFakes(): Promise { - const { vscode: vscodeApi } = (await getCursorlessApi()).testHelpers!; + const { vscodeApi: vscodeApi } = (await getCursorlessApi()).testHelpers!; const dispose = sinon.fake<[number], void>(); let decorationIndex = 0; const createTextEditorDecorationType = sinon.fake< - Parameters, + Parameters, MockDecorationType >((_options: DecorationRenderOptions) => { const id = decorationIndex++; @@ -29,7 +29,7 @@ export async function injectFakes(): Promise { const setDecorations = sinon.fake< SetDecorationsParameters, - ReturnType + ReturnType >(); const getConfigurationValue = sinon.fake.returns(COLOR_CONFIG); diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts index f1bcc01bd9..71a9359546 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/scopeVisualizerTest.types.ts @@ -1,5 +1,5 @@ import { RangePlainObject } from "@cursorless/common"; -import { Vscode } from "@cursorless/vscode-common"; +import { VscodeApi } from "@cursorless/vscode-common"; import * as sinon from "sinon"; import * as vscode from "vscode"; @@ -17,10 +17,10 @@ export type SetDecorationsParameters = [ export interface Fakes { setDecorations: sinon.SinonSpy< SetDecorationsParameters, - ReturnType + ReturnType >; createTextEditorDecorationType: sinon.SinonSpy< - Parameters, + Parameters, MockDecorationType >; dispose: sinon.SinonSpy<[number], void>; diff --git a/packages/cursorless-vscode/src/constructTestHelpers.ts b/packages/cursorless-vscode/src/constructTestHelpers.ts index 631f4604a6..bb00ca3b8b 100644 --- a/packages/cursorless-vscode/src/constructTestHelpers.ts +++ b/packages/cursorless-vscode/src/constructTestHelpers.ts @@ -16,10 +16,11 @@ import { plainObjectToTarget, takeSnapshot, } from "@cursorless/cursorless-engine"; -import { TestHelpers, Vscode } from "@cursorless/vscode-common"; +import { TestHelpers } from "@cursorless/vscode-common"; +import type { TextEditor as VscodeTextEditor } from "vscode"; import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { toVscodeEditor } from "./ide/vscode/toVscodeEditor"; -import type { TextEditor as VscodeTextEditor } from "vscode"; +import { vscodeApi } from "./vscodeApi"; export function constructTestHelpers( commandServerApi: CommandServerApi | null, @@ -29,7 +30,6 @@ export function constructTestHelpers( normalizedIde: NormalizedIDE, injectIde: (ide: IDE) => void, runIntegrationTests: () => Promise, - vscode: Vscode, ): TestHelpers | undefined { return { commandServerApi: commandServerApi!, @@ -75,6 +75,6 @@ export function constructTestHelpers( }, hatTokenMap, runIntegrationTests, - vscode, + vscodeApi, }; } diff --git a/packages/cursorless-vscode/src/createVscodeApi.ts b/packages/cursorless-vscode/src/createVscodeApi.ts deleted file mode 100644 index 587f12eba1..0000000000 --- a/packages/cursorless-vscode/src/createVscodeApi.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { workspace, window } from "vscode"; -import { Vscode } from "@cursorless/vscode-common"; - -export function createVscodeApi(): Vscode { - return { - workspace, - window, - editor: { - setDecorations(editor, ...args) { - return editor.setDecorations(...args); - }, - }, - }; -} diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index ce0195ab77..61036295a7 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -36,8 +36,6 @@ import { ScopeVisualizerCommandApi, VisualizationType, } from "./ScopeVisualizerCommandApi"; -import { createVscodeApi } from "./createVscodeApi"; -import { Vscode } from "@cursorless/vscode-common"; import { ExtensionContext, Location } from "vscode"; /** @@ -89,18 +87,13 @@ export async function activate( const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); const keyboardCommands = KeyboardCommands.create(context, statusBarItem); - const vscode = createVscodeApi(); registerCommands( context, vscodeIDE, commandApi, testCaseRecorder, - createScopeVisualizerCommandApi( - vscode, - normalizedIde ?? vscodeIDE, - scopeProvider, - ), + createScopeVisualizerCommandApi(normalizedIde ?? vscodeIDE, scopeProvider), keyboardCommands, hats, ); @@ -115,7 +108,6 @@ export async function activate( normalizedIde!, injectIde, runIntegrationTests, - vscode, ) : undefined, @@ -158,7 +150,6 @@ function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter { } function createScopeVisualizerCommandApi( - vscode: Vscode, ide: IDE, scopeProvider: ScopeProvider, ): ScopeVisualizerCommandApi { @@ -168,7 +159,6 @@ function createScopeVisualizerCommandApi( start(scopeType: ScopeType, visualizationType: VisualizationType) { scopeVisualizer?.dispose(); scopeVisualizer = createVscodeScopeVisualizer( - vscode, ide, scopeProvider, scopeType, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 9abc54267d..c1c4602374 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -1,6 +1,5 @@ import { GeneralizedRange, Range } from "@cursorless/common"; import { flatmap } from "itertools"; -import { Vscode } from "@cursorless/vscode-common"; import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import { RangeTypeColors } from "../RangeTypeColors"; import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; @@ -24,8 +23,8 @@ import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRang export class VscodeFancyRangeHighlighter { private renderer: VscodeFancyRangeHighlighterRenderer; - constructor(vscode: Vscode, colors: RangeTypeColors) { - this.renderer = new VscodeFancyRangeHighlighterRenderer(vscode, colors); + constructor(colors: RangeTypeColors) { + this.renderer = new VscodeFancyRangeHighlighterRenderer(colors); } setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index 2668e13744..74e9a40bc6 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -5,7 +5,7 @@ import { DecorationRenderOptions, TextEditorDecorationType, } from "vscode"; -import { Vscode } from "@cursorless/vscode-common"; +import { vscodeApi } from "../../../../vscodeApi"; import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import { RangeTypeColors } from "../RangeTypeColors"; import { @@ -28,9 +28,9 @@ export class VscodeFancyRangeHighlighterRenderer { TextEditorDecorationType >; - constructor(private vscode: Vscode, colors: RangeTypeColors) { + constructor(colors: RangeTypeColors) { this.decorationTypes = new CompositeKeyDefaultMap( - ({ style }) => getDecorationStyle(this.vscode, colors, style), + ({ style }) => getDecorationStyle(colors, style), ({ style: { top, right, bottom, left, isWholeLine }, differentiationIndex, @@ -61,7 +61,7 @@ export class VscodeFancyRangeHighlighterRenderer { ({ differentiatedStyles: styleParameters, ranges }) => { const decorationType = this.decorationTypes.get(styleParameters); - this.vscode.editor.setDecorations( + vscodeApi.editor.setDecorations( editor.vscodeEditor, decorationType, ranges.map(toVscodeRange), @@ -84,7 +84,6 @@ export class VscodeFancyRangeHighlighterRenderer { } function getDecorationStyle( - vscode: Vscode, colors: RangeTypeColors, borders: DecorationStyle, ): TextEditorDecorationType { @@ -112,7 +111,7 @@ function getDecorationStyle( isWholeLine: borders.isWholeLine, }; - return vscode.window.createTextEditorDecorationType(options); + return vscodeApi.window.createTextEditorDecorationType(options); } function getBorderStyle(borders: DecorationStyle): string { diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts index 24fa1ceb1c..ffa893d880 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts @@ -3,7 +3,6 @@ import { GeneralizedRange, isGeneralizedRangeEqual, } from "@cursorless/common"; -import { Vscode } from "@cursorless/vscode-common"; import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { RangeTypeColors } from "./RangeTypeColors"; import { VscodeFancyRangeHighlighter } from "./VscodeFancyRangeHighlighter"; @@ -20,20 +19,14 @@ export class VscodeScopeRenderer implements Disposable { private domainEqualsNestedHighlighter: VscodeFancyRangeHighlighter; constructor( - vscode: Vscode, domainColors: RangeTypeColors, nestedRangeColors: RangeTypeColors, ) { - this.domainHighlighter = new VscodeFancyRangeHighlighter( - vscode, - domainColors, - ); + this.domainHighlighter = new VscodeFancyRangeHighlighter(domainColors); this.nestedRangeHighlighter = new VscodeFancyRangeHighlighter( - vscode, nestedRangeColors, ); this.domainEqualsNestedHighlighter = new VscodeFancyRangeHighlighter( - vscode, blendRangeTypeColors(domainColors, nestedRangeColors), ); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index a6675e2bcd..df2f11c730 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -10,9 +10,9 @@ import { ScopeRangeType, ScopeVisualizerColorConfig, } from "@cursorless/vscode-common"; -import { getColorsFromConfig } from "./getColorsFromConfig"; +import { vscodeApi } from "../../../vscodeApi"; import { VscodeScopeRenderer } from "./VscodeScopeRenderer"; -import { Vscode } from "@cursorless/vscode-common"; +import { getColorsFromConfig } from "./getColorsFromConfig"; export abstract class VscodeScopeVisualizer { protected renderer!: VscodeScopeRenderer; @@ -24,13 +24,12 @@ export abstract class VscodeScopeVisualizer { protected abstract getScopeSupport(editor: TextEditor): ScopeSupport; constructor( - private vscode: Vscode, private ide: IDE, protected scopeProvider: ScopeProvider, protected scopeType: ScopeType, ) { this.disposables.push( - this.vscode.workspace.onDidChangeConfiguration( + vscodeApi.workspace.onDidChangeConfiguration( ({ affectsConfiguration }) => { if (affectsConfiguration("cursorless.scopeVisualizer.colors")) { this.initialize(); @@ -67,13 +66,12 @@ export abstract class VscodeScopeVisualizer { } private initialize() { - const colorConfig = this.vscode.workspace + const colorConfig = vscodeApi.workspace .getConfiguration("cursorless.scopeVisualizer") .get("colors")!; this.renderer?.dispose(); this.renderer = new VscodeScopeRenderer( - this.vscode, getColorsFromConfig(colorConfig, "domain"), getColorsFromConfig(colorConfig, this.getNestedScopeRangeType()), ); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts index c36ab06066..6ec597e7cd 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts @@ -6,10 +6,8 @@ import { VscodeScopeContentVisualizer, VscodeScopeRemovalVisualizer, } from "./VscodeScopeTargetVisualizer"; -import { Vscode } from "@cursorless/vscode-common"; export function createVscodeScopeVisualizer( - vscode: Vscode, ide: IDE, scopeProvider: ScopeProvider, scopeType: ScopeType, @@ -17,25 +15,10 @@ export function createVscodeScopeVisualizer( ) { switch (visualizationType) { case "content": - return new VscodeScopeContentVisualizer( - vscode, - ide, - scopeProvider, - scopeType, - ); + return new VscodeScopeContentVisualizer(ide, scopeProvider, scopeType); case "removal": - return new VscodeScopeRemovalVisualizer( - vscode, - ide, - scopeProvider, - scopeType, - ); + return new VscodeScopeRemovalVisualizer(ide, scopeProvider, scopeType); case "iteration": - return new VscodeScopeIterationVisualizer( - vscode, - ide, - scopeProvider, - scopeType, - ); + return new VscodeScopeIterationVisualizer(ide, scopeProvider, scopeType); } } diff --git a/packages/cursorless-vscode/src/vscodeApi.ts b/packages/cursorless-vscode/src/vscodeApi.ts new file mode 100644 index 0000000000..8275a485c3 --- /dev/null +++ b/packages/cursorless-vscode/src/vscodeApi.ts @@ -0,0 +1,19 @@ +import { workspace, window } from "vscode"; +import { VscodeApi } from "@cursorless/vscode-common"; + +/** + * A very thin wrapper around the VSCode API that allows us to mock it for + * testing. This is necessary because the test harness gets bundled separately + * from the extension code, so if we just import the VSCode API directly from + * the extension code, and from the test harness, we'll end up with two copies + * of the VSCode API, so the mocks won't work. + */ +export const vscodeApi: VscodeApi = { + workspace, + window, + editor: { + setDecorations(editor, ...args) { + return editor.setDecorations(...args); + }, + }, +}; diff --git a/packages/vscode-common/src/vscode.ts b/packages/vscode-common/src/VscodeApi.ts similarity index 94% rename from packages/vscode-common/src/vscode.ts rename to packages/vscode-common/src/VscodeApi.ts index 916f44aa4d..34048bb2b0 100644 --- a/packages/vscode-common/src/vscode.ts +++ b/packages/vscode-common/src/VscodeApi.ts @@ -3,7 +3,7 @@ import { workspace, window, TextEditor } from "vscode"; /** * Subset of VSCode api that we need to be able to mock for testing */ -export interface Vscode { +export interface VscodeApi { workspace: typeof workspace; window: typeof window; diff --git a/packages/vscode-common/src/getExtensionApi.ts b/packages/vscode-common/src/getExtensionApi.ts index a0f2ee6142..be99d542f2 100644 --- a/packages/vscode-common/src/getExtensionApi.ts +++ b/packages/vscode-common/src/getExtensionApi.ts @@ -13,7 +13,7 @@ import type { } from "@cursorless/common"; import * as vscode from "vscode"; import type { Language, SyntaxNode, Tree } from "web-tree-sitter"; -import { Vscode } from "./vscode"; +import { VscodeApi } from "./VscodeApi"; export interface TestHelpers { ide: NormalizedIDE; @@ -43,7 +43,7 @@ export interface TestHelpers { ): Promise; runIntegrationTests(): Promise; - vscode: Vscode; + vscodeApi: VscodeApi; } export interface CursorlessApi { diff --git a/packages/vscode-common/src/index.ts b/packages/vscode-common/src/index.ts index 8e5d7ec3d1..c6a679e3bc 100644 --- a/packages/vscode-common/src/index.ts +++ b/packages/vscode-common/src/index.ts @@ -3,5 +3,5 @@ export * from "./notebook"; export * from "./testUtil/openNewEditor"; export * from "./vscodeUtil"; export * from "./runCommand"; -export * from "./vscode"; +export * from "./VscodeApi"; export * from "./ScopeVisualizerColorConfig"; From 325a9954b58f2c2b1ee8757e5d4541e0d7b10d99 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 14 Jul 2023 16:40:37 +0100 Subject: [PATCH 54/61] More docs --- packages/common/src/index.ts | 1 + packages/common/src/util/range.ts | 22 ++ .../scopeVisualizer/checkAndResetFakes.ts | 6 +- .../src/suite/scopeVisualizer/colorConfig.ts | 5 + .../src/suite/scopeVisualizer/injectFakes.ts | 2 +- .../runBasicMultilineContentTest.png | Bin 0 -> 36350 bytes .../runBasicMultilineContentTest.ts | 7 + .../scopeVisualizer/runBasicRemovalTest.png | Bin 0 -> 11851 bytes .../scopeVisualizer/runBasicRemovalTest.ts | 6 + .../runNestedMultilineContentTest.png | Bin 0 -> 44485 bytes .../runNestedMultilineContentTest.ts | 7 + .../suite/scopeVisualizer/runUpdateTest.ts | 4 + .../scopeVisualizer.vscode.test.ts | 2 +- ...lainObject.ts => spyCallsToPlainObject.ts} | 0 packages/cursorless-vscode/package.json | 14 +- .../VSCodeScopeVisualizer/RangeTypeColors.ts | 3 + .../VscodeFancyRangeHighlighter.ts | 2 +- .../VscodeFancyRangeHighlighterRenderer.ts | 41 ++-- ...nges.types.ts => decorationStyle.types.ts} | 35 +-- .../generateDecorationsForCharacterRange.ts | 35 --- .../generateDecorationsForCharacterRange.ts | 28 +++ .../generateLineInfos.ts | 70 ++++++ .../handleMultipleLines.ts | 208 ++++++++++++++++++ .../index.ts | 1 + .../generateDecorationsForLineRange.ts | 2 +- .../generateDifferentiatedRanges.ts | 46 +++- .../getDifferentiatedStyleMapKey.ts | 12 + .../groupDifferentiatedStyledRanges.ts | 24 +- .../handleMultipleLines.ts | 207 ----------------- ...r.ts => VscodeIterationScopeVisualizer.ts} | 2 +- .../VscodeScopeRenderer.ts | 10 + .../VscodeScopeVisualizer.ts | 11 +- .../blendRangeTypeColors.ts | 20 ++ .../createVscodeScopeVisualizer.ts | 4 +- packages/vscode-common/src/getExtensionApi.ts | 4 + 35 files changed, 535 insertions(+), 306 deletions(-) create mode 100644 packages/common/src/util/range.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.png create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.png create mode 100644 packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.png rename packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/{toPlainObject.ts => spyCallsToPlainObject.ts} (100%) rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/{getDecorationRanges.types.ts => decorationStyle.types.ts} (52%) delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/index.ts create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts delete mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts rename packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/{VscodeScopeIterationVisualizer.ts => VscodeIterationScopeVisualizer.ts} (94%) diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index edc4b239be..186323cc9e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -52,6 +52,7 @@ export { default as DefaultMap } from "./util/DefaultMap"; export * from "./types/GeneralizedRange"; export * from "./types/RangeOffsets"; export * from "./util/omitByDeep"; +export * from "./util/range"; export * from "./testUtil/isTesting"; export * from "./testUtil/testConstants"; export * from "./testUtil/getFixturePaths"; diff --git a/packages/common/src/util/range.ts b/packages/common/src/util/range.ts new file mode 100644 index 0000000000..8ea42b9d06 --- /dev/null +++ b/packages/common/src/util/range.ts @@ -0,0 +1,22 @@ +import { range as lodashRange } from "lodash"; +import { Range } from "../types/Range"; +import { TextEditor } from "../types/TextEditor"; + +/** + * @param editor The editor containing the range + * @param range The range to get the line ranges for + * @returns A list of ranges, one for each line in the given range, with the + * first and last ranges trimmed to the start and end of the given range. + */ +export function getLineRanges(editor: TextEditor, range: Range): Range[] { + const { document } = editor; + const lineRanges = lodashRange(range.start.line, range.end.line + 1).map( + (lineNumber) => document.lineAt(lineNumber).range, + ); + lineRanges[0] = lineRanges[0].with(range.start); + lineRanges[lineRanges.length - 1] = lineRanges[lineRanges.length - 1].with( + undefined, + range.end, + ); + return lineRanges; +} diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts index 0171a97962..662cd1bb87 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/checkAndResetFakes.ts @@ -3,15 +3,15 @@ import * as sinon from "sinon"; import { createDecorationTypeCallToPlainObject, setDecorationsCallToPlainObject, -} from "./toPlainObject"; +} from "./spyCallsToPlainObject"; import { Fakes, ExpectedArgs } from "./scopeVisualizerTest.types"; export function checkAndResetFakes(fakes: Fakes, expected: ExpectedArgs) { - const actual = getAndResetFakes(fakes); + const actual = getSpyCallsAndResetFakes(fakes); assert.deepStrictEqual(actual, expected, JSON.stringify(actual)); } -export function getAndResetFakes({ +function getSpyCallsAndResetFakes({ createTextEditorDecorationType, setDecorations, dispose, diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts index d485a97432..93467cafe8 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/colorConfig.ts @@ -1,5 +1,10 @@ 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: { diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts index 5b8d478a32..d444a8377a 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/injectFakes.ts @@ -9,7 +9,7 @@ import { } from "./scopeVisualizerTest.types"; export async function injectFakes(): Promise { - const { vscodeApi: vscodeApi } = (await getCursorlessApi()).testHelpers!; + const { vscodeApi } = (await getCursorlessApi()).testHelpers!; const dispose = sinon.fake<[number], void>(); diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.png b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.png new file mode 100644 index 0000000000000000000000000000000000000000..72696dddb843ca038456cc7f6a10918f5f4c1336 GIT binary patch literal 36350 zcmdqIg?(Vk0qPu7yxVuZR;O;CCB)D5}XK?}x zEY8b0_uS{X=iKM}2fo?g%=A=Ob#+PiRCUh=E6PjYJ|TXBf`Wo8B`Kzaf`S1-K|wWm z{OAGF)kMmSg7Q?zQdCq?N>r3w(Gg^3X=93lA{iX7j-{d8N0^}<6D4ecijnKTgt3DW zg)f2wqx$t&%12S;DaI?lrtd3r-Dq4+s$xH$1ih=Qe$2HWiuuY^_rtGvZn~_!))Uxe zYa?RWd-w)E*vO7tLKQxsmZ0tzpF-^=APiX-LEAP~R@%ueK*37!dBWjC7pP5c^s`G0 zMI&IYEzy-5h0?ZsPNw#5_Fg~qQKWu5CQ7g`Pr7>hklzJ4irXu7Y9px|I*oQbb@nFrY}&zm4D_K$&{hI}zHDtSDMbW+XwOK#n0C z{;U$DrzeQF#t~GS9sby;P>|{tD}AudG9S;{ktexRLy$=G^OKh^gB*D$?naz$j$#rS z-kca>7fUDbaL5DAlj>7>FISaH&>S*Z1n7y4XK>^X{g8{G0o;wv~MWv%uGIilxVtzb+k~*|@!}FE7hC+1E zi2efcwsNq1a>i3io>Dpc9vC{JwryP0t%=?DT$!AU8J}AGW66_B(U0;T4IO5(4ulL= zy`Me=$NmI8b9k%&8lQZoCxor*hr^TWs+KR=mqw}882j%rm-Lh3pVN=vP4{p1t_6aB z053$$)}o%Z8Q64<>bTvslV2`WntK=;wTQs|zO#_a3lbItIU2t_Z=e(67VA$H+1FBh zS^hc{@TvooZl;G_>?~I4c#-HCqy@GCsSttmy4X8y)cdSGcy2y>lS>s;I z*8Dhg&|e^l^ilWLNOyR2$jcFVbnXxEm+uhma2DZSou>X6d{OY8>Ld3gp4tZGZI8}M zg5m@&Ub|-&Q01qyy{AEBvxt!F0v^Q0?}N;XdJcxu%$j>HKeCK~j);B9PkmIqcP4w8 z5=C=GO}NF9;9Fy_jmmsHcq-=tqS|{JZx;T8>&9HhhQbAfFEG7>$v~p=lW>o`ft&`3 zi)=`s`i$}HF7XK=AQXVL`}l4ZwB0AouDPPI_iOJ$d`Z1GlipJTU9Ht3MI$O%RnokJ z87*^|(DB`;vGjN8x+qaz`Wxlt+?7>FkG@Tur>7#ZPy_*wu&^RM;2rWP6(i_&_nphU z){kzS-lN;!v8Yor#ef@5MSMWmoDCDFJ}+3%r|kuY+uymNdq2nM5ay6Xp);Ti6T~S4 zvnHXL`7~y|jPo-!;MBn5@+~z0{6atX`(*zz4bA)q%_wS_AD{i%sSi;WSqpmR3k(Vh z75{*3il0H4Mu9w3PrHb)MJS_qs4=BP*g=W4KpPKU z5FsUp^;&Nbgk;lyC$tLE`j+$gQNLK{7={yCO0fK=(6P&Hd?!Y&@WW3eqsrSfr@*=J zR$(j_A39@T61G_+D+?iEc%IRlq}Qom;|*VFaH<7zcEUYg?ftx}h~#-uuX0BYAhF1|o-A-n&Sh5z)i3kEY)Mu6iN4nAU(JJI`+g1Hv>fhv;pG^JDrwCUu! z6lYO_@97xcFz_;9GUU-?$4kf2$GOnGQ*ZiqsrfjqJr{~ve5T&m3Jz$Mf_8OU|Gsi#2HTOm!f4X>4(|ih0{GZn^^1QwbJ{-x42)KXb432O*oNoNt{kn@pR| znr57ZcE--WoI0K+>~ZccPV}7Fo*ADNY=b6+a+bN3taFi)hrLzN*Ah!y!niiLYftk@ zY&b30U$bzqJ!O6Qri`86_Lq6C@dhZ)wx)l(;d43b(61Q{gO>ga|6_0ySGv?lUU^>q z__f?qSrIvET5Z`mIl)w|WZIO3r0LWp_M9ZvLAF7)+W{)(4Q6;lSo*Mc73&M$ErG;;{sN<&Edz*6+KB7BQ* zr>?!U`=Gcvu=y8k7&g~eG~V(MsVA!!qVQ%-qzkbNW1qmDmL4H^!Mmy zwyupRSE6p`As*s!>YHieGJI09R>2!Xe_MZ^Z@l>7PeXOyxO4YYO23|Zjr(nOj3MW! zTccYxf)`&VEIhb0%o88}lAG#Kho8|)>89+$6&n>>hTxD|3m}cJ9Wxp86EH;$Q+Ot; z7rh|ha{4RTm^p8|;IUGQl2oBo;k+`|Pt0&_fim-!`HS&mZdwhwB;aa?*llnlL8P2g zYFDxod3aPpS{=gy4j@H&=#8X#fqEVithvCsK*aC8M6buZ9N6~do}&|BEdH}vHghmO z8K&u_`AX9hEx`b`qUvICp1+Ue9pmM5d<2dK?;+w&#!sH%aDL_OnD)rIDZEUl1=TWbWx@@myW)nnisq@I^mrV)GH3pif;zeC^g4^V ziM5rr<^1)D#tCh+q`@S<^&hfZgN)Ttnmg56W~oCNLe1v#y=m!SIxu+aPH#=4>f6Ra z%ZB?90vA4#e7~I-rEevpD$|{(i_=16qZP#b@`H{3MwP|lH;`#dRp@5K=BrJ8LNJm3 z&D73JZQC=ZK8^9#Nza|1rd_5TrrZhFjLVH8#^T2z5)4%8TqWPi>9x;X&T^-S=ye#l zO0!F%OLp`~D(fuMABOE-YsrX~H;gT-shsh!ZVRqD*A=K_-lU<>W1?W_%Mf?H zzm4Vhx{|VA(hFKOXMJ_Kws0@w8?|a(Z|LQDIiEjYlk3j|o6_U&g|ns=)|QM~421`a z0$#2?8SMgc&gahByUx1<$S<%M*a$N3pt(}W!+QYrr*D%VAy)CTv1jp|WS0vTM z&8bQ$K)k?rc9!RMf=*qly%9OI5)6@+WZIB4M~}PX9|YZiNAe-^=jr|YXWvbg2GS>t z74)scZEb}JytZy;SE>s}SVz)%j=*=@nB_Qu%xp|tdX8KTk3RhIE|UI`WTE`7b1U-74tZ#dhelQ-5a#U$e}Nne?ZJ#}J00{E@l-mVys#W%hnq`4 z{h)jqkkk+yryNVl(eeFrzayO&{L?J_u3uhwz~||GpQf!@FB_LalHm;rIIJS$mAqrHCr| z8{>QEfc-mQao=$KxQIX8XNXmQ0x|xhjqZdUj^}W{fL2(f5<2j zOf{s;u>u4?h%CVie3j%P1()s3iYWRziLM7Y-T7Rx6Z)^-m7W%)-s0I&pC?AzYrKBG2%EpeSrgmTp zkP|2!8u0+YwwKfdqo5Gd{Bfd6DN+AMK|%XvsiNVeAtwto2H7$hnt+T%9$1StQ+06vue05emP|B2#cEkLOur${aeax^99Vq#%pp%i>V zPEOA6XkrFb68rQQ`NNX{rG=A|J&>8%)zy{Bm7NLXXwJ;a%gf8m!p6+T#`u822zIw~ zGIV3K15^F0k$<-%W(qcTw6u4!1lf`Q(XOEp$k|DNlJXBm|8xASpQdh>|I5h^{Fhh{ z0y6(m!_3OW!u)@cIa!+he~|rA^DnYL<@y&p{y!Q6Dq6ak+GvPb+CHfIp=*MytQ=hY zfAaaSs{ifjzerWVrjDW@+XqS~!T&YZU&Q}e`A@<>d20SIPj*hW|77_eHUB~RhYUa^ zQ!vQJ`41hc+F3dYK9K!i;D1tS{x6vzD+kA4ME?o>CxyoUN%5c1e^MwoT0ZE(@QvKp&<9Twe8SKCKVB>NgqN8o8wEueMM~_WiW}(*Sh4>y5w~>DF?)v z96#5K1?P%={?@ew#ln704iugQE$2i4S6s}>iB^P0MA7Qm`S`Bqn>!Dn%4|i&#n)f` zQP3Wf`~3Y9b&i&7>HR3}@6dlC`6&1Z<0)WZJQqem{oBPL=<)OIHlKN|k!J+y-`9QTS$5G`!!ZVPW!)Sr9z8(^P8hD1LwW zUnVtpK1!}2*NrV&=FaX*T8+zV_@tzyL3|0gif(u)?(kpgfBZ6tqc_L-`8Iak1~Lfq z@bxueqw#(!)l%lW7ymS*Fm(!f3&LA2YtcDg38*B;kmWYITzfA@QqjR*IA+9;FKK#=m8pL^Q?2*AVsFVBpjYJV%x z=Rk%p@eclUkvPq1%@qH?MwUPG??F+L&SlF{($noX7#*FXm?70NeoyvxwW$BzyNC8A zBOV6FMgv}Oj+q&Tkf(--r=*USJlfwmlgnYgZi>$*iZG-4ryANv@!!9ve=L~&b49L> z5d&fa-1sk5p`c;oYt-$YWKr2dey?WP*5Upz2HE8GiT=ZUe>_TRqM`}VK`kv_|Ce0k z&odtS`n%3T^aHmEaNR}lxbIWn5MJ3C zT!=A&pa&ZrW1!1iM&7P%!3i9wQ(q+dQ%(2jp^2suula~cU%3~Vr*=cf7>N@4Kjlfp zxI3H(SJL?9y;FIgI6ZxV1*xjKHtnuWUn5j}&0QuTcpUx28+`Ew;Cpl9H3o10MngnR zMJ10_sWR|ZfbebAKPSh>5-ZG0Q)|QD{s)nZQ z5App5QEk;l$OeY{>4)txf>diM#^C4nBH78jywg-?$n%gpsy{7AtxwL*F8U;7ak-vP zt2wWn`66|AqaFy;syNLLj!7rDM7nswZmW)u*RgqbW@Uc%_DUm3A{9w0ZX1a$ZFis4 z@@zhT&4spes`orHR7o+8Dp~Vzkrp@)&8;tBCJl5hRfZn7+?8?7v`i<+y6;De%(Q5L zZhp8*Z$iu)qO{91B0auo>iybgD`S`re;shPqgd*F`w6zOL5*a8=9^)7u^|(vJp4|d z<}27tW+@jtMpupY&Frx7&*xVywFe7~a58twck>Hvy`{d+#qQ-d#VF z?(B@jG_Sz)eUC|4{+9XZV$x_vYdGC5mKlbmo8jjQptZklHX9g14n8fd$Wx(26IjoW z>kn+JS{M%*t(wz*NQSN^vR~!DMC%kIPhY}WbC$Z>X-%bW8@-)A_r0HG8WHf|&b9XeZ!*3V_J3*)XcE4g>{Ieur|)B8CfP0Mzod8&ENv`@hN|b5 zQK+%PhQ-5BKew?_7jnpm+W+ewry&6?&n|l$_kurZyA9M;9f6Vb6n89QHN6Urb zB+FDXiH*y-+?;0p%H9!W>c$OTLQb@vdd_|m#XXx;D~ll8SRp|TY9giv51iMBzVE%I zR%jF3=kf;Cy>?vJgwM`29k-kkCNl?OA|k?MW>$kGaPsoa_EvN|y!IXYI?Fm6MD_Gq z2b0+Kr3XGWB<#2j+xH|8<<;i(ab|{h%4B(*ohf;Ae_M4PPhsmQCr(U)PNaAK&y{@t2ME|{Sn?; z^LRpP*&~O$8?o#KM{TI0OC7r*LPBVa`u-TS`O)1bNLCoW)i+R$+|pb>tr|CUo?EBS zpkwmQ_|id{EZ~~0qHuO7zu?xYcfSAY?FcyckmLD#ZD+=h85|Ar954QwCh!3)MozvP zuabny6jFF8vwbtLC;CgkBF-+S)XxH`D58wenwrKp=nl($eW1KG8>=F z-9fnfPvzD57{Ist0GJYVx@rlzlI~{nQlk;*mZW1~crbQ$a4$XB`wF2S(N6gF0_q@@ zJeb@j`BNbce~t8|s4z8X4TG-H5&PxCoY%fx-!kT&5#eg9e{hQ@sXp}P!FehFoT8tH zw~xW#rOlDaiY7ym&St8$o?g+$qzB>7(C>J@D)<-0ZLE<>mp@zymQ#B`Ip9|QJ4xS# zySWuTfw}AL*i5T9h5Li|-VR=$C|GPE^$wtSWqAVbB2qa&8&0c$ulh5}6N06(3Ou^r z1KO*_=!+LX{(1s$+bEuYQ1n{wUkR^c^xV=!Tr*eT2Zl$q^kP&=@OCP&FE$UEM}7c| z61-u6QuqkRp)m`2vgO8`oTN`#l(iUTtOA?K8BzjZ=Hq|}!MXB!~$@2~Ol*ldS% zzcJ(_s}T_M(pG{?!jmmB8_u>#p$1m(byHPWDj-U-B%|OPK*Zznz`f7VGDH zD=qS`urUee?ZRi~s}Y89V(p|;VFj>g!OfPps~hK3V%trEm9YU;?vntG?79RdIy%=( zw`LRn>vOjkVI>?`2~L;05nPD+*<&k$vydZCa@{4IfZa35JfBv0!TJ8x#v}I{!?|Mf z`_{L~ob$JwS1#sx-$9=2tfC=dilXE>xsz|V3k#+8e>VhmxES}y#ZA?>=#}YuGJMRS z`0X#dMfY+z-B`e5J*C$1eCaxOy`5r*cUdy3r%0Ukuc7yp(>EV{$Z)akPDt&>$v$u8 zeo%VI9VL1Iu@`*&lyEe!-&WK`$rKy0eE*YwQW?HWARNXH+xTH#HRit3H*kS&>g(gx~9%$MbDYgx5EBw2 zBaMAReX{%1&v;WMDbTjo4&wg$v3Q7mhmqgD$7NPFvS}NQYOmkQK){7C z8z&1LN+r8i-U_F441HqiHDdd)NrH;fn$xbFt<{J=p4g#OgvqQU5*f9HjU`~u>u8`m_Zfeowh*#uT2RLE!VMv?1zq&Ra`X_*Q`%99 zc-tRMX#R>t83KoD%WB#vu%m)qLs%5V(D|{D2QlbS7fJP*uPn&?i;9;O9t^ppFvczJ zVZ)tnslP(U_6maSokN1##PoG9uo^GxM_dCwtbsy>qC?gUI2joS zGK2Ln&L<2$y*a>GU*Al+({H-a)7+7}`U%=;|pYm*2aZSxvVu|Vi3YIhswMB~zADzG*{zSBa zqGsJlq1&amKK;XoiM)xIqh6=_Q^WzQ8??dM-@PiCHBrlWVZ` zw2$;N)9hwmjH(v>o8;v2nb{rf*^=hNw3?#@MuG72vejT7zIXkJLU~$_fUv}pZz6q8 zcc!lhr9at~RU4@!o^qrm+QIcl8X-F_cRTBMf`S^fYja(Q*X~mU>3f2jRnj5)jfdi` zdk6fV^U7gE?iqEV&o*tAtMwtNsq3?G6??;_#0>6JvDDC9#Fe9-HsslwN7J6O8t)!h z?OBWcNnFj*9D^s(Ah)FJFc8oGMYc@kv*G;d!JfSP@OLGI{Vb|^P0V6g-T+aOrBj0V^35RKY<(WnZfv>G zJkw^*qIxG?x&1Qf*+(o@#m}bC-p`_a_A9CsoSeB1(Jsy_YEqF-oAER(tnN<)cc!Jn z8-V3GT=27U0)?Y_9RRa{C%x?#nRtRw)8SXfX@)L&!`E8TVfJNtg6fl{t@(?{43}N| z>V3#+82hC8g;&`a^3GbHPyDCs`x7}fJ(ZL_?V+N)N*Urkpcvpr@A|rP+->-+L8agi zqcrz!CB*VM0(WbN&S)X5qPV!Q&ry$W?Pkbv`D`vey~^CR3`(m_Ksl;NbnBVCdeT(i zR+}d^r|(pivYTQA#tADBbre^qboEtI_L@vK(iGn&r}Ok>2agh|;)d z*QH&p)`CK*VS(RS)_UJbWD=O7zYV~f2|+s5YCybq4nyGiB)(Q@`a?*mc7lbr_0jdF zPs=2p@ZMi2k2|c4u24F7rX?o=Xyo~D@)exz z5+s5gJf#a>@fv>`Sc$CGHMh#M-Q%)chACV@2N%t;GX0mb)LV|7{kWS2hd;dam{bo9 zZ`gT6r|*1QOd@h42f7*YxbLAz;368xg5mXZXh$94(t=67(qChY?z7 z?Z)p5_phwe;Ff_-Zv`ob_A(Hd^rExOacp(S((^^AxPJLvTy#2PcPMk>8Uvw>TIF@U zQ7gQvTJ*<$@*i>SPFZPefRF(F;@P>B>TS>K2qEW~nOF6y1Gu0RrktnTam2shtbY*X za6Xi5ysGPK&Fc4CUZf)-6C!o38XSDuxOq@*a(uR)_Yfu?(-d$QK&T2V??Nga;ZWw- z{44p{-`lM2`{vhcOY+a23;71K_ivtg^Qsq_hubFrxY|JBPG4T#)!cB#di;2-%exny zc^D64mLNRk1(%5D50Pw^yR?sdtLj>aN=B2LOS)pWMW1^)j1Eg~yEeu#nO+-H(y-BM z75IH2qb3&x6>OCJBcCuQ6Qm!xVYUmlfpCAlvQ_8?#yl%1TZJ%sEEa z6HW!>;DY1GCvtxzRPG0Kmy510S7<-&uAkQ0UM<}>A@ylxKq>F9bnIoCs?y|vg07l* zC0-L8g#xp?P$AD*>|9!Qxn!$nYh=Ke)3z=h3ORL5GDSrTeAa{d!jT#N`cWUT7VbJS zK5BP(zn*5VD6JL0yZH_36nX-E%HX?SrBu22Ixysg)H}^h(`>aPhQysy!m$h{JG$zX zDLgB?%WNA4y{gg6fMr;5wbwP7eE;A8cas^`!h7>A_&`#dTeV3f@f-Ow`NgknyJdWy z0?&tUcU{NH;2DrNjl4=Oi=0X=quBnKH9{qy_`ZUBBBB?x?P5++)nxVt=x2<@3Oa9NVZV#gWRC#+>%ZP__AcL*{R zvZYHa4uo>9Oe zA4%NZY0t>ae!r<0W^ZHBKDqddw6gUZA~*=8%IzEbX^D^&6!ljzDo_o|`n5_KY~qpdZ52W8IhW7v*UacjkIAFB&>-x*Tec8_ z-Metf_kbAq(^O)_Fmz*I!RyeY_GRW_=htDmt6ptT2z@*Ig^ts&#PckAy#`@GIXY#1tst zKP|4&IXW*VwC%m2uciT&bW5)-1D+-yl_xN_f#hTZ9W8veK=;>873hlj=exf%koW6K z?v_JR%W{Qr}1Y(5!=QU+hG@0}-j>As|Kz6vrfE3$cXWLZy$r1vt z)NwgbGDTgZ2Q8oISPP`v!eA;c;s^(nr*e>eymM5-alXRxuBS%Vubb>LvDgD<=yk%( zoEk!7ave6nutB8{qc4Jo-6fALF^KXmo|ny@5R2j!DvoIDXWo0<51lZRX%Or(u`;+W z(j&=(?@>lTkB>re?!AJqE>C3K9T%mM;8F2t^TGWD^y|)hO7_nOaGoW!Ew6&5Yo&+v zHzi47_)?)08pjxh`%OVCdy~kgMTgLPLs{?L->K2{ZM69xGuj`SZ6pZ=JxW+dc0Ji1 z{hoyBj+Rm;P))y#h^T;8V5+7KXNaY!3|NG#R?`iNbuRVhO|i zw0wJ&^hm+U?!^Jc%K??+{F&f$3vyg$RM?j3O!tv!(jz4O@-5M~&hoWrHk=dYNA5-% zmygZAXP=9Z9PZ@YH+Afgo3=JSosnphnjsY#%TLB(@DS~DLi^>G*C@l_DN4AeDs=V< zkVOD*J;IU6*=dK*`fz)=W$1u=Ohz2<&~($cR$s2C>DlJz5-iMp;b_WieQM5B#G*=( zlkQ$9D}}CDTOdNxl-Ic;h=5J~q)^N^_OI53c5K^ci2>4^_XIMV+47Gpg9QV{-YTFk zmfyE8B#~Nm)UZ{a@r3UD&?~gr^KSslHaPU+xPc!3Sr%z&9^uS zW=U?b?;f$0Pp$~ry%&Oxa9^ppSKXOn=Y8XeALFg)FKY(o&Yd;Rx$ife0%Jq3%R<4( zDE9o018})tCM6goZU*W;8X6dy)+_m&=*TY}60=)nS>b zz;flCse!z{WEDsQZTitq4tV&pkG$#=Td83+zZzNJvgVI!xKu39-+NVh94{(7TY7t9 z{0edVd5StDq$i_AE1gOA8Xk(-s~r#?aeQc4_FX<@>5^fkMiiU zgne+_1J9}O#qIEQT-lwPYtayPzRAT_@HmAMWc^LQeL1(6TdR$(_gsC-LY_2k8wa}U74i-0QTOVwz^(&@ix#pT6?gLH{LDpdW$L#xZnTeCdk?^R`aw|0|(jRmc5dAyaLHVgM!_O;K0M6wXuth~3&_i$Z5cGM&Pw(EvUouT?R zvT1thpCJL30ZSfS{8#1x4g4Dli9~f~@Rsygyq}E}^mKb;xalR7Ln?8EsnIdm{1q{a zvZzi>yS6_Ot{c{^Vto+tnso!nwM92SDjWp%sKN7-)=iJe`*58%d52@H|Mf! z()~8xWomuEDd)smast5mrVYZGK%`RR*(Q20vYMp>6041eg&6Tuvo6M+BH62h*7Jo* zns*)5paI_Hn6Wy?4I25CciwHi>`7Ta0t-la-bVr%3LA&Qa|EW!z_iQLQ~ow10NCtI z``XOCRXQAdnwLTLQr(F}$8C@Y+4fn>ZmVMy0++8m4Ir`B;FR72IVSMo(J(p&sLExm zmz3>9yYR+7zduQx4=WGW4E;2~cV}2xT>`reO{u?MG%{~Ik%!qI22%hSZy1>bnVC1(%r}YCc5b~ZswfHkt3VW{5@zuajd4Vx={cKxJCD5UJj~Pu)>m2ha36c%%va8jeye;oG=DEVhinP?`CjDErUGMv-a*3o=h`vO@YXhCS|y1vV^sdMbqUBJ-dXbYW}PBnG9HQa>6q zWU9;MrnpDWhM#mY+p#@zck({iLBN<>$c1j_%=R$Gh%wv>z!TEaxW1LK(qicNb-5$H z<*oOYmlKiaul*hqKh}E4EKe@$1y=WtO^X#k=gxLS&CIeO!xbqU@5Ca$NokVR9N#(}Lwgx;dV8dOU-6^f#N%?7%->xfM4Nh6e$NOxRN(^59?x@zO@oP#6xZj{m1S~*AHrJ|Nq#>%31 z*^P(l%J9tmG7fS-#G^7zJre&4bY4sX5r`8*@qPKiV)nF(Sp#1h%ptrsm6K&)X2LCd zx1Oj6j)$`_=n}&2eh5kFt6!}9I!mTJDsj4ZE5+XuBJv6VC{|4hk|{^B@0T3a^BKy!8Kxf6V;vqB%G_O&nUZ)MfV}3wF7*iYF|O{_S$$og z*zrQA#OBU2XsxblKN0G< z<})j{cf5=Gy1$Z=E(hk;db@+$+8$pxlc^2Es!RWKv&nq}0Hr#N-&Owq-!k>hr`j5hAlY9TV`*22|{Q}pXw{d%MqvCZkWC$tm>af(Q@pLlnaVVo{j zyC+h9GXu!1ePkosGihkjcDej6-)32d1$5cq`M%V_oXv>dem4GmW~00fMzuj6*=9Hw zx;~%jwRAsvR#28=`lRZ@m8Yfo$G!FF$QL6dD8$+EL~)DwMEyHMcq5JOv&Faw-p-ho zaa>S_A>%g~2bUn}(si-ggG?Rj^-~SNtPV?fcXvETLO~P;OV<-fj4{j%>6yGt5xjAL z;#I$!Us%prcNfHftQE>EKBm^l+WabKNLJZaATj51z;X&=tvmR&`E<;>K90q_)@E-1DVz;?*H6#*D!m*q zUFa~>R8hPSR5gCPmKVRN685BdIJ>SB5lLG8$aY~^-H)kR|7k`~kG>k=~HPtg4ZMfc$58qxD%k~&UcOP#k zq}tfZDklkyJW|-z1kEicy|Q z-M1}Ct$%!`;+aAhkh1L$k2(c=)Akt$nG~n+0u|1jBhO(Y4djkI z-wL6%jLaIhR)1Yw!5D0}&=)gSs*j0C!kOf1R6QlOIB#O@W8Fl~|=iHPB| z^#n8kk;}l0?F4f@QfJ}0Av4|maZTbxpP!tPz`THDp6v{&c=jO8MmhU}aC#(-^}xbo$X`7@C3uwO2sgob{WmhN!J#k-Wc$aeKDyrn01jQH2<;hjLC z(pTDwBLkoA?H2AQ@W+G|*kT0262k}`WHFuxNoEyq$%a~vA zv(ob{4On5ezf_IWZ=Ju}PNT%pSw@-dHBIF2N7aKMyScUdrZNX$W=+>;tktku?Q>AL zhhD~Wyx4^95XSP-6yq(5((H_qncqJY_pocz&JS7g4tra{oT@}K2gE_6-uWs@Y%S;l z-7hk{q0j*SbNlR;y$seDSnl$qyi|Vo$z`#Y&^3|Fe8P%%s2E0+SupBvQgbAd(A5T- z^a8+w3X5}yb7Eb3wChBznylG4<_WD_MO`*~muhiIJWsN&`LOhl)55ZD4xYjhxFoa@ z_xt4K%Uqd!iIfsw(t`29g@qusw9p!X!kSF=^0(!6yWBq@#=8km7{MVJ2{VmyEs%Si z>$%V)^JHJ&MjvKdX4}@&(5KsUtIKH=Z>D(-I4m|48Ek}GFIroBA8H$L{Y zPg{~3Oj9B}WD#kv*17l{KiDUtw(arLhL(|V8>HTU+{`t)FK^AvUkMP>$o(i$-ZDX~A`QNw?;cmGH-k6G9HlEE+oa9Fia}%al8pQFxv6uh( z<+I7driRT+DBhwjr=rE~i+uBU{U%F#!0Y^kJYEF z0*WTQY+xMi!1L|CURH$;3(DSi@K(pb@|NmxB?Xx0$tPu)je;j?Id(wvh?vk!F#UEK zZ@zHk*4~0rleBtGVcWc?e3;_iLG9VL1ASkwvz~rJU!JIdJTQUJ^Vj%1oj<|u?tc8k z8(mnz&dY+#!A!^Q=r%q!l@4?jboq$Q-z-3fx{alVD^Q05Q2^?0SGOKmX4Wo5BAoHjE!5sGDCeI|BraN2EXprw3Rk5wfRIGYMO=g+-2J zE@`p0JCp>w*figD45MMCxVVSj`UI0LHNe3xDFG$cCb_NQqnN-u#ciA`>T8S!NM9@3 zTQc9**uMfQ{hfkX5*yEpCb$*`iUa-B7dHy(d~D?b=^XrXm+m*_N3XgnYdiHN-V4D3 zxFAz6V)exJu%sqD(yM7xCTGm=qll#!sTi=9-Ue1FwDc$0DXly6?|Q0cH4H9+In}sh zfC`a}ui}#eBX_*&gcaPyfA|yuMjXhVG;}BH=uDi$tscc8g}$fo+Y82r&?Bt7tS?L* zl5ah%B0dS(^eo#=h)tzrsz1Nw-oIN<@;da2CKa&b3U;&YrqtM&cA2Q79VpUDKK3q3 zPI+~HK1Mpw1ugA=b&eW%@eC95)^T=$>2lJXcoW{n%K&QN$9x^=GL(xoKp>zb1MlQDF3$=2}l8~Ce+lN7KdTrfm+*M8W%!nf^k zf8k=ubB`0-)X2&k8Ze3NHu{^ zXUR?DVn9mxO(>&C(dJ|*r<6WLTpoz4FHqv@`aU%uextm56s((7ux#9*td@Pb)io(t ze^B{UzHv%Or~d3*Y?%Z==t4uK{P4Lw+d}a zYt%-ev0pRQwbF%W5~mH%;HH6O0Uhc57Pa&+-{jSjxXnzBUmSt)kGM}C%_qoSUm-0) zp^MA@WlRWj3T_;45~E{w)uj<52d2uhP6oG~v$s!b1?py0zdQSDm1PTF)V^u}eVtI= znv_DoSK6!kZWgUCPv_c<5sV9$20#??tS z*fSlE3@xpC{OOpK4gBA1R|?6qRgxvL^#G&B(%Bdg;^C%c4U0$? zZ((C!_UtO{6Xe!0{9s-Oe(&5q;;s&Z#2Q*DTgToCXfV{2*5zK#;Q-VM=aZ`k;;Pmb zw9Dr4P1tHJ8*J+3!tSLDDw7m{>8Z2w3JcS~clS}BADl%$>w6Hj+I%0L{0#oOoQo8g zM74YaN%lAx*c_i&Cv8BILC;Nf*+9O?>%2CP60f=YqVC=IlYS^@oeJ&K1(iYqcPP)= zLJ3pZhkI*oeuQr7DC#-q&5sjRt`;P)V6xecs27xlN0KGxU-`nwJNU(VyRVH|;EKIi zIV&i>gC7vCnUyVX&fZPFg}>OlhBGiuPwmPQEbKm!0P6Sc(;PBD58UnseGp4khjA@i zw(M9o6Ic^l;U~5e6gv5RaiYn_1AAw$2H-8W!%P#3jadYd_M#<_JTbS6n#G~>&#e2g zRV`&5Wdp`oJ!r2Bd|l7l#C~>31{~f3&0xm`w<7SUx@^ zybM&(^UyylVpqGwfEdw82|OleQ4|Js;BwC;yI-OYoLERcOn|`IfYYy`QOV|{l=;qQ z&a_)w{0lD1g0&bYZi&rCO&&dpy1lEml5Yp9?;cbWD_JMaKT4MQiGf`L9HEE9=U(R$18BmR;w8 zkshfvh%l*oLSB;dsYW&DI>_2b3a!?QL z;qk%d+RBZGh~Zrmyt{nKLoe&%%4FsI0A8uc%KTCGL4AFHJuI3R!tK;itRDJCb@2gg zI7tMZr*Utd%wK;1!%a-)0mQiQ8(Yz?8d5ZYRf}iKRZSnN8E5TCZLgyf_0aUVbMz2l z{@}VgJ*I5gZRoEmG3aJjD1%wNDu#FVx6WLrlg%H6cQ(KMdmAq`zrp$KilFV1dpr&m z4)&@aBVl^_n51ATOjV27sP&rJIF2N7=?I@0Fi;IvYExdUk=ghNVl!c^$2CH_66Or0 zECrps1p74SnO-b)imhD^T!-dWPb}oO331aJ@Tu9;8IBR(vNm8e2O_ zC#Akc>8Cu$Jxs8w`A2PQdpKkGxv-VxQ-J6QGYg)|wu>4A6^%sJkTDY=orj;W@4P8Q z{MK)eN7>k`RaG6@7J)UB7}islt&MfweTdLv8)h8qnIu(=yeS-o{L-u@AVHxXHHN)4 zdFJf9RL6y&OYy7P@i9lq0QQztIT<66iK4&1uaH~`^Z1F zC0^rnl=c1aI7yQCHxE?bCDYCQI>5KH@#b2mu14wSYcv6fvM~f1tzLPt{liz&`w<6QtLxn~p2G`j+0$`pj=+x%Q zBR#YziD}g`!=GKXG6?3rRaJGJf1nm2F$N6>ve!Dch;`Wh7dOqjUQypAEiXs~W#0*;R!0TKII=8EJ~E+6dxUjPOn5 zjzRQj%YVT67sz_nVZs2c`n+KFZ{D`u^v|uW;VUg?BfrooLW$=6(){*hPS^7W_79*g z>9;rdi%U*COG^SS#J(Ppc%J9_cZp2S#cHKQWW9 z-ztzX`($PVDaakT3>Y8A0W5-OZ?3l@$l&*!MVXDp3+nM>(#{dzTV0SeK{>$pe0aR5 z2V=&&@3H~fmfK@d^3TKJ`vm{5z4wZWYT5pUw+N^p!j_<9l%zz-u>nQN2!bTZlC#ht zG^uR^2$Ce{AUQS?1&M;B2AbUD3{7rb55F6V+t~R^Zt4Yk1e+#k zW#)+Pequ>TogcN@@#2S<-rvno@y(48g)x@*jT%9p;;r=#Tdt))m#AbYv}~+J>dn%P zZfLpjzc)5NE|T7};2kFfDnlsbuwu=3KQ56=ZPBwwO75(5mL{S?)9c~c+H{h`mGhlK zwOqOhdl1{rj>jG=DV4RcLo9_)u~Njq37CEhDl~cM8#}kpnjsn#kf`aEXWPW!I|2^^ z30>%luDJV0aVW$UFJlB;OWUdDLFJ=d(;vbz#{>1mJokY)lIM!r z^iYQCa^i#TLvPn#M(!sag#8S3bCn)n5)Niw=H6HnWdq(4j~D=M+5efh+%-4Ar6JmL zmE9haao3FIwuE$I>}+nuIc$G)YLyQ&(ylcl&tBPBkW9W_%KFxQsNE^INpzV|b_bpL zDiWmK=d$0fYBJ|JeXr29_+XUTT`r+va^7PFSuQNdX|6G%89~LzRf+@JikwoMBmcU1 zX&PVGHK*UM*dY!dG|%Ze{{4%b=whB7U5sY1->1vOUhTXzO?Kmf(qb#1$k(;n)Y?bs z6UwBci4xOvmX#$Yb=+kdKS~B&;0ZTXMT-nGxo?||t=E--2M(vgV*+;GR;sU6xAy7# zjs;TE?|FpOWGKioSqPx?BSmy_)ZF~GfTJV#@4dQ<@WD=RwyyK@TWH%$KxPB3AK&77 z?3`560HT`vQ;z4-MAr>PXwPBxw!=rI%rGJ2cuNM8Z<#?ySMqTg4L#xnBsF;LF6HJG zO>%8y_pF(grRFx>v~*m1_3u|FlcI9 zlTIjZe-~Q56{_>fMdNPVG=xK!IK=H~P(HYSu*VJ)ZHCxx_h|5)*Fn&XxQ@rtM?;*0 zA#_!BPRA4F4q59bOZ(tW(SwkYdNEqbE@8K@_4Rp+6&FMAj&E58hJ*#YTf){0{q5FW1kNCzoBg?`QCG^)b5tk|~S#dwW}|C11W!U)8Z5H2(z{cXBi zF$<_%R(@`8X4Y4&o+oD&3zXTK+aM4H6>Kld53|GaVSUGIbc^D0$<6MUT4)%$mN-s4 zEPeN=p+`-7UWVVC>LeDo>jo5Md^^da<}%vvC-7cZ?7^QG4dda4cBFkP*~&JgF99H2 zhBoAUDYG$isKLwD!Q3(PhQw2rK4ga?L?q=@*=WtU`lpK2*hjf z*J2a)mF0coo`8#wn}XYgG*(Ay7F)vreTh8P7Ylk(Yp8FjrZP-`jpu{1 zXKcP9)3-mtxBumb4$3E+=nAi=Z208}CuLX^cj|MWpUZ`Zy(V_ajt$+I{02R@kKX5d>vr;-A z=JzJ94qjW@V7HlWCu(akr{7(xrJvKa_r!Oj>V!V3O^Cj_m+vacH$$dc6pa9JX?Kc* z!^StApxNlpYv9pT3xpjep4O^=;K(_MuDrq;$92C>64+qZVj0DD!}DMHT#9wLT8fg- z+T7d?9vd=GG-)uDfi8s<)<|HIRagDG>=gv+(!!#B!xEt@(WSd>LP`;GkuBb12j~~R ztCk_leugXMbROT#Sb$~B<2mh!%^F??JdoJi3Njcq&0YIVNvVObp@8EDZK8M2Xs*TL z#xDcbKLzjPjS&zYn5mWSRnOEL%1Q)q<@u-%vK1N)(NG>DE8i)a??SWn*o;YW7CE{g zeaBO&o^z!GzCKf z-2QlcdhCyN(sx2cZhA#8W865mKt{gC8_9Of_RBF8oe7N0ic#7KTyt|%v(^RIDh=Mv zRg-321GoFHguG=Lm@cilOuD3cp{}4?fhq|{xve3B#b2ql`{m>tNXhRy*f?9=Kk779 zgdsLw4ZZC`2cHV)>BGQ;naUYOz2g z4(Cga^^12rw+SePD*D_PfE1{F{=3<*&m?Xnu}gISasIr49c;_x{cR(HQFQi|mFje? zz%QW_h@CbF5jn>HOtaSN#ia3DZ%LPL@}k3)@5+eLM8F#1w&Lv(T-pkjX<3#e*|(?{ z8|RZX*E9HxRXmM>13{Nu=;9%SYBp-~sOppsp`CbYJ>Zm__}+VwS94{=Bh!ozoSb6- z-?;Xmwde@T%j=4DJZ!mSWl!LBShV_W_Ri!j%aI!O zwu0KGlLreHKASXj2Q{t=M@e)ML0(thZX8i7#2e6>p$a~_3H#S3rfe4<=vDJemOoQ~ zmqFF}ek_hX^Za?&%58yXv4C*3Ap>7aX0%Fl4U|;^5~Y#zzC1kBYVp;Y2%)>|RyFZ1> zfdg_*5nr*V1&8TL6C5Ln`JaK;*N@St#hS_EnvveITWY}+py&auWu-RTI2ujgS6+*^ zgb?MD)eo(M%5INjDTbZ&*TM@2(#334zQ}qPp|peVr*50(1|-aaRN@T~syzc+nV`$8 zFH~kkz$Bf{@r1OXqN&fRPFt&C%1=CD-?_)3#iJ*I`5PO(^HZ9lh6CRv!5=KRg;zVk z6R4GjkAZ=dKPUk;=|W}QK+s6`(*S!%O;)_dKt_)SQ$H~*x0m?2Hl63#c{3e1KFTIm9_%a@k!JgSie+=zwV`+$sV#5yi$v7&b=e*w(k(vYK>t(o z=OVC^>3&`pV?o(cQ9N~;>aw-1uxxU(kGuhJ^o2H|M}NY|@KT%39m~FD`>4bwmeaE5 zf?k21-aZbUCr?6!tlrM5#MoBH1ZEjaTe1>i7pm9{q=%h56WeyG^xa3|?-*VQ{s2fp+xStO?W5ThZO9F&ZXI`i?(!Gh(<5^DAEE-h@@uac>$dvF2$eqTtSCuWO28 z=kbEYp8}GZFT=y*UX1{F7ogr#EP#n`ym?+O^y+O%;_@=JTY(w2s8vpC!wr3URJ)eo zwyTR`17q-kfmoT7wcs%+{dzyD#v_SaHG|a1B7*D_z8k-GILPUJK7nB({tVWQ3HL6! zrOjF>{o&@Pr*G=%q6aFN){X~py6Ge;_qz&iAw`7_JHBjYED+^y3sdq`N;N6pk67#! z>ZIcDsxS*p@ZMadt=Z#n#=EahB~YeX03CZiA*oI|<~=Mj0ry&<)cu^bu`P-)PI7VA z(RhJFDeBszmZ{}?T+Cc;eAuTS{9gPHeB9Lgc$_+L!hQK`i3KvVwphWrCQMl+A!MeL zO>HAV)PyiP76(w~uGg=uaOIJ+wRS))M?c^Kk={Dyc$i9f`-DgCkUNT*KVx7wN)l zNgFxW-;lSK1{oJc;a3$s^Oap{Y%Pmu##ZcgLbW*@4u6Sv*Y(0yx75(MCD<*#vdYI4 z`a%Xk+4%i3bYU$Km3EQx$||=I4U}cMdeYa>^O?j6IkL! zF7{JGqHk={%G8T@NqjpeC6ABy*ooc=9nc{YP;(yiU3Df!>Sku}0Gc^E5>(mP4yljb z6OTZ550{Sx2S2%9`E)SzJ|SC>H@z%Ef#XMF>A68kc@!z+M#uKON(pON-@ce=cHrS2 zFQQP%TpYp`;_tEsyJEItSkqiA>l4}kM{H*x+1jC8r9+k4<|eno`WJ-s+Sl}IPpr7o z{aoZx9Gm)H-Kq{V^V4L6w+X4XMEFEQK78}nGo(~7ot8(vzVPcu8#$oiWnA^mOi>sw z+AuA9YO2{2ULm(*A&L9k2!klyNaJgtZ4&aRMJ@{=!ptKNj?>>I;)@R)ZikFwBPpK= zxz0xa$XfoDGg$+jxC`BpL_x?m+fmDDCy(?Y^6!L1dBFFOuxA<{oDd@xkh1#cQt#eV z_4zZ=jxCQNfckVT=c0AhFno4d&0%;xN?azx#EL}za(2?LPsOcLs38%)6^34ym;tI& zom|}B!n=@vG2OxI|&I*kXtVu15EqZe+BO-H0W&l zv$tADjX0_ZE4tN|%%|yUj+QTiCx1#NeOz`7S`*=(IQT*MC;g1}Rajy%Yr8aJ^ahQ> zuY2LV_AbkUv^RE(p%XMHl%|Jbl;_rEn`zOKE@$t>5)idUvD7_qsJsK+Wo7L+ZoMu#(3;h#8`yzu2lQn`A(>TF za+~fE4w}xhh0+>)hsC0=WLsxZ_|-iY7ov29{mS?$g`M%JGC_A`mm7Ch^PdptCr>O^ zKlhm6eo3daRX%xW0*xH#&^;pF^OI7-b^g(V@m$*r@qjhXCSS{X?4jYg=Vjw54Zr#7 zTNbh7M6KNo8?Ll0;@hhAM-ziL%T1OWn@{$HmM;fU9K%Ih3$?h@k47HFZFWMjtJIyl zP=VtBmafzYd*IFH!mCoZ>t@~kh#G8S=8vVHrsCaq5M3 zs69kx3O3H|k+rQKnR(CPDkvcrms&x*FHK*1fOkjcPAYc&3BR!01LY}q6ZasaravYI z#?5cQ1M(GCj^0%;PbJ;*U%haxX_Q6kh;NI63%ZX|&>zX64MRCtY%D2nMSNJHJDV87WM(3hh7X|3K%vCXGzqqcMul%~WbcjY((fpDQ3pqTA5XI(zCvWaa&-#dQxw>9h8H^xn~)uqRNjKqm4_vvA;iBv{vG|uCL<%R8WaPnIQ&) zce0LJo|`5xN-6En569oGIXuqv1gBTbu{yT+GDZ0Axa-b1mc`>$y_5U zu0q^l94E8al@>kqcX`?9Z*Xah@xgC*t)jOrKa@oSo@!yON6Pp{^#kX^w)dUJ#Y!?qRFzw4?vmx2EGy4c8;&zEGj7Z# z)*9U7OMo!i<~l+25nL8#Sq);#W@Ge0yDW~|Z1l)Ofm)H-QA^rC_3ac-0+U|M$6*D- z{p@FkxXLR97Pf)%!#l-M0iVgsH4)D(9*w`fYZ2;gv0)Hgu3NtJV>kn(YVZQAu(sc& zH<4pj0hy|GSR~p4-XG4QPrhnkMM!e{R7ci4>?`{-L4wY2=U#=DDf`xwMSkOc-8KKG zHUV0jl~|xPYWyJFf!9u7b3{7^n2@)~qGCo_&R%qf;;P5mE@o1nc!t_99uz{{H@YWMj+HOSm!1uY?b^M4~eetNX9J%AChSbhPh1*_@ZFU(pE# zeNo~~&`3Ibc%O(1>*Bnt`MwA85U8Ou5mpJmCCVr^QqCJSeux%DWB_NM3<+Wv0$*hm z|A;Jjo-Q<7=~-3~KHychJ^kRpQJSoR;wAkT)m@N;Ipy2X^;q=$?psMJrCyuyiJ3fu z`NM*TgHKtbhs$h&dr3Kp7}Q_jbT5RHWyR`Ef1gUKb}Jzh%jKRE7CNRnT!(9>2@Q%~ zD;Pf%5Yi*5ewZiLKOeWCiB- z&lu^&Z?*;=!{FT)ruoM3$Ap&2m?j})2qIzG@OdOVmFgC}ARU(Q6kS;!U(3KP?yZ;T z=rgdi%t79p0Cx!6shT(fw+(PYHhv;K;CHE(H}}Vbm(pIAcqF5fB!tw;99)Q3nks1< zQX~>y?XlKom3m+529A3yqvjA{Pl*eGm^%UORM_4o_EziUaNwOG5trrAqvYnd(vkW` zSy$XwEL6Dub!NRDCnkpSj#mRs6|3%FN4IhCaNeO|BkD&x9#Bp8xw(4Sy(5E-cNkhu z^wJLu`Rzp!Zhh~E5=SCCiC(@p=36FCwzA}M%_ZN}2_1ho$WS8k`ltyfeYd>ai@b5m z;)AdogY5CLWF~%p&2}H=ZP}3Ew}6maH|D z5vztT*-*bb{xHJ!*Ip&v|BUi)oSa)QPHuGT*VHxjsZQN4=7@;3hAh1tf9qZV+(qr# zlsAXI+49Oz;}(bi(&HXmi|S*WBrZAk+-q2-qnllSoQ3{@rQH3s1?6>bh#xOgn-#6a;Gob_Uxw%Dtd;t@MQ;2XFW`UhqNL>} z`&w-wjwUr6F7Ex;pUyOWZ?6&irWzico8xZQ*kXW^YH*;xcT@jHBAFq~!Mj@N_}BMG z6SJem^Ueo879ZRj&2lO5$;b8@V^6Vxev2VQl2M1M39S-&Wo=zF8|WHDRQ!`HxU}wb zgUH4QWB^t}?4XD%26KHZPCQ+fe^3i}RW&rUui`0LS`NHg-RHP}udQ~M!T;nM_A#uv z;O~>+|G)^nQ8vP?>2*U>(jr{ntF;;Uh`6sv%>D~W;|$`F6{A!#s~s0P(UXhVMJqVC z|5vmMNf6{rPq{eBIuZ)EsYSbINwl!UtZV17vrgyp(I7cBpK(b60JajiL;gPk{->+# zJHYe>3NGiLN9O`sf1xm1(CmBv{0zVT)$}oU;YQHh!-y5nSIS+2BlzyvLFbm=pg;A< zy^&I((&vcv|3@@`Ukw8ISpR=oMF$LJNE>M5Y%!Nk3p)1k5!+qQvG^JIiCIWe$ns#4 z2zj(;wph~zGTMwlafW>9`EhAvl&-48{(FgFIdMy+C$JyKQ)yc#Sbk;1aFCJ6Mfq}( zxuJw|(^DFdQQXM8YF*;Cm@qWHsd55Ho<*4+DD@h7*WeL^SeMScVyYbfg1MI~m#(m5 zwcf8mz{zsw>m{9t8FtgoFN>GUDnP=^C9c7}Yjw{Jp0N=|8B%;}-P&macNDqItX+aU zVgBGWw?&9=3-;(5=;4YRu%{1+h>K%nw>RKD9c8V+9)Ew82zRGyD zfm;p=J$ciSBEg*mbt=|dSfMJ!gxbu@%R|=R*x3^c!YA%`DCb`;qTomXpo0Q#pVAg| zYe(K|b?=f6&}zp{j7flX34cY*W#DlPzsccgtDqIJJIy!ZsX_#d+q7dOTsKaCJgwKe z;z~;!B4DofL9cA^0HWiuAHOm#{tKcbnDd-MpZv#WQ9Z=K{`PJa2M1S3Iy*Og7e)D% z)qL8GX}%GBQ{Zh6LI7Uz@|3%vG0?`!3^$&czUv48N{2nRHycakRxos!cyj~gHoT+in08FDn18ko*SD=dBtJr$R&w|x|Uk^^Okz)719Z%)qvqypS4A>#Uaa?@$x zFk4LnWG>}pPrQv0=Ew{N@c(1QZQd#+@))0vYmY>%_eb!W9Fvt#UoK0Kc&!OM#%naP zkn@?>wYB}U`@VkumV&4q;ubdAXB?m4iGRsB!-rz1$Qbn|@9h^)oigjGQ-+9+h?&jA z0=xzOW`}j*GyrcOI;g<&ptkC&1EuA9_jQJJ9*$gb&^}pUSzKMEErjeXeF!y-mOPc` zt;BIQTDt^5GWpT$W{lDh15r#A`bv%Al0c8mTm+VCv6MiGF9F>oj24-WxFL)TRPO{c zJe4#r_B%ceqs0ruTGo04>W|~^^i`RZuWjL zcs*^W4bOu`8^ejqRiQQ8TOfu9iylEWJ8sNg$nKrQy6~8Y`>3%>{#Rkk7%wlcqP5Sr z-fdv3H7CH;p*5%_4|}7Y9XBudj>0}29J2+pn({}ad74^HvY;l)#cOqAm*TF>E>7?t zjm$@29b%8|2$lBBpPyv7Gau>ZK-Dj+mu?Dq`frwxveUXA`_R8Wj=Yk=hUQ!4Ko{q0 ziOzVw9$fbHxvG>JkL1`)Kyn#m*YC9#jqR=6j8;Iz?;vcdC%AQ~bm!(QT=Cb~Af43> z8l9l`T1!q6sdld6-l^j>E;lB^IpJGgrIPe^7nXT z7EE4}blJiLo|fddRqSZggPh)v8D(Fza6D3?i=O9*=ZYY$N{aF*qX{q0#FyaS8kfdA ztO_0XSOCcx#q!(5D<$yVKV?ueabjeHM=)^T|?py?dq z!0=nq{s|rk_IM8^c;7s5`rT=b=d$`!gCISxu$}5pBb#c=j^Cj-@+5mEswG`yUho|d zv0ozE=zD(s@G@`SAHq6k7jK|GQws2u1bXkC9dI5dI@i7}p+6fe7-gN}WU^`T%A+5K zu*(0Up*Va0k4v`zoIS34wgk#=7*s5uhW`t|=$}Q6;Tg_p$b8v?QMvE4$kSm$&y@ls zZo)h0v{|`)dd=3P*1NXiOXoYqeaQ;o4Gik2k)96{fRp6Uff42B(3AfxCx2f_tpLuw zu`@LGJfs}}3zSAcvLMeUc}4ag+Wx)*21{mr9ohFAbWtV^fIn(`BElExAQ1!T_*$IC za-OFEu1hwsgom~v>(7Hj0(1;sBU$iEU>5idx}iLIOIAU_nu;}{u-h;}J3@z6bHw#E z{93N~hYKAsh+lHYzDGZ?`pxKOcvt@mhT$72H-NGqn_u`Z&L^Y@XXyS2FID8mUjUYU zO%`;+3nTN9<(m)Z7p~!$TmT;AAxoB3xz3Xfl=+_Lw_FB*uxPpjw!Lsiiopk<;N?eo zjlYHV_rlEZMv508oJl>&V!{w1ljQqFn(0%RC^9}r*6J`MFnIzFZVbliTa zM*f@WDgoka?L!a#hK}n19gQSmIp?PZh|iS3^tqPSPV<*bKbh0jB#Iu%1Z0IA{&>{d z(cyYLr=a;d2#a+_eCT|-xU>}Bx&2y1%T;D+xpTj)d3vz#GYt$PlG`Bqt8-1jI3n}G z1*+b&hR_c?tIi#noI{6&V?y09v7E{HlCTebI%)=EEtqwJJB(#+W@`+eg;aNT~CaWk&x2 za#ve<+eLF$QZQp~5~twAY`%Hm#nq?yDQyUkZ11=fm3XWwNJB8s>(Pr?Y7_74Y>)r8 zp?|0V00vuNiIm0YX66>st9q*1OhTjXQB}WI)YZUCZblTfnaOo+Vv$!$mtuSc_samd z70(b6_qTO}=L0^e>v)fnv{`+wY~@%F2?)vkjAnzsoc)b%T*bZGDA1LfYv=O18RmcCb*G7uw@AVcID= z5v2@(xL|<5#A#qzmqDz-^q-{y2Z?dn83NzBx$u?X^EtTJQ=aLOU1je|1+UULyJJ>HN;$v$fi%MEj|jGYyVn1u zp;9TkeQ_&QvC`E={{gx2Y02z)M!#Vs*)VOV3Wux&*M{nk82HPF$Zj3%@6Y~3CvI1f zm?TW;UIcsvBKPlrpy?7m$+GGNx%56~hmyJta-a6=?^AWi zuSrhs`Y#V%*BtZ5d#P^nt~B=W5e`T*naNCIvrA^?vmK-Swx2oY*tK#_=-1TKq zLJAHl!KK%KV+McVzzLYdyHeS0^($WX=W@Q<%b)La6mi5RE8d7ZN5NnJD&brs!kU_! zxA`v{GV18)WNh!w&qAzbUYJNw{*GU3I74f3GsPMn8{y%Rl+qAodyd&wBLlt7q4k@??>RPY`L=hfFpmLZDjB7rL=|W-Y&Qek8AM$;lR28}G1VznZ{Za>bO@Ffua5Wllnr zO42K(V&DzagU%#`CM1~He!zDfkl16bc$Lzt?Q{J%DhB(7_mX0vwY4=OLTKzhWN)uY z6T7|LHY_R0v}%|CU&eXNLJ}sWqN!P?=i0EUrmAgf`veM2$}`;jSi&zvg$TK*0#_MG zDx~^giS6x57SnSmIeDFmQgS5d=o`Oad~qZI>Uohg1}9h+4v3PHvS=li+H^LHXMkei z<`R0T6Oh(#oHzqXf)oH2${uOR9ic&*$LY4XSL!s6y=FuCTQ~9L)?cAwa9ZpKt-O%d zUUMDSvC2ge5x~F!)mdbJz-{589XK&m&62l4CUf44!G7W1UFzoB*0rT|@0^)isTtwHI89k%s7@V<4ibIP}jIM?6t80#F>&GQW$3>JMGHD4W|9ICt>e zoi;b5fBbs!fjv#(`iJe+VVUG+qXzS{L!7pi!ut`MdWHAmjXz+0GVtL1DXw%+Jira*LUKB6y0onTh<}?c+JVT=S zg6jfK^4)4484~RPn%}#Q_q;ti~lHzV!bmuMl0TbRrHy;x>J8L=))huN{ zFaCu^&LH_LUO_1rE7$rqqKNy@!Q zIviX`&x~ld!ddCTcCS951bgcF+T~~Xg^fx5xKF)wOdt2XG5_Akd948)C-5Z*il~^d z3!x8eia{#bLRinN^KW$Xe;WIIIGuuHjEMZ%p#lGJ`vzdbJ`Ec4G#6~xNE;w*v+!H{ zx=5=ft9p=1ZWj4Dj{=)w2NZA0V^suvvFDekJrCl|IsZe*zpspT2_87x>>3j@@4qNc`&}TwW?UQ>l=!dKd{!;V!7= zx-}(VQB-kT6$+J(7ak{rAEpV=o!H9;1evQTD6;&=3{RVzGct7XQ8Sw{Itx;AreUa1K68WP9!#DP+}#&7l)4BO`ma7mj_bJJL6&Q$zZnEC<@~UA>BJ|AR?P z-^lG16IX4oB2pqV)r3uAbLe5n85mH*nI=Qh}D zeM+|XvR7+QzG>Jz2wqNB@R;q~Fd?iDmH}U?h(7i$Yw9s$cPi#lD4u4&Ml!aaD3*zt zktJ>})AEjw+u{pF$zR)I4xvdsMnNl>Sc+nH2f44O-&jMhQ7p*+{QU4A=~{LgP- z#42P}{AiaDVr6x0W+q11@e9vDK?em#k(0|5_U#Aeo98s*S*(CIvrm=D=9puz9cE=l zeL6=&%M8b%HiFK^*3PlfJ))Xx?9P9xHH_`nJ)30KxQ?euMFIi>A{IU!u4DAR1k5^=|N9Jpjw*pnn2TCZAE?p2S-HE1>a?B7OdGL299KCt0pZunN<`Mww)2Mo@&a(9{4MG1koKASKSMqpVjCn zk~iO9nfT8BG(nh-{%|nUzvj)B&=^aX`tz(ZKIMU&)t$_pGhRwnAE$Ucl~qTg6pHB- z2Jbg&;6cVghfHoINBz^8{G0|w{4Yj^$B&|Gfp3NVC+GjxD)q^sP$p(XQ7E(i&jNCG zyEeUc+4I&T#qeh|fD_T<7v?~(D4_-~3kvEJr#wT)UtMFawz~%>I=xLuGL}*C@URex z)7l{?01WwoUN$dfaP4pVt2MEXmu44?Xq8Q@!ahzyuchCoXu=MOE)vQn~y@K z0Pc0<>^a+=vb0=9_vaaVfU(`YHx!VVOQ~L@WP|KPy>zquWu3Eb8%Pi@I&^#DFzU&A zZW5gw?L+n6=ZD+34yL1^W)E)ATL%dd3;l#0b$rDH&flhOO=vhi(K113n>N4T^4yX? zD-4QoDGglr0lRO^RMb=^6N$8rH9ISB-7n!E1OX*bkY@q2ID)m?F~_44PZ!y8wJaDb z%EeW`wfFVW|5UF%N&gX}c2?=75(u9^NoM7#u^$WUb-lR&nHpm4Sj&ddkAIbBRTxHK zjy)_6cZfx&duzMtgKcU3Iy+lK2jINt7>B`0C&ZGCn3~=2SFDWs=__%pxSq;dd&>)r z?3j{&!p>O;MNNeYiSUQ})gHwC?A`H?7#gh+Z*atTrGA~tSPv~vKO1r|s5;;<6Jo_T zXJ_#2hKI#oK0{59Rx?#naQm6DYa-o5wEz{i^}>d&AM&X@E_zsOGj(m$3u9?})b+}C zl%HZuv13KYt@aPbGa(R+{ZBv*S@YLFPo>DM@nv#xZR&irRvvZ1NcUb!EUttLWOB6xhGs3bv?24T;J(z{_^Cln9H^`k?$F^W@Av@mQqRKVS1Rvda^vy z>#;FfJU1oxR@4fWup|DUI)I(iaR2byUsgV&RUE&JfOeCXj|+Zhe$lE>pOWGu46eK* zBg>sw*}pMxu(8hsJJbDD4l)?z8dk3k^m^7vDImD{GBI@bk=i)AXfF5VUYkHScz-$| zx$4o)`6rW=L^d14)jd*32k$bd=EKAjuQJY>u#u8IqczK|MI%D!aa&ElO|zgl_w?!X(u>iASnkv^Ja3_LR+ahbrQE7|zyl&r z2HM^yp}=6G~H!3 zJ!^cIaQsd!BQz*p%tLffuDX3eK0SLcpr8OtS{51@S$nLl)e+qN$GMp^W~&$)VDLZFSsdgyefwXzb~L}7XR(;V(FDd~pi)*Y?5@Z;m0J->`?b^LRrQNS{+ z7h6Kp5uaY0dFJijO!`8M;P_N`tnZ6Z!+cO+Eq>CeSF75>dbSzEFTT?$6E1^PiQ`fFb_eT6UJ*3Y9m$!<@7PAh+U0L`*30&dLPPenK51O~vvxkBNbW z1wFEZ*O-r&9jfR^NoSaO5T?A2n0NFanFiHYmOv;{>35fZ`nSYgfr*umenu^A-*QE# z%b)Sez)aZ~ALIhD2Oy^TZ>N#vLB{v#mXsq$epkZ|j<{v}Z#;ryE`}wguI<7-mpxoN zv}IszgdDmRX&ZsopU+w&i5z*fOEUTqQH+vwUhVq8qFBt(=0JCY<5HvU=am$`-n_Vh zvTg0e5ogGz{*J7NEcyf6C-3c@gkTc~I*u1I=Bot!f&=ywTj%n+=W(am1%^(F7hy5x zqnm+Y^0Kz(4XtY*T##F%3j{XWEs>miOYLLDF%_F@mE4C%J)4s4{bDXtxkypcg#|fi z`B1AUb>7qkHPX{aMsL(G7kg1~>$pQU$f3}=5A;t?_Qu)0$KUmH7iO`8Vi{|+jxZ|3 zXO@QKO58SZxaPH{-C;%K&u+#m3aVjjIXaWWHmqvjg~a3KJHpPT7*}Tr!<}VRytlwA zl+@ILw~~&Ma)%cnYx(XkYj6uH1mlLWdj{0IY11v|1cZlcPQD-le0n;scCjp!3&GYE zB{w{@KCJk;BLlf{4;x=H4A}qJV=~=JmwD&@r^30)ud2^F_R3q)z&s~w#y*b*Dl30= z>L1oFogdZ)cer$S54rS9r7|))P6q1tK1?(f47M@8T2l7(C%xyQUUz-Xe%QdpSP-U& z2hddUOYZf{yFm0V>g50x_>!J3pqXb7C#I3h?wX@{fNgzYXGhx$WtGil&2-ZL7MeQP z+}w2c+B`N9L<-7OdCHBXqFJa_ZL~-rUz5#kOzVXozSc0)Fzf)=Iz;ayk~7n>JluM085s|&2802IRs(Z3he^t2)W!tM5)N= z>aJ6YWfP-$kJocQeKJEZo^xIRw9{(YAO2~3sE|x?Q4YJV@UcsUZt& z-(7zRWK!KGvh=4`%_s;;+r@k4DUvMXORz*LZ^-7@7N;(zZTZJ16+47t+9aJr$BSPJr1{ z9<9z7if4WW7y~N;hzO6+n!9kL?b=oB^Z#=#_#;YRN1#gU6jhE&3Yslmouw~%0NCBK zUJcZE2KB}F&CL?3JnyEpE9Zn%g>Z;s4wE)#`%K3IkWyz=s7?uI?}QSGeea2*!B&`N zCU1bmXW8HAIiI4qlNUPY8cf*YHEqFE#6wVRy}`91QJUHBT|N!W3AlL9S{lUSp5rzg z#~j!`JlM!wY3VCLjdS0$2si=6YGWE`;lB^pot3x6-X%iz&00jf^jdu&s(; zX$|8*vGIqoQXcL+^~|*=x*l+L4IsNovT#4`?y5iTKw^bNP2qYXyL(w5@zL^Ec+>i4 zAw~WC{j zg8?EDGUwtBU=OtHRK=FP7CFz^k`x0VIM)By;y>2B|1Vxmp8Vk~RWNngGkW3=;7|U^ L)5ismjNbk)28J=B literal 0 HcmV?d00001 diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts index e3d21fd3c2..910fa540b1 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts @@ -4,6 +4,13 @@ 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 + * as follows: + * + * ![basic multiline content](./runBasicMultilineContentTest.png) + */ export async function runBasicMultilineContentTest() { await openNewEditor(contents, { languageId: "typescript", diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.png b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.png new file mode 100644 index 0000000000000000000000000000000000000000..585504092efc10861473deab599802dba05cd2b2 GIT binary patch literal 11851 zcmd72WmH_t)-K$*yL;pA?jGDBSa2E%8faVt1lQmeED+o+xVt+93GTry*zN4S&wI{z zzxVrb$GCTl+dWpVS+nY?Im@0^YgR|7tIDAv6C(ov05k=88BG8HN*N*tA;Lp`SsyRh z0RU8STWM)^1!-xZx{H&Qtvv_;kdH{#MbOh4#{XjYIbOmB3ifNrI_x2AJhl`{JM|Kx zLZG@7DlDFGOYzT@erP_hj!ZXlI9W{{BHu|A93IGsWht4TG55IRvi+gs`_)Fk#MA!R zcka7&D2Y=VIhqmKd8i>A{K#!7=mQHa&BL$X00$EMxuyYYe@6{6{(APl5#SM26oPC)DKb?I}WhS~bLn>2D5#Ui3 zvQBHt6nxm!EQkJ(!y(EAMCNvY4ne=2$3UB__BuH$EFs!`6rL04I5YoH@SrB=Cv7o>F?uoaz3Gv}UYoZqZLc zl}an`RVZrm?SVyUzdq71rWTNo9h*j0vK+ZaT2j@wxyMTR13vTnp*JiMi33jPA4p9I zv4M+&kz9SP3r?`x1o%STGR}B%tkth7gt|rq&mi5YBc)y8~hRfH>zL+k=RR6IhP1 zdhA3P-~*)k5a4>Zp)K2e)nOm1ddr{QV{lov%hqBE{zU5Y0+R0Z_(a$zIWHMvd!iC` zi?_L&?)G@PnkC=9`uK-GPjFD;AEMeL-Qy<3zqvpo8FBxOfm)J(4&&_()$`!HpDF6IXc%37xkB0lZq9zD zajjC~%BvXO?!hcdSWP_4*E#jCe8Ci7LVIFQ^IhUqR^EFTeH;?qab zBOD>VY&snbD{|}q)H_}}zL#Cs9m-+ylY@ENVUwX3pROZs-NO!@GlB0y_GUVpEZYbW z?{BhGRmERZdj_9Bdz+nkhX4?xhetq&^WE=J1yoPMIKK972-?9vff-<&UpREB*gm^9 zUrPl#A@MfPUI$`vz$`e6O>~iY!USN#_DJx^0~k%IqQy`uT{+XBtpdO2;wA-yOnLP% z_&!#c(l5c>2ETE}&4RY>rk#SS3>J32xeg@ArD%i6!Gfiv)D8(fpd1K?GY=D>M(ra& zlA?+ipn+46=E9<+lC%O|#k*L-)sim<%>f_ddxsczpl2dhrO1KpLv}kjk$FtT`0v9F z@(VQJM`U`ZVZqQD5vp&ZrXLQl!K?-`zuu5cX&umB3$4U-NFZ-KE%YBD^ z{e5VC-dhUh^cPqk0%fQU6HesCzS>}iY0ERwR#2bPWdn^UZ{o!m7@3Kf1)1TP3z?9T z6_c2f+!@JqTk@~954rWT?Imxia~onjCPD} zN!&@TN&F)tBSj-$MruZ&cLhi6lJA&ubp3OmG-S2<-+n6OGM3G+l>vo~C?p#)ITdS{ z($42(D6)QK%Ue)a(>l`g=R`4lxFTCYEbtF?B)C{Xg zEUVMD?wml&R-;Ch+OYoT^~c6(?6wcNBUx``i7-vnK|T&EoK9<9v| z-Z z`m4o`Q<6jd$U(D473cWUqMm8n$bHC#YYSht!en7pVbjc`3aYY{3Jsm1@`{RBra?Mg zMoQX3<~nyl8s`|-*xU4mw3|$lOu39njw;?x2gycLJxl%a8UgEz^_dZ_WtWejf?aNA zBj!e(8l8%HBfHXuoEyvI_s4d8G9;lYvE%TW=!SC(<9wryGq<&5@An<)tACg-3Py5_ zxF$Jk-xRa?;Q18!CHPf5!yu4_A%?e2|kdqTQGCa!rcu&rb6wk}?~@ApSM)(*U9WA^Y58@np{Ps>`vT9?`<+829Q zsmBXO3ZnA*!_8Q%vhHX+;XNmAMUEb(N7u@x>^c`s)^FDZ*IV0iVrqzcDe4@?s4284 zVkhx)98FrBGR~|v9K~_OV@(=1qyxAEzP!4>ep4o~;&bJ{0tH$G3j9>>@<}1EqdT?) z;uB));V+SYB}yW!7d!B;wsp30c5n^|HypZ-Ps|IMI-lCjGqRWFOEv2Kg>i+LNwh#* ziA_$?A@*bz;t(Q`FNht38ugCIi+_Ylk#L?6Eq{5Mx!|l{uU|P<5L+oaCZZzR4|^Y% zpZeE35mtZAr^`Jsg{02gB@T4~+(<9_CXpH5FDO=z=6<~ia!PMnHnW?woO7!#k8)yRB`LZ6{E zP9$&rO}FsQRezOtl|Uq5oynMeBdjy%m8X~9LUy1|IcF?6yog8r;fYL zZPCe{;IyEy3%qNh>+x06<;*2I3hyVuo(126r;>+kDq)S1VxQW@o%zJa{>`mZIQj07 zpdu;Od+H6knv^FhFZCTmZT)LA#hD}+E#4yJZ;dK-*^M?Uvs*v6Hj1`qzt0+4rH!Qt zZ+9#2jj`6n>mSw`SY?iX5pT6t9m>jfWps7jdokY9tIgjzZQJo0ze3xeOn*H{jW>C( zq@&bdXoS*6U~dr4j>}?ivQulbmhS{=tBu-?-NoBA!FMGvd73|5Z0JO18`hiYnDaXv z0QG@-K>R6>tQ+5@EMzYt<(R2;`O5RFm<(^+Z@$hGFuh~utH>))C_gkNscE#$hAi9T zj`B%^fOf9j+K!Rj_Zst$Xl-b^e7~K_e^@q`_|ASC4;trh3bD5>ZY-}{Qd~8tzZq`) z>ag!`@$|OND8T38wn{iexW!}O)V|`PxMUerZD-4Bz+7^=ChG4o_@zr!#qHM3@W}0G z^e(9VbNl37;c07)xNpEH)WFATULj$W1Xwb2NL_Le0c3#=$`x$A9l>`Dfj? zNzTb^fiu^a1Gp-bFm^6BK4X{8xv9Mc_XCfn4UC4?CjNJ~&8u^p%hRiZMOXKDoy^}V z;f+OoV*E~LZi{SrN{mMc~bs~ic|UVN=5jC$5;eU9#!evzzJz4Y$I z{dTsEsh#>CKD7NoL;v;p+fR_E?Y}KKy8hEFNCVmb z&fl=V+V!_Nkw29QsoQ#j?Db@99Uxr|aZQYyn@i*`Gyk8Qe>?g&QpXkKBJJb=0fNQ; zJy!pK|1E zi6(|D!v0^ARt!08q7yQc$ypU-B(*)EPK}Vj#Q{u0hpS^4TP!)lw! z^D07-y-{^Hk=;6ex0~sE=A$|>o-OS0>q^X{V}C+SaDINCtZx9717-}s{c}`;e}j

E0}|D<|K)U<7CIy5D>~pSB|AsRVXZ3n1hNJH7t08V1q==389PNkKyo~ zh#(LM504-fK4Rhni-qaUrh7(%8FvLgFzXH6)l5bLPrFkM2+XxwjcUL4Ta1^F&&H_? zlXj##`)Ryf^wLa08lk?eEeIZ+G(W0mu>}!k^2EOA+hTdlu1aNb&?*u0rviQxVMi7t z^_4*x&8#<}-0Ire$`JCYciJ$fong`_C0!aeM>6ftw?xVE5mn4cPKGHieIGxAfPUhQ z8qG9RfUOaw*CPE|v*_jcbrykk)HMzz#h(48AL3?9hO_Iil8N5gh7 zF7LthOQEPw26n1ah=9=?a~NMEvxSzH9D;-6aqb#9t=NsxOCGHhhEj++#(dIy0@5SQ z$ybhw{Cw;T_XW863F^b-;|0y_i8t?;8fK~C5v;jjsFk8@dIBYR%Aa$w>8{^@pQ>PR z$fzmXS&So5+5P^D3NGaGkhS4@VI6;ExfH(gIuwRG^dk%_YyEDp0VTO`Np5Z+^3_@c z;x*mkf}?_|Sc9CZRC5-(1cNS0-YQK<{k+$7m*7kYp`$7yFGn-ZIF8O0tK28wnXg*1 z2x<|`*dZ?6Te5QuA=n*t%fokK62E=0}!+VJy67FP1Z%Fpik5I&V~?S}lLR)y=g?1J_k z+J}9Vll)Qpyv&N#^ZC~id@81JR^Pe@b&eMVf*)4R;m=jAEA$i?+62*B;GU7~YH-oE zS#F(Y57Rys#*^_(Y^cb+_^Kf$^H*sL`odb*!#!JJrd}Q*Id%sO$fH3*lF|C zdK`7XZ|bff{Dc__g+In&|KS~ke1_h`|6j`ZxP*cj=0!LNg6B&97_{D=JcUjsBJU7} z!Cd9`*3{Tv%x%=IDg`yGD^tM=X93t&V+wB;u?@C`2c|T=KJ6<~2={FbdiXw!j@V`6 z-Jj+>g?Qh(OhzDl>3sUVSpWWTW+G_MyHZ8Z=qDc3J6hfN9?7vlOF(AvvgLz0$k%X; zuKcqWzVa|EMfx9-JvR%4n+*eYjQyeLK+8ZlEv|04zsT2@(6sxSGMt_J4|M~HOuLhm z&lCQlZVE`(_#tkMLiAmz@{j~Aj<4k(O79AT!dIpUy61xEIbc3R693y~4wB|0f1?pf z;3X$(Yv{;As8D;RiT#F=ky3d;++_!`;bcP%;r6uP+2?Mia^Q`@@0{1ESce}pd-dc| zu$3>t@U+z_cgJ#tySqr`pZtE)IM+AU)0{&euNTo7Fusk{?=+V+;2a5C4$(1hyjg0O z!^n7=j+^PqlP=NT*;w!IpEF|lzCd*ebJ_i6t<`p!#A&MKZhaJyK-}eWn8#&RfC=k? z;Nh#Tv9UR0v!Bl`O||2@y3lq18=dz~PG{h@^k`nzpoqd^x@sEeU=C;P}W6TZ(9T)lSar4~v;grZtei@M~=z=EUx5f55ie$Dq-W$KJ>U@rA zEH|l&&MB2JtS(aKNU){L@nM^frck2=foi-$56H#eYj0r`j?VKBlNxpwx=cDHOD&=zx`awppAmho5#p+J7%uk=R8N?@;P@qwt#)jo0&D>O2zwV zS`LTeRU4)e$U24R1?*0nbX~-yLA%Y zbcs7zEO+akZCO13zBJD3DB)l7JTZb*nL6) z8!GxQ1~~pNG-2ly9X)M_Kkc$6l+!!1U~gh zwH#Kq7N;&@Q{!3Tyglq}2x26Y5T2%8d`GRu{W~1JfJ7Ev#+G_^oUXiN@3Q`Cg8Mfj z59@gIFQEe4kv`QgT(U2AJK=P>B(e@{j@-$}(JFehFTXBxCIraEz~Sz^JX7S;Wz8l$ z;RS==tBV>L#ohO;%=+bqu|7AvZFf2oFKKS!uxdLx<YsU&oxZl zifTUJcr9AX2ZyB|-82l=5zDMHZ1buKJYV++i9vDwm9bk{J$_17Sq+9q9d~mwb+?>@ zB?!4O$}iC3>f+R2OLp|i6{@(;ewayiaH`)4R#_ztmWEbr2u5I0`Ak&e@Me@zS3 z+4aVpsoGi)m#zf(9Z!P|+*+ofdJIhy21k4Nq2|baOXIP906QuzVQ$LoPArlNCjF zHT|Fw(|Zke@w;~}@y7vRJvdu;mo-9Q7k)V-j}pA*yM+s|`5^;1O$pnK^2H3ltI2YpO0_Sps}ueHR|Kb9k5j9RKD zvddb_+e1r2TQWw^31PN$F#H30Lx*NL_#-w_D~F*P@{8ece$+sBx&=iLaSQb(^(k|) z&}@Nr<@QN^jMO}uRrF@Z(S(=_h*z>WX~su6e9gnniT z?e6F7jO*p#JYNriJS~4`E__J4%o`qb;dI7VQZ@U@KM-Q}%Z)#0J{mlq34~co%%RwO z?K3r*mWV4{Q)dbk7|qi%K0NToQ+exv@c){ao2aj(RrtFDDxfeP7gSis+;4y9kx}3!%2sU`mBl#Goq48QwTFrVB)q; zB>q4eMQU>Yee&ULUE*29RX`IV+B2V7Os@vrYOCuYIyJ}C?XMdwKH4<1!3b}`(1$(a zZIz%gu1iIJ!Bx6)Y>yFG9O#H-FZG+o?xHBo!oztj&D|yyJ*|F8p;-q=cOwQ1|fZq`~*iiOj4z#6R+d--73H$zv>mALFLlACUX zKfu41wn{FyXoQ&fg{hJeYsQTjiHA{ ze4!VNj+t)p)`xH6VZWOUTpXPzEUJ(?^e{d1vZcY4mL2wSPsAVkkW-38?N5HweR-Ii z>IGsm=Mt{GPz^<%-gmdrrxf@Yp9}}V$@UP!KV)DlL&kTLVRqa6UT^8H@=}4mEll>Y z2#K2(EF_Xn2R`g#+KBnk-A7I)1hX9Xy2oFyXfH=H54Irb{TkuM_)J`Ff}=Dl$xvX0 zZBJ?6W8d?>XG2*L#==nrv52jzWT7;==VQr6XeIqqcwU7k#L77|~*H>hmP$U}TB4$RP zwE|?wtnYKu=5P%ougXpnKcjd>Y~M+OWr~B5P2LEhygDi>aKJId5V?8W81zqCem5~!>?hfgVj6;lYI~AE^=SywtRD?n=O@QxBU0J*XtC)n;kGCvg^mHs z6v^V$oA*6$!b@yg);qlN9e69v4AIuOgNmGR7IMXwaWPtL26)A%*Vt}1{NTfuizHUIJPXtUQ% z1NzSf!1NnSWVjWcc=wg7oUWp5Se&DJtI5j5aattl;6V8J6xb^NmUb4hrQ;yM8;cFD z_>8bvQ?=A79J^_^!*9u`{F~787YMVsnKqKhL$$uT_fPBooNR(P7~ zq%y%jDTY|w_)%o-xj0tWMktoUe`OAp>}NF+^LYl|82*{x080OPh5!}#r7)G)2)97L z4w^6g!p>ESl)cycBb^Yk14jqLt~4C1KB*Y`r(HQi^bnCg-$0h#9nRX5f21 zDr+E19^LSyR;>MUw-kb>N{BZ`O^hYguy@JsH=HQ226y7;+M<^_^VI>bHx$gb-Es_0 z6|FMsI@2*nP)vjjS)#wSfq`xWYg(`WwK!b}ibYsE>ih>B6a?YfL7_?zrJdVgL~Kcd zm>=vFS~PhLQJF@!y^lwFVwA%uafO@@BPF+ZI0n5ULw!c*SM2)Sp>uOADjsl2IZr3n z4ean298-5()99X%t-q{eoTQM@W0$p=-Zj4YJgpIuWKkUU`6-5o={pwC-+j7;#rz{+ zTTc8kyPNm)a`aF!CjIZ?eMa^Nef&O+&{{540S@osp8jq$#2jR;H;Be5Etg`|-ssc?tr-rdY z#Z7%_imVRzT8@krFL^Bk$B!4L)e}B|Lz!SYdhSu|^g=iK+*|m}&_)wtE-p4HfL4G_ z_DHkV18to_9Z!yj%`6mlZKF%HUcT-+|2yFg&mt;IuD1maX+YFEFLE3mEu1>&%|L-Zl;QS?QjdS4i;0 zPI_N!6cB>U|Ey65@qER|CoGSp&9i{SslD!7K#M$!1IQ_IqKF%+p^PW5_O`4XXZYo0 z|2geRdT<`FW!^yhu;3_&{c$$nzLzs<{K$Ra2YusSIK+jREt8fr$zifW#(E_om4(m1 z42+2N52#XugmP=ptrI-eaHA*b+!hrunOSuH1jgBl_qjdlB)vi8OB|EOO`o%ho+$j; zd^h2|Kit|A;U3#S0!p!a+PbnH~q>%q-bZT0Sjs=jYdDMwl;R`+~Y@CV*Sro{Rz)%aV_aS)W-;kO?m&%%9G;b@nT;7L@9Grr)We1}}b;${g=$BtUkHxhK% zFBAre0n@>97wTLBcb2JA&imo>lCmHhd-FYN?)XF0ujRA!vt-u3f5o&yK!I@FEEc3W zkZ@OR3M434?ssz{010aSKbJvAc9uI-rk)vs#lY$l5APC2U6}k)FK9VGP^h+E9&1F> z=+no@+732d14E1%$xrCXEd(MSYni2i*!^{eaBocAGDhgFNyVBnp_183u16y9gdtZ1#%0`Q~dn=8i`9dgSOJG%-W+wjI#T*Ypauv_?RE(ZFl`fvn74k z`w7!HOyftXll{L_2|}AA?frg6ARWd+V`nS6%$ism?T3qmiRK9WoL0)yXdx#$&ur`! zv7}vIrd#0`ffh}hM&f>&KOZK`1}0QY=N^dFNO?o5@i2!C4`e8vq3v^-{9^SUn|6#9d0K{X zR;f$5HP(F;$NQ((uO!Np;?FS59KvXYV|Da7mDkfmsgtLs0N%CH(vjpsr33tH=JMT? zq@-1%+oGNOn|v%kMK669^{v<1Sa+csj=0j_x*CQ*Bv(po>^;`{posbbu(jS(m>JNN zSU-kK?KgYiz~`oRTiCEAZ>AmfnO1P(gbj|Qm(O~aO6T%FNixXMhj4{mo&!F$dlzrN z=KNS)Ri?(J9qD<3-aIqiv=b!5{2@w#`D3zn84^nOoM1ea-bW*q0GXp$CX3cP;+#gF z`&&5FF@Jn#!2P-Cjy2_*?-(|GqoZFAF$qcRMWa;swy7-AS(?19SP5RCi8V}smlqrw zsQ}Hm{QZaYt}brj%)>)oJ8H=Y9okg2zTW-Q4<3NwV_(RTko|`7KDLtQvh&yABV00x z?0$eyL6$iZ3#RvdSBep;#JBuN+O$X`+Eg(6+O)(PviEE;v>TS*z;?kiYOdvXi$Qla zx!Z_RFOpsLnuN)keG6Z9h}vknlF4bc$%(z8sYycvYcZCF-smCx^R9_*MeJ^V0+Sb4 z&?X4PW3i-8JWt(w1R4fDyvn2U@Shd^{w>6LFRb^&xWh)R9M>WBklbL72DVid5fmi= zU(?p+TUuV8zqK_3oED4)q2>l#FNBWQi%HSP&FX9iF;Z|SBkup}lrWq^rK7D&PKodU zGuzEUiqz2d^`(7oZVp$EAsaoP8lFtNy&sEeWsQh(&nvT;Vk`W!L!u2fB2&`Dpp<|t zJp&&8ZJx-q{#3hgJaU))Th72q!3;Bw32f=G{OeF3!7Q%sYj0rKU-5NQW|-{N5ld!= z=*&NylK|lyd*9;|ZV2bdm_x`2+rbwHB3n|!u%s>DFDzR_LiFK%02C{lm3z8B#}#k> z99LXa)J;MN3^M`(m#@E2fynVOsMV%c+lJOTkZ?Z`8v;+T+0OiP{7M1|>5mOGO^87p nkf4FUsiz0{IUzD1*Li@viW6ILFRk96e?Tb6s>)PKnFjqY%Q);# literal 0 HcmV?d00001 diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts index 88cc727c0c..79fcbd934b 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts @@ -4,6 +4,12 @@ import { checkAndResetFakes } from "./checkAndResetFakes"; import { injectFakes } from "./injectFakes"; import { ExpectedArgs } from "./scopeVisualizerTest.types"; +/** + * Tests that the scope visualizer works with removal ranges, by ensuring that + * the correct decorations are applied so that it looks as follows: + * + * ![basic removal](./runBasicRemovalTest.png) + */ export async function runBasicRemovalTest() { await openNewEditor("aaa bbb"); diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.png b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.png new file mode 100644 index 0000000000000000000000000000000000000000..6a017857700c3d265a68c7b78d67276d3c8bc4f0 GIT binary patch literal 44485 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 >pze^r;9krkmg1O)J; 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 W5AuAYl?gorel*o|t`w runBasicMultilineContentTest()), ); test( - "basic nested multiline content", + "nested multiline content", asyncSafety(() => runNestedMultilineContentTest()), ); test( diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/spyCallsToPlainObject.ts similarity index 100% rename from packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/toPlainObject.ts rename to packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/spyCallsToPlainObject.ts diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index 404f35472e..1f83481861 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -65,7 +65,9 @@ "onCommand:cursorless.showDocumentation", "onCommand:cursorless.showQuickPick", "onCommand:cursorless.takeSnapshot", - "onCommand:cursorless.toggleDecorations" + "onCommand:cursorless.toggleDecorations", + "onCommand:cursorless.showScopeVisualizer", + "onCommand:cursorless.hideScopeVisualizer" ], "main": "./extension.js", "capabilities": { @@ -168,6 +170,16 @@ "command": "cursorless.keyboard.modal.modeToggle", "title": "Cursorless: Toggle the cursorless modal mode", "enablement": "false" + }, + { + "command": "cursorless.showScopeVisualizer", + "title": "Cursorless: Show the scope visualizer", + "enablement": "false" + }, + { + "command": "cursorless.hideScopeVisualizer", + "title": "Cursorless: Hide the scope visualizer", + "enablement": "false" } ], "colors": [ diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts index 633e5dea67..b086632f4c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts @@ -1,3 +1,6 @@ +/** + * The colors used to render a range type, such as "domain", "content", etc. + */ export interface RangeTypeColors { background: ThemeColors; borderSolid: ThemeColors; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index c1c4602374..ca4c7a9928 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -6,7 +6,7 @@ import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlight import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; -import { DifferentiatedStyledRange } from "./getDecorationRanges.types"; +import { DifferentiatedStyledRange } from "./decorationStyle.types"; import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRanges"; /** diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index 74e9a40bc6..a466d41556 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -13,7 +13,8 @@ import { DecorationStyle, DifferentiatedStyle, DifferentiatedStyledRangeList, -} from "./getDecorationRanges.types"; +} from "./decorationStyle.types"; +import { getDifferentiatedStyleMapKey } from "./getDifferentiatedStyleMapKey"; const BORDER_WIDTH = "1px"; const BORDER_RADIUS = "2px"; @@ -31,34 +32,39 @@ export class VscodeFancyRangeHighlighterRenderer { constructor(colors: RangeTypeColors) { this.decorationTypes = new CompositeKeyDefaultMap( ({ style }) => getDecorationStyle(colors, style), - ({ - style: { top, right, bottom, left, isWholeLine }, - differentiationIndex, - }) => [ - top, - right, - bottom, - left, - isWholeLine ?? false, - differentiationIndex, - ], + getDifferentiatedStyleMapKey, ); } + /** + * Renders the given ranges in the given editor. + * + * @param editor The editor to render the decorations in. + * @param decoratedRanges A list with one element per differentiated style, + * each of which contains a list of ranges to render for that style. We render + * the ranges in order of increasing differentiation index. + * {@link VscodeFancyRangeHighlighter} uses this to ensure that nested ranges + * are rendered after their parents. Otherwise they partially interleave, + * which looks bad. + */ setRanges( editor: VscodeTextEditorImpl, decoratedRanges: DifferentiatedStyledRangeList[], - ) { + ): void { + /** + * Keep track of which styles have no ranges, so that we can set their + * range list to `[]` + */ const untouchedDecorationTypes = new Set(this.decorationTypes.values()); decoratedRanges.sort( (a, b) => - a.differentiatedStyles.differentiationIndex - - b.differentiatedStyles.differentiationIndex, + a.differentiatedStyle.differentiationIndex - + b.differentiatedStyle.differentiationIndex, ); decoratedRanges.forEach( - ({ differentiatedStyles: styleParameters, ranges }) => { + ({ differentiatedStyle: styleParameters, ranges }) => { const decorationType = this.decorationTypes.get(styleParameters); vscodeApi.editor.setDecorations( @@ -141,6 +147,9 @@ function getBorderRadius(borders: DecorationStyle): string { } function getSingleCornerBorderRadius(side1: BorderStyle, side2: BorderStyle) { + // We only round the corners if both sides are solid, as that makes them look + // more finished, whereas we want the dotted borders to look unfinished / cut + // off. return side1 === BorderStyle.solid && side2 === BorderStyle.solid ? BORDER_RADIUS : "0px"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts similarity index 52% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts index 7fabdbf11b..f20c439cde 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDecorationRanges.types.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts @@ -1,9 +1,4 @@ -import { - CharacterRange, - GeneralizedRange, - LineRange, - Range, -} from "@cursorless/common"; +import { GeneralizedRange, Range } from "@cursorless/common"; export enum BorderStyle { porous = "dashed", @@ -19,8 +14,19 @@ export interface DecorationStyle { isWholeLine?: boolean; } +/** + * A decoration style that is differentiated from other styles by a number. We + * use this number to ensure that adjacent ranges are rendered with different + * TextEditorDecorationTypes, so that they don't get merged together due to a + * VSCode bug. + */ export interface DifferentiatedStyle { style: DecorationStyle; + + /** + * A number that is different from the differentiation indices of any other + * ranges that are touching this range. + */ differentiationIndex: number; } @@ -35,21 +41,16 @@ export interface DifferentiatedStyledRange { } export interface DifferentiatedStyledRangeList { - differentiatedStyles: DifferentiatedStyle; + differentiatedStyle: DifferentiatedStyle; ranges: Range[]; } export interface DifferentiatedGeneralizedRange { range: GeneralizedRange; - differentiationIndex: number; -} -export interface DifferentiatedCharacterRange - extends DifferentiatedGeneralizedRange { - range: CharacterRange; -} - -export interface DifferentiatedLineRange - extends DifferentiatedGeneralizedRange { - range: LineRange; + /** + * A number that is different from the differentiation indices of any other + * ranges that are touching this range. + */ + differentiationIndex: number; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts deleted file mode 100644 index cbb5cdedb4..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Range, TextEditor } from "@cursorless/common"; -import { range } from "lodash"; -import { BorderStyle, StyledRange } from "./getDecorationRanges.types"; -import { handleMultipleLines } from "./handleMultipleLines"; - -export function* generateDecorationsForCharacterRange( - editor: TextEditor, - characterRange: Range, -): Iterable { - if (characterRange.isSingleLine) { - yield { - range: characterRange, - style: { - top: BorderStyle.solid, - right: BorderStyle.solid, - bottom: BorderStyle.solid, - left: BorderStyle.solid, - }, - }; - return; - } - - const { document } = editor; - const lineRanges = range( - characterRange.start.line, - characterRange.end.line + 1, - ).map((lineNumber) => document.lineAt(lineNumber).range); - lineRanges[0] = lineRanges[0].with(characterRange.start); - lineRanges[lineRanges.length - 1] = lineRanges[lineRanges.length - 1].with( - undefined, - characterRange.end, - ); - - yield* handleMultipleLines(lineRanges); -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts new file mode 100644 index 0000000000..0cebef7d09 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts @@ -0,0 +1,28 @@ +import { Range, TextEditor, getLineRanges } from "@cursorless/common"; +import { BorderStyle, StyledRange } from "../decorationStyle.types"; +import { handleMultipleLines } from "./handleMultipleLines"; + +/** + * Returns an iterable of styled ranges for the given range. If the range spans + * multiple lines, we have complex logic to draw dotted / solid / no borders to ensure + * that the range is visually distinct from adjacent ranges but looks continuous. + */ +export function* generateDecorationsForCharacterRange( + editor: TextEditor, + range: Range, +): Iterable { + if (range.isSingleLine) { + yield { + range, + style: { + top: BorderStyle.solid, + right: BorderStyle.solid, + bottom: BorderStyle.solid, + left: BorderStyle.solid, + }, + }; + return; + } + + yield* handleMultipleLines(getLineRanges(editor, range)); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts new file mode 100644 index 0000000000..9202923d0c --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts @@ -0,0 +1,70 @@ +import { Range } from "@cursorless/common"; + +/** + * Generates a line info for each line in the given range, which includes + * information about the given line range, as well as the previous and next + * lines, and whether each line is first / last, etc. For use in + * {@link handleMultipleLines}. + * @param lineRanges A list of ranges, one for each line in the given range, + * with the first and last ranges trimmed to the start and end of the original + * range. + */ +export function* generateLineInfos(lineRanges: Range[]): Iterable { + for (let i = 0; i < lineRanges.length; i++) { + const previousLine = i === 0 ? null : lineRanges[i - 1]; + const currentLine = lineRanges[i]; + const nextLine = i === lineRanges.length - 1 ? null : lineRanges[i + 1]; + + yield { + lineNumber: currentLine.start.line, + + previousLine: + previousLine == null + ? null + : { + start: previousLine.start.character, + end: previousLine.end.character, + isFirst: i === 1, + isLast: false, + }, + + currentLine: { + start: currentLine.start.character, + end: currentLine.end.character, + isFirst: i === 0, + isLast: i === lineRanges.length - 1, + }, + + nextLine: + nextLine == null + ? null + : { + start: nextLine.start.character, + end: nextLine.end.character, + isFirst: false, + isLast: i === lineRanges.length - 2, + }, + }; + } +} + +export interface LineInfo { + lineNumber: number; + previousLine: Line | null; + currentLine: Line; + nextLine: Line | null; +} + +interface Line { + /** + * Start character. Always 0, except for possibly the first line of the + * original range. + */ + start: number; + /** End character */ + end: number; + /** `true` if this line is the first line in the original range */ + isFirst: boolean; + /** `true` if this line is the last line in the original range */ + isLast: boolean; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts new file mode 100644 index 0000000000..572f164c06 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts @@ -0,0 +1,208 @@ +import { Range } from "@cursorless/common"; +import { + BorderStyle, + DecorationStyle, + StyledRange, +} from "../decorationStyle.types"; +import { flatmap } from "itertools"; +import { generateLineInfos, LineInfo } from "./generateLineInfos"; + +/** + * Generates decorations for a range, which has already been split up into line + * ranges. This function implements the core logic that determines how we + * render multiline ranges, ensuring that we use dotted borders to indicate line + * continuations. + * + * @param lineRanges A list of ranges, one for each line in the given range, + * with the first and last ranges trimmed to the start and end of the original + * range. + */ +export function* handleMultipleLines( + lineRanges: Range[], +): Iterable { + yield* flatmap(generateLineInfos(lineRanges), handleLine); +} + +/** + * Returns an iterable of decorations to use to render the given line. Because + * we may want to use different borders to render different parts of the line, + * depending what is above and below the line, we may yield multiple decorations + * for a single line. + * + * We move from the start of the line to the end, keeping a state machine to + * keep track of what borders we should render. At each character where the + * previous, current, or next line starts or ends, we transition states, and + * potentially yield a decoration. + * @param lineInfo Info about the line to render, including context about the + * previous and next lines. + */ +function* handleLine(lineInfo: LineInfo): Iterable { + const { lineNumber, previousLine, currentLine, nextLine } = lineInfo; + + /** A list of "events", corresponding to the start or end of a line */ + const events: Event[] = getEvents(lineInfo); + + /** + * Keep track of current borders, except for `right`, which is computed on + * the fly. + */ + const currentDecoration: Omit = { + // Draw a solid top line if whatever is above us isn't part of our range. + // Otherwise draw no line so it merges in with the line above. + top: + previousLine == null || previousLine.isFirst + ? BorderStyle.solid + : BorderStyle.none, + // Analogous to above, but only care if we're last; doesn't matter if next + // line is last because it is guaranteed to start at char 0 + bottom: currentLine.isLast ? BorderStyle.solid : BorderStyle.none, + // Start with a porous border if we're continuing from previous line + left: currentLine.isFirst ? BorderStyle.solid : BorderStyle.porous, + }; + + let currentOffset = currentLine.start; + let yieldedAnything = false; + let isDone = false; + + for (const { offset, lineType, isStart } of events) { + if (isDone) { + break; + } + + if (offset > currentOffset) { + // If we've moved forward at all since the last event, yield a decoration + // for the range between the last event and this one. + yield { + range: new Range(lineNumber, currentOffset, lineNumber, offset), + style: { + ...currentDecoration, + // If we're done with this line, draw a border, otherwise don't, so that + // it merges in with the next decoration for this line. + right: + offset === currentLine.end + ? currentLine.isLast + ? BorderStyle.solid + : BorderStyle.porous + : BorderStyle.none, + }, + }; + yieldedAnything = true; + currentDecoration.left = BorderStyle.none; + } + + switch (lineType) { + case LineType.previous: + // Use no top border when overlapping with previous line so it visually + // merges; otherwise use porous border to show nice cutoff effect. + currentDecoration.top = isStart ? BorderStyle.none : BorderStyle.porous; + break; + case LineType.current: + if (!isStart) { + isDone = true; + } + break; + case LineType.next: + // Blend with next line while it is overlapping with us; then switch + // to solid or porous, depending if it is the last line. + if (isStart) { + currentDecoration.bottom = BorderStyle.none; + } else { + currentDecoration.bottom = nextLine!.isLast + ? BorderStyle.solid + : BorderStyle.porous; + } + break; + } + + if (currentOffset < offset) { + // This guard is necessary so we don't accidentally jump backward if an + // adjacent line starts before we do. + currentOffset = offset; + } + } + + if (!yieldedAnything) { + yield { + range: new Range( + lineNumber, + currentLine.start, + lineNumber, + currentLine.end, + ), + style: { + ...currentDecoration, + right: currentLine.isLast ? BorderStyle.solid : BorderStyle.porous, + }, + }; + } +} + +interface Event { + offset: number; + lineType: LineType; + isStart: boolean; +} + +enum LineType { + previous = -1, + current = 0, + next = 1, +} + +/** + * Generate "events" for our state machine. + * @param param0 Info about the line to render + * @returns A list of "events", corresponding to the start or end of a line + */ +function getEvents({ previousLine, currentLine, nextLine }: LineInfo) { + const events: Event[] = []; + + if (previousLine != null) { + events.push( + { + offset: previousLine.start, + lineType: LineType.previous, + isStart: true, + }, + { + offset: previousLine.end, + lineType: LineType.previous, + isStart: false, + }, + ); + } + + // Note that the current and next line will always start before or equal to + // our starting offset, so we don't need to add events for them. + events.push({ + offset: currentLine.end, + lineType: LineType.current, + isStart: false, + }); + + if (nextLine != null) { + events.push({ + offset: nextLine.end, + lineType: LineType.next, + isStart: false, + }); + } + + // Sort the events by offset. If two events have the same offset, we want to + // handle the current line last, so that it takes into account whether an adjacent + // line has started or ended. If two events have the same offset and line type, + // we want to handle the start event first, as we always assume we'll handle a + // line beginning before it ends. + events.sort((a, b) => { + if (a.offset === b.offset) { + if (a.lineType === LineType.current) { + return 1; + } + return a.isStart ? -1 : 1; + } + + return a.offset - b.offset; + }); + + return events; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/index.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/index.ts new file mode 100644 index 0000000000..6dcd5da671 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/index.ts @@ -0,0 +1 @@ +export * from "./generateDecorationsForCharacterRange"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts index a3c103490b..23ac9a6d33 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts @@ -1,5 +1,5 @@ import { Range } from "@cursorless/common"; -import { BorderStyle, StyledRange } from "./getDecorationRanges.types"; +import { BorderStyle, StyledRange } from "./decorationStyle.types"; export function* generateDecorationsForLineRange( startLine: number, diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts index 2e0a3b5802..08ae9488a0 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDifferentiatedRanges.ts @@ -5,16 +5,28 @@ import { } from "@cursorless/common"; import { max } from "lodash"; -import { DifferentiatedGeneralizedRange } from "./getDecorationRanges.types"; +import { DifferentiatedGeneralizedRange } from "./decorationStyle.types"; +/** + * Given a list of generalized ranges, returns a list of differentiated ranges, + * where any ranges that are touching have different differentiation indices. + * We ensure that nested ranges have a greater differentiation index than their + * parents, so that we can then render them in order of increasing + * differentiation index to ensure that nested ranges are rendered after their + * parents, so that we don't get strange interleaving artifacts. + * @param ranges A list of generalized ranges. + * @returns An iterable of differentiated generalized ranges. + */ export function* generateDifferentiatedRanges( ranges: GeneralizedRange[], ): Iterable { ranges.sort(compareGeneralizedRangesByStart); + /** A list of ranges that may touch the current range */ let currentRanges: DifferentiatedGeneralizedRange[] = []; for (const range of ranges) { + // Remove any ranges that have ended before the start of the current range. currentRanges = [ ...currentRanges.filter(({ range: previousRange }) => generalizedRangeTouches(previousRange, range), @@ -32,6 +44,16 @@ export function* generateDifferentiatedRanges( } } +/** + * Returns the differentiation index to use for the given range, given a list of + * ranges that touch the current range. We return a differentiation index that + * differs from any of the given ranges, and is greater than any range + * containing {@link range}. + * + * @param currentRanges A list of ranges that touch the current range + * @param range The range to get the differentiation index for + * @returns The differentiation index to use for the given range + */ function getDifferentiationIndex( currentRanges: DifferentiatedGeneralizedRange[], range: GeneralizedRange, @@ -42,11 +64,12 @@ function getDifferentiationIndex( .map((r) => r.differentiationIndex), ); - if (maxContainingDifferentiationIndex != null) { - return maxContainingDifferentiationIndex + 1; - } + let i = + maxContainingDifferentiationIndex == null + ? 0 + : maxContainingDifferentiationIndex + 1; - for (let i = 0; ; i++) { + for (; ; i++) { if ( !currentRanges.some( ({ differentiationIndex }) => differentiationIndex === i, @@ -57,6 +80,14 @@ function getDifferentiationIndex( } } +/** + * Compares two generalized ranges by their start positions, with line ranges + * sorted before character ranges that start on the same line. + * @param a A generalized range + * @param b A generalized range + * @returns -1 if {@link a} should be sorted before {@link b}, 1 if {@link b} + * should be sorted before {@link a}, and 0 if they are equal. + */ function compareGeneralizedRangesByStart( a: GeneralizedRange, b: GeneralizedRange, @@ -68,7 +99,8 @@ function compareGeneralizedRangesByStart( } // a.type === "character" && b.type === "line" - // Line ranges are always sorted before character ranges + // Line ranges are always sorted before character ranges that start on the + // same line. return a.start.line === b.start ? 1 : a.start.line - b.start; } @@ -78,5 +110,5 @@ function compareGeneralizedRangesByStart( } // a.type === "line" && b.type === "character" - return b.start.line === a.start ? -1 : a.start - b.start.line; + return a.start === b.start.line ? -1 : a.start - b.start.line; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts new file mode 100644 index 0000000000..4bda9d4585 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts @@ -0,0 +1,12 @@ +import { DifferentiatedStyle } from "./decorationStyle.types"; + +/** + * Returns a list of values that uniquely definees a differentiated style, for + * use as a key in a {@link CompositeKeyDefaultMap}. + */ +export function getDifferentiatedStyleMapKey({ + style: { top, right, bottom, left, isWholeLine }, + differentiationIndex, +}: DifferentiatedStyle) { + return [top, right, bottom, left, isWholeLine ?? false, differentiationIndex]; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts index 0a5f543dfd..22f692f04d 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/groupDifferentiatedStyledRanges.ts @@ -1,10 +1,19 @@ import { CompositeKeyDefaultMap } from "@cursorless/common"; import { - DifferentiatedStyledRangeList, DifferentiatedStyle, DifferentiatedStyledRange, -} from "./getDecorationRanges.types"; + DifferentiatedStyledRangeList, +} from "./decorationStyle.types"; +import { getDifferentiatedStyleMapKey } from "./getDifferentiatedStyleMapKey"; +/** + * Given a list of differentiated styled ranges, groups them by differentiated + * style. + * + * @param decoratedRanges An iterable of differentiated styled ranges to group. + * @returns A list where each elements contains a list of ranges that have the + * same differentiated style. + */ export function groupDifferentiatedStyledRanges( decoratedRanges: Iterable, ): DifferentiatedStyledRangeList[] { @@ -12,8 +21,8 @@ export function groupDifferentiatedStyledRanges( DifferentiatedStyle, DifferentiatedStyledRangeList > = new CompositeKeyDefaultMap( - (differentiatedStyles) => ({ differentiatedStyles, ranges: [] }), - getStyleKey, + (differentiatedStyle) => ({ differentiatedStyle, ranges: [] }), + getDifferentiatedStyleMapKey, ); for (const { range, differentiatedStyle } of decoratedRanges) { @@ -22,10 +31,3 @@ export function groupDifferentiatedStyledRanges( return Array.from(decorations.values()); } - -function getStyleKey({ - style: { top, right, left, bottom, isWholeLine }, - differentiationIndex, -}: DifferentiatedStyle) { - return [top, right, left, bottom, isWholeLine ?? false, differentiationIndex]; -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts deleted file mode 100644 index d0cd5645b9..0000000000 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/handleMultipleLines.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { Range } from "@cursorless/common"; -import { - BorderStyle, - DecorationStyle, - StyledRange, -} from "./getDecorationRanges.types"; -import { flatmap } from "itertools"; - -export function* handleMultipleLines( - lineRanges: Range[], -): Iterable { - yield* flatmap(generateLineGroupings(lineRanges), handleLine); -} - -export function* generateLineGroupings( - lineRanges: Range[], -): Iterable { - for (let i = 0; i < lineRanges.length; i++) { - const previousLine = i === 0 ? null : lineRanges[i - 1]; - const currentLine = lineRanges[i]; - const nextLine = i === lineRanges.length - 1 ? null : lineRanges[i + 1]; - yield { - lineNumber: currentLine.start.line, - - previousLine: - previousLine == null - ? null - : { - start: previousLine.start.character, - end: previousLine.end.character, - isFirst: i === 1, - isLast: false, - }, - - currentLine: { - start: currentLine.start.character, - end: currentLine.end.character, - isFirst: i === 0, - isLast: i === lineRanges.length - 1, - }, - - nextLine: - nextLine == null - ? null - : { - start: nextLine.start.character, - end: nextLine.end.character, - isFirst: false, - isLast: i === lineRanges.length - 2, - }, - }; - } -} - -interface LineGrouping { - lineNumber: number; - previousLine: Line | null; - currentLine: Line; - nextLine: Line | null; -} - -interface Line { - start: number; - end: number; - isFirst: boolean; - isLast: boolean; -} - -function* handleLine({ - lineNumber, - previousLine, - currentLine, - nextLine, -}: LineGrouping): Iterable { - const events: Event[] = [ - ...(previousLine == null - ? [] - : [ - { - offset: previousLine.start, - lineType: LineType.previous, - isStart: true, - }, - { - offset: previousLine.end, - lineType: LineType.previous, - isStart: false, - }, - ]), - { - offset: currentLine.end, - lineType: LineType.current, - isStart: false, - }, - ...(nextLine == null - ? [] - : [ - { - offset: nextLine.end, - lineType: LineType.next, - isStart: false, - }, - ]), - ]; - - events.sort((a, b) => { - if (a.offset === b.offset) { - if (a.lineType === LineType.current) { - return 1; - } - return a.isStart ? -1 : 1; - } - - return a.offset - b.offset; - }); - - const currentDecoration: DecorationStyle = { - top: - previousLine == null || previousLine.isFirst - ? BorderStyle.solid - : BorderStyle.none, - bottom: currentLine.isLast ? BorderStyle.solid : BorderStyle.none, - left: currentLine.isFirst ? BorderStyle.solid : BorderStyle.porous, - right: BorderStyle.none, - }; - - let currentOffset = currentLine.start; - let yieldedAnything = false; - let isDone = false; - - for (const { offset, lineType, isStart } of events) { - if (isDone) { - break; - } - if (offset > currentOffset) { - yield { - range: new Range(lineNumber, currentOffset, lineNumber, offset), - style: { - ...currentDecoration, - right: - offset === currentLine.end - ? currentLine.isLast - ? BorderStyle.solid - : BorderStyle.porous - : BorderStyle.none, - }, - }; - yieldedAnything = true; - currentDecoration.left = BorderStyle.none; - } - - switch (lineType) { - case LineType.previous: - if (isStart) { - currentDecoration.top = BorderStyle.none; - } else { - currentDecoration.top = BorderStyle.porous; - } - break; - case LineType.current: - if (!isStart) { - isDone = true; - } - break; - case LineType.next: - if (isStart) { - currentDecoration.bottom = BorderStyle.none; - } else { - currentDecoration.bottom = nextLine!.isLast - ? BorderStyle.solid - : BorderStyle.porous; - } - break; - } - - if (currentOffset < offset) { - currentOffset = offset; - } - } - - if (!yieldedAnything) { - yield { - range: new Range( - lineNumber, - currentLine.start, - lineNumber, - currentLine.end, - ), - style: { - ...currentDecoration, - right: currentLine.isLast ? BorderStyle.solid : BorderStyle.porous, - }, - }; - } -} - -interface Event { - offset: number; - lineType: LineType; - isStart: boolean; -} - -enum LineType { - previous = -1, - current = 0, - next = 1, -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeIterationScopeVisualizer.ts similarity index 94% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts rename to packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeIterationScopeVisualizer.ts index b2f5f5dd58..b160f8631c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeIterationVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeIterationScopeVisualizer.ts @@ -3,7 +3,7 @@ import { VscodeTextEditorImpl } from "../VscodeTextEditorImpl"; import { VscodeScopeVisualizer } from "./VscodeScopeVisualizer"; import { ScopeSupport } from "@cursorless/cursorless-engine"; -export class VscodeScopeIterationVisualizer extends VscodeScopeVisualizer { +export class VscodeIterationScopeVisualizer extends VscodeScopeVisualizer { protected getScopeSupport(editor: TextEditor): ScopeSupport { return this.scopeProvider.getIterationScopeSupport(editor, this.scopeType); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts index ffa893d880..c9004dab50 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeRenderer.ts @@ -13,9 +13,19 @@ export interface RendererScope { nestedRanges: GeneralizedRange[]; } +/** + * Responsible for rendering scopes, as used by {@link VscodeScopeVisualizer}. + * Includes a hack where we color blend domain and nested ranges that are + * identical, to reduce load on VSCode renderer and to work around some + * glitchiness. + */ export class VscodeScopeRenderer implements Disposable { private domainHighlighter: VscodeFancyRangeHighlighter; private nestedRangeHighlighter: VscodeFancyRangeHighlighter; + /** + * A highlighter that blends domain and nested range colors when they have + * identical ranges + */ private domainEqualsNestedHighlighter: VscodeFancyRangeHighlighter; constructor( diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts index df2f11c730..1768d47e71 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeScopeVisualizer.ts @@ -44,7 +44,11 @@ export abstract class VscodeScopeVisualizer { this.checkScopeSupport(); } - private checkScopeSupport() { + /** + * Checks if the scope type is supported in the active editor, and shows an + * error if not. + */ + private checkScopeSupport(): void { const editor = this.ide.activeTextEditor; if (editor == null) { @@ -65,6 +69,7 @@ export abstract class VscodeScopeVisualizer { } } + /** This function is called initially, as well as whenever color config changes */ private initialize() { const colorConfig = vscodeApi.workspace .getConfiguration("cursorless.scopeVisualizer") @@ -76,7 +81,9 @@ export abstract class VscodeScopeVisualizer { getColorsFromConfig(colorConfig, this.getNestedScopeRangeType()), ); - // Reregister to cause the renderer to be updated with the new colors + // Note that on color config change, we want to re-register the listener + // so that the provider will call us again with the current scope ranges + // so that we can re-render them with the new colors. this.scopeListenerDisposable?.dispose(); this.scopeListenerDisposable = this.registerListener(); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts index 40dfa01a55..c884a49d36 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts @@ -1,6 +1,15 @@ import tinycolor = require("tinycolor2"); import { RangeTypeColors } from "./RangeTypeColors"; +/** + * Blends two {@link RangeTypeColors} color configurations together according to + * their alpha channels, with the top color rendered on top of the base color. + * + * @param baseColors The colors to render underneath + * @param topColors The colors to render on top + * @returns A color config that is a blend of the two color configs, with the + * top color rendered on top of the base color + */ export function blendRangeTypeColors( baseColors: RangeTypeColors, topColors: RangeTypeColors, @@ -36,6 +45,17 @@ export function blendRangeTypeColors( }; } +/** + * Blends two colors together according to their alpha channels, with the top + * color rendered on top of the base color. + * + * Basd on https://gist.github.com/JordanDelcros/518396da1c13f75ee057 + * + * @param base The color to render underneath + * @param top The color to render on top + * @returns A color that is a blend of the two colors, with the top color + * rendered on top of the base color + */ function blendColors(base: string, top: string): string { const baseRgba = tinycolor(base).toRgb(); const topRgba = tinycolor(top).toRgb(); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts index 6ec597e7cd..4abd4b6515 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/createVscodeScopeVisualizer.ts @@ -1,7 +1,7 @@ import { IDE, ScopeType } from "@cursorless/common"; import { ScopeProvider } from "@cursorless/cursorless-engine"; import { VisualizationType } from "../../../ScopeVisualizerCommandApi"; -import { VscodeScopeIterationVisualizer } from "./VscodeScopeIterationVisualizer"; +import { VscodeIterationScopeVisualizer } from "./VscodeIterationScopeVisualizer"; import { VscodeScopeContentVisualizer, VscodeScopeRemovalVisualizer, @@ -19,6 +19,6 @@ export function createVscodeScopeVisualizer( case "removal": return new VscodeScopeRemovalVisualizer(ide, scopeProvider, scopeType); case "iteration": - return new VscodeScopeIterationVisualizer(ide, scopeProvider, scopeType); + return new VscodeIterationScopeVisualizer(ide, scopeProvider, scopeType); } } diff --git a/packages/vscode-common/src/getExtensionApi.ts b/packages/vscode-common/src/getExtensionApi.ts index be99d542f2..079f85b704 100644 --- a/packages/vscode-common/src/getExtensionApi.ts +++ b/packages/vscode-common/src/getExtensionApi.ts @@ -43,6 +43,10 @@ export interface TestHelpers { ): Promise; runIntegrationTests(): Promise; + + /** + * A thin wrapper around the VSCode API that allows us to mock it for testing. + */ vscodeApi: VscodeApi; } From 1861f62bbc9e386a69d93be852f04153061c2f6d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 14:57:13 +0100 Subject: [PATCH 55/61] Add tests for handleMultipleLInes --- .../handleMultipleLines.test.ts | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts new file mode 100644 index 0000000000..63ae77e269 --- /dev/null +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts @@ -0,0 +1,192 @@ +import assert = require("assert"); +import { BorderStyle, DecorationStyle } from "../decorationStyle.types"; +import { handleMultipleLines } from "./handleMultipleLines"; +import { Range } from "@cursorless/common"; + +type CharacterOffsets = [number, number]; + +interface StyledOffsets { + style: DecorationStyle; + offsets: CharacterOffsets; +} + +interface TestCase { + input: CharacterOffsets[]; + expected: StyledOffsets[][]; +} + +const testCases: TestCase[] = [ + { + input: [ + [0, 1], + [0, 1], + ], + expected: [ + [ + { + offsets: [0, 1], + style: { + left: BorderStyle.solid, + right: BorderStyle.porous, + top: BorderStyle.solid, + bottom: BorderStyle.none, + }, + }, + ], + [ + { + offsets: [0, 1], + style: { + left: BorderStyle.porous, + right: BorderStyle.solid, + top: BorderStyle.none, + bottom: BorderStyle.solid, + }, + }, + ], + ], + }, + { + input: [ + [1, 2], + [0, 1], + ], + expected: [ + [ + { + offsets: [1, 2], + style: { + left: BorderStyle.solid, + right: BorderStyle.porous, + top: BorderStyle.solid, + bottom: BorderStyle.solid, + }, + }, + ], + [ + { + offsets: [0, 1], + style: { + left: BorderStyle.porous, + right: BorderStyle.solid, + top: BorderStyle.solid, + bottom: BorderStyle.solid, + }, + }, + ], + ], + }, + { + input: [ + [1, 3], + [0, 2], + ], + expected: [ + [ + { + offsets: [1, 2], + style: { + left: BorderStyle.solid, + right: BorderStyle.none, + top: BorderStyle.solid, + bottom: BorderStyle.none, + }, + }, + { + offsets: [2, 3], + style: { + left: BorderStyle.none, + right: BorderStyle.porous, + top: BorderStyle.solid, + bottom: BorderStyle.solid, + }, + }, + ], + [ + { + offsets: [0, 1], + style: { + left: BorderStyle.porous, + right: BorderStyle.none, + top: BorderStyle.solid, + bottom: BorderStyle.solid, + }, + }, + { + offsets: [1, 2], + style: { + left: BorderStyle.none, + right: BorderStyle.solid, + top: BorderStyle.none, + bottom: BorderStyle.solid, + }, + }, + ], + ], + }, + { + input: [ + [0, 0], + [0, 0], + [0, 0], + ], + expected: [ + [ + { + offsets: [0, 0], + style: { + left: BorderStyle.solid, + right: BorderStyle.porous, + top: BorderStyle.solid, + bottom: BorderStyle.none, + }, + }, + ], + [ + { + offsets: [0, 0], + style: { + left: BorderStyle.porous, + right: BorderStyle.porous, + top: BorderStyle.porous, + bottom: BorderStyle.none, + }, + }, + ], + [ + { + offsets: [0, 0], + style: { + left: BorderStyle.porous, + right: BorderStyle.solid, + top: BorderStyle.porous, + bottom: BorderStyle.solid, + }, + }, + ], + ], + }, +]; + +suite("handleMultipleLines", () => { + for (const testCase of testCases) { + test(JSON.stringify(testCase.input), () => { + const actual = [ + ...handleMultipleLines( + testCase.input.map( + ([start, end], index) => new Range(index, start, index, end), + ), + ), + ]; + assert.deepStrictEqual( + actual, + testCase.expected.flatMap((lineOffsets, index) => + lineOffsets.map(({ style, offsets: [start, end] }) => ({ + range: new Range(index, start, index, end), + style, + })), + ), + ); + }); + } +}); From e8bc2bcc0fb8f4b95648cacb2f47cb2ac5d36087 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 15:16:21 +0100 Subject: [PATCH 56/61] Compactify handleMultipleLines tests --- .../handleMultipleLines.test.ts | 198 +++++------------- 1 file changed, 52 insertions(+), 146 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts index 63ae77e269..4d5cc20c5f 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts @@ -10,160 +10,65 @@ interface StyledOffsets { offsets: CharacterOffsets; } +/** + * Compact representation of an input to `handleMultipleLines`. The first + * element is the character offsets of the first line, and the rest are the + * character offsets of the end of the remaining lines. We use a single number + * for lines after the first because they always start at character 0. + */ +type Input = [CharacterOffsets, ...number[]]; + +/** + * Compact representation of the expected highlights for a single line. The + * first element is the line number, the second is the character offsets, and + * the third is the border styles for the top, bottom, left, and right borders + * respectively. + */ +type LineDecorations = [ + number, + CharacterOffsets, + [BorderStyle, BorderStyle, BorderStyle, BorderStyle], +]; + interface TestCase { - input: CharacterOffsets[]; - expected: StyledOffsets[][]; + input: Input; + expected: LineDecorations[]; } +const solid = BorderStyle.solid; +const porous = BorderStyle.porous; +const none = BorderStyle.none; + const testCases: TestCase[] = [ { - input: [ - [0, 1], - [0, 1], - ], + input: [[0, 1], 1], expected: [ - [ - { - offsets: [0, 1], - style: { - left: BorderStyle.solid, - right: BorderStyle.porous, - top: BorderStyle.solid, - bottom: BorderStyle.none, - }, - }, - ], - [ - { - offsets: [0, 1], - style: { - left: BorderStyle.porous, - right: BorderStyle.solid, - top: BorderStyle.none, - bottom: BorderStyle.solid, - }, - }, - ], + [0, [0, 1], [solid, porous, none, solid]], + [1, [0, 1], [none, solid, solid, porous]], ], }, { - input: [ - [1, 2], - [0, 1], - ], + input: [[1, 2], 1], expected: [ - [ - { - offsets: [1, 2], - style: { - left: BorderStyle.solid, - right: BorderStyle.porous, - top: BorderStyle.solid, - bottom: BorderStyle.solid, - }, - }, - ], - [ - { - offsets: [0, 1], - style: { - left: BorderStyle.porous, - right: BorderStyle.solid, - top: BorderStyle.solid, - bottom: BorderStyle.solid, - }, - }, - ], + [0, [1, 2], [solid, porous, solid, solid]], + [1, [0, 1], [solid, solid, solid, porous]], ], }, { - input: [ - [1, 3], - [0, 2], - ], + input: [[1, 3], 2], expected: [ - [ - { - offsets: [1, 2], - style: { - left: BorderStyle.solid, - right: BorderStyle.none, - top: BorderStyle.solid, - bottom: BorderStyle.none, - }, - }, - { - offsets: [2, 3], - style: { - left: BorderStyle.none, - right: BorderStyle.porous, - top: BorderStyle.solid, - bottom: BorderStyle.solid, - }, - }, - ], - [ - { - offsets: [0, 1], - style: { - left: BorderStyle.porous, - right: BorderStyle.none, - top: BorderStyle.solid, - bottom: BorderStyle.solid, - }, - }, - { - offsets: [1, 2], - style: { - left: BorderStyle.none, - right: BorderStyle.solid, - top: BorderStyle.none, - bottom: BorderStyle.solid, - }, - }, - ], + [0, [1, 2], [solid, none, none, solid]], + [0, [2, 3], [solid, porous, solid, none]], + [1, [0, 1], [solid, none, solid, porous]], + [1, [1, 2], [none, solid, solid, none]], ], }, { - input: [ - [0, 0], - [0, 0], - [0, 0], - ], + input: [[0, 0], 0, 0], expected: [ - [ - { - offsets: [0, 0], - style: { - left: BorderStyle.solid, - right: BorderStyle.porous, - top: BorderStyle.solid, - bottom: BorderStyle.none, - }, - }, - ], - [ - { - offsets: [0, 0], - style: { - left: BorderStyle.porous, - right: BorderStyle.porous, - top: BorderStyle.porous, - bottom: BorderStyle.none, - }, - }, - ], - [ - { - offsets: [0, 0], - style: { - left: BorderStyle.porous, - right: BorderStyle.solid, - top: BorderStyle.porous, - bottom: BorderStyle.solid, - }, - }, - ], + [0, [0, 0], [solid, porous, none, solid]], + [1, [0, 0], [porous, porous, none, porous]], + [2, [0, 0], [porous, solid, solid, porous]], ], }, ]; @@ -171,20 +76,21 @@ const testCases: TestCase[] = [ suite("handleMultipleLines", () => { for (const testCase of testCases) { test(JSON.stringify(testCase.input), () => { + const [firstLine, ...rest] = testCase.input; + const actual = [ - ...handleMultipleLines( - testCase.input.map( - ([start, end], index) => new Range(index, start, index, end), - ), - ), + ...handleMultipleLines([ + new Range(0, firstLine[0], 0, firstLine[1]), + ...rest.map((end, index) => new Range(index + 1, 0, index + 1, end)), + ]), ]; assert.deepStrictEqual( actual, - testCase.expected.flatMap((lineOffsets, index) => - lineOffsets.map(({ style, offsets: [start, end] }) => ({ - range: new Range(index, start, index, end), - style, - })), + testCase.expected.map( + ([lineNumber, [start, end], [top, right, bottom, left]]) => ({ + range: new Range(lineNumber, start, lineNumber, end), + style: { top, right, bottom, left }, + }), ), ); }); From b43c13df51c926f11c240c09483c16830e30084f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:40:27 +0100 Subject: [PATCH 57/61] Cleanup; more tests --- .../handleMultipleLines.test.ts | 105 ++++++++++----- .../handleMultipleLines.ts | 122 +++++++++++------- 2 files changed, 145 insertions(+), 82 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts index 4d5cc20c5f..f1e3ae2022 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts @@ -1,29 +1,17 @@ import assert = require("assert"); -import { BorderStyle, DecorationStyle } from "../decorationStyle.types"; +import { BorderStyle } from "../decorationStyle.types"; import { handleMultipleLines } from "./handleMultipleLines"; import { Range } from "@cursorless/common"; +import { map } from "itertools"; -type CharacterOffsets = [number, number]; +const solid = BorderStyle.solid; +const porous = BorderStyle.porous; +const none = BorderStyle.none; -interface StyledOffsets { - style: DecorationStyle; - offsets: CharacterOffsets; -} +type CharacterOffsets = [number, number]; -/** - * Compact representation of an input to `handleMultipleLines`. The first - * element is the character offsets of the first line, and the rest are the - * character offsets of the end of the remaining lines. We use a single number - * for lines after the first because they always start at character 0. - */ type Input = [CharacterOffsets, ...number[]]; -/** - * Compact representation of the expected highlights for a single line. The - * first element is the line number, the second is the character offsets, and - * the third is the border styles for the top, bottom, left, and right borders - * respectively. - */ type LineDecorations = [ number, CharacterOffsets, @@ -31,14 +19,30 @@ type LineDecorations = [ ]; interface TestCase { + /** + * The input to `handleMultipleLines`, in the format + * + * ``` + * [[firstLineStart, firstLineEnd], ...restLineEnds] + * ``` + * + * We use a single number for lines after the first because they always start + * at character 0. The first line will have line number 0, and the rest will + * count up from there. + */ input: Input; + + /** + * Each entry in this array is a list of expected highlights for a single + * line, each in the format + * + * ``` + * [lineNumber, [start, end], [top, right, bottom, left] + * ``` + */ expected: LineDecorations[]; } -const solid = BorderStyle.solid; -const porous = BorderStyle.porous; -const none = BorderStyle.none; - const testCases: TestCase[] = [ { input: [[0, 1], 1], @@ -71,6 +75,43 @@ const testCases: TestCase[] = [ [2, [0, 0], [porous, solid, solid, porous]], ], }, + { + input: [[2, 3], 1], + expected: [ + [0, [2, 3], [solid, porous, solid, solid]], + [1, [0, 1], [solid, solid, solid, porous]], + ], + }, + { + input: [[1, 3], 4, 2], + expected: [ + [0, [1, 3], [solid, porous, none, solid]], + + [1, [0, 1], [solid, none, none, porous]], + [1, [1, 2], [none, none, none, none]], + [1, [2, 3], [none, none, solid, none]], + [1, [3, 4], [porous, porous, solid, none]], + + [2, [0, 2], [none, solid, solid, porous]], + ], + }, + { + input: [[0, 2], 1], + expected: [ + [0, [0, 1], [solid, none, none, solid]], + [0, [1, 2], [solid, porous, solid, none]], + [1, [0, 1], [none, solid, solid, porous]], + ], + }, + { + input: [[0, 2], 1, 0], + expected: [ + [0, [0, 1], [solid, none, none, solid]], + [0, [1, 2], [solid, porous, porous, none]], + [1, [0, 1], [none, porous, solid, porous]], + [2, [0, 0], [none, solid, solid, porous]], + ], + }, ]; suite("handleMultipleLines", () => { @@ -78,21 +119,19 @@ suite("handleMultipleLines", () => { test(JSON.stringify(testCase.input), () => { const [firstLine, ...rest] = testCase.input; - const actual = [ - ...handleMultipleLines([ + const actual: LineDecorations[] = map( + handleMultipleLines([ new Range(0, firstLine[0], 0, firstLine[1]), ...rest.map((end, index) => new Range(index + 1, 0, index + 1, end)), ]), - ]; - assert.deepStrictEqual( - actual, - testCase.expected.map( - ([lineNumber, [start, end], [top, right, bottom, left]]) => ({ - range: new Range(lineNumber, start, lineNumber, end), - style: { top, right, bottom, left }, - }), - ), + ({ range, style }) => [ + range.start.line, + [range.start.character, range.end.character], + [style.top, style.right, style.bottom, style.left], + ], ); + + assert.deepStrictEqual(actual, testCase.expected); }); } }); diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts index 572f164c06..b4c63df3c3 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts @@ -37,7 +37,7 @@ export function* handleMultipleLines( * previous and next lines. */ function* handleLine(lineInfo: LineInfo): Iterable { - const { lineNumber, previousLine, currentLine, nextLine } = lineInfo; + const { lineNumber, currentLine, nextLine } = lineInfo; /** A list of "events", corresponding to the start or end of a line */ const events: Event[] = getEvents(lineInfo); @@ -47,39 +47,36 @@ function* handleLine(lineInfo: LineInfo): Iterable { * the fly. */ const currentDecoration: Omit = { - // Draw a solid top line if whatever is above us isn't part of our range. - // Otherwise draw no line so it merges in with the line above. - top: - previousLine == null || previousLine.isFirst - ? BorderStyle.solid - : BorderStyle.none, - // Analogous to above, but only care if we're last; doesn't matter if next - // line is last because it is guaranteed to start at char 0 + // Start with a solid top border. We'll switch to no border when previous + // line begins. Don't need to worry about porous because only the first + // line can start after char 0. + top: BorderStyle.solid, + + // Start with a solid bottom border if we're the last line, otherwise no + // border because we'll blend with the next line. bottom: currentLine.isLast ? BorderStyle.solid : BorderStyle.none, + // Start with a porous border if we're continuing from previous line left: currentLine.isFirst ? BorderStyle.solid : BorderStyle.porous, }; let currentOffset = currentLine.start; let yieldedAnything = false; - let isDone = false; - - for (const { offset, lineType, isStart } of events) { - if (isDone) { - break; - } - if (offset > currentOffset) { + // NB: The `loop` label here allows us to break out of the loop from inside + // the switch statement. + loop: for (const event of events) { + if (event.offset > currentOffset) { // If we've moved forward at all since the last event, yield a decoration // for the range between the last event and this one. yield { - range: new Range(lineNumber, currentOffset, lineNumber, offset), + range: new Range(lineNumber, currentOffset, lineNumber, event.offset), style: { ...currentDecoration, // If we're done with this line, draw a border, otherwise don't, so that // it merges in with the next decoration for this line. right: - offset === currentLine.end + event.offset === currentLine.end ? currentLine.isLast ? BorderStyle.solid : BorderStyle.porous @@ -88,40 +85,30 @@ function* handleLine(lineInfo: LineInfo): Iterable { }; yieldedAnything = true; currentDecoration.left = BorderStyle.none; + currentOffset = event.offset; } - switch (lineType) { + switch (event.lineType) { case LineType.previous: // Use no top border when overlapping with previous line so it visually // merges; otherwise use porous border to show nice cutoff effect. - currentDecoration.top = isStart ? BorderStyle.none : BorderStyle.porous; + currentDecoration.top = event.isLineStart + ? BorderStyle.none + : BorderStyle.porous; break; - case LineType.current: - if (!isStart) { - isDone = true; - } + case LineType.current: // event.isLineStart === false + break loop; + case LineType.next: // event.isLineStart === false + currentDecoration.bottom = nextLine!.isLast + ? BorderStyle.solid + : BorderStyle.porous; break; - case LineType.next: - // Blend with next line while it is overlapping with us; then switch - // to solid or porous, depending if it is the last line. - if (isStart) { - currentDecoration.bottom = BorderStyle.none; - } else { - currentDecoration.bottom = nextLine!.isLast - ? BorderStyle.solid - : BorderStyle.porous; - } - break; - } - - if (currentOffset < offset) { - // This guard is necessary so we don't accidentally jump backward if an - // adjacent line starts before we do. - currentOffset = offset; } } if (!yieldedAnything) { + // If current line is empty, then we didn't yield anything in the loop above, + // so yield a decoration for the whole line. yield { range: new Range( lineNumber, @@ -137,12 +124,49 @@ function* handleLine(lineInfo: LineInfo): Iterable { } } -interface Event { +interface LineEventBase { + /** + * The character offset at which this event occurs. This is the offset of the + * character that is the start or end of the line, depending on whether + * `isLineStart` is true or false. + */ offset: number; + + /** + * The type of line that this event corresponds to. + * -1: previous line + * 0: current line + * 1: next line + */ lineType: LineType; - isStart: boolean; + + /** + * Whether this event corresponds to the start of a line. If `false`, it + * corresponds to the end of a line. + */ + isLineStart: boolean; +} + +interface PreviousLineEvent extends LineEventBase { + offset: number; + lineType: LineType.previous; + isLineStart: boolean; +} + +interface CurrentLineEvent extends LineEventBase { + offset: number; + lineType: LineType.current; + isLineStart: false; +} + +interface NextLineEvent extends LineEventBase { + offset: number; + lineType: LineType.next; + isLineStart: false; } +type Event = PreviousLineEvent | CurrentLineEvent | NextLineEvent; + enum LineType { previous = -1, current = 0, @@ -151,7 +175,7 @@ enum LineType { /** * Generate "events" for our state machine. - * @param param0 Info about the line to render + * @param lineInfo Info about the line to render * @returns A list of "events", corresponding to the start or end of a line */ function getEvents({ previousLine, currentLine, nextLine }: LineInfo) { @@ -162,12 +186,12 @@ function getEvents({ previousLine, currentLine, nextLine }: LineInfo) { { offset: previousLine.start, lineType: LineType.previous, - isStart: true, + isLineStart: true, }, { offset: previousLine.end, lineType: LineType.previous, - isStart: false, + isLineStart: false, }, ); } @@ -177,14 +201,14 @@ function getEvents({ previousLine, currentLine, nextLine }: LineInfo) { events.push({ offset: currentLine.end, lineType: LineType.current, - isStart: false, + isLineStart: false, }); if (nextLine != null) { events.push({ offset: nextLine.end, lineType: LineType.next, - isStart: false, + isLineStart: false, }); } @@ -198,7 +222,7 @@ function getEvents({ previousLine, currentLine, nextLine }: LineInfo) { if (a.lineType === LineType.current) { return 1; } - return a.isStart ? -1 : 1; + return a.isLineStart ? -1 : 1; } return a.offset - b.offset; From 733b1ed415be4f4f016863070753d60eb73bfda9 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:44:48 +0100 Subject: [PATCH 58/61] JSDoc --- .../cursorless-engine/src/typings/target.types.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 2310901037..c0a34f0606 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -136,6 +136,19 @@ export interface Target { /** The range of the delimiter after the content selection */ getTrailingDelimiterTarget(): Target | undefined; getRemovalRange(): Range; + + /** + * The range that should be highlighted when the target is removed. Note that + * we can't just use `getRemovalRange()`, because when we highlight a line for + * removal, we don't know which line to highlight just based on the removal + * range. + * + * For example, assume that the document, represented as a string, is `"\n"`. + * This corresponds to a document with two empty lines. If we say `"chuck + * line"` on either line, the removal range will be the entire document, but + * we want to highlight the line that they were on when they said `"chuck + * line"`, as that is logically the line they've deleted. + */ getRemovalHighlightRange(): Range; withThatTarget(thatTarget: Target): Target; withContentRange(contentRange: Range): Target; From 3fe5390a62c9f210de3c3ea56f1877fd42582114 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:56:59 +0100 Subject: [PATCH 59/61] Tweak --- .../handleMultipleLines.test.ts | 9 +++++++-- .../handleMultipleLines.ts | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts index f1e3ae2022..a482068c42 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts @@ -8,10 +8,13 @@ const solid = BorderStyle.solid; const porous = BorderStyle.porous; const none = BorderStyle.none; +/** `[start, end]` */ type CharacterOffsets = [number, number]; +/** `[[firstLineStart, firstLineEnd], ...restLineEnds]` */ type Input = [CharacterOffsets, ...number[]]; +/** `[lineNumber, [start, end], [top, right, bottom, left]` */ type LineDecorations = [ number, CharacterOffsets, @@ -27,8 +30,10 @@ interface TestCase { * ``` * * We use a single number for lines after the first because they always start - * at character 0. The first line will have line number 0, and the rest will - * count up from there. + * at character 0. + * + * The first line will have line number 0, and the rest will count up from + * there. */ input: Input; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts index b4c63df3c3..95c8365022 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts @@ -73,8 +73,8 @@ function* handleLine(lineInfo: LineInfo): Iterable { range: new Range(lineNumber, currentOffset, lineNumber, event.offset), style: { ...currentDecoration, - // If we're done with this line, draw a border, otherwise don't, so that - // it merges in with the next decoration for this line. + // If we're done with this line, draw a right border, otherwise don't, + // so that it merges in with the next decoration for this line. right: event.offset === currentLine.end ? currentLine.isLast From 5f9a30d2c870a90aec854608334b9aac11d65e16 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 17:05:25 +0100 Subject: [PATCH 60/61] Remove broken image embeds from JSDocs --- .../suite/scopeVisualizer/runBasicMultilineContentTest.ts | 4 +--- .../src/suite/scopeVisualizer/runBasicRemovalTest.ts | 5 ++--- .../suite/scopeVisualizer/runNestedMultilineContentTest.ts | 6 ++---- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts index 910fa540b1..5e7a7993a3 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicMultilineContentTest.ts @@ -7,9 +7,7 @@ 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 - * as follows: - * - * ![basic multiline content](./runBasicMultilineContentTest.png) + * like `./runBasicMultilineContentTest.png`. */ export async function runBasicMultilineContentTest() { await openNewEditor(contents, { diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts index 79fcbd934b..54b701883f 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runBasicRemovalTest.ts @@ -6,9 +6,8 @@ import { ExpectedArgs } from "./scopeVisualizerTest.types"; /** * Tests that the scope visualizer works with removal ranges, by ensuring that - * the correct decorations are applied so that it looks as follows: - * - * ![basic removal](./runBasicRemovalTest.png) + * the correct decorations are applied so that it looks like + * `./runBasicRemovalTest.png`. */ export async function runBasicRemovalTest() { await openNewEditor("aaa bbb"); diff --git a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts index bbdfaf82fc..29570fa63d 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts +++ b/packages/cursorless-vscode-e2e/src/suite/scopeVisualizer/runNestedMultilineContentTest.ts @@ -6,10 +6,8 @@ import { ExpectedArgs } from "./scopeVisualizerTest.types"; /** * Tests that the scope visualizer works with nested multiline content, by - * ensuring that the correct decorations are applied so that it looks as - * follows: - * - * ![nested multiline content](./runNestedMultilineContentTest.png) + * ensuring that the correct decorations are applied so that it looks like + * `./runNestedMultilineContentTest.png`. */ export async function runNestedMultilineContentTest() { await openNewEditor(contents, { From 960fce20ebc1a7a2107bae8661a61b6b0a8e16a7 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:20:50 +0100 Subject: [PATCH 61/61] try bumping stack size to fix CI --- scripts/build-and-assemble-website.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-and-assemble-website.sh b/scripts/build-and-assemble-website.sh index e07b18d17a..a7a1de98e1 100755 --- a/scripts/build-and-assemble-website.sh +++ b/scripts/build-and-assemble-website.sh @@ -4,7 +4,7 @@ set -euo pipefail pnpm install pnpm compile -NODE_OPTIONS="--max-old-space-size=4096" \ +NODE_OPTIONS="--max-old-space-size=6144" \ pnpm \ --filter 'cursorless-org' \ --filter 'cursorless-org-*' \