From 0b5cdee9f5d1cceb043ee756ec4ee5b117903b09 Mon Sep 17 00:00:00 2001 From: Colton Loftus <70598503+C-Loftus@users.noreply.github.com> Date: Mon, 18 Mar 2024 07:40:27 -0400 Subject: [PATCH 1/7] Add a page about visual impairments to the docs (#2258) Fixes https://github.com/cursorless-dev/cursorless/issues/935 I specifically addressed everything in that issue. If desired, I could reference my sight free repository or any of the discussions Pokey and I had with Parham. I wasn't entirely sure since on one hand what I wrote up doesn't discuss anything about blindness or visual impairments that affect the ability to see the hats regardless of size, but on the other hand that is relatively specific and would require referencing another package outside the repository which I don't want to cause issues. I continue to be interested generally about ways to make cursorless better for non-visual use but realistically this might be more of a stretch goal and not for this PR ## Checklist (First take in checklist not relevant since no code is changed) - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [x] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- docs/user/README.md | 2 +- docs/user/visualAccessibility.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 docs/user/visualAccessibility.md diff --git a/docs/user/README.md b/docs/user/README.md index 5b425e8f81..25eb4fd2ef 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -66,7 +66,7 @@ The following colors are supported. Note that to target the default (gray) hat y You can enable or disable colors in your VSCode settings, by searching for `cursorless.hatEnablement.colors` and checking the box next to the internal ID for the given shape as listed above. To navigate to your VSCode settings, either say "show settings", or go to File --> Preferences --> Settings. -You can also tweak the visible colors for any of these colors in your VSCode settings, by searching for `cursorless.colors` and changing the hex color code next to the internal ID for the given shape as listed above. Note that you can configure different colors for dark and light themes. +You can also tweak the visible colors for any of these colors in your VSCode settings, by searching for `cursorless.colors` and changing the hex color code next to the internal ID for the given shape as listed above. Note that you can configure different colors for dark and light themes. See our [visual accessibility guide](visualAccessibility.md) for more on visual accessibility. If you find these color names unintuitive / tough to remember, their spoken forms can be [customized](customization.md) like any other spoken form diff --git a/docs/user/visualAccessibility.md b/docs/user/visualAccessibility.md new file mode 100644 index 0000000000..84e00eb61d --- /dev/null +++ b/docs/user/visualAccessibility.md @@ -0,0 +1,28 @@ +# Visual Accessibility + +Cursorless has multiple settings that can be customized to improve accessibility for users with color blindness or other vision impairments. The primary visual elements of Cursorless are the [hats](./README.md#decorated-symbol), so this guide will focus on customizing the hats. + +## Make the hats bigger + +Say `"cursorless settings"` and find the `cursorless.hatSize` setting. This setting allows you to increase the size of the hats. You may need to change the vertical offset of the hats to keep them from clipping the line below / above. + +You may also want to increase your line height to allow you to make the hats even larger: say `"show settings say line height"` and increase the line height to your preference. + +A reasonable place to start is to set the line height to 1.6 and the hat size to 70. + +## Use shapes instead of colors + +If you are a user with color blindness, it may be helpful to disable a subset or all colors and enable shapes instead. You can do so by saying `"cursorless settings"` and finding the `cursorless.hatEnablement.colors` and `cursorless.hatEnablement.shapes` settings. + +## Tweak your color scheme + +You can change the colors of the hats by saying `"cursorless settings"` and finding the `cursorless.colors.light` and `cursorless.colors.dark` settings. + +There are several user-created color schemes available [on the Cursorless wiki](https://github.com/cursorless-dev/cursorless/wiki/Color-schemes). One notable color scheme is the [Greyscale for Night Owl theme](https://github.com/cursorless-dev/cursorless/wiki/Color-schemes#greyscale-for-night-owl-theme), which is designed to reduce visual stimulation and be compatible with most forms of color blindness. Instead of colors like "yellow", "green", etc, it uses "bright" and "dark". Here's how you use it: + +1. Say `"cursorless settings"` and find the `cursorless.colors.light` or `cursorless.colors.dark` settings depending on your preferred mode +1. Change your `default` color to `#848384` +1. Change your `blue` color to `#ffffff` +1. Change your `green` color to `#333333` +1. Disable the other colors using the `cursorless.hatEnablement.colors` setting +1. Change spoken forms within your Cursorless settings folder located at `cursorless-settings/hat_styles.csv` so that you have `bright, blue` and `dark, green`. This is within your Talon configuration, not the IDE settings. This will change the spoken forms to match the new colors so that you can say eg `"take bright air"` to select the word "air" with a bright hat. See [Customization](./customization.md) for more information on how to change spoken forms. From 1c18476c85c4e7e2b75da90780f515f48de79e53 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Mar 2024 17:27:59 +0100 Subject: [PATCH 2/7] Fallback to Talon actions when focus is not on the text editor (#2235) Edit operations supported by community will now work in vscode outside of the text editor. eg the search widget `take line` `chuck token` Everything appears to be working when I have tested it. With that said I have not tested on community and we should probably have a discussion about some of the finer details of this. ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [x] I have not broken the cheatsheet --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- changelog/2024-03-fallBackToTalonActions.md | 6 + cursorless-talon/src/command.py | 16 +- cursorless-talon/src/fallback.py | 107 +++++++++++ cursorless-talon/src/versions.py | 1 + packages/common/src/FakeCommandServerApi.ts | 25 +++ .../common/src/getFakeCommandServerApi.ts | 11 -- packages/common/src/index.ts | 4 +- .../src/testUtil/serializeTestFixture.ts | 2 + packages/common/src/types/CommandServerApi.ts | 4 + packages/common/src/types/TestCaseFixture.ts | 16 +- .../src/types/command/CommandV7.types.ts | 8 + .../common/src/types/command/command.types.ts | 25 ++- .../common/src/util/clientSupportsFallback.ts | 5 + .../cursorless-engine/src/CommandRunner.ts | 4 +- .../src/api/CursorlessEngineApi.ts | 21 ++- .../core/commandRunner/CommandRunnerImpl.ts | 26 ++- .../canonicalizeAndValidateCommand.ts | 4 + .../commandVersionUpgrades/upgradeV6ToV7.ts | 5 + .../src/core/getCommandFallback.ts | 166 ++++++++++++++++++ .../cursorless-engine/src/cursorlessEngine.ts | 10 +- .../generateSpokenForm/getHatMapCommand.ts | 2 +- packages/cursorless-engine/src/runCommand.ts | 34 +++- .../src/testCaseRecorder/TestCase.ts | 21 ++- .../src/testCaseRecorder/TestCaseRecorder.ts | 16 +- .../fixtures/recorded/fallback/bringFine.yml | 30 ++++ .../fixtures/recorded/fallback/bringFine2.yml | 30 ++++ .../fixtures/recorded/fallback/chuckFine.yml | 25 +++ .../fixtures/recorded/fallback/chuckFine2.yml | 25 +++ .../fixtures/recorded/fallback/moveFine.yml | 30 ++++ .../fixtures/recorded/fallback/takeThis.yml | 26 +++ .../fixtures/recorded/fallback/takeToken.yml | 29 +++ .../recorded/testCaseRecorder/takeHarp.yml | 2 +- .../src/suite/recorded.vscode.test.ts | 30 +++- .../src/constructTestHelpers.ts | 6 +- packages/cursorless-vscode/src/extension.ts | 20 ++- packages/vscode-common/src/TestHelpers.ts | 4 +- 36 files changed, 728 insertions(+), 68 deletions(-) create mode 100644 changelog/2024-03-fallBackToTalonActions.md create mode 100644 cursorless-talon/src/fallback.py create mode 100644 cursorless-talon/src/versions.py create mode 100644 packages/common/src/FakeCommandServerApi.ts delete mode 100644 packages/common/src/getFakeCommandServerApi.ts create mode 100644 packages/common/src/types/command/CommandV7.types.ts create mode 100644 packages/common/src/util/clientSupportsFallback.ts create mode 100644 packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV6ToV7.ts create mode 100644 packages/cursorless-engine/src/core/getCommandFallback.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml diff --git a/changelog/2024-03-fallBackToTalonActions.md b/changelog/2024-03-fallBackToTalonActions.md new file mode 100644 index 0000000000..70ce93a215 --- /dev/null +++ b/changelog/2024-03-fallBackToTalonActions.md @@ -0,0 +1,6 @@ +--- +tags: [enhancement] +pullRequest: 2235 +--- + +- Fall back to text-based Talon actions when editor is not focused. This allows you to say things like "take token", "bring air", etc, when in the terminal, search bar, etc. diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index f7f05e447e..329678649c 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -3,10 +3,13 @@ from talon import Module, actions, speech_system +from .fallback import perform_fallback +from .versions import COMMAND_VERSION + @dataclasses.dataclass class CursorlessCommand: - version = 6 + version = COMMAND_VERSION spokenForm: str usePrePhraseSnapshot: bool action: dict @@ -30,10 +33,12 @@ def on_phrase(d): class Actions: def private_cursorless_command_and_wait(action: dict): """Execute cursorless command and wait for it to finish""" - actions.user.private_cursorless_run_rpc_command_and_wait( + response = actions.user.private_cursorless_run_rpc_command_get( CURSORLESS_COMMAND_ID, construct_cursorless_command(action), ) + if "fallback" in response: + perform_fallback(response["fallback"]) def private_cursorless_command_no_wait(action: dict): """Execute cursorless command without waiting""" @@ -44,10 +49,15 @@ def private_cursorless_command_no_wait(action: dict): def private_cursorless_command_get(action: dict): """Execute cursorless command and return result""" - return actions.user.private_cursorless_run_rpc_command_get( + response = actions.user.private_cursorless_run_rpc_command_get( CURSORLESS_COMMAND_ID, construct_cursorless_command(action), ) + if "fallback" in response: + return perform_fallback(response["fallback"]) + if "returnValue" in response: + return response["returnValue"] + return None def construct_cursorless_command(action: dict) -> dict: diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py new file mode 100644 index 0000000000..55d86ab8bd --- /dev/null +++ b/cursorless-talon/src/fallback.py @@ -0,0 +1,107 @@ +from typing import Callable + +from talon import actions + +from .versions import COMMAND_VERSION + +# This ensures that we remember to update fallback if the response payload changes +assert COMMAND_VERSION == 7 + +action_callbacks = { + "getText": lambda: [actions.edit.selected_text()], + "setSelection": actions.skip, + "setSelectionBefore": actions.edit.left, + "setSelectionAfter": actions.edit.right, + "copyToClipboard": actions.edit.copy, + "cutToClipboard": actions.edit.cut, + "pasteFromClipboard": actions.edit.paste, + "clearAndSetSelection": actions.edit.delete, + "remove": actions.edit.delete, + "editNewLineBefore": actions.edit.line_insert_up, + "editNewLineAfter": actions.edit.line_insert_down, +} + +modifier_callbacks = { + "extendThroughStartOf.line": actions.user.select_line_start, + "extendThroughEndOf.line": actions.user.select_line_end, + "containingScope.document": actions.edit.select_all, + "containingScope.paragraph": actions.edit.select_paragraph, + "containingScope.line": actions.edit.select_line, + "containingScope.token": actions.edit.select_word, +} + + +def call_as_function(callee: str): + wrap_with_paired_delimiter(f"{callee}(", ")") + + +def wrap_with_paired_delimiter(left: str, right: str): + selected = actions.edit.selected_text() + actions.insert(f"{left}{selected}{right}") + for _ in right: + actions.edit.left() + + +def containing_token_if_empty(): + if actions.edit.selected_text() == "": + actions.edit.select_word() + + +def perform_fallback(fallback: dict): + try: + modifier_callbacks = get_modifier_callbacks(fallback) + action_callback = get_action_callback(fallback) + for callback in reversed(modifier_callbacks): + callback() + return action_callback() + except ValueError as ex: + actions.app.notify(str(ex)) + + +def get_action_callback(fallback: dict) -> Callable: + action = fallback["action"] + + if action in action_callbacks: + return action_callbacks[action] + + match action: + case "insert": + return lambda: actions.insert(fallback["text"]) + case "callAsFunction": + return lambda: call_as_function(fallback["callee"]) + case "wrapWithPairedDelimiter": + return lambda: wrap_with_paired_delimiter( + fallback["left"], fallback["right"] + ) + + raise ValueError(f"Unknown Cursorless fallback action: {action}") + + +def get_modifier_callbacks(fallback: dict) -> list[Callable]: + return [get_modifier_callback(modifier) for modifier in fallback["modifiers"]] + + +def get_modifier_callback(modifier: dict) -> Callable: + modifier_type = modifier["type"] + + match modifier_type: + case "containingTokenIfEmpty": + return containing_token_if_empty + case "containingScope": + scope_type_type = modifier["scopeType"]["type"] + return get_simple_modifier_callback(f"{modifier_type}.{scope_type_type}") + case "extendThroughStartOf": + if "modifiers" not in modifier: + return get_simple_modifier_callback(f"{modifier_type}.line") + case "extendThroughEndOf": + if "modifiers" not in modifier: + return get_simple_modifier_callback(f"{modifier_type}.line") + + raise ValueError(f"Unknown Cursorless fallback modifier: {modifier_type}") + + +def get_simple_modifier_callback(key: str) -> Callable: + try: + return modifier_callbacks[key] + except KeyError: + raise ValueError(f"Unknown Cursorless fallback modifier: {key}") diff --git a/cursorless-talon/src/versions.py b/cursorless-talon/src/versions.py new file mode 100644 index 0000000000..056299a93a --- /dev/null +++ b/cursorless-talon/src/versions.py @@ -0,0 +1 @@ +COMMAND_VERSION = 7 diff --git a/packages/common/src/FakeCommandServerApi.ts b/packages/common/src/FakeCommandServerApi.ts new file mode 100644 index 0000000000..b37500d407 --- /dev/null +++ b/packages/common/src/FakeCommandServerApi.ts @@ -0,0 +1,25 @@ +import { + CommandServerApi, + FocusedElementType, + InboundSignal, +} from "./types/CommandServerApi"; + +export class FakeCommandServerApi implements CommandServerApi { + private focusedElementType: FocusedElementType | undefined; + signals: { prePhrase: InboundSignal }; + + constructor() { + this.signals = { prePhrase: { getVersion: async () => null } }; + this.focusedElementType = "textEditor"; + } + + getFocusedElementType(): FocusedElementType | undefined { + return this.focusedElementType; + } + + setFocusedElementType( + focusedElementType: FocusedElementType | undefined, + ): void { + this.focusedElementType = focusedElementType; + } +} diff --git a/packages/common/src/getFakeCommandServerApi.ts b/packages/common/src/getFakeCommandServerApi.ts deleted file mode 100644 index 3a1adb23fa..0000000000 --- a/packages/common/src/getFakeCommandServerApi.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CommandServerApi } from "./types/CommandServerApi"; - -export function getFakeCommandServerApi(): CommandServerApi { - return { - signals: { - prePhrase: { - getVersion: async () => null, - }, - }, - }; -} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 2770a19025..61badf4813 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -85,16 +85,18 @@ export * from "./types/command/legacy/ActionCommandV5"; export * from "./types/command/legacy/CommandV5.types"; export * from "./types/command/legacy/PartialTargetDescriptorV5.types"; export * from "./types/command/CommandV6.types"; +export * from "./types/command/CommandV7.types"; export * from "./types/command/legacy/PartialTargetDescriptorV3.types"; export * from "./types/command/legacy/PartialTargetDescriptorV4.types"; export * from "./types/CommandServerApi"; export * from "./util/itertools"; export * from "./extensionDependencies"; -export * from "./getFakeCommandServerApi"; +export * from "./FakeCommandServerApi"; export * from "./types/TestCaseFixture"; export * from "./util/getEnvironmentVariableStrict"; export * from "./util/CompositeKeyDefaultMap"; export * from "./util/toPlainObject"; +export * from "./util/clientSupportsFallback"; export * from "./scopeSupportFacets/scopeSupportFacets.types"; export * from "./scopeSupportFacets/scopeSupportFacetInfos"; export * from "./scopeSupportFacets/textualScopeSupportFacetInfos"; diff --git a/packages/common/src/testUtil/serializeTestFixture.ts b/packages/common/src/testUtil/serializeTestFixture.ts index 4e49c08303..07c5ef45c1 100644 --- a/packages/common/src/testUtil/serializeTestFixture.ts +++ b/packages/common/src/testUtil/serializeTestFixture.ts @@ -7,6 +7,7 @@ function reorderFields( ): EnforceUndefined { return { languageId: fixture.languageId, + focusedElementType: fixture.focusedElementType, postEditorOpenSleepTimeMs: fixture.postEditorOpenSleepTimeMs, postCommandSleepTimeMs: fixture.postCommandSleepTimeMs, command: fixture.command, @@ -15,6 +16,7 @@ function reorderFields( initialState: fixture.initialState, finalState: fixture.finalState, returnValue: fixture.returnValue, + fallback: fixture.fallback, thrownError: fixture.thrownError, ide: fixture.ide, }; diff --git a/packages/common/src/types/CommandServerApi.ts b/packages/common/src/types/CommandServerApi.ts index 27b531c239..d051dee5db 100644 --- a/packages/common/src/types/CommandServerApi.ts +++ b/packages/common/src/types/CommandServerApi.ts @@ -2,11 +2,15 @@ * API object for interacting with the command server */ export interface CommandServerApi { + getFocusedElementType: () => FocusedElementType | undefined; + signals: { prePhrase: InboundSignal; }; } +export type FocusedElementType = "textEditor" | "terminal"; + export interface InboundSignal { getVersion(): Promise; } diff --git a/packages/common/src/types/TestCaseFixture.ts b/packages/common/src/types/TestCaseFixture.ts index 5424b7d0c2..7b366e694a 100644 --- a/packages/common/src/types/TestCaseFixture.ts +++ b/packages/common/src/types/TestCaseFixture.ts @@ -1,6 +1,6 @@ -import { Command, CommandLatest } from ".."; -import { TestCaseSnapshot } from "../testUtil/TestCaseSnapshot"; -import { PlainSpyIDERecordedValues } from "../testUtil/spyToPlainObject"; +import type { Command, CommandLatest, Fallback, FocusedElementType } from ".."; +import type { TestCaseSnapshot } from "../testUtil/TestCaseSnapshot"; +import type { PlainSpyIDERecordedValues } from "../testUtil/spyToPlainObject"; export type ThrownError = { name: string; @@ -12,6 +12,11 @@ interface TestCaseFixtureBase { postCommandSleepTimeMs?: number; spokenFormError?: string; + /** + * The type of element that is focused before the command is executed. If undefined default to text editor. + */ + focusedElementType?: FocusedElementType | "other"; + /** * A list of marks to check in the case of navigation map test otherwise undefined */ @@ -30,6 +35,11 @@ interface TestCaseFixtureBase { * error test case. */ returnValue?: unknown; + + /** + * The fallback of the command. Will be undefined if the command was executed by the extension. + */ + fallback?: Fallback; } export interface TestCaseFixture extends TestCaseFixtureBase { diff --git a/packages/common/src/types/command/CommandV7.types.ts b/packages/common/src/types/command/CommandV7.types.ts new file mode 100644 index 0000000000..554347b16b --- /dev/null +++ b/packages/common/src/types/command/CommandV7.types.ts @@ -0,0 +1,8 @@ +import type { CommandV6 } from "./CommandV6.types"; + +export interface CommandV7 extends Omit { + /** + * The version number of the command API + */ + version: 7; +} diff --git a/packages/common/src/types/command/command.types.ts b/packages/common/src/types/command/command.types.ts index 6dc0f9cad5..1a6f9e260a 100644 --- a/packages/common/src/types/command/command.types.ts +++ b/packages/common/src/types/command/command.types.ts @@ -1,4 +1,7 @@ -import { CommandV6 } from "./CommandV6.types"; +import type { ActionDescriptor } from "./ActionDescriptor"; +import type { CommandV6 } from "./CommandV6.types"; +import type { CommandV7 } from "./CommandV7.types"; +import type { Modifier } from "./PartialTargetDescriptor.types"; import type { CommandV0, CommandV1 } from "./legacy/CommandV0V1.types"; import type { CommandV2 } from "./legacy/CommandV2.types"; import type { CommandV3 } from "./legacy/CommandV3.types"; @@ -7,7 +10,7 @@ import type { CommandV5 } from "./legacy/CommandV5.types"; export type CommandComplete = Required> & Pick; -export const LATEST_VERSION = 6 as const; +export const LATEST_VERSION = 7 as const; export type CommandLatest = Command & { version: typeof LATEST_VERSION; @@ -20,4 +23,20 @@ export type Command = | CommandV3 | CommandV4 | CommandV5 - | CommandV6; + | CommandV6 + | CommandV7; + +export type CommandResponse = { returnValue: unknown } | { fallback: Fallback }; + +export type FallbackModifier = Modifier | { type: "containingTokenIfEmpty" }; + +export type Fallback = + | { action: ActionDescriptor["name"]; modifiers: FallbackModifier[] } + | { action: "insert"; modifiers: FallbackModifier[]; text: string } + | { action: "callAsFunction"; modifiers: FallbackModifier[]; callee: string } + | { + action: "wrapWithPairedDelimiter" | "rewrapWithPairedDelimiter"; + modifiers: FallbackModifier[]; + left: string; + right: string; + }; diff --git a/packages/common/src/util/clientSupportsFallback.ts b/packages/common/src/util/clientSupportsFallback.ts new file mode 100644 index 0000000000..beb07f13a9 --- /dev/null +++ b/packages/common/src/util/clientSupportsFallback.ts @@ -0,0 +1,5 @@ +import type { Command } from "../types/command/command.types"; + +export function clientSupportsFallback(command: Command): boolean { + return command.version >= 7; +} diff --git a/packages/cursorless-engine/src/CommandRunner.ts b/packages/cursorless-engine/src/CommandRunner.ts index 39942f1226..d70f19d58b 100644 --- a/packages/cursorless-engine/src/CommandRunner.ts +++ b/packages/cursorless-engine/src/CommandRunner.ts @@ -1,5 +1,5 @@ -import { CommandComplete } from "@cursorless/common"; +import type { CommandComplete, CommandResponse } from "@cursorless/common"; export interface CommandRunner { - run(command: CommandComplete): Promise; + run(command: CommandComplete): Promise; } diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 331d535a75..63e4b8d591 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -1,9 +1,14 @@ -import { Command, HatTokenMap, IDE } from "@cursorless/common"; -import { Snippets } from "../core/Snippets"; -import { StoredTargetMap } from "../core/StoredTargets"; -import { ScopeProvider } from "@cursorless/common"; -import { CommandRunner } from "../CommandRunner"; -import { ReadOnlyHatMap } from "@cursorless/common"; +import type { + Command, + CommandResponse, + HatTokenMap, + IDE, + ReadOnlyHatMap, + ScopeProvider, +} from "@cursorless/common"; +import type { CommandRunner } from "../CommandRunner"; +import type { Snippets } from "../core/Snippets"; +import type { StoredTargetMap } from "../core/StoredTargets"; export interface CursorlessEngine { commandApi: CommandApi; @@ -34,13 +39,13 @@ export interface CommandApi { * Runs a command. This is the core of the Cursorless engine. * @param command The command to run */ - runCommand(command: Command): Promise; + 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; + runCommandSafe(...args: unknown[]): Promise; } export interface CommandRunnerDecorator { diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 277eeaff81..9e030348d1 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -1,8 +1,11 @@ import { + ActionDescriptor, CommandComplete, + CommandResponse, + CommandServerApi, DestinationDescriptor, - ActionDescriptor, PartialTargetDescriptor, + clientSupportsFallback, } from "@cursorless/common"; import { CommandRunner } from "../../CommandRunner"; import { ActionRecord, ActionReturnValue } from "../../actions/actions.types"; @@ -12,6 +15,7 @@ import { ModifierStage } from "../../processTargets/PipelineStages.types"; import { SelectionWithEditor } from "../../typings/Types"; import { Destination, Target } from "../../typings/target.types"; import { Debug } from "../Debug"; +import { getCommandFallback } from "../getCommandFallback"; import { inferFullTargetDescriptor } from "../inferFullTargetDescriptor"; import { selectionToStoredTarget } from "./selectionToStoredTarget"; @@ -21,11 +25,13 @@ export class CommandRunnerImpl implements CommandRunner { private noAutomaticTokenExpansion: boolean | undefined; constructor( + private commandServerApi: CommandServerApi | null, private debug: Debug, private storedTargets: StoredTargetMap, private pipelineRunner: TargetPipelineRunner, private actions: ActionRecord, ) { + this.runAction = this.runAction.bind(this); this.inferenceContext = new InferenceContext(this.debug); } @@ -46,7 +52,19 @@ export class CommandRunnerImpl implements CommandRunner { * action, and returns the desired return value indicated by the action, if * it has one. */ - async run({ action }: CommandComplete): Promise { + async run(command: CommandComplete): Promise { + if (clientSupportsFallback(command)) { + const fallback = await getCommandFallback( + this.commandServerApi, + this.runAction, + command, + ); + + if (fallback != null) { + return { fallback }; + } + } + const { returnValue, thatSelections: newThatSelections, @@ -55,7 +73,7 @@ export class CommandRunnerImpl implements CommandRunner { sourceTargets: newSourceTargets, instanceReferenceTargets: newInstanceReferenceTargets, keyboardTargets: newKeyboardTargets, - } = await this.runAction(action); + } = await this.runAction(command.action); this.storedTargets.set( "that", @@ -68,7 +86,7 @@ export class CommandRunnerImpl implements CommandRunner { this.storedTargets.set("instanceReference", newInstanceReferenceTargets); this.storedTargets.set("keyboard", newKeyboardTargets); - return returnValue; + return { returnValue }; } private runAction( diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts index 5206f86b6e..b9692e8ddd 100644 --- a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts @@ -20,6 +20,7 @@ import { upgradeV3ToV4 } from "./upgradeV3ToV4"; import { upgradeV4ToV5 } from "./upgradeV4ToV5/upgradeV4ToV5"; import { upgradeV5ToV6 } from "./upgradeV5ToV6"; import produce from "immer"; +import { upgradeV6ToV7 } from "./upgradeV6ToV7"; /** * Given a command argument which comes from the client, normalize it so that it @@ -72,6 +73,9 @@ function upgradeCommand(command: Command): CommandLatest { case 5: command = upgradeV5ToV6(command); break; + case 6: + command = upgradeV6ToV7(command); + break; default: throw new Error( `Can't upgrade from unknown version ${command.version}`, diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV6ToV7.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV6ToV7.ts new file mode 100644 index 0000000000..5af1045689 --- /dev/null +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV6ToV7.ts @@ -0,0 +1,5 @@ +import type { CommandV6, CommandV7 } from "@cursorless/common"; + +export function upgradeV6ToV7(command: CommandV6): CommandV7 { + return { ...command, version: 7 }; +} diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts new file mode 100644 index 0000000000..a1383227b0 --- /dev/null +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -0,0 +1,166 @@ +import { + ActionDescriptor, + CommandComplete, + CommandServerApi, + DestinationDescriptor, + Fallback, + FallbackModifier, + PartialTargetDescriptor, +} from "@cursorless/common"; +import type { ActionReturnValue } from "../actions/actions.types"; + +export async function getCommandFallback( + commandServerApi: CommandServerApi | null, + runAction: (actionDescriptor: ActionDescriptor) => Promise, + command: CommandComplete, +): Promise { + if ( + commandServerApi == null || + commandServerApi.getFocusedElementType() === "textEditor" + ) { + return null; + } + + const action = command.action; + + switch (action.name) { + case "replace": + return destinationIsSelection(action.destination) && + Array.isArray(action.replaceWith) + ? { + action: "insert", + modifiers: getModifiersFromDestination(action.destination), + text: action.replaceWith.join("\n"), + } + : null; + + case "replaceWithTarget": + if (destinationIsSelection(action.destination)) { + return { + action: "insert", + modifiers: getModifiersFromDestination(action.destination), + text: await getText(runAction, action.source), + }; + } + return null; + + case "moveToTarget": + if (destinationIsSelection(action.destination)) { + const text = await getText(runAction, action.source); + await remove(runAction, action.source); + return { + action: "insert", + modifiers: getModifiersFromDestination(action.destination), + text, + }; + } + return null; + + case "callAsFunction": + if (targetIsSelection(action.argument)) { + return { + action: action.name, + modifiers: getModifiersFromTarget(action.argument), + callee: await getText(runAction, action.callee), + }; + } + return null; + + case "wrapWithPairedDelimiter": + case "rewrapWithPairedDelimiter": + return targetIsSelection(action.target) + ? { + action: action.name, + modifiers: getModifiersFromTarget(action.target), + left: action.left, + right: action.right, + } + : null; + + case "pasteFromClipboard": + return destinationIsSelection(action.destination) + ? { + action: action.name, + modifiers: getModifiersFromDestination(action.destination), + } + : null; + + case "swapTargets": + case "editNew": + case "insertSnippet": + case "generateSnippet": + case "wrapWithSnippet": + return null; + + default: + return targetIsSelection(action.target) + ? { + action: action.name, + modifiers: getModifiersFromTarget(action.target), + } + : null; + } +} + +function destinationIsSelection(destination: DestinationDescriptor): boolean { + if (destination.type === "implicit") { + return true; + } + if (destination.type === "primitive") { + return ( + destination.insertionMode === "to" && + targetIsSelection(destination.target) + ); + } + return false; +} + +function targetIsSelection(target: PartialTargetDescriptor): boolean { + if (target.type === "implicit") { + return true; + } + if (target.type === "primitive") { + return target.mark == null || target.mark.type === "cursor"; + } + return false; +} + +function getModifiersFromDestination( + destination: DestinationDescriptor, +): FallbackModifier[] { + if (destination.type === "primitive") { + return getModifiersFromTarget(destination.target); + } + return []; +} + +function getModifiersFromTarget( + target: PartialTargetDescriptor, +): FallbackModifier[] { + if (target.type === "primitive") { + if (target.modifiers != null && target.modifiers.length > 0) { + return target.modifiers; + } + + if (target.mark?.type === "cursor") { + return [{ type: "containingTokenIfEmpty" }]; + } + } + return []; +} + +async function getText( + runAction: (actionDescriptor: ActionDescriptor) => Promise, + target: PartialTargetDescriptor, +): Promise { + const response = await runAction({ name: "getText", target }); + const texts = response.returnValue as string[]; + return texts.join("\n"); +} + +async function remove( + runAction: (actionDescriptor: ActionDescriptor) => Promise, + target: PartialTargetDescriptor, +): Promise { + await runAction({ name: "remove", target }); +} diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index cee5a2aa85..71dd1b1d4e 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -6,8 +6,6 @@ import { IDE, ScopeProvider, } from "@cursorless/common"; -import { StoredTargetMap } from "./core/StoredTargets"; -import { TreeSitter } from "./typings/TreeSitter"; import { CommandRunnerDecorator, CursorlessEngine, @@ -15,10 +13,12 @@ import { import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; +import { StoredTargetMap } from "./core/StoredTargets"; import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { CustomSpokenFormGeneratorImpl } from "./generateSpokenForm/CustomSpokenFormGeneratorImpl"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; +import { TalonSpokenFormsJsonReader } from "./nodeCommon/TalonSpokenFormsJsonReader"; import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; import { runCommand } from "./runCommand"; @@ -28,8 +28,8 @@ import { ScopeRangeProvider } from "./scopeProviders/ScopeRangeProvider"; import { ScopeRangeWatcher } from "./scopeProviders/ScopeRangeWatcher"; import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker"; import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher"; -import { TalonSpokenFormsJsonReader } from "./nodeCommon/TalonSpokenFormsJsonReader"; import { injectIde } from "./singletons/ide.singleton"; +import { TreeSitter } from "./typings/TreeSitter"; export function createCursorlessEngine( treeSitter: TreeSitter, @@ -74,6 +74,7 @@ export function createCursorlessEngine( runCommand(command: Command) { return runCommand( treeSitter, + commandServerApi, debug, hatTokenMap, snippets, @@ -85,9 +86,10 @@ export function createCursorlessEngine( ); }, - runCommandSafe(...args: unknown[]) { + async runCommandSafe(...args: unknown[]) { return runCommand( treeSitter, + commandServerApi, debug, hatTokenMap, snippets, diff --git a/packages/cursorless-engine/src/generateSpokenForm/getHatMapCommand.ts b/packages/cursorless-engine/src/generateSpokenForm/getHatMapCommand.ts index 0d88a7f7ea..aea52cbe5f 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/getHatMapCommand.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/getHatMapCommand.ts @@ -34,7 +34,7 @@ export function getHatMapCommand(marks: string[]): CommandLatest { elements: primitiveTargets, }, }, - version: 6, + version: 7, usePrePhraseSnapshot: false, }; } diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 331200b030..fb2a4b2abc 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -1,6 +1,14 @@ -import { Command, HatTokenMap, ReadOnlyHatMap } from "@cursorless/common"; +import { + Command, + CommandResponse, + CommandServerApi, + HatTokenMap, + ReadOnlyHatMap, + clientSupportsFallback, +} from "@cursorless/common"; import { CommandRunner } from "./CommandRunner"; import { Actions } from "./actions/Actions"; +import { CommandRunnerDecorator } from "./api/CursorlessEngineApi"; import { Debug } from "./core/Debug"; import { Snippets } from "./core/Snippets"; import { CommandRunnerImpl } from "./core/commandRunner/CommandRunnerImpl"; @@ -12,7 +20,6 @@ import { TargetPipelineRunner } from "./processTargets"; import { MarkStageFactoryImpl } from "./processTargets/MarkStageFactoryImpl"; import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; -import { CommandRunnerDecorator } from "./api/CursorlessEngineApi"; /** * Entry point for Cursorless commands. We proceed as follows: @@ -28,6 +35,7 @@ import { CommandRunnerDecorator } from "./api/CursorlessEngineApi"; */ export async function runCommand( treeSitter: TreeSitter, + commandServerApi: CommandServerApi | null, debug: Debug, hatTokenMap: HatTokenMap, snippets: Snippets, @@ -36,7 +44,7 @@ export async function runCommand( rangeUpdater: RangeUpdater, commandRunnerDecorators: CommandRunnerDecorator[], command: Command, -): Promise { +): Promise { if (debug.active) { debug.log(`command:`); debug.log(JSON.stringify(command, null, 2)); @@ -50,6 +58,7 @@ export async function runCommand( let commandRunner = createCommandRunner( treeSitter, + commandServerApi, languageDefinitions, debug, storedTargets, @@ -62,11 +71,27 @@ export async function runCommand( commandRunner = decorator.wrapCommandRunner(readableHatMap, commandRunner); } - return await commandRunner.run(commandComplete); + const response = await commandRunner.run(commandComplete); + + return await unwrapLegacyCommandResponse(command, response); +} + +async function unwrapLegacyCommandResponse( + command: Command, + response: CommandResponse, +): Promise { + if (clientSupportsFallback(command)) { + return response; + } + if ("returnValue" in response) { + return response.returnValue; + } + return undefined; } function createCommandRunner( treeSitter: TreeSitter, + commandServerApi: CommandServerApi | null, languageDefinitions: LanguageDefinitions, debug: Debug, storedTargets: StoredTargetMap, @@ -90,6 +115,7 @@ function createCommandRunner( ); markStageFactory.setPipelineRunner(targetPipelineRunner); return new CommandRunnerImpl( + commandServerApi, debug, storedTargets, targetPipelineRunner, diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts index 04b85fa684..0dd2dad532 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts @@ -1,9 +1,12 @@ import { ActionType, CommandLatest, + CommandResponse, EnforceUndefined, extractTargetedMarks, ExtraSnapshotField, + Fallback, + FocusedElementType, marksToPlainObject, PartialTargetDescriptor, PlainSpyIDERecordedValues, @@ -32,6 +35,7 @@ export class TestCase { private finalState?: TestCaseSnapshot; thrownError?: ThrownError; private returnValue?: unknown; + private fallback?: Fallback; private targetKeys: string[]; private _awaitingFinalMarkInfo: boolean; private marksToCheck?: string[]; @@ -40,6 +44,7 @@ export class TestCase { constructor( command: CommandLatest, + private focusedElementType: FocusedElementType | undefined, private hatTokenMap: ReadOnlyHatMap, private storedTargets: StoredTargetMap, private spyIde: SpyIDE, @@ -135,6 +140,10 @@ export class TestCase { } const fixture: EnforceUndefined = { languageId: this.languageId, + focusedElementType: + this.focusedElementType !== "textEditor" + ? this.focusedElementType ?? "other" + : undefined, postEditorOpenSleepTimeMs: undefined, postCommandSleepTimeMs: undefined, command: this.command, @@ -143,6 +152,7 @@ export class TestCase { initialState: this.initialState, finalState: this.finalState, returnValue: this.returnValue, + fallback: this.fallback, thrownError: this.thrownError, ide: this.spyIdeValues, }; @@ -162,9 +172,16 @@ export class TestCase { ); } - async recordFinalState(returnValue: unknown) { + async recordFinalState(returnValue: CommandResponse) { const excludeFields = this.getExcludedFields(false); - this.returnValue = returnValue; + + if ("returnValue" in returnValue) { + this.returnValue = returnValue.returnValue; + } + if ("fallback" in returnValue) { + this.fallback = returnValue.fallback; + } + this.finalState = await takeSnapshot( this.storedTargets, excludeFields, diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts index c6d4e5375a..8be2e5bb06 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts @@ -1,6 +1,8 @@ import { CommandComplete, CommandLatest, + CommandResponse, + CommandServerApi, DecoratedSymbolMark, DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, extractTargetedMarks, @@ -26,14 +28,14 @@ import { access, readFile } from "fs/promises"; import { invariant } from "immutability-helper"; import { merge } from "lodash"; import * as path from "path"; -import { ide, injectIde } from "../singletons/ide.singleton"; -import { takeSnapshot } from "../testUtil/takeSnapshot"; -import { TestCase } from "./TestCase"; -import { StoredTargetMap } from "../core/StoredTargets"; import { CommandRunner } from "../CommandRunner"; -import { RecordTestCaseCommandOptions } from "./RecordTestCaseCommandOptions"; +import { StoredTargetMap } from "../core/StoredTargets"; import { SpokenFormGenerator } from "../generateSpokenForm"; +import { ide, injectIde } from "../singletons/ide.singleton"; import { defaultSpokenFormMap } from "../spokenForms/defaultSpokenFormMap"; +import { takeSnapshot } from "../testUtil/takeSnapshot"; +import { RecordTestCaseCommandOptions } from "./RecordTestCaseCommandOptions"; +import { TestCase } from "./TestCase"; const CALIBRATION_DISPLAY_DURATION_MS = 50; @@ -63,6 +65,7 @@ export class TestCaseRecorder { private spokenFormGenerator = new SpokenFormGenerator(defaultSpokenFormMap); constructor( + private commandServerApi: CommandServerApi | null, private hatTokenMap: HatTokenMap, private storedTargets: StoredTargetMap, ) { @@ -292,6 +295,7 @@ export class TestCaseRecorder { ? spokenForm.spokenForms[0] : command.spokenForm, }, + this.commandServerApi?.getFocusedElementType(), hatTokenMap, this.storedTargets, this.spyIde, @@ -321,7 +325,7 @@ export class TestCaseRecorder { } } - async postCommandHook(returnValue: any) { + async postCommandHook(returnValue: CommandResponse) { if (this.testCase == null) { // If test case is null then this means that this was just a follow up // command for a navigation map test diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml new file mode 100644 index 0000000000..ea198925ff --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml @@ -0,0 +1,30 @@ +languageId: plaintext +focusedElementType: other +command: + version: 7 + spokenForm: bring fine + action: + name: replaceWithTarget + source: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + destination: {type: implicit} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 3} +fallback: + action: insert + modifiers: [] + text: foo diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine2.yml new file mode 100644 index 0000000000..c958dbced7 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine2.yml @@ -0,0 +1,30 @@ +languageId: plaintext +focusedElementType: terminal +command: + version: 7 + spokenForm: bring fine + action: + name: replaceWithTarget + source: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + destination: {type: implicit} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fallback: + action: insert + modifiers: [] + text: foo diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine.yml new file mode 100644 index 0000000000..a985094caa --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine.yml @@ -0,0 +1,25 @@ +languageId: plaintext +focusedElementType: terminal +command: + version: 7 + spokenForm: chuck fine + action: + name: remove + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine2.yml new file mode 100644 index 0000000000..11d80818b5 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/chuckFine2.yml @@ -0,0 +1,25 @@ +languageId: plaintext +focusedElementType: other +command: + version: 7 + spokenForm: chuck fine + action: + name: remove + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml new file mode 100644 index 0000000000..7bf0f45f6b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml @@ -0,0 +1,30 @@ +languageId: plaintext +focusedElementType: other +command: + version: 7 + spokenForm: move fine + action: + name: moveToTarget + source: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + destination: {type: implicit} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fallback: + action: insert + modifiers: [] + text: foo diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml new file mode 100644 index 0000000000..3ede086e32 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml @@ -0,0 +1,26 @@ +languageId: plaintext +focusedElementType: other +command: + version: 7 + spokenForm: take this + action: + name: setSelection + target: + type: primitive + mark: {type: cursor} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fallback: + action: setSelection + modifiers: + - {type: containingTokenIfEmpty} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml new file mode 100644 index 0000000000..e42b5c19c3 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml @@ -0,0 +1,29 @@ +languageId: plaintext +focusedElementType: other +command: + version: 7 + spokenForm: take token + action: + name: setSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true +initialState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fallback: + action: setSelection + modifiers: + - type: containingScope + scopeType: {type: token} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml index 6d55e46b53..429d8b32cf 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml @@ -1,6 +1,6 @@ languageId: plaintext command: - version: 6 + version: 7 spokenForm: take harp action: name: setSelection diff --git a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts index 83bae63d40..22258d170c 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -1,8 +1,10 @@ import { asyncSafety, + CommandResponse, DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, ExcludableSnapshotField, extractTargetedMarks, + Fallback, getRecordedTestPaths, HatStability, marksToPlainObject, @@ -22,6 +24,7 @@ import { TestCaseFixtureLegacy, TextEditor, TokenHat, + clientSupportsFallback, } from "@cursorless/common"; import { getCursorlessApi, @@ -30,11 +33,11 @@ import { } from "@cursorless/vscode-common"; import { assert } from "chai"; import * as yaml from "js-yaml"; +import { isUndefined } from "lodash"; import { promises as fsp } from "node:fs"; import * as vscode from "vscode"; import { endToEndTestSetup, sleepWithBackoff } from "../endToEndTestSetup"; import { setupFake } from "./setupFake"; -import { isUndefined } from "lodash"; function createPosition(position: PositionPlainObject) { return new vscode.Position(position.line, position.character); @@ -74,7 +77,7 @@ async function runTest(file: string, spyIde: SpyIDE) { const usePrePhraseSnapshot = false; const cursorlessApi = await getCursorlessApi(); - const { hatTokenMap, takeSnapshot, setStoredTarget } = + const { hatTokenMap, takeSnapshot, setStoredTarget, commandServerApi } = cursorlessApi.testHelpers!; const editor = await openNewEditor(fixture.initialState.documentContents, { @@ -101,6 +104,12 @@ async function runTest(file: string, spyIde: SpyIDE) { // spyIde.clipboard.writeText(fixture.initialState.clipboard); } + commandServerApi.setFocusedElementType( + fixture.focusedElementType === "other" + ? undefined + : fixture.focusedElementType ?? "textEditor", + ); + // Ensure that the expected hats are present await hatTokenMap.allocateHats( getTokenHats(fixture.initialState.marks, spyIde.activeTextEditor!), @@ -112,12 +121,22 @@ async function runTest(file: string, spyIde: SpyIDE) { checkMarks(fixture.initialState.marks, readableHatMap); let returnValue: unknown; + let fallback: Fallback | undefined; try { returnValue = await runCursorlessCommand({ ...fixture.command, usePrePhraseSnapshot, }); + if (clientSupportsFallback(fixture.command)) { + const commandResponse = returnValue as CommandResponse; + returnValue = + "returnValue" in commandResponse + ? commandResponse.returnValue + : undefined; + fallback = + "fallback" in commandResponse ? commandResponse.fallback : undefined; + } } catch (err) { const error = err as Error; @@ -188,6 +207,7 @@ async function runTest(file: string, spyIde: SpyIDE) { ...fixture, finalState: resultState, returnValue, + fallback, ide: actualSpyIdeValues, thrownError: undefined, }; @@ -212,6 +232,12 @@ async function runTest(file: string, spyIde: SpyIDE) { "Unexpected return value", ); + assert.deepStrictEqual( + fallback, + fixture.fallback, + "Unexpected fallback value", + ); + assert.deepStrictEqual( omitByDeep(actualSpyIdeValues, isUndefined), fixture.ide, diff --git a/packages/cursorless-vscode/src/constructTestHelpers.ts b/packages/cursorless-vscode/src/constructTestHelpers.ts index 8543e642c3..676c55ddd4 100644 --- a/packages/cursorless-vscode/src/constructTestHelpers.ts +++ b/packages/cursorless-vscode/src/constructTestHelpers.ts @@ -1,7 +1,7 @@ import { - CommandServerApi, ExcludableSnapshotField, ExtraSnapshotField, + FakeCommandServerApi, HatTokenMap, IDE, NormalizedIDE, @@ -19,13 +19,13 @@ import { } from "@cursorless/cursorless-engine"; import { TestHelpers } from "@cursorless/vscode-common"; import * as vscode from "vscode"; +import { VscodeFileSystem } from "./ide/vscode/VscodeFileSystem"; import { VscodeIDE } from "./ide/vscode/VscodeIDE"; import { toVscodeEditor } from "./ide/vscode/toVscodeEditor"; import { vscodeApi } from "./vscodeApi"; -import { VscodeFileSystem } from "./ide/vscode/VscodeFileSystem"; export function constructTestHelpers( - commandServerApi: CommandServerApi | null, + commandServerApi: FakeCommandServerApi, storedTargets: StoredTargetMap, hatTokenMap: HatTokenMap, vscodeIDE: VscodeIDE, diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 9b51839c86..fcc7e2334d 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -1,7 +1,7 @@ import { Disposable, + FakeCommandServerApi, FakeIDE, - getFakeCommandServerApi, IDE, isTesting, NormalizedIDE, @@ -48,8 +48,8 @@ import { VisualizationType, } from "./ScopeVisualizerCommandApi"; import { StatusBarItem } from "./StatusBarItem"; -import { vscodeApi } from "./vscodeApi"; import { storedTargetHighlighter } from "./storedTargetHighlighter"; +import { vscodeApi } from "./vscodeApi"; /** * Extension entrypoint called by VSCode on Cursorless startup. @@ -75,10 +75,10 @@ export async function activate( vscodeIDE.runMode === "test", ); - const commandServerApi = - vscodeIDE.runMode === "test" - ? getFakeCommandServerApi() - : await getCommandServerApi(); + const fakeCommandServerApi = new FakeCommandServerApi(); + const commandServerApi = isTesting() + ? fakeCommandServerApi + : await getCommandServerApi(); const treeSitter: TreeSitter = createTreeSitter(parseTreeApi); @@ -104,7 +104,11 @@ export async function activate( new CommandHistory(normalizedIde, commandServerApi, fileSystem), ); - const testCaseRecorder = new TestCaseRecorder(hatTokenMap, storedTargets); + const testCaseRecorder = new TestCaseRecorder( + commandServerApi, + hatTokenMap, + storedTargets, + ); addCommandRunnerDecorator(testCaseRecorder); const statusBarItem = StatusBarItem.create("cursorless.showQuickPick"); @@ -145,7 +149,7 @@ export async function activate( return { testHelpers: isTesting() ? constructTestHelpers( - commandServerApi, + fakeCommandServerApi, storedTargets, hatTokenMap, vscodeIDE, diff --git a/packages/vscode-common/src/TestHelpers.ts b/packages/vscode-common/src/TestHelpers.ts index 4028ea5673..0b28d7e39e 100644 --- a/packages/vscode-common/src/TestHelpers.ts +++ b/packages/vscode-common/src/TestHelpers.ts @@ -1,7 +1,7 @@ import type { - CommandServerApi, ExcludableSnapshotField, ExtraSnapshotField, + FakeCommandServerApi, HatTokenMap, IDE, NormalizedIDE, @@ -22,7 +22,7 @@ export interface TestHelpers { hatTokenMap: HatTokenMap; - commandServerApi: CommandServerApi; + commandServerApi: FakeCommandServerApi; toVscodeEditor(editor: TextEditor): vscode.TextEditor; From aa768597e66990f4799e60324147e87a48c3adb3 Mon Sep 17 00:00:00 2001 From: Aaron Adams Date: Wed, 20 Mar 2024 01:26:38 +0800 Subject: [PATCH 3/7] Initial lua support (#1962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds support for the `lua` programming language ## Checklist - [x] Recorded tests for the new language - [x] Used `"change"` / `"clear"` instead of` "take"` for selection tests to make recorded tests easier to read - [x] Added a few specific tests that use `"chuck"` instead of `"change"` to test removal behaviour when it's interesting, especially: - [x] `"chuck arg"` with single argument in list - [x] `"chuck arg"` with multiple arguments in list - [x] `"chuck item"` with single argument in list - [x] `"chuck item"` with multiple arguments in list - [x] Added `@textFragment` captures. Usually you want to put these on comment and string nodes. This enables `"take round"` to work within comments and strings. - [x] Added a test for `"change round"` inside a string, eg `"hello (there)"` - [-] Supported` "type"` both for type annotations (eg `foo: string`) and declarations (eg `interface Foo {}`) (and added tests for this behaviour 😊) - [x] Supported` "item"` both for map pairs and list entries (with tests of course) ## Scope Support | Supported | Tested | Term | Capture | Definition | Comment | | - | - | - | - | - | - | | ✓ | ✓ | `list` | `@list` | List type equivalent | - | | ✓ | ✓ | `inside list` | `@list.interior` | Inside of a list | - | | ✓ | ✓ | `map` | `@map` | Dictionary type equivalent | - | | ✓ | ✓ | `inside map` | `@map.interior` | Inside of a dictionary | - | | ✓ | ✓ | `key` | `@collectionKey` | Dictionary key equivalent | - | | ✓ | ✓ | `funk` | `@namedFunction` | A named function declaration | - | | ✓ | ✓ | `inside funk` | `@namedFunction.interior` | The inside of a lambda declaration | - | | ✓ | ✓ | `funk name` | `@functionName` | Name of declared function | - | | ✓ | ✓ | `lambda` | `@anonymousFunction` | A lambda declaration | - | | ✓ | ✓ | `inside lambda` | `@anonymousFunction.interior` | The inside of a lambda declaration | - | | ✓ | ✓ | `name` | `@name` | Variable name | - | | ✓ | ✓ | `value` | `@value` | Right-hand-side value of an assignment | - | | ✓ | ✓ | `value` | `@value` | Value returned from a function | - | | ✓ | ✓ | `value` | `@value` | Value of a key-value pair | - | | ✓ | ✓ | `state` | `@statement` | Any single coded statement | - | | ✓ | ✓ | `if state` | `@ifStatement` | An if conditional block | - | | ✓ | ✓ | `condition` | `@condition` | Condition of an if block | - | | ✓ | ✓ | `condition` | `@condition` | Condition of a while loop | - | | ✓ | ✓ | `condition` | `@condition` | Condition of a do while style loop | - | | - | - | `condition` | `@condition` | Condition of a for loop | - | | ✓ | ✓ | `condition` | `@condition` | Condition of a ternary expression | - | | ✓ | ✓ | `branch` | `@branch` | The resulting code associated with a conditional expression | - | | ✓ | ✓ | `comment` | `@comment` | Code comment | - | | ✓ | ✓ | `string` | `@string` | Single line strings | - | | ✗ | ✗ | `string` | `@string` | Multi-line strings | https://github.com/cursorless-dev/cursorless/pull/1962#issuecomment-1783674916 | | ✓ | ✓ | - | `@textFragment` | Used to capture string-type nodes (strings and comments) | - | | ✓ | ✓ | `call` | `@functionCall` | A function call (not a function definition) | - | | ✓ | ✓ | `callee` | `@functionCallee` | Name of the function being called | - | | ✓ | ✓ | `arg` | `@argumentOrParameter` | Arguments to functions and calls | - | | _ | _ | `class` | `@class` | Class or structure declaration | - | | _ | _ | `inside class` | `@class.interior` | The inside of a class declaration | - | | _ | _ | `class name` | `@className` | Name of class or structure declaration | - | | _ | _ | `type` | `@type` | Type declarations | - | --------- Co-authored-by: fidgetingbits Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- data/playground/lua/lua.lua | 131 ++++++++ .../getLanguageScopeSupport.ts | 3 + packages/common/src/scopeSupportFacets/lua.ts | 21 ++ .../languages/lua/bringArgAirAfterBat.yml | 50 +++ .../recorded/languages/lua/changeArg.yml | 29 ++ .../recorded/languages/lua/changeArg2.yml | 25 ++ .../recorded/languages/lua/changeCall.yml | 25 ++ .../recorded/languages/lua/changeComment.yml | 25 ++ .../recorded/languages/lua/changeComment2.yml | 27 ++ .../languages/lua/changeCondition.yml | 23 ++ .../languages/lua/changeCondition2.yml | 33 ++ .../languages/lua/changeCondition3.yml | 33 ++ .../recorded/languages/lua/changeFunkName.yml | 29 ++ .../recorded/languages/lua/changeIfState.yml | 30 ++ .../languages/lua/changeInsideLambda.yml | 37 +++ .../languages/lua/changeInsideList.yml | 26 ++ .../recorded/languages/lua/changeItem.yml | 23 ++ .../recorded/languages/lua/changeKey.yml | 41 +++ .../recorded/languages/lua/changeLambda.yml | 34 ++ .../recorded/languages/lua/changeList.yml | 41 +++ .../recorded/languages/lua/changeMap.yml | 19 ++ .../recorded/languages/lua/changeName.yml | 23 ++ .../recorded/languages/lua/changeName2.yml | 23 ++ .../recorded/languages/lua/changeRound.yml | 25 ++ .../recorded/languages/lua/changeState.yml | 25 ++ .../recorded/languages/lua/changeState2.yml | 37 +++ .../recorded/languages/lua/changeState3.yml | 34 ++ .../recorded/languages/lua/changeState4.yml | 37 +++ .../recorded/languages/lua/changeString.yml | 25 ++ .../recorded/languages/lua/changeValue.yml | 23 ++ .../recorded/languages/lua/changeValue2.yml | 38 +++ .../recorded/languages/lua/changeValue3.yml | 41 +++ .../recorded/languages/lua/changeValue4.yml | 34 ++ .../recorded/languages/lua/changeValue5.yml | 23 ++ .../recorded/languages/lua/chuckArg.yml | 37 +++ .../recorded/languages/lua/chuckArg2.yml | 37 +++ .../recorded/languages/lua/chuckItem.yml | 23 ++ .../recorded/languages/lua/chuckItem2.yml | 37 +++ .../recorded/languages/lua/chuckItem3.yml | 23 ++ .../recorded/languages/lua/chuckKey.yml | 29 ++ .../recorded/languages/lua/chuckName.yml | 25 ++ .../recorded/languages/lua/chuckName2.yml | 25 ++ .../recorded/languages/lua/chuckName3.yml | 23 ++ .../recorded/languages/lua/chuckName4.yml | 23 ++ .../recorded/languages/lua/chuckValue.yml | 23 ++ .../suite/fixtures/scopes/lua/branch.if.scope | 52 +++ .../fixtures/scopes/lua/functionCallee.scope | 13 + .../src/suite/fixtures/scopes/lua/map.scope | 24 ++ .../fixtures/scopes/lua/name.assignment.scope | 20 ++ .../fixtures/scopes/lua/name.variable.scope | 24 ++ .../fixtures/scopes/lua/namedFunction.scope | 32 ++ .../scopes/lua/value.assignment.scope | 20 ++ .../fixtures/scopes/lua/value.variable.scope | 20 ++ queries/lua.scm | 298 ++++++++++++++++++ 54 files changed, 1901 insertions(+) create mode 100644 data/playground/lua/lua.lua create mode 100644 packages/common/src/scopeSupportFacets/lua.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/bringArgAirAfterBat.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCall.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition3.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeFunkName.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeIfState.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideLambda.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideList.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeKey.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeLambda.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeList.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeMap.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeRound.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState3.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState4.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeString.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue3.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue4.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue5.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem3.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckKey.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName3.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName4.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckValue.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/branch.if.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/functionCallee.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/map.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.assignment.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.variable.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/namedFunction.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.assignment.scope create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.variable.scope create mode 100644 queries/lua.scm diff --git a/data/playground/lua/lua.lua b/data/playground/lua/lua.lua new file mode 100644 index 0000000000..7673cb7220 --- /dev/null +++ b/data/playground/lua/lua.lua @@ -0,0 +1,131 @@ +-- This is a single-line comment + +--[[ + This is a multi-line comment. + It spans multiple lines. +--]] + +-- Variables +local a = 42 +local b, c = "Hello", "World" + +-- Data Types +local number = 3.14 +local boolean = true +local string = "Lua is awesome!" +local table = { 1, 2, 3 } +local nilValue = nil + +-- Conditional Constructs +local x = 10 +local y = 20 + +-- if-then-else +if x < y then + print("x is less than y") +elseif x > y then + print("x is greater than y") +else + print("x is equal to y") +end + +-- ternary conditional (short if-then-else) +local max = x > y and x or y +print("The maximum value is: " .. max) + +-- Functions +function add(x, b) + return x + y +end + +local sum = add(5, 7) +print("Sum:", sum) + +-- Tables +local person = { + name = "John", + age = 30, + hobbies = { "reading", "gaming", "programming" }, + address = { + street = "123 Main St", + city = "Example City", + }, +} + +-- String manipulation +local concatString = "Hello " .. "World" + +-- Metatables and metatable operations +local mt = { + __add = function(a, b) + return a + b + end, + __sub = function(a, b) + return a - b + end, +} + +setmetatable(a, mt) + +-- Closures +function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end +end + +local counter = makeCounter() + +-- Coroutines +local co = coroutine.create(function() + for i = 1, 3 do + print("Coroutine", i) + coroutine.yield() + end +end) + +-- Error handling +local success, result = pcall(function() + error("This is an error") +end) + +if not success then + print("Error:", result) +end + +-- Loop Constructs +-- while loop +local i = 1 +i = 2 +while i <= 5 do + print("While loop iteration: " .. i) + i = i + 1 +end + +-- repeat-until loop +i = 1 +repeat + print("Repeat-Until loop iteration: " .. i) + i = i + 1 +until i > 5 + +-- for loop +for j = 1, 5 do + print("For loop iteration: " .. j) +end + +-- numeric for loop with step +for k = 10, 1, -1 do + print("Numeric for loop with step: " .. k) +end + +-- for-in loop (iterating over a table) +local fruits = { "apple", "banana", "cherry" } +for key, value in pairs(fruits) do + print("For-In loop: " .. key .. " = " .. value) +end + +-- ternary +local max = x > y and x or y diff --git a/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts b/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts index 76f57d6216..618a9b3dc4 100644 --- a/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts +++ b/packages/common/src/scopeSupportFacets/getLanguageScopeSupport.ts @@ -3,6 +3,7 @@ import { javaScopeSupport } from "./java"; import { javascriptScopeSupport } from "./javascript"; import { jsonScopeSupport } from "./json"; import { pythonScopeSupport } from "./python"; +import { luaScopeSupport } from "./lua"; import { LanguageScopeSupportFacetMap } from "./scopeSupportFacets.types"; import { talonScopeSupport } from "./talon"; import { typescriptScopeSupport } from "./typescript"; @@ -25,6 +26,8 @@ export function getLanguageScopeSupport( return talonScopeSupport; case "typescript": return typescriptScopeSupport; + case "lua": + return luaScopeSupport; } throw Error(`Unsupported language: '${languageId}'`); } diff --git a/packages/common/src/scopeSupportFacets/lua.ts b/packages/common/src/scopeSupportFacets/lua.ts new file mode 100644 index 0000000000..17e2bd8d12 --- /dev/null +++ b/packages/common/src/scopeSupportFacets/lua.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { + LanguageScopeSupportFacetMap, + ScopeSupportFacetLevel, +} from "./scopeSupportFacets.types"; + +const { supported, notApplicable } = ScopeSupportFacetLevel; + +export const luaScopeSupport: LanguageScopeSupportFacetMap = { + "key.attribute": notApplicable, + tags: notApplicable, + "name.assignment": supported, + "name.variable": supported, + "value.assignment": supported, + "value.variable": supported, + functionCallee: supported, + map: supported, + "branch.if": supported, + namedFunction: supported, +}; diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/bringArgAirAfterBat.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/bringArgAirAfterBat.yml new file mode 100644 index 0000000000..d55446ad05 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/bringArgAirAfterBat.yml @@ -0,0 +1,50 @@ +languageId: lua +command: + version: 6 + spokenForm: bring arg air after bat + action: + name: replaceWithTarget + source: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + destination: + type: primitive + insertionMode: after + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: b} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter(a, b) + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 0, character: 21} + active: {line: 0, character: 21} + marks: + default.a: + start: {line: 0, character: 21} + end: {line: 0, character: 22} + default.b: + start: {line: 0, character: 24} + end: {line: 0, character: 25} +finalState: + documentContents: |- + function makeCounter(a, b, a) + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 0, character: 21} + active: {line: 0, character: 21} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg.yml new file mode 100644 index 0000000000..af707cdb31 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg.yml @@ -0,0 +1,29 @@ +languageId: lua +command: + version: 6 + spokenForm: change arg + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function add(x, b) + return x + y + end + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} + marks: {} +finalState: + documentContents: |- + function add(, b) + return x + y + end + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg2.yml new file mode 100644 index 0000000000..ddf39a380b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeArg2.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: change arg + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: | + local sum = add(5, 7) + selections: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 16} + marks: {} +finalState: + documentContents: | + local sum = add(, 7) + selections: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 16} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCall.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCall.yml new file mode 100644 index 0000000000..a8520aa736 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCall.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: change call + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: functionCall} + usePrePhraseSnapshot: true +initialState: + documentContents: | + print("a is greater than 10") + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment.yml new file mode 100644 index 0000000000..6b3eedecff --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: comment} + usePrePhraseSnapshot: true +initialState: + documentContents: | + -- This is a single-line comment + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment2.yml new file mode 100644 index 0000000000..a4b799ca7a --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeComment2.yml @@ -0,0 +1,27 @@ +languageId: lua +command: + version: 6 + spokenForm: change comment + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: comment} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + --[[ + This is a multi-line comment. + It spans multiple lines. + --]] + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition.yml new file mode 100644 index 0000000000..61d0b7a0f2 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: change condition + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: local max = x > y and x or y + selections: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 12} + marks: {} +finalState: + documentContents: local max = and x or y + selections: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 12} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition2.yml new file mode 100644 index 0000000000..fea6b7a7d2 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition2.yml @@ -0,0 +1,33 @@ +languageId: lua +command: + version: 6 + spokenForm: change condition + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local i = 1 + while i <= 5 do + print("While loop iteration: " .. i) + i = i + 1 + end + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} + marks: {} +finalState: + documentContents: |- + local i = 1 + while do + print("While loop iteration: " .. i) + i = i + 1 + end + selections: + - anchor: {line: 1, character: 6} + active: {line: 1, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition3.yml new file mode 100644 index 0000000000..5211c20dba --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeCondition3.yml @@ -0,0 +1,33 @@ +languageId: lua +command: + version: 6 + spokenForm: change condition + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: | + i = 1 + repeat + print("Repeat-Until loop iteration: " .. i) + i = i + 1 + until i > 5 + selections: + - anchor: {line: 4, character: 6} + active: {line: 4, character: 6} + marks: {} +finalState: + documentContents: | + i = 1 + repeat + print("Repeat-Until loop iteration: " .. i) + i = i + 1 + until + selections: + - anchor: {line: 4, character: 6} + active: {line: 4, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeFunkName.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeFunkName.yml new file mode 100644 index 0000000000..3682b8b651 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeFunkName.yml @@ -0,0 +1,29 @@ +languageId: lua +command: + version: 6 + spokenForm: change funk name + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: functionName} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function add(x, b) + return x + y + end + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} + marks: {} +finalState: + documentContents: |- + function (x, b) + return x + y + end + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeIfState.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeIfState.yml new file mode 100644 index 0000000000..e1d1d4b8c2 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeIfState.yml @@ -0,0 +1,30 @@ +languageId: lua +command: + version: 6 + spokenForm: change if state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: ifStatement} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + if a > 10 then + print("a is greater than 10") + elseif a < 10 then + print("a is less than 10") + else + print("a is equal to 10") + end + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideLambda.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideLambda.yml new file mode 100644 index 0000000000..86c83ab5af --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideLambda.yml @@ -0,0 +1,37 @@ +languageId: lua +command: + version: 6 + spokenForm: change inside lambda + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: interiorOnly} + - type: containingScope + scopeType: {type: anonymousFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 3, character: 19} + active: {line: 3, character: 19} + marks: {} +finalState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + + end + end + selections: + - anchor: {line: 3, character: 8} + active: {line: 3, character: 8} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideList.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideList.yml new file mode 100644 index 0000000000..06802e045d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeInsideList.yml @@ -0,0 +1,26 @@ +languageId: lua +command: + version: 6 + spokenForm: change inside list + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: interiorOnly} + - type: containingScope + scopeType: {type: list} + usePrePhraseSnapshot: true +initialState: + documentContents: | + foo = {"a", "b", "c"}, + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: | + foo = {}, + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeItem.yml new file mode 100644 index 0000000000..849765d0c9 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeItem.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: change item + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: local table = {1, 2, 3} + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + marks: {} +finalState: + documentContents: local table = {1, , 3} + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeKey.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeKey.yml new file mode 100644 index 0000000000..ad80c87ae4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeKey.yml @@ -0,0 +1,41 @@ +languageId: lua +command: + version: 6 + spokenForm: change key + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionKey} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 2, character: 6} + active: {line: 2, character: 6} + marks: {} +finalState: + documentContents: |- + local person = { + name = "John", + = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeLambda.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeLambda.yml new file mode 100644 index 0000000000..f86b34630e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeLambda.yml @@ -0,0 +1,34 @@ +languageId: lua +command: + version: 6 + spokenForm: change lambda + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: anonymousFunction} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 3, character: 12} + active: {line: 3, character: 12} + marks: {} +finalState: + documentContents: |- + function makeCounter() + local count = 0 + return + end + selections: + - anchor: {line: 2, character: 11} + active: {line: 2, character: 11} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeList.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeList.yml new file mode 100644 index 0000000000..277c830a92 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeList.yml @@ -0,0 +1,41 @@ +languageId: lua +command: + version: 6 + spokenForm: change list + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: list} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 3, character: 17} + active: {line: 3, character: 17} + marks: {} +finalState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = , + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 3, character: 14} + active: {line: 3, character: 14} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeMap.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeMap.yml new file mode 100644 index 0000000000..d453f1ff21 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeMap.yml @@ -0,0 +1,19 @@ +languageId: lua +command: + version: 6 + spokenForm: change map + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: map} + usePrePhraseSnapshot: true +initialState: + documentContents: local table = {1, 2, 3} + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + marks: {} +thrownError: {name: NoContainingScopeError} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName.yml new file mode 100644 index 0000000000..1906e84703 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: change name + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: name} + usePrePhraseSnapshot: true +initialState: + documentContents: local a = 42 + selections: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 12} + marks: {} +finalState: + documentContents: local = 42 + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName2.yml new file mode 100644 index 0000000000..59a7975d92 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeName2.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: change name + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: name} + usePrePhraseSnapshot: true +initialState: + documentContents: local i = 1 + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: local = 1 + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeRound.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeRound.yml new file mode 100644 index 0000000000..8af4bd4791 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeRound.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: change round + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: surroundingPair, delimiter: parentheses} + usePrePhraseSnapshot: true +initialState: + documentContents: | + local string = "Lua is (awesome)!" + selections: + - anchor: {line: 0, character: 30} + active: {line: 0, character: 30} + marks: {} +finalState: + documentContents: | + local string = "Lua is !" + selections: + - anchor: {line: 0, character: 23} + active: {line: 0, character: 23} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState.yml new file mode 100644 index 0000000000..d5a25add4d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: change state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: true +initialState: + documentContents: | + local sum = add(5, 7) + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: |+ + + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState2.yml new file mode 100644 index 0000000000..3e0de207f5 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState2.yml @@ -0,0 +1,37 @@ +languageId: lua +command: + version: 6 + spokenForm: change state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 1, character: 12} + active: {line: 1, character: 12} + marks: {} +finalState: + documentContents: |- + function makeCounter() + + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState3.yml new file mode 100644 index 0000000000..3ec98a9cdc --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState3.yml @@ -0,0 +1,34 @@ +languageId: lua +command: + version: 6 + spokenForm: change state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + marks: {} +finalState: + documentContents: |- + function makeCounter() + local count = 0 + + end + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState4.yml new file mode 100644 index 0000000000..9ade8e188a --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeState4.yml @@ -0,0 +1,37 @@ +languageId: lua +command: + version: 6 + spokenForm: change state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 3, character: 11} + active: {line: 3, character: 11} + marks: {} +finalState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + + return count + end + end + selections: + - anchor: {line: 3, character: 8} + active: {line: 3, character: 8} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeString.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeString.yml new file mode 100644 index 0000000000..ead7c7d2a4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeString.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: change string + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: surroundingPair, delimiter: string} + usePrePhraseSnapshot: true +initialState: + documentContents: | + local string = "Lua is awesome!" + selections: + - anchor: {line: 0, character: 19} + active: {line: 0, character: 19} + marks: {} +finalState: + documentContents: | + local string = + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue.yml new file mode 100644 index 0000000000..8e26cf7f6c --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: change value + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: local a = 42 + selections: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 12} + marks: {} +finalState: + documentContents: "local a = " + selections: + - anchor: {line: 0, character: 10} + active: {line: 0, character: 10} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue2.yml new file mode 100644 index 0000000000..062f8d5390 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue2.yml @@ -0,0 +1,38 @@ +languageId: lua +command: + version: 6 + spokenForm: change value + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 4, character: 10} + active: {line: 4, character: 10} + marks: {} +finalState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = + } + selections: + - anchor: {line: 4, character: 14} + active: {line: 4, character: 14} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue3.yml new file mode 100644 index 0000000000..d2ebe53dd0 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue3.yml @@ -0,0 +1,41 @@ +languageId: lua +command: + version: 6 + spokenForm: change value + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 5, character: 11} + active: {line: 5, character: 11} + marks: {} +finalState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = , + city = "Example City" + } + } + selections: + - anchor: {line: 5, character: 17} + active: {line: 5, character: 17} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue4.yml new file mode 100644 index 0000000000..aede3b01d9 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue4.yml @@ -0,0 +1,34 @@ +languageId: lua +command: + version: 6 + spokenForm: change value + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + marks: {} +finalState: + documentContents: |- + function makeCounter() + local count = 0 + return + end + selections: + - anchor: {line: 2, character: 11} + active: {line: 2, character: 11} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue5.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue5.yml new file mode 100644 index 0000000000..dcbe2ce46e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/changeValue5.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: change value + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: local i = 1 + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: "local i = " + selections: + - anchor: {line: 0, character: 10} + active: {line: 0, character: 10} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg.yml new file mode 100644 index 0000000000..78f9721c88 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg.yml @@ -0,0 +1,37 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck arg + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter(a, b) + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 0, character: 22} + active: {line: 0, character: 22} + marks: {} +finalState: + documentContents: |- + function makeCounter(b) + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 0, character: 21} + active: {line: 0, character: 21} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg2.yml new file mode 100644 index 0000000000..27f9f6542b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckArg2.yml @@ -0,0 +1,37 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck arg + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function makeCounter(a) + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 0, character: 22} + active: {line: 0, character: 22} + marks: {} +finalState: + documentContents: |- + function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end + end + selections: + - anchor: {line: 0, character: 21} + active: {line: 0, character: 21} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem.yml new file mode 100644 index 0000000000..2b37895abf --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: local table = {1, 2, 3} + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + marks: {} +finalState: + documentContents: local table = {1, 3} + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem2.yml new file mode 100644 index 0000000000..773ad85061 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem2.yml @@ -0,0 +1,37 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"}, + address = { + street = "123 Main St", + city = "Example City" + } + } + selections: + - anchor: {line: 4, character: 6} + active: {line: 4, character: 6} + marks: {} +finalState: + documentContents: |- + local person = { + name = "John", + age = 30, + hobbies = {"reading", "gaming", "programming"} + } + selections: + - anchor: {line: 3, character: 50} + active: {line: 3, character: 50} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem3.yml new file mode 100644 index 0000000000..9b415c190b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckItem3.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: hobbies = {"reading"} + selections: + - anchor: {line: 0, character: 19} + active: {line: 0, character: 19} + marks: {} +finalState: + documentContents: hobbies = {} + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckKey.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckKey.yml new file mode 100644 index 0000000000..d9c7b87b81 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckKey.yml @@ -0,0 +1,29 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck key + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionKey} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + local person = { + name = "John", + } + selections: + - anchor: {line: 1, character: 8} + active: {line: 1, character: 8} + marks: {} +finalState: + documentContents: |- + local person = { + "John", + } + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName.yml new file mode 100644 index 0000000000..5c17921b12 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck name + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: name} + usePrePhraseSnapshot: true +initialState: + documentContents: | + local a = 42 + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: | + 42 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName2.yml new file mode 100644 index 0000000000..7d796ad01e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName2.yml @@ -0,0 +1,25 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck name + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: name} + usePrePhraseSnapshot: true +initialState: + documentContents: | + a = 42 + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: | + 42 + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName3.yml new file mode 100644 index 0000000000..bd70cdc83d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName3.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck name + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: name} + usePrePhraseSnapshot: true +initialState: + documentContents: local a,b = "Hello", "World" + selections: + - anchor: {line: 0, character: 28} + active: {line: 0, character: 28} + marks: {} +finalState: + documentContents: "\"Hello\", \"World\"" + selections: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 16} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName4.yml new file mode 100644 index 0000000000..9bd9b6ba0e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckName4.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck name + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: name} + usePrePhraseSnapshot: true +initialState: + documentContents: a,b = "Hello", "World" + selections: + - anchor: {line: 0, character: 22} + active: {line: 0, character: 22} + marks: {} +finalState: + documentContents: "\"Hello\", \"World\"" + selections: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 16} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckValue.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckValue.yml new file mode 100644 index 0000000000..25c9d49f2a --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/lua/chuckValue.yml @@ -0,0 +1,23 @@ +languageId: lua +command: + version: 6 + spokenForm: chuck value + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: value} + usePrePhraseSnapshot: true +initialState: + documentContents: local table = {1, 2, 3} + selections: + - anchor: {line: 0, character: 23} + active: {line: 0, character: 23} + marks: {} +finalState: + documentContents: local table + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/branch.if.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/branch.if.scope new file mode 100644 index 0000000000..20e001180e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/branch.if.scope @@ -0,0 +1,52 @@ +if x < y then + print("x is less than y") +elseif x > y then + print("x is greater than y") +else + print("x is equal to y") +end +--- + +[#1 Content] = +[#1 Removal] = +[#1 Domain] = 0:0-1:29 + >------------- +0| if x < y then +1| print("x is less than y") + -----------------------------< + +[#1 Interior] = 1:4-1:29 + >-------------------------< +1| print("x is less than y") + +[#1 Insertion delimiter] = "\n" + + +[#2 Content] = +[#2 Removal] = +[#2 Domain] = 2:0-3:32 + >----------------- +2| elseif x > y then +3| print("x is greater than y") + --------------------------------< + +[#2 Interior] = 3:4-3:32 + >----------------------------< +3| print("x is greater than y") + +[#2 Insertion delimiter] = "\n" + + +[#3 Content] = +[#3 Removal] = +[#3 Domain] = 4:0-5:28 + >---- +4| else +5| print("x is equal to y") + ----------------------------< + +[#3 Interior] = 5:4-5:28 + >------------------------< +5| print("x is equal to y") + +[#3 Insertion delimiter] = "\n" diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/functionCallee.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/functionCallee.scope new file mode 100644 index 0000000000..d922c928de --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/functionCallee.scope @@ -0,0 +1,13 @@ +print("a is greater than 10") +--- + +[Content] = +[Removal] = 0:0-0:5 + >-----< +0| print("a is greater than 10") + +[Domain] = 0:0-0:29 + >-----------------------------< +0| print("a is greater than 10") + +[Insertion delimiter] = " " diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/map.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/map.scope new file mode 100644 index 0000000000..cbb86ec2a4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/map.scope @@ -0,0 +1,24 @@ +foo = { bar = "a", baz = "b" } +--- + +[Content] = +[Domain] = 0:6-0:30 + >------------------------< +0| foo = { bar = "a", baz = "b" } + +[Removal] = 0:5-0:30 + >-------------------------< +0| foo = { bar = "a", baz = "b" } + +[Leading delimiter] = 0:5-0:6 + >-< +0| foo = { bar = "a", baz = "b" } + +[Interior: Content] = 0:8-0:28 + >--------------------< +0| foo = { bar = "a", baz = "b" } +[Interior: Removal] = 0:7-0:29 + >----------------------< +0| foo = { bar = "a", baz = "b" } + +[Insertion delimiter] = " " diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.assignment.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.assignment.scope new file mode 100644 index 0000000000..b74dbc1331 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.assignment.scope @@ -0,0 +1,20 @@ +a, b = 1, 2 +--- + +[Content] = 0:0-0:4 + >----< +0| a, b = 1, 2 + +[Removal] = 0:0-0:7 + >-------< +0| a, b = 1, 2 + +[Trailing delimiter] = 0:4-0:7 + >---< +0| a, b = 1, 2 + +[Domain] = 0:0-0:11 + >-----------< +0| a, b = 1, 2 + +[Insertion delimiter] = " " diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.variable.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.variable.scope new file mode 100644 index 0000000000..0636e4e0dc --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/name.variable.scope @@ -0,0 +1,24 @@ +local a, b = 1, 2 +--- + +[Content] = 0:6-0:10 + >----< +0| local a, b = 1, 2 + +[Removal] = 0:0-0:13 + >-------------< +0| local a, b = 1, 2 + +[Leading delimiter] = 0:5-0:6 + >-< +0| local a, b = 1, 2 + +[Trailing delimiter] = 0:10-0:11 + >-< +0| local a, b = 1, 2 + +[Domain] = 0:0-0:17 + >-----------------< +0| local a, b = 1, 2 + +[Insertion delimiter] = " " diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/namedFunction.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/namedFunction.scope new file mode 100644 index 0000000000..a869646a86 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/namedFunction.scope @@ -0,0 +1,32 @@ +function makeCounter() + local count = 0 + return function() + count = count + 1 + return count + end +end +--- + +[Content] = +[Removal] = +[Domain] = 0:0-6:3 + >---------------------- +0| function makeCounter() +1| local count = 0 +2| return function() +3| count = count + 1 +4| return count +5| end +6| end + ---< + +[Interior] = 1:4-5:7 + >--------------- +1| local count = 0 +2| return function() +3| count = count + 1 +4| return count +5| end + -------< + +[Insertion delimiter] = "\n\n" diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.assignment.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.assignment.scope new file mode 100644 index 0000000000..6c167887b4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.assignment.scope @@ -0,0 +1,20 @@ +a, b = 1, 2 +--- + +[Content] = 0:7-0:11 + >----< +0| a, b = 1, 2 + +[Removal] = 0:4-0:11 + >-------< +0| a, b = 1, 2 + +[Leading delimiter] = 0:4-0:7 + >---< +0| a, b = 1, 2 + +[Domain] = 0:0-0:11 + >-----------< +0| a, b = 1, 2 + +[Insertion delimiter] = " " diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.variable.scope b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.variable.scope new file mode 100644 index 0000000000..925e70be86 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/scopes/lua/value.variable.scope @@ -0,0 +1,20 @@ +local a, b = 1, 2 +--- + +[Content] = 0:13-0:17 + >----< +0| local a, b = 1, 2 + +[Removal] = 0:10-0:17 + >-------< +0| local a, b = 1, 2 + +[Leading delimiter] = 0:10-0:13 + >---< +0| local a, b = 1, 2 + +[Domain] = 0:0-0:17 + >-----------------< +0| local a, b = 1, 2 + +[Insertion delimiter] = " " diff --git a/queries/lua.scm b/queries/lua.scm new file mode 100644 index 0000000000..82d413e820 --- /dev/null +++ b/queries/lua.scm @@ -0,0 +1,298 @@ +;; Statements +[ + (variable_declaration) + (break_statement) + (do_statement) + (empty_statement) + (for_statement) + (goto_statement) + (if_statement) + (label_statement) + (repeat_statement) + (return_statement) + (while_statement) +] @statement + +;; Only treat function declarions and calls as statements if they +;; aren't part of assignments, etc +( + [ + (function_declaration) + (function_call) + ] @statement + (#not-parent-type? @statement expression_list) +) +[ + (block) + (chunk) +] @statement.iteration @namedFunction.iteration @functionCall.iteration + +;; Duplicate above due to 3 label node limit +[ + (block) + (chunk) +] @ifStatement.iteration @value.iteration @name.iteration + +;; Capture assignment only if without variable prefix +;;!! count = count + 1 +;;! ^^^^^^^^^^^^^^^^^ +( + (assignment_statement) @statement + (#not-parent-type? @statement variable_declaration) +) + +;; Conditionals +;;!! if x < y then +;;! ---^^^^^----- +;;! ---xxxxxx---- +;;!! end +;;! --- +(if_statement + _ @condition.domain.start.startOf + condition: (_) @condition + consequence: (_) + !alternative + "end" @condition.domain.end.endOf +) + +;;!! if x < y then +;;! ---^^^^^----- +;;! ---xxxxxx---- +;;!! elseif x < y then +(if_statement + _ @_.domain.start.startOf + condition: (_) @condition + consequence: (_) @_.domain.end.endOf + alternative: (_) +) + +;;!! elseif x < y then +;;! -------^^^^^----- +;;! -------xxxxxx---- +(elseif_statement + condition: (_) @condition +) @_.domain + +;;!! +(if_statement + "if" @branch.start + consequence: (_) @branch.end @branch.interior +) @ifStatement @branch.iteration @condition.iteration + +;;!! if x < y then +;;!! print("x smaller") +;;!! else +;;! ^^^^ +;;!! print("x bigger") +;;! ^^^^^^^^^^^^^^^^^ +;;!! end +[ + (elseif_statement + consequence: (_) @branch.interior + ) + (else_statement + body: (_) @branch.interior + ) +] @branch @_.domain + +;;!! while i <= 5 do +;;! ^^^^^^ +;;! xxxxxx +(while_statement + condition: (_) @condition +) @_.domain + +;;!! repeat +;;!! ... +;;!! until i > 5 +;;! ^^^^^ +;;! xxxxx +(repeat_statement + condition: (_) @condition +) @_.domain + +;; Lists and maps +(table_constructor + "{" @_.interior.start.endOf @value.iteration.start.endOf @collectionKey.iteration.start.endOf + (field + name: (_) + ) + "}" @_.interior.end.startOf @value.iteration.end.startOf @collectionKey.iteration.end.startOf +) @map +;;!! a = { foo = "bar" } +;;! ^^^-------- +;;! xxxxxx----- +;;!! a = { foo = "bar" } +;;! ------^^^^^ +;;! ---xxxxxxxx +(field + name: (_) @collectionKey @value.leading.endOf + value: (_) @value @collectionKey.trailing.startOf +) @_.domain + +;; In lua everything is a map, but a map that omits keys for entries +;; is similar enough to a list to warrant having that scope. +;;!! a = { "1", "2", "3" } +;;! ^^^^^^^^^^^^^^^^^ +(table_constructor + "{" @_.interior.start.endOf + (field + !name + ) + "}" @_.interior.end.startOf +) @list + +;; Strings + +(comment) @comment @textFragment +(string) @string +(string_content) @textFragment + +;; Functions + +;; callee: +;;!! local sum = add(5, 7) +;;! ^^^------ +;; call: +;;!! local sum = add(5, 7) +;;! ^^^^^^^^^ +(function_call + name: (_) @functionCallee +) @_.domain @functionCall + +;;!!local sum = add(5, 7) +;;! ^--- +;;! xxx- +( + (arguments + (_)? @_.leading.endOf + . + (_) @argumentOrParameter + . + (_)? @_.trailing.startOf + ) @_dummy + (#single-or-multi-line-delimiter! @argumentOrParameter @_dummy ", " ",\n") +) + +;;!!local sum = add(5, 7) +;;! **** +(arguments + "(" @argumentOrParameter.iteration.start.endOf + ")" @argumentOrParameter.iteration.end.startOf +) + +;;!!function add(5, 7) +;;! ^--- +;;! xxx- +( + (parameters + (_)? @_.leading.endOf + . + (_) @argumentOrParameter + . + (_)? @_.trailing.startOf + ) @_dummy + (#single-or-multi-line-delimiter! @argumentOrParameter @_dummy ", " ",\n") +) + +;;!!function add(5, 7) +;;! **** +(parameters + "(" @argumentOrParameter.iteration.start.endOf + ")" @argumentOrParameter.iteration.end.startOf +) + +;; funk name: +;;!! function add(x, b) return x + y end +;;! ---------^^^----------------------- +;; inside funk: +;;!! function add(x, b) return x + y end +;;! -------------------^^^^^^^^^^^^---- +;;! funk: +;;!! function add(x, b) return x + y end +;;! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +(function_declaration + name: (_) @functionName + body: (_)? @namedFunction.interior +) @functionName.domain @namedFunction + +;; inside lambda: +;;!! __add = function(a, b) return a + b end +;;! ---------------^^^^^^^^^^^^---- +;; lambda: +;;!! __add = function(a, b) return a + b end +;;! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +(function_definition + !name + body: (_)? @_.interior +) @anonymousFunction + +;; Names and values + +;; Handle variable assignments +;;!! a = 42 +;;! ^----- +;;! xxxx-- +( + (assignment_statement + (variable_list) @name + (_) @_.trailing.startOf + ) @_dummy @_.domain + (#not-parent-type? @_dummy variable_declaration) +) +;; Handle variable declarations +;;!! local a = 42 +;;! ------^----- +;;! xxxxxxxxxx-- +local_declaration: (variable_declaration + (assignment_statement + (variable_list) @name + (_) @_.removal.end.startOf + ) +) @_.domain @_.removal.start.startOf + +;; Handle assignment values +;;!! a = 42 +;;! ----^^ +;;! -xxxxx +( + (assignment_statement + (_) @_.leading.endOf + (expression_list) @value + ) @_dummy @_.domain + (#not-parent-type? @_dummy variable_declaration) +) + +;; Handle variable declaration values +;;!! local a = 42 +;;! ----------^^ +;;! -------xxxxx +local_declaration: (variable_declaration + (assignment_statement + (_) @_.leading.endOf + (expression_list) @value + ) +) @_.domain + +;;!! return a + b +;;! -------^^^^^ +;;! ------xxxxxx +(return_statement + (_) @value +) @_.domain + +;; match a ternary condition +;;!! local max = x > y and x or y +;;! ^^^^^ +;;! xxxxx +(binary_expression + left: (binary_expression + left: (binary_expression) @condition + . + "and" + ) +) + +;; Structures and object access + +;; (method_index_expression) @private.fieldAccess From 463a00f1964f3406b5d1b0371199955e938a205f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 25 Mar 2024 20:07:47 +0100 Subject: [PATCH 4/7] Added spread support for relative and ordinal scopes (#2254) `"change every two tokens"` Fixes #1514 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] I have updated the [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [x] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) - [x] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- changelog/2024-03-addedSpreadModifier.md | 9 + .../src/cheatsheet/sections/modifiers.py | 81 ++++++--- .../src/modifiers/ordinal_scope.py | 27 ++- .../src/modifiers/relative_scope.py | 19 ++- .../src/modifiers/simple_scope_modifier.py | 40 +++-- cursorless-talon/src/spoken_forms.json | 3 +- docs/user/README.md | 9 + .../lib/sampleSpokenFormInfos/defaults.json | 72 +++++++- .../command/PartialTargetDescriptor.types.ts | 6 + .../primitiveTargetToSpokenForm.ts | 30 +++- .../processTargets/modifiers/InstanceStage.ts | 2 +- .../modifiers/OrdinalScopeStage.ts | 5 + .../modifiers/RelativeExclusiveScopeStage.ts | 82 --------- .../modifiers/RelativeInclusiveScopeStage.ts | 83 --------- .../modifiers/RelativeScopeStage.ts | 159 +++++++++++++++--- .../src/processTargets/modifiers/listUtils.ts | 27 +++ .../modifiers/relativeScopeLegacy.ts | 2 +- .../modifiers/targetSequenceUtils.ts | 12 +- .../changeEveryFirstThreeTokens.yml | 30 ++++ .../changeEveryLastThreeTokens.yml | 30 ++++ .../changeSpreadFirstTwoTokens.yml | 28 +++ .../changeSpreadLastTwoTokens.yml | 28 +++ .../changeEveryNextThreeTokens.yml | 31 ++++ .../changeEveryPreviousThreeTokens.yml | 31 ++++ .../relativeScopes/changeEveryThreeTokens.yml | 31 ++++ .../changeEveryThreeTokensBackward.yml | 31 ++++ .../changeSpreadNextTwoTokens.yml | 29 ++++ .../relativeScopes/changeSpreadTwoTokens.yml | 29 ++++ .../changeSpreadTwoTokensBackward.yml | 29 ++++ 29 files changed, 721 insertions(+), 274 deletions(-) create mode 100644 changelog/2024-03-addedSpreadModifier.md delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml diff --git a/changelog/2024-03-addedSpreadModifier.md b/changelog/2024-03-addedSpreadModifier.md new file mode 100644 index 0000000000..cfe6733226 --- /dev/null +++ b/changelog/2024-03-addedSpreadModifier.md @@ -0,0 +1,9 @@ +--- +tags: [enhancement] +pullRequest: 2254 +--- + +- Added every/spread ordinal/relative modifier. Turns relative and ordinal range modifiers into multiple target selections instead of contiguous range. + +- `"take every two tokens"` selects two tokens as separate selections +- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors) diff --git a/cursorless-talon/src/cheatsheet/sections/modifiers.py b/cursorless-talon/src/cheatsheet/sections/modifiers.py index 55d3f396a3..af8164d312 100644 --- a/cursorless-talon/src/cheatsheet/sections/modifiers.py +++ b/cursorless-talon/src/cheatsheet/sections/modifiers.py @@ -1,10 +1,14 @@ +from itertools import chain +from typing import TypedDict + from ..get_list import get_raw_list, make_dict_readable MODIFIER_LIST_NAMES = [ "simple_modifier", "interior_modifier", "head_tail_modifier", - "simple_scope_modifier", + "every_scope_modifier", + "ancestor_scope_modifier", "first_modifier", "last_modifier", "previous_next_modifier", @@ -132,10 +136,6 @@ def get_modifiers(): "spokenForm": f" {complex_modifiers['next']} ", "description": " instance of after target", }, - { - "spokenForm": f"{complex_modifiers['previous']} s", - "description": "previous instances of ", - }, { "spokenForm": f" {complex_modifiers['backward']}", "description": "single instance of including target, going backwards", @@ -144,18 +144,25 @@ def get_modifiers(): "spokenForm": f" {complex_modifiers['forward']}", "description": "single instance of including target, going forwards", }, - { - "spokenForm": f" s {complex_modifiers['backward']}", - "description": " instances of including target, going backwards", - }, - { - "spokenForm": " s", - "description": " instances of including target, going forwards", - }, - { - "spokenForm": f"{complex_modifiers['next']} s", - "description": "next instances of ", - }, + *generateOptionalEvery( + complex_modifiers["every"], + { + "spokenForm": f" s {complex_modifiers['backward']}", + "description": " instances of including target, going backwards", + }, + { + "spokenForm": " s", + "description": " instances of including target, going forwards", + }, + { + "spokenForm": f"{complex_modifiers['previous']} s", + "description": "previous instances of ", + }, + { + "spokenForm": f"{complex_modifiers['next']} s", + "description": "next instances of ", + }, + ), ], }, { @@ -170,14 +177,40 @@ def get_modifiers(): "spokenForm": f" {complex_modifiers['last']} ", "description": "-to-last instance of in iteration scope", }, + *generateOptionalEvery( + complex_modifiers["every"], + { + "spokenForm": f"{complex_modifiers['first']} s", + "description": "first instances of in iteration scope", + }, + { + "spokenForm": f"{complex_modifiers['last']} s", + "description": "last instances of in iteration scope", + }, + ), + ], + }, + ] + + +class Entry(TypedDict): + spokenForm: str + description: str + + +def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]: + return list( + chain.from_iterable( + [ { - "spokenForm": f"{complex_modifiers['first']} s", - "description": "First instances of in iteration scope", + "spokenForm": entry["spokenForm"], + "description": f"{entry['description']}, as contiguous range", }, { - "spokenForm": f"{complex_modifiers['last']} s", - "description": "Last instances of in iteration scope", + "spokenForm": f"{every} {entry['spokenForm']}", + "description": f"{entry['description']}, as individual targets", }, - ], - }, - ] + ] + for entry in entries + ) + ) diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py index 57e321ed87..0ff0ac3982 100644 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -44,18 +44,27 @@ def cursorless_ordinal_range(m) -> dict[str, Any]: @mod.capture( - rule="({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " + rule=( + "[{user.cursorless_every_scope_modifier}] " + "({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " + " " + ), ) def cursorless_first_last(m) -> dict[str, Any]: """First/last `n` scopes; eg "first three funks""" - if m[0] == "first": + is_every = hasattr(m, "cursorless_every_scope_modifier") + if hasattr(m, "cursorless_first_modifier"): return create_ordinal_scope_modifier( - m.cursorless_scope_type_plural, 0, m.private_cursorless_number_small + m.cursorless_scope_type_plural, + 0, + m.private_cursorless_number_small, + is_every, ) return create_ordinal_scope_modifier( m.cursorless_scope_type_plural, -m.private_cursorless_number_small, m.private_cursorless_number_small, + is_every, ) @@ -65,10 +74,18 @@ def cursorless_ordinal_scope(m) -> dict[str, Any]: return m[0] -def create_ordinal_scope_modifier(scope_type: dict, start: int, length: int = 1): - return { +def create_ordinal_scope_modifier( + scope_type: dict, + start: int, + length: int = 1, + is_every: bool = False, +): + res = { "type": "ordinalScope", "scopeType": scope_type, "start": start, "length": length, } + if is_every: + res["isEvery"] = True + return res diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py index 5d20d42a1b..1fd60ac693 100644 --- a/cursorless-talon/src/modifiers/relative_scope.py +++ b/cursorless-talon/src/modifiers/relative_scope.py @@ -26,11 +26,12 @@ def cursorless_relative_scope_singular(m) -> dict[str, Any]: getattr(m, "ordinals_small", 1), 1, m.cursorless_relative_direction, + False, ) @mod.capture( - rule=" " + rule="[{user.cursorless_every_scope_modifier}] " ) def cursorless_relative_scope_plural(m) -> dict[str, Any]: """Relative previous/next plural scope. `next three funks`""" @@ -39,11 +40,12 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]: 1, m.private_cursorless_number_small, m.cursorless_relative_direction, + hasattr(m, "cursorless_every_scope_modifier"), ) @mod.capture( - rule=" [{user.cursorless_forward_backward_modifier}]" + rule="[{user.cursorless_every_scope_modifier}] [{user.cursorless_forward_backward_modifier}]" ) def cursorless_relative_scope_count(m) -> dict[str, Any]: """Relative count scope. `three funks`""" @@ -52,6 +54,7 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]: 0, m.private_cursorless_number_small, getattr(m, "cursorless_forward_backward_modifier", "forward"), + hasattr(m, "cursorless_every_scope_modifier"), ) @@ -65,6 +68,7 @@ def cursorless_relative_scope_one_backward(m) -> dict[str, Any]: 0, 1, m.cursorless_forward_backward_modifier, + False, ) @@ -82,12 +86,19 @@ def cursorless_relative_scope(m) -> dict[str, Any]: def create_relative_scope_modifier( - scope_type: dict, offset: int, length: int, direction: str + scope_type: dict, + offset: int, + length: int, + direction: str, + is_every: bool, ) -> dict[str, Any]: - return { + res = { "type": "relativeScope", "scopeType": scope_type, "offset": offset, "length": length, "direction": direction, } + if is_every: + res["isEvery"] = True + return res diff --git a/cursorless-talon/src/modifiers/simple_scope_modifier.py b/cursorless-talon/src/modifiers/simple_scope_modifier.py index 5d51ba8a4e..cb0d2d4868 100644 --- a/cursorless-talon/src/modifiers/simple_scope_modifier.py +++ b/cursorless-talon/src/modifiers/simple_scope_modifier.py @@ -5,31 +5,35 @@ mod = Module() mod.list( - "cursorless_simple_scope_modifier", - desc='Cursorless simple scope modifiers, eg "every"', + "cursorless_every_scope_modifier", + desc="Cursorless every scope modifiers", +) +mod.list( + "cursorless_ancestor_scope_modifier", + desc="Cursorless ancestor scope modifiers", ) @mod.capture( - rule="[{user.cursorless_simple_scope_modifier}] " + rule=( + "[{user.cursorless_every_scope_modifier} | {user.cursorless_ancestor_scope_modifier}] " + "" + ), ) def cursorless_simple_scope_modifier(m) -> dict[str, Any]: """Containing scope, every scope, etc""" - if hasattr(m, "cursorless_simple_scope_modifier"): - modifier = m.cursorless_simple_scope_modifier - - if modifier == "every": - return { - "type": "everyScope", - "scopeType": m.cursorless_scope_type, - } - - if modifier == "ancestor": - return { - "type": "containingScope", - "scopeType": m.cursorless_scope_type, - "ancestorIndex": 1, - } + if hasattr(m, "cursorless_every_scope_modifier"): + return { + "type": "everyScope", + "scopeType": m.cursorless_scope_type, + } + + if hasattr(m, "cursorless_ancestor_scope_modifier"): + return { + "type": "containingScope", + "scopeType": m.cursorless_scope_type, + "ancestorIndex": 1, + } return { "type": "containingScope", diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index 863808f455..4322a3e6ff 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -83,7 +83,8 @@ "its": "inferPreviousMark", "visible": "visible" }, - "simple_scope_modifier": { "every": "every", "grand": "ancestor" }, + "every_scope_modifier": { "every": "every" }, + "ancestor_scope_modifier": { "grand": "ancestor" }, "interior_modifier": { "inside": "interiorOnly" }, diff --git a/docs/user/README.md b/docs/user/README.md index 25eb4fd2ef..bf68a77d9a 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -207,6 +207,8 @@ And here is a table of the spoken forms: | `"previous [number] [scope]s"` | previous `[number]` instances of `[scope]` | `"take previous three funks"` | | `"previous [scope]"` | Previous instance of `[scope]` | `"take previous funk"` | +You can prefix the modifier with `"every"` to yield multiple targets rather than a range. For example, `"take every two tokens"` selects two tokens as separate selections. + ##### `"every"` The modifier `"every"` can be used to select a syntactic element and all of its matching siblings. @@ -217,6 +219,13 @@ The modifier `"every"` can be used to select a syntactic element and all of its For example, the command `"take every key [blue] air"` will select every key in the map/object/dict including the token with a blue hat over the letter 'a'. +###### Use with relative / ordinal modifiers + +The modifier `every` can also be used to cause [relative / ordinal modifiers](#previous--next--ordinal--number) to yield multiple targets rather than a range: + +- `"take every two tokens"` selects two tokens as separate selections +- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors) + ##### `"grand"` The modifier `"grand"` can be used to select the parent of the containing syntactic element. diff --git a/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json b/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json index f70df30696..a4a808d164 100644 --- a/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json +++ b/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json @@ -68,6 +68,16 @@ } ] }, + { + "id": "decrement", + "type": "action", + "variations": [ + { + "spokenForm": "decrement ", + "description": "Decrement" + } + ] + }, { "id": "deselect", "type": "action", @@ -168,6 +178,16 @@ } ] }, + { + "id": "increment", + "type": "action", + "variations": [ + { + "spokenForm": "increment ", + "description": "Increment" + } + ] + }, { "id": "indentLine", "type": "action", @@ -710,6 +730,16 @@ "name": "Modifiers", "id": "modifiers", "items": [ + { + "id": "ancestor", + "type": "modifier", + "variations": [ + { + "spokenForm": "grand ", + "description": "Grandparent containing instance of " + } + ] + }, { "id": "containingScope", "type": "modifier", @@ -842,11 +872,19 @@ }, { "spokenForm": "first s", - "description": "First instances of in iteration scope" + "description": "first instances of in iteration scope, as contiguous range" + }, + { + "spokenForm": "every first s", + "description": "first instances of in iteration scope, as individual targets" }, { "spokenForm": "last s", - "description": "Last instances of in iteration scope" + "description": "last instances of in iteration scope, as contiguous range" + }, + { + "spokenForm": "every last s", + "description": "last instances of in iteration scope, as individual targets" } ] }, @@ -870,10 +908,6 @@ "spokenForm": " next ", "description": " instance of after target" }, - { - "spokenForm": "previous s", - "description": "previous instances of " - }, { "spokenForm": " backward", "description": "single instance of including target, going backwards" @@ -884,15 +918,35 @@ }, { "spokenForm": " s backward", - "description": " instances of including target, going backwards" + "description": " instances of including target, going backwards, as contiguous range" + }, + { + "spokenForm": "every s backward", + "description": " instances of including target, going backwards, as individual targets" }, { "spokenForm": " s", - "description": " instances of including target, going forwards" + "description": " instances of including target, going forwards, as contiguous range" + }, + { + "spokenForm": "every s", + "description": " instances of including target, going forwards, as individual targets" + }, + { + "spokenForm": "previous s", + "description": "previous instances of , as contiguous range" + }, + { + "spokenForm": "every previous s", + "description": "previous instances of , as individual targets" }, { "spokenForm": "next s", - "description": "next instances of " + "description": "next instances of , as contiguous range" + }, + { + "spokenForm": "every next s", + "description": "next instances of , as individual targets" } ] }, diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index 505a46d4ee..61690ced1a 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -272,6 +272,9 @@ export interface OrdinalScopeModifier { /** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */ length: number; + + /** If true, yields individual targets instead of contiguous range. Defaults to `false` */ + isEvery?: boolean; } export type Direction = "forward" | "backward"; @@ -297,6 +300,9 @@ export interface RelativeScopeModifier { /** Indicates which direction both {@link offset} and {@link length} go * relative to input target */ direction: Direction; + + /** If true use individual targets instead of combined range */ + isEvery?: boolean; } /** diff --git a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts index c42e5452f6..f9695c8399 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts @@ -83,28 +83,33 @@ export class PrimitiveTargetSpokenFormGenerator { case "ordinalScope": { const scope = this.handleScopeType(modifier.scopeType); + const isEvery = modifier.isEvery + ? this.spokenFormMap.simpleModifier.everyScope + : []; if (modifier.length === 1) { if (modifier.start === -1) { - return [this.spokenFormMap.modifierExtra.last, scope]; + return [isEvery, this.spokenFormMap.modifierExtra.last, scope]; } if (modifier.start === 0) { - return [this.spokenFormMap.modifierExtra.first, scope]; + return [isEvery, this.spokenFormMap.modifierExtra.first, scope]; } if (modifier.start < 0) { return [ + isEvery, ordinalToSpokenForm(Math.abs(modifier.start)), this.spokenFormMap.modifierExtra.last, scope, ]; } - return [ordinalToSpokenForm(modifier.start + 1), scope]; + return [isEvery, ordinalToSpokenForm(modifier.start + 1), scope]; } const number = numberToSpokenForm(modifier.length); if (modifier.start === 0) { return [ + isEvery, this.spokenFormMap.modifierExtra.first, number, pluralize(scope), @@ -112,6 +117,7 @@ export class PrimitiveTargetSpokenFormGenerator { } if (modifier.start === -modifier.length) { return [ + isEvery, this.spokenFormMap.modifierExtra.last, number, pluralize(scope), @@ -157,6 +163,9 @@ export class PrimitiveTargetSpokenFormGenerator { modifier: RelativeScopeModifier, ): SpokenFormComponent { const scope = this.handleScopeType(modifier.scopeType); + const isEvery = modifier.isEvery + ? this.spokenFormMap.simpleModifier.everyScope + : []; if (modifier.length === 1) { const direction = @@ -165,7 +174,7 @@ export class PrimitiveTargetSpokenFormGenerator { : connectives.backward; // token forward/backward - return [scope, direction]; + return [isEvery, scope, direction]; } const length = numberToSpokenForm(modifier.length); @@ -174,11 +183,11 @@ export class PrimitiveTargetSpokenFormGenerator { // two tokens // This could also have been "two tokens forward"; there is no way to disambiguate. if (modifier.direction === "forward") { - return [length, scopePlural]; + return [isEvery, length, scopePlural]; } // two tokens backward - return [length, scopePlural, connectives.backward]; + return [isEvery, length, scopePlural, connectives.backward]; } private handleRelativeScopeExclusive( @@ -189,25 +198,28 @@ export class PrimitiveTargetSpokenFormGenerator { modifier.direction === "forward" ? connectives.next : connectives.previous; + const isEvery = modifier.isEvery + ? this.spokenFormMap.simpleModifier.everyScope + : []; if (modifier.offset === 1) { const number = numberToSpokenForm(modifier.length); if (modifier.length === 1) { // next/previous token - return [direction, scope]; + return [isEvery, direction, scope]; } const scopePlural = pluralize(scope); // next/previous two tokens - return [direction, number, scopePlural]; + return [isEvery, direction, number, scopePlural]; } if (modifier.length === 1) { const ordinal = ordinalToSpokenForm(modifier.offset); // second next/previous token - return [ordinal, direction, scope]; + return [isEvery, ordinal, direction, scope]; } throw new NoSpokenFormError( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts index 156819c7c8..952645d92e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts @@ -15,8 +15,8 @@ import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; import { PlainTarget } from "../targets"; import { ContainingTokenIfUntypedEmptyStage } from "./ConditionalModifierStages"; -import { OutOfRangeError } from "./targetSequenceUtils"; import { StoredTargetMap } from "../.."; +import { OutOfRangeError } from "./listUtils"; export class InstanceStage implements ModifierStage { constructor( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts index db6d863e4e..babdcd2801 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -6,6 +6,7 @@ import { createRangeTargetFromIndices, getEveryScopeTargets, } from "./targetSequenceUtils"; +import { sliceStrict } from "./listUtils"; export class OrdinalScopeStage implements ModifierStage { constructor( @@ -24,6 +25,10 @@ export class OrdinalScopeStage implements ModifierStage { this.modifier.start + (this.modifier.start < 0 ? targets.length : 0); const endIndex = startIndex + this.modifier.length - 1; + if (this.modifier.isEvery) { + return sliceStrict(targets, startIndex, endIndex); + } + return [ createRangeTargetFromIndices( target.isReversed, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts deleted file mode 100644 index 9dc97beee3..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { RelativeScopeModifier } from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; -import { ModifierStageFactory } from "../ModifierStageFactory"; -import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; -import { runLegacy } from "./relativeScopeLegacy"; -import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; -import { TargetScope } from "./scopeHandlers/scope.types"; -import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types"; -import { OutOfRangeError } from "./targetSequenceUtils"; - -/** - * Handles relative modifiers that don't include targets intersecting with the - * input, eg "next funk", "previous two tokens". Proceeds by running - * {@link ScopeHandler.generateScopes} to get the desired scopes, skipping the - * first scope if input range is empty and is at start of that scope. - */ -export class RelativeExclusiveScopeStage implements ModifierStage { - constructor( - private modifierStageFactory: ModifierStageFactory, - private scopeHandlerFactory: ScopeHandlerFactory, - private modifier: RelativeScopeModifier, - ) {} - - run(target: Target): Target[] { - const scopeHandler = this.scopeHandlerFactory.create( - this.modifier.scopeType, - target.editor.document.languageId, - ); - - if (scopeHandler == null) { - return runLegacy(this.modifierStageFactory, this.modifier, target); - } - - const { isReversed, editor, contentRange: inputRange } = target; - const { length: desiredScopeCount, direction, offset } = this.modifier; - - const initialPosition = - direction === "forward" ? inputRange.end : inputRange.start; - - // If inputRange is empty, then we skip past any scopes that start at - // inputRange. Otherwise just disallow any scopes that start strictly - // before the end of input range (strictly after for "backward"). - const containment: ContainmentPolicy | undefined = inputRange.isEmpty - ? "disallowed" - : "disallowedIfStrict"; - - let scopeCount = 0; - let proximalScope: TargetScope | undefined; - for (const scope of scopeHandler.generateScopes( - editor, - initialPosition, - direction, - { containment, skipAncestorScopes: true }, - )) { - scopeCount += 1; - - if (scopeCount < offset) { - // Skip until we hit `offset` - continue; - } - - if (scopeCount === offset) { - // When we hit offset, that becomes proximal scope - if (desiredScopeCount === 1) { - // Just yield it if we only want 1 scope - return scope.getTargets(isReversed); - } - - proximalScope = scope; - continue; - } - - if (scopeCount === offset + desiredScopeCount - 1) { - // Then make a range when we get the desired number of scopes - return constructScopeRangeTarget(isReversed, proximalScope!, scope); - } - } - - throw new OutOfRangeError(); - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts deleted file mode 100644 index 73528db44c..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - NoContainingScopeError, - RelativeScopeModifier, -} from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; -import { ModifierStageFactory } from "../ModifierStageFactory"; -import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; -import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; -import { runLegacy } from "./relativeScopeLegacy"; -import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; -import { itake } from "itertools"; -import { OutOfRangeError } from "./targetSequenceUtils"; -/** - * Handles relative modifiers that include targets intersecting with the input, - * eg `"two funks"`, `"token backward"`, etc. Proceeds as follows: - * - * 1. Constructs the initial range to use as the starting point for the relative - * scope search. Expands from the proximal end of the input target (start - * for "forward", end for "backward") to the smallest containing scope, - * breaking ties in direction {@link direction} rather than the tie-breaking - * heuristics we use for containing scope, to make this modifier easier to - * reason about when between scopes. - * 2. Calls {@link ScopeHandler.generateScopes} to get as many scopes as - * desired, starting from the proximal end of the initial range (ie the start - * if direction is "forward", the end if direction is "backward"). - */ -export class RelativeInclusiveScopeStage implements ModifierStage { - constructor( - private modifierStageFactory: ModifierStageFactory, - private scopeHandlerFactory: ScopeHandlerFactory, - private modifier: RelativeScopeModifier, - ) {} - - run(target: Target): Target[] { - const scopeHandler = this.scopeHandlerFactory.create( - this.modifier.scopeType, - target.editor.document.languageId, - ); - - if (scopeHandler == null) { - return runLegacy(this.modifierStageFactory, this.modifier, target); - } - - const { isReversed, editor, contentRange } = target; - const { length: desiredScopeCount, direction } = this.modifier; - - const initialRange = getPreferredScopeTouchingPosition( - scopeHandler, - editor, - direction === "forward" ? contentRange.start : contentRange.end, - direction, - )?.domain; - - if (initialRange == null) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - const scopes = Array.from( - itake( - desiredScopeCount, - scopeHandler.generateScopes( - editor, - direction === "forward" ? initialRange.start : initialRange.end, - direction, - { - skipAncestorScopes: true, - }, - ), - ), - ); - - if (scopes.length < desiredScopeCount) { - throw new OutOfRangeError(); - } - - return constructScopeRangeTarget( - isReversed, - scopes[0], - scopes[scopes.length - 1], - ); - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index abe3fb0095..0bd7a14cbb 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,40 +1,145 @@ -import type { RelativeScopeModifier } from "@cursorless/common"; +import { + NoContainingScopeError, + type RelativeScopeModifier, +} from "@cursorless/common"; import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { RelativeExclusiveScopeStage } from "./RelativeExclusiveScopeStage"; -import { RelativeInclusiveScopeStage } from "./RelativeInclusiveScopeStage"; +import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; +import { TargetScope } from "./scopeHandlers/scope.types"; +import type { + ContainmentPolicy, + ScopeHandler, +} from "./scopeHandlers/scopeHandler.types"; +import { islice, itake } from "itertools"; +import { OutOfRangeError } from "./listUtils"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; /** - * Implements relative scope modifiers like "next funk", "two tokens", etc. - * Proceeds by determining whether the modifier wants to include the scope(s) - * touching the input target (ie if {@link modifier.offset} is 0), and then - * delegating to {@link RelativeInclusiveScopeStage} if so, or - * {@link RelativeExclusiveScopeStage} if not. + * Handles relative modifiers input, eg "next funk", "two funks", "previous two + * tokens". Proceeds by running {@link ScopeHandler.generateScopes} to get the + * desired scopes, skipping the first scope if input range is empty and is at + * start of that scope. */ export class RelativeScopeStage implements ModifierStage { - private modiferStage: ModifierStage; constructor( - modifierStageFactory: ModifierStageFactory, - scopeHandlerFactory: ScopeHandlerFactory, - modifier: RelativeScopeModifier, - ) { - this.modiferStage = - modifier.offset === 0 - ? new RelativeInclusiveScopeStage( - modifierStageFactory, - scopeHandlerFactory, - modifier, - ) - : new RelativeExclusiveScopeStage( - modifierStageFactory, - scopeHandlerFactory, - modifier, - ); - } + private modifierStageFactory: ModifierStageFactory, + private scopeHandlerFactory: ScopeHandlerFactory, + private modifier: RelativeScopeModifier, + ) {} run(target: Target): Target[] { - return this.modiferStage.run(target); + const scopeHandler = this.scopeHandlerFactory.create( + this.modifier.scopeType, + target.editor.document.languageId, + ); + + if (scopeHandler == null) { + return runLegacy(this.modifierStageFactory, this.modifier, target); + } + + const scopes = Array.from( + this.modifier.offset === 0 + ? generateScopesInclusive(scopeHandler, target, this.modifier) + : generateScopesExclusive(scopeHandler, target, this.modifier), + ); + + if (scopes.length < this.modifier.length) { + throw new OutOfRangeError(); + } + + const { isReversed } = target; + + if (this.modifier.isEvery) { + return scopes.flatMap((scope) => scope.getTargets(isReversed)); + } + + return constructScopeRangeTarget( + isReversed, + scopes[0], + scopes[scopes.length - 1], + ); + } +} + +/** + * Handles relative modifiers that include targets intersecting with the input, + * eg `"two funks"`, `"token backward"`, etc. Proceeds as follows: + * + * 1. Constructs the initial range to use as the starting point for the relative + * scope search. Expands from the proximal end of the input target (start + * for "forward", end for "backward") to the smallest containing scope, + * breaking ties in direction {@link direction} rather than the tie-breaking + * heuristics we use for containing scope, to make this modifier easier to + * reason about when between scopes. + * 2. Calls {@link ScopeHandler.generateScopes} to get as many scopes as + * desired, starting from the proximal end of the initial range (ie the start + * if direction is "forward", the end if direction is "backward"). + */ +function generateScopesInclusive( + scopeHandler: ScopeHandler, + target: Target, + modifier: RelativeScopeModifier, +): Iterable { + const { editor, contentRange } = target; + const { length: desiredScopeCount, direction } = modifier; + + const initialRange = getPreferredScopeTouchingPosition( + scopeHandler, + editor, + direction === "forward" ? contentRange.start : contentRange.end, + direction, + )?.domain; + + if (initialRange == null) { + throw new NoContainingScopeError(modifier.scopeType.type); } + + return itake( + desiredScopeCount, + scopeHandler.generateScopes( + editor, + direction === "forward" ? initialRange.start : initialRange.end, + direction, + { + skipAncestorScopes: true, + }, + ), + ); +} + +/** + * Handles relative modifiers that don't include targets intersecting with the + * input, eg "next funk", "previous two tokens". Proceeds by running + * {@link ScopeHandler.generateScopes} to get the desired scopes, skipping the + * first scope if input range is empty and is at start of that scope. + */ +function generateScopesExclusive( + scopeHandler: ScopeHandler, + target: Target, + modifier: RelativeScopeModifier, +): Iterable { + const { editor, contentRange: inputRange } = target; + const { length: desiredScopeCount, direction, offset } = modifier; + + const initialPosition = + direction === "forward" ? inputRange.end : inputRange.start; + + // If inputRange is empty, then we skip past any scopes that start at + // inputRange. Otherwise just disallow any scopes that start strictly + // before the end of input range (strictly after for "backward"). + const containment: ContainmentPolicy | undefined = inputRange.isEmpty + ? "disallowed" + : "disallowedIfStrict"; + + return islice( + scopeHandler.generateScopes(editor, initialPosition, direction, { + containment, + skipAncestorScopes: true, + }), + offset - 1, + offset + desiredScopeCount - 1, + ); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts new file mode 100644 index 0000000000..1b28e296d8 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts @@ -0,0 +1,27 @@ +export class OutOfRangeError extends Error { + constructor() { + super("Scope index out of range"); + this.name = "OutOfRangeError"; + } +} + +/** Slice list of by given indices */ +export function sliceStrict( + targets: T[], + startIndex: number, + endIndex: number, +): T[] { + assertIndices(targets, startIndex, endIndex); + + return targets.slice(startIndex, endIndex + 1); +} + +export function assertIndices( + targets: T[], + startIndex: number, + endIndex: number, +): void { + if (startIndex < 0 || endIndex >= targets.length) { + throw new OutOfRangeError(); + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts b/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts index 5c952a7270..02d85bc92e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts @@ -6,9 +6,9 @@ import { UntypedTarget } from "../targets"; import { createRangeTargetFromIndices, getEveryScopeTargets, - OutOfRangeError, } from "./targetSequenceUtils"; import { TooFewScopesError } from "./TooFewScopesError"; +import { OutOfRangeError } from "./listUtils"; interface ContainingIndices { start: number; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts index c3439a88cd..6da4a41524 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts @@ -2,13 +2,7 @@ import { ScopeType } from "@cursorless/common"; import { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import { createContinuousRangeTarget } from "../createContinuousRangeTarget"; - -export class OutOfRangeError extends Error { - constructor() { - super("Scope index out of range"); - this.name = "OutOfRangeError"; - } -} +import { assertIndices } from "./listUtils"; /** * Construct a single range target between two targets in a list of targets, @@ -25,9 +19,7 @@ export function createRangeTargetFromIndices( startIndex: number, endIndex: number, ): Target { - if (startIndex < 0 || endIndex >= targets.length) { - throw new OutOfRangeError(); - } + assertIndices(targets, startIndex, endIndex); if (startIndex === endIndex) { return targets[startIndex]; diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml new file mode 100644 index 0000000000..5bf1c3fb76 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every first three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: 0 + length: 3 + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " ddd" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml new file mode 100644 index 0000000000..0cbcecb0a5 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every last three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: -3 + length: 3 + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml new file mode 100644 index 0000000000..d0d2a375c4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every first two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: 0 + length: 2 + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " baz" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml new file mode 100644 index 0000000000..9e5f640cd7 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every last two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: -2 + length: 2 + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "foo " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml new file mode 100644 index 0000000000..0c7b8e4673 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every next three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 3 + direction: forward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml new file mode 100644 index 0000000000..902ea42729 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every previous three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 3 + direction: backward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + marks: {} +finalState: + documentContents: " ddd" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml new file mode 100644 index 0000000000..162f94b5a7 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 3 + direction: forward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " ddd" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml new file mode 100644 index 0000000000..e14810748b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every three tokens backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 3 + direction: backward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml new file mode 100644 index 0000000000..f4313682e0 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every next two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: forward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "foo " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml new file mode 100644 index 0000000000..5e06b34203 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " baz" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml new file mode 100644 index 0000000000..e9faecff0d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every two tokens backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + marks: {} +finalState: + documentContents: "foo " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} From 4af66c98149fc92ac125cbb4f9d3bbe2b3ae71fc Mon Sep 17 00:00:00 2001 From: Cedric Halbronn <387346+saidelike@users.noreply.github.com> Date: Mon, 25 Mar 2024 19:38:25 +0000 Subject: [PATCH 5/7] refactor: make the text editor edit() interface better for neovim (#2270) ## Checklist - [ ] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet ` 7189 passing (5m)` all tests passing. Just marking it as a draft PR for now until I confirm from neovim side that this is good interface. Co-authored-by: Cedric Halbronn --- packages/common/src/index.ts | 1 + packages/common/src/types/Edit.ts | 18 ++++++++++ packages/common/src/types/TextEditor.ts | 15 +++----- .../src/actions/BreakLine.ts | 9 +++-- .../src/actions/JoinLines.ts | 5 ++- .../cursorless-engine/src/actions/Wrap.ts | 2 +- .../src/core/updateSelections/RangeUpdater.ts | 2 +- .../core/updateSelections/updateSelections.ts | 4 +-- .../cursorless-engine/src/typings/Types.ts | 19 +--------- .../src/util/performDocumentEdits.ts | 15 ++------ .../src/ide/vscode/VscodeEdit.ts | 35 +++++++------------ .../src/ide/vscode/VscodeTextEditorImpl.ts | 9 ++--- 12 files changed, 54 insertions(+), 80 deletions(-) create mode 100644 packages/common/src/types/Edit.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 61badf4813..224b5708aa 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -35,6 +35,7 @@ export * from "./types/RangeExpansionBehavior"; export * from "./types/InputBoxOptions"; export * from "./types/Position"; export * from "./types/Range"; +export * from "./types/Edit"; export * from "./types/RevealLineAt"; export * from "./types/Selection"; export * from "./types/TextDocument"; diff --git a/packages/common/src/types/Edit.ts b/packages/common/src/types/Edit.ts new file mode 100644 index 0000000000..9c51b1a9f7 --- /dev/null +++ b/packages/common/src/types/Edit.ts @@ -0,0 +1,18 @@ +import { Range } from ".."; + +/** Represent a single edit/change in the document */ +export interface Edit { + range: Range; + text: string; + + /** + * If this edit is an insertion, ie the range has zero length, then this + * field can be set to `true` to indicate that any adjacent empty selection + * should *not* be shifted to the right, as would normally happen with an + * insertion. This is equivalent to the + * [distinction](https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit) + * in a vscode edit builder between doing a replace with an empty range + * versus doing an insert. + */ + isReplace?: boolean; +} diff --git a/packages/common/src/types/TextEditor.ts b/packages/common/src/types/TextEditor.ts index 6eb46fb015..ef44236212 100644 --- a/packages/common/src/types/TextEditor.ts +++ b/packages/common/src/types/TextEditor.ts @@ -1,10 +1,10 @@ import type { + Edit, Position, Range, RevealLineAt, Selection, TextDocument, - TextEditorEdit, TextEditorOptions, } from ".."; @@ -81,18 +81,11 @@ export interface EditableTextEditor extends TextEditor { /** * Perform an edit on the document associated with this text editor. * - * The given callback-function is invoked with an {@link TextEditorEdit edit-builder} which must - * be used to make edits. Note that the edit-builder is only valid while the - * callback executes. - * - * @param callback A function which can create edits using an {@link TextEditorEdit edit-builder}. - * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. + * @param edits the list of edits that need to be applied to the document + * (note that the implementation might need to sort them in reverse order) * @return A promise that resolves with a value indicating if the edits could be applied. */ - edit( - callback: (editBuilder: TextEditorEdit) => void, - options?: { undoStopBefore: boolean; undoStopAfter: boolean }, - ): Promise; + edit(edits: Edit[]): Promise; /** * Edit a new new notebook cell above. diff --git a/packages/cursorless-engine/src/actions/BreakLine.ts b/packages/cursorless-engine/src/actions/BreakLine.ts index 003b6715be..9ecda208e7 100644 --- a/packages/cursorless-engine/src/actions/BreakLine.ts +++ b/packages/cursorless-engine/src/actions/BreakLine.ts @@ -1,9 +1,14 @@ -import { FlashStyle, Position, Range, TextEditor } from "@cursorless/common"; +import { + Edit, + FlashStyle, + Position, + Range, + TextEditor, +} from "@cursorless/common"; import { flatten, zip } from "lodash"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; -import { Edit } from "../typings/Types"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; import type { ActionReturnValue } from "./actions.types"; diff --git a/packages/cursorless-engine/src/actions/JoinLines.ts b/packages/cursorless-engine/src/actions/JoinLines.ts index 8c6ec1b6e7..895de4b0db 100644 --- a/packages/cursorless-engine/src/actions/JoinLines.ts +++ b/packages/cursorless-engine/src/actions/JoinLines.ts @@ -1,13 +1,12 @@ -import { FlashStyle, Range, TextEditor } from "@cursorless/common"; +import { Edit, FlashStyle, Range, TextEditor } from "@cursorless/common"; +import { range as iterRange, map, pairwise } from "itertools"; import { flatten, zip } from "lodash"; import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; -import { Edit } from "../typings/Types"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; import type { ActionReturnValue } from "./actions.types"; -import { range as iterRange, map, pairwise } from "itertools"; export default class JoinLines { constructor(private rangeUpdater: RangeUpdater) { diff --git a/packages/cursorless-engine/src/actions/Wrap.ts b/packages/cursorless-engine/src/actions/Wrap.ts index 53f320b436..feb93be933 100644 --- a/packages/cursorless-engine/src/actions/Wrap.ts +++ b/packages/cursorless-engine/src/actions/Wrap.ts @@ -1,4 +1,5 @@ import { + Edit, FlashStyle, RangeExpansionBehavior, Selection, @@ -10,7 +11,6 @@ import { performEditsAndUpdateFullSelectionInfos, } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; -import { Edit } from "../typings/Types"; import { Target } from "../typings/target.types"; import { FullSelectionInfo } from "../typings/updateSelections"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; diff --git a/packages/cursorless-engine/src/core/updateSelections/RangeUpdater.ts b/packages/cursorless-engine/src/core/updateSelections/RangeUpdater.ts index bed97ab337..e261ff2fa9 100644 --- a/packages/cursorless-engine/src/core/updateSelections/RangeUpdater.ts +++ b/packages/cursorless-engine/src/core/updateSelections/RangeUpdater.ts @@ -1,12 +1,12 @@ import type { Disposable, + Edit, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, } from "@cursorless/common"; import { pull } from "lodash"; import { ide } from "../../singletons/ide.singleton"; -import type { Edit } from "../../typings/Types"; import { ExtendedTextDocumentChangeEvent, FullRangeInfo, diff --git a/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts b/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts index c2dded43e2..ea758d40cb 100644 --- a/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts +++ b/packages/cursorless-engine/src/core/updateSelections/updateSelections.ts @@ -1,12 +1,12 @@ import { - RangeExpansionBehavior, + Edit, EditableTextEditor, Range, + RangeExpansionBehavior, Selection, TextDocument, } from "@cursorless/common"; import { flatten } from "lodash"; -import { Edit } from "../../typings/Types"; import { FullSelectionInfo, SelectionInfo, diff --git a/packages/cursorless-engine/src/typings/Types.ts b/packages/cursorless-engine/src/typings/Types.ts index 47cbc073f1..fd3e33e357 100644 --- a/packages/cursorless-engine/src/typings/Types.ts +++ b/packages/cursorless-engine/src/typings/Types.ts @@ -1,4 +1,4 @@ -import type { Range, Selection, TextEditor } from "@cursorless/common"; +import type { Edit, Range, Selection, TextEditor } from "@cursorless/common"; import type { SyntaxNode } from "web-tree-sitter"; export interface SelectionWithEditor { @@ -72,23 +72,6 @@ export type SelectionExtractor = ( nodes: SyntaxNode, ) => SelectionWithContext; -/** Represent a single edit/change in the document */ -export interface Edit { - range: Range; - text: string; - - /** - * If this edit is an insertion, ie the range has zero length, then this - * field can be set to `true` to indicate that any adjacent empty selection - * should *not* be shifted to the right, as would normally happen with an - * insertion. This is equivalent to the - * [distinction](https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit) - * in a vscode edit builder between doing a replace with an empty range - * versus doing an insert. - */ - isReplace?: boolean; -} - export interface EditWithRangeUpdater extends Edit { /** * This function will be passed the resulting range containing {@link text} diff --git a/packages/cursorless-engine/src/util/performDocumentEdits.ts b/packages/cursorless-engine/src/util/performDocumentEdits.ts index de573b87f8..d339dc41e6 100644 --- a/packages/cursorless-engine/src/util/performDocumentEdits.ts +++ b/packages/cursorless-engine/src/util/performDocumentEdits.ts @@ -1,6 +1,5 @@ -import { EditableTextEditor } from "@cursorless/common"; +import { Edit, EditableTextEditor } from "@cursorless/common"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { Edit } from "../typings/Types"; export async function performDocumentEdits( rangeUpdater: RangeUpdater, @@ -12,17 +11,7 @@ export async function performDocumentEdits( edits.filter((edit) => edit.isReplace), ); - const wereEditsApplied = await editor.edit((editBuilder) => { - edits.forEach(({ range, text, isReplace }) => { - if (text === "") { - editBuilder.delete(range); - } else if (range.isEmpty && !isReplace) { - editBuilder.insert(range.start, text); - } else { - editBuilder.replace(range, text); - } - }); - }); + const wereEditsApplied = await editor.edit(edits); deregister(); diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeEdit.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeEdit.ts index b738394255..48556be83d 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeEdit.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeEdit.ts @@ -1,31 +1,20 @@ -import { TextEditorEdit } from "@cursorless/common"; -import { - toVscodeEndOfLine, - toVscodePosition, - toVscodePositionOrRange, - toVscodeRange, -} from "@cursorless/vscode-common"; +import { Edit } from "@cursorless/common"; +import { toVscodePosition, toVscodeRange } from "@cursorless/vscode-common"; import type * as vscode from "vscode"; export default async function vscodeEdit( editor: vscode.TextEditor, - callback: (editBuilder: TextEditorEdit) => void, - options?: { undoStopBefore: boolean; undoStopAfter: boolean }, + edits: Edit[], ): Promise { return await editor.edit((editBuilder) => { - callback({ - replace: (location, value) => { - editBuilder.replace(toVscodePositionOrRange(location), value); - }, - insert: (location, value) => { - editBuilder.insert(toVscodePosition(location), value); - }, - delete: (location) => { - editBuilder.delete(toVscodeRange(location)); - }, - setEndOfLine: (endOfLine) => { - editBuilder.setEndOfLine(toVscodeEndOfLine(endOfLine)); - }, + edits.forEach(({ range, text, isReplace }) => { + if (text === "") { + editBuilder.delete(toVscodeRange(range)); + } else if (range.isEmpty && !isReplace) { + editBuilder.insert(toVscodePosition(range.start), text); + } else { + editBuilder.replace(toVscodeRange(range), text); + } }); - }, options); + }); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts index 15733f6969..3cd8aae30c 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts @@ -1,5 +1,6 @@ import { BreakpointDescriptor, + Edit, EditableTextEditor, Position, Range, @@ -8,7 +9,6 @@ import { sleep, TextDocument, TextEditor, - TextEditorEdit, TextEditorOptions, } from "@cursorless/common"; import { @@ -84,11 +84,8 @@ export class VscodeTextEditorImpl implements EditableTextEditor { return vscodeRevealLine(this, lineNumber, at); } - public edit( - callback: (editBuilder: TextEditorEdit) => void, - options?: { undoStopBefore: boolean; undoStopAfter: boolean }, - ): Promise { - return vscodeEdit(this.editor, callback, options); + public edit(edits: Edit[]): Promise { + return vscodeEdit(this.editor, edits); } public focus(): Promise { From 918fd781fdafc62cfbcb80553fc4756b7868ab0f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 25 Mar 2024 19:38:42 +0000 Subject: [PATCH 6/7] Add changelog entry for #1962 (#2269) ## Checklist - [-] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [-] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [-] I have not broken the cheatsheet --- changelog/2024-03-addLuaSupport.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/2024-03-addLuaSupport.md diff --git a/changelog/2024-03-addLuaSupport.md b/changelog/2024-03-addLuaSupport.md new file mode 100644 index 0000000000..e898f12a50 --- /dev/null +++ b/changelog/2024-03-addLuaSupport.md @@ -0,0 +1,6 @@ +--- +tags: [enhancement] +pullRequest: 1962 +--- + +- Add support for the Lua programming language From de37ebf77834afad9a102cd5a4235886c72ece65 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 25 Mar 2024 19:50:54 +0000 Subject: [PATCH 7/7] Add `prettier-plugin-tailwindcss` (#2245) Sorts in [this order](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted), which seems reasonable ## Checklist - [-] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [-] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [-] I have not broken the cheatsheet --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .prettierrc | 3 +- package.json | 1 + packages/cheatsheet/src/lib/cheatsheet.tsx | 10 ++-- .../components/CheatsheetLegendComponent.tsx | 10 ++-- .../components/CheatsheetListComponent.tsx | 8 +-- .../components/CheatsheetNotesComponent.tsx | 4 +- .../src/lib/components/formatCaptures.tsx | 2 +- .../cursorless-org/src/components/Layout.tsx | 36 ++++++------ .../src/components/SpamProofEmailLink.tsx | 2 +- .../src/components/embedded-video.tsx | 2 +- packages/cursorless-org/src/pages/index.tsx | 20 +++---- pnpm-lock.yaml | 55 +++++++++++++++++++ 12 files changed, 105 insertions(+), 48 deletions(-) diff --git a/.prettierrc b/.prettierrc index bf357fbbc0..685f8968dc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,4 @@ { - "trailingComma": "all" + "trailingComma": "all", + "plugins": ["prettier-plugin-tailwindcss"] } diff --git a/package.json b/package.json index bb74081913..93909a75cb 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-unicorn": "49.0.0", "eslint-plugin-unused-imports": "^3.0.0", "prettier": "3.0.3", + "prettier-plugin-tailwindcss": "0.5.11", "syncpack": "9.8.4", "typescript": "^5.2.2" }, diff --git a/packages/cheatsheet/src/lib/cheatsheet.tsx b/packages/cheatsheet/src/lib/cheatsheet.tsx index 23eed38083..d484c45d54 100644 --- a/packages/cheatsheet/src/lib/cheatsheet.tsx +++ b/packages/cheatsheet/src/lib/cheatsheet.tsx @@ -17,14 +17,14 @@ export const CheatsheetPage: React.FC = ({ }) => { return (
-

+

Cursorless Cheatsheet{" "} - + - + See the{" "} full documentation @@ -42,7 +42,7 @@ type CheatsheetProps = { }; const Cheatsheet: React.FC = ({ cheatsheetInfo }) => ( -
+
{cheatsheetInfo.sections .filter((section) => section.items.length > 0) .map((section) => ( @@ -64,7 +64,7 @@ type CheatsheetSectionProps = { }; const CheatsheetSection: React.FC = ({ children }) => ( -
+
{children}
); diff --git a/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx b/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx index a9e38ad60f..3709031459 100644 --- a/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx +++ b/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx @@ -20,16 +20,16 @@ export default function CheatsheetLegendComponent({ return (
-

+

Legend

- - + @@ -38,7 +38,7 @@ export default function CheatsheetLegendComponent({ {data.map(({ term, definition, link, id }) => (
Term + Term Definition
{formatCaptures(`<${term}>`)} diff --git a/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx b/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx index 368d32fd8e..db7c8beff1 100644 --- a/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx +++ b/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx @@ -24,14 +24,14 @@ export default function CheatsheetListComponent({ return (
-

{section.name}

+

{section.name}

- - + diff --git a/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx b/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx index 744b133b44..7ea235af04 100644 --- a/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx +++ b/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx @@ -5,9 +5,9 @@ export default function CheatsheetNotesComponent(): JSX.Element { return (
-

+

Notes

Spoken form + Spoken form Meaning