From dcfe499632691637e1052acfee3482cb223c6edb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 10:33:55 +0100 Subject: [PATCH 01/34] Started implementing extension side fallback --- cursorless-talon/src/command.py | 14 +- cursorless-talon/src/fallback.py | 60 +++++ .../common/src/getFakeCommandServerApi.ts | 2 + packages/common/src/types/CommandServerApi.ts | 4 + .../src/api/CursorlessEngineApi.ts | 18 +- .../src/core/getCommandFallback.ts | 222 ++++++++++++++++++ .../cursorless-engine/src/cursorlessEngine.ts | 2 + packages/cursorless-engine/src/runCommand.ts | 29 ++- 8 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 cursorless-talon/src/fallback.py create mode 100644 packages/cursorless-engine/src/core/getCommandFallback.ts diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index f7f05e447e..133df815f2 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -3,6 +3,8 @@ from talon import Module, actions, speech_system +from .fallback import perform_fallback + @dataclasses.dataclass class CursorlessCommand: @@ -30,10 +32,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 type(response) is dict and "fallback" in response: + perform_fallback(response["fallback"]) def private_cursorless_command_no_wait(action: dict): """Execute cursorless command without waiting""" @@ -44,10 +48,16 @@ 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 type(response) is dict: + if "fallback" in response: + return perform_fallback(response["fallback"]) + if "returnValue" in response: + return response["returnValue"] + return response 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..0514e8eade --- /dev/null +++ b/cursorless-talon/src/fallback.py @@ -0,0 +1,60 @@ +from talon import actions + + +action_callbacks = { + "setSelection": actions.skip, + "copyToClipboard": actions.edit.copy, + "cutToClipboard": actions.edit.cut, + "pasteFromClipboard": actions.edit.paste, + "clearAndSetSelection": actions.edit.delete, + "remove": actions.edit.delete, + "applyFormatter": actions.user.reformat_selection, + "editNewLineBefore": actions.edit.line_insert_up, + "editNewLineAfter": actions.edit.line_insert_down, + "nextHomophone": actions.user.homophones_cycle_selected, + # "replaceWithTarget": replace_with_target, +} + +scope_callbacks = { + "cursor": actions.skip, + "extendThroughStartOf": actions.user.select_line_start, + "extendThroughEndOf": actions.user.select_line_end, + "containing_document": actions.edit.select_all, + "containing_paragraph": actions.edit.select_paragraph, + "containing_line": actions.edit.select_line, + "containing_token": actions.edit.select_word, +} + + +def perform_fallback(fallback: dict): + try: + scope_callback = get_scope_callback(fallback) + action_callback = get_action_callback(fallback) + scope_callback() + action_callback() + except ValueError as ex: + actions.app.notify(str(ex)) + + +def get_action_callback(fallback: dict): + action = fallback["action"] + if action in action_callbacks: + return action_callbacks[action] + + if action == "wrapWithPairedDelimiter": + return lambda: actions.user.delimiters_pair_wrap_selection_with( + fallback["left"], fallback["right"] + ) + + raise ValueError(f"Unknown Cursorless fallback action: {action}") + + +def get_scope_callback(fallback: dict): + if "scope" not in fallback: + return actions.skip + + scope = fallback["scope"] + if scope in scope_callbacks: + return scope_callbacks[scope] + + raise ValueError(f"Unknown Cursorless fallback scope: {scope}") diff --git a/packages/common/src/getFakeCommandServerApi.ts b/packages/common/src/getFakeCommandServerApi.ts index 3a1adb23fa..f5183c9745 100644 --- a/packages/common/src/getFakeCommandServerApi.ts +++ b/packages/common/src/getFakeCommandServerApi.ts @@ -2,6 +2,8 @@ import { CommandServerApi } from "./types/CommandServerApi"; export function getFakeCommandServerApi(): CommandServerApi { return { + getFocusedElementType: () => "textEditor", + signals: { prePhrase: { getVersion: async () => null, 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/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 331d535a75..38e8f9e2f5 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -1,4 +1,4 @@ -import { Command, HatTokenMap, IDE } from "@cursorless/common"; +import { Command, HatTokenMap, IDE, ReplaceWith } from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { StoredTargetMap } from "../core/StoredTargets"; import { ScopeProvider } from "@cursorless/common"; @@ -34,15 +34,27 @@ 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 type CommandResponse = { returnValue: unknown } | { fallback: Fallback }; + +export type Fallback = + | { action: string; scope: string | null } + | { action: "insert"; scope: string | null; text: string } + | { + action: "wrapWithPairedDelimiter" | "rewrapWithPairedDelimiter"; + scope: string | null; + left: string; + right: string; + }; + export interface CommandRunnerDecorator { /** * @param commandRunner: A CommandRunner. diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts new file mode 100644 index 0000000000..7b8a3067ed --- /dev/null +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -0,0 +1,222 @@ +import { + CommandComplete, + CommandServerApi, + DestinationDescriptor, + PartialTargetDescriptor, +} from "@cursorless/common"; + +import { Fallback } from "../api/CursorlessEngineApi"; + +export function getCommandFallback( + commandServerApi: CommandServerApi | null, + command: CommandComplete, +): Fallback | null { + if (commandServerApi == null) { + return null; + } + + const focusedElement = commandServerApi?.getFocusedElementType(); + + if (focusedElement === "textEditor") { + return null; + } + + const action = command.action; + + switch (action.name) { + case "replace": + return destinationIsSelection(action.destination) && + Array.isArray(action.replaceWith) + ? { + action: "insert", + scope: getScopeFromDestination(action.destination), + text: action.replaceWith.join("\n"), + } + : null; + + case "replaceWithTarget": + case "moveToTarget": + // if (action.destination.type === "implicit") { + // } + return null; + + case "callAsFunction": + return null; + // return action.argument.type === "implicit" && + // targetIsSelection(action.callee) + // ? { action: action.name } + // : null; + + case "wrapWithPairedDelimiter": + case "rewrapWithPairedDelimiter": + return targetIsSelection(action.target) + ? { + action: action.name, + scope: getScopeFromTarget(action.target), + left: action.left, + right: action.right, + } + : null; + + case "pasteFromClipboard": + return destinationIsSelection(action.destination) + ? { + action: action.name, + scope: getScopeFromDestination(action.destination), + } + : null; + + case "swapTargets": + case "editNew": + case "insertSnippet": + case "generateSnippet": + case "wrapWithSnippet": + return null; + + default: + return targetIsSelection(action.target) + ? { action: action.name, scope: getScopeFromTarget(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 getScopeFromDestination( + destination: DestinationDescriptor, +): string | null { + if (destination.type === "primitive") { + return getScopeFromTarget(destination.target); + } + return null; +} + +function getScopeFromTarget(target: PartialTargetDescriptor): string | null { + if (target.type === "primitive") { + if (target.modifiers != null && target.modifiers.length > 0) { + const modifier = target.modifiers[0]; + + switch (modifier.type) { + case "containingScope": + return `containing_${modifier.scopeType.type}`; + case "extendThroughStartOf": + case "extendThroughEndOf": + return modifier.type; + } + + if (target.mark?.type === "cursor") { + return target.mark.type; + } + + throw Error( + `Unknown Cursorless fallback modifier type: ${modifier.type}`, + ); + } + } + return null; +} + +// function targetIsSelection(target: PartialTargetDescriptor|DestinationDescriptor): boolean { + +// } + +// function getTarget(action: ActionDescriptor): PartialTargetDescriptor|DestinationDescriptor { +// switch (action.name) { +// // case "editNew": +// // case "getText": +// // case "replace": +// // case "executeCommand": +// // case "private.getTargets": +// // case "private.setKeyboardTarget": + +// case "replaceWithTarget": +// case "moveToTarget": +// return action.destination + +// case "swapTargets": +// return [ +// actions[action.name], +// this.handleTarget(action.target1), +// connectives.swapConnective, +// this.handleTarget(action.target2), +// ]; + +// case "callAsFunction": +// if (action.argument.type === "implicit") { +// return [actions[action.name], this.handleTarget(action.callee)]; +// } +// return [ +// actions[action.name], +// this.handleTarget(action.callee), +// "on", +// this.handleTarget(action.argument), +// ]; + +// case "wrapWithPairedDelimiter": +// case "rewrapWithPairedDelimiter": +// return [ +// surroundingPairDelimitersToSpokenForm( +// this.spokenFormMap, +// action.left, +// action.right, +// ), +// actions[action.name], +// this.handleTarget(action.target), +// ]; + +// case "pasteFromClipboard": +// return [actions[action.name], this.handleDestination(action.destination)]; + +// case "insertSnippet": +// return [ +// actions[action.name], +// insertionSnippetToSpokenForm(action.snippetDescription), +// this.handleDestination(action.destination), +// ]; + +// case "generateSnippet": +// if (action.snippetName != null) { +// throw new NoSpokenFormError(`${action.name}.snippetName`); +// } +// return [actions[action.name], this.handleTarget(action.target)]; + +// case "wrapWithSnippet": +// return [ +// wrapperSnippetToSpokenForm(action.snippetDescription), +// actions[action.name], +// this.handleTarget(action.target), +// ]; + +// case "highlight": { +// if (action.highlightId != null) { +// throw new NoSpokenFormError(`${action.name}.highlightId`); +// } +// return [actions[action.name], this.handleTarget(action.target)]; +// } + +// default: { +// return [actions[action.name], this.handleTarget(action.target)]; +// } +// } +// } diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index cee5a2aa85..2fdb336733 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -74,6 +74,7 @@ export function createCursorlessEngine( runCommand(command: Command) { return runCommand( treeSitter, + commandServerApi, debug, hatTokenMap, snippets, @@ -88,6 +89,7 @@ export function createCursorlessEngine( runCommandSafe(...args: unknown[]) { return runCommand( treeSitter, + commandServerApi, debug, hatTokenMap, snippets, diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 331200b030..4f1ae8a4b4 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -1,10 +1,20 @@ -import { Command, HatTokenMap, ReadOnlyHatMap } from "@cursorless/common"; +import { + Command, + CommandServerApi, + HatTokenMap, + ReadOnlyHatMap, +} from "@cursorless/common"; import { CommandRunner } from "./CommandRunner"; import { Actions } from "./actions/Actions"; +import { + CommandResponse, + CommandRunnerDecorator, +} from "./api/CursorlessEngineApi"; import { Debug } from "./core/Debug"; import { Snippets } from "./core/Snippets"; import { CommandRunnerImpl } from "./core/commandRunner/CommandRunnerImpl"; import { canonicalizeAndValidateCommand } from "./core/commandVersionUpgrades/canonicalizeAndValidateCommand"; +import { getCommandFallback } from "./core/getCommandFallback"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { StoredTargetMap, TreeSitter } from "./index"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; @@ -12,7 +22,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 +37,7 @@ import { CommandRunnerDecorator } from "./api/CursorlessEngineApi"; */ export async function runCommand( treeSitter: TreeSitter, + commandServerApi: CommandServerApi | null, debug: Debug, hatTokenMap: HatTokenMap, snippets: Snippets, @@ -36,14 +46,23 @@ 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)); } + const useFallback = command.version >= 6; // TODO: change to 7 const commandComplete = canonicalizeAndValidateCommand(command); + if (useFallback) { + const fallback = getCommandFallback(commandServerApi, commandComplete); + + if (fallback != null) { + return { fallback }; + } + } + const readableHatMap = await hatTokenMap.getReadableMap( commandComplete.usePrePhraseSnapshot, ); @@ -62,7 +81,9 @@ export async function runCommand( commandRunner = decorator.wrapCommandRunner(readableHatMap, commandRunner); } - return await commandRunner.run(commandComplete); + const returnValue = await commandRunner.run(commandComplete); + + return useFallback ? { returnValue } : returnValue; } function createCommandRunner( From 56b3f447850d704799a724ce6075bc71e678897a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 10:54:43 +0100 Subject: [PATCH 02/34] Made homophones work --- cursorless-talon/src/fallback.py | 20 ++++++++++++------- .../src/core/getCommandFallback.ts | 14 +++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 0514e8eade..15c3696514 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -2,6 +2,7 @@ action_callbacks = { + "getText": lambda: [actions.edit.selected_text()], "setSelection": actions.skip, "copyToClipboard": actions.edit.copy, "cutToClipboard": actions.edit.cut, @@ -17,12 +18,12 @@ scope_callbacks = { "cursor": actions.skip, - "extendThroughStartOf": actions.user.select_line_start, - "extendThroughEndOf": actions.user.select_line_end, - "containing_document": actions.edit.select_all, - "containing_paragraph": actions.edit.select_paragraph, - "containing_line": actions.edit.select_line, - "containing_token": actions.edit.select_word, + "extendThroughStartOf.line": actions.user.select_line_start, + "extendThroughEndOf.line": actions.user.select_line_end, + "containing.document": actions.edit.select_all, + "containing.paragraph": actions.edit.select_paragraph, + "containing.line": actions.edit.select_line, + "containing.token": actions.edit.select_word, } @@ -31,16 +32,20 @@ def perform_fallback(fallback: dict): scope_callback = get_scope_callback(fallback) action_callback = get_action_callback(fallback) scope_callback() - action_callback() + return action_callback() except ValueError as ex: actions.app.notify(str(ex)) def get_action_callback(fallback: dict): action = fallback["action"] + if action in action_callbacks: return action_callbacks[action] + if action == "insert": + return lambda: actions.insert(fallback["text"]) + if action == "wrapWithPairedDelimiter": return lambda: actions.user.delimiters_pair_wrap_selection_with( fallback["left"], fallback["right"] @@ -54,6 +59,7 @@ def get_scope_callback(fallback: dict): return actions.skip scope = fallback["scope"] + if scope in scope_callbacks: return scope_callbacks[scope] diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index 7b8a3067ed..428c8d712f 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -119,20 +119,22 @@ function getScopeFromTarget(target: PartialTargetDescriptor): string | null { switch (modifier.type) { case "containingScope": - return `containing_${modifier.scopeType.type}`; + return `containing.${modifier.scopeType.type}`; case "extendThroughStartOf": case "extendThroughEndOf": - return modifier.type; - } - - if (target.mark?.type === "cursor") { - return target.mark.type; + if (modifier.modifiers == null) { + return `${modifier.type}.line`; + } } throw Error( `Unknown Cursorless fallback modifier type: ${modifier.type}`, ); } + + if (target.mark?.type === "cursor") { + return target.mark.type; + } } return null; } From b44593dadcdcd0cc2173c11dbba8c2a3a7310c11 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 11:31:17 +0100 Subject: [PATCH 03/34] Implemented bring and call --- cursorless-talon/src/fallback.py | 12 ++ .../src/api/CursorlessEngineApi.ts | 1 + .../src/core/getCommandFallback.ts | 136 ++++++------------ packages/cursorless-engine/src/runCommand.ts | 20 +-- 4 files changed, 69 insertions(+), 100 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 15c3696514..c761174418 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -27,8 +27,14 @@ } +def callAsFunction(callee: str): + actions.insert(f"{callee}()") + actions.edit.left() + + def perform_fallback(fallback: dict): try: + print(fallback) scope_callback = get_scope_callback(fallback) action_callback = get_action_callback(fallback) scope_callback() @@ -46,6 +52,9 @@ def get_action_callback(fallback: dict): if action == "insert": return lambda: actions.insert(fallback["text"]) + if action == "callAsFunction": + return lambda: callAsFunction(fallback["callee"]) + if action == "wrapWithPairedDelimiter": return lambda: actions.user.delimiters_pair_wrap_selection_with( fallback["left"], fallback["right"] @@ -60,6 +69,9 @@ def get_scope_callback(fallback: dict): scope = fallback["scope"] + if scope is None: + return actions.skip + if scope in scope_callbacks: return scope_callbacks[scope] diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 38e8f9e2f5..7b79f33860 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -48,6 +48,7 @@ export type CommandResponse = { returnValue: unknown } | { fallback: Fallback }; export type Fallback = | { action: string; scope: string | null } | { action: "insert"; scope: string | null; text: string } + | { action: "callAsFunction"; scope: string | null; callee: string } | { action: "wrapWithPairedDelimiter" | "rewrapWithPairedDelimiter"; scope: string | null; diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index 428c8d712f..2e5005b859 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -2,15 +2,18 @@ import { CommandComplete, CommandServerApi, DestinationDescriptor, + LATEST_VERSION, PartialTargetDescriptor, } from "@cursorless/common"; +import { CommandRunner } from ".."; import { Fallback } from "../api/CursorlessEngineApi"; -export function getCommandFallback( +export async function getCommandFallback( commandServerApi: CommandServerApi | null, + commandRunner: CommandRunner, command: CommandComplete, -): Fallback | null { +): Promise { if (commandServerApi == null) { return null; } @@ -35,17 +38,32 @@ export function getCommandFallback( : null; case "replaceWithTarget": - case "moveToTarget": - // if (action.destination.type === "implicit") { - // } + if (destinationIsSelection(action.destination)) { + return { + action: "insert", + scope: getScopeFromDestination(action.destination), + text: await getText( + commandRunner, + command.usePrePhraseSnapshot, + action.source, + ), + }; + } return null; case "callAsFunction": + if (targetIsSelection(action.argument)) { + return { + action: action.name, + scope: getScopeFromTarget(action.argument), + callee: await getText( + commandRunner, + command.usePrePhraseSnapshot, + action.callee, + ), + }; + } return null; - // return action.argument.type === "implicit" && - // targetIsSelection(action.callee) - // ? { action: action.name } - // : null; case "wrapWithPairedDelimiter": case "rewrapWithPairedDelimiter": @@ -66,6 +84,7 @@ export function getCommandFallback( } : null; + case "moveToTarget": case "swapTargets": case "editNew": case "insertSnippet": @@ -139,86 +158,19 @@ function getScopeFromTarget(target: PartialTargetDescriptor): string | null { return null; } -// function targetIsSelection(target: PartialTargetDescriptor|DestinationDescriptor): boolean { - -// } - -// function getTarget(action: ActionDescriptor): PartialTargetDescriptor|DestinationDescriptor { -// switch (action.name) { -// // case "editNew": -// // case "getText": -// // case "replace": -// // case "executeCommand": -// // case "private.getTargets": -// // case "private.setKeyboardTarget": - -// case "replaceWithTarget": -// case "moveToTarget": -// return action.destination - -// case "swapTargets": -// return [ -// actions[action.name], -// this.handleTarget(action.target1), -// connectives.swapConnective, -// this.handleTarget(action.target2), -// ]; - -// case "callAsFunction": -// if (action.argument.type === "implicit") { -// return [actions[action.name], this.handleTarget(action.callee)]; -// } -// return [ -// actions[action.name], -// this.handleTarget(action.callee), -// "on", -// this.handleTarget(action.argument), -// ]; - -// case "wrapWithPairedDelimiter": -// case "rewrapWithPairedDelimiter": -// return [ -// surroundingPairDelimitersToSpokenForm( -// this.spokenFormMap, -// action.left, -// action.right, -// ), -// actions[action.name], -// this.handleTarget(action.target), -// ]; - -// case "pasteFromClipboard": -// return [actions[action.name], this.handleDestination(action.destination)]; - -// case "insertSnippet": -// return [ -// actions[action.name], -// insertionSnippetToSpokenForm(action.snippetDescription), -// this.handleDestination(action.destination), -// ]; - -// case "generateSnippet": -// if (action.snippetName != null) { -// throw new NoSpokenFormError(`${action.name}.snippetName`); -// } -// return [actions[action.name], this.handleTarget(action.target)]; - -// case "wrapWithSnippet": -// return [ -// wrapperSnippetToSpokenForm(action.snippetDescription), -// actions[action.name], -// this.handleTarget(action.target), -// ]; - -// case "highlight": { -// if (action.highlightId != null) { -// throw new NoSpokenFormError(`${action.name}.highlightId`); -// } -// return [actions[action.name], this.handleTarget(action.target)]; -// } - -// default: { -// return [actions[action.name], this.handleTarget(action.target)]; -// } -// } -// } +async function getText( + commandRunner: CommandRunner, + usePrePhraseSnapshot: boolean, + target: PartialTargetDescriptor, +): Promise { + const returnValue = await commandRunner.run({ + version: LATEST_VERSION, + usePrePhraseSnapshot, + action: { + name: "getText", + target, + }, + }); + const replaceWith = returnValue as string[]; + return replaceWith.join("\n"); +} diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 4f1ae8a4b4..d654c269e7 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -55,14 +55,6 @@ export async function runCommand( const useFallback = command.version >= 6; // TODO: change to 7 const commandComplete = canonicalizeAndValidateCommand(command); - if (useFallback) { - const fallback = getCommandFallback(commandServerApi, commandComplete); - - if (fallback != null) { - return { fallback }; - } - } - const readableHatMap = await hatTokenMap.getReadableMap( commandComplete.usePrePhraseSnapshot, ); @@ -81,6 +73,18 @@ export async function runCommand( commandRunner = decorator.wrapCommandRunner(readableHatMap, commandRunner); } + if (useFallback) { + const fallback = await getCommandFallback( + commandServerApi, + commandRunner, + commandComplete, + ); + + if (fallback != null) { + return { fallback }; + } + } + const returnValue = await commandRunner.run(commandComplete); return useFallback ? { returnValue } : returnValue; From a716e078359c5dcd736f040cd455e6f1dc2c7327 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 11:34:46 +0100 Subject: [PATCH 04/34] Added move --- .../src/core/getCommandFallback.ts | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index 2e5005b859..4d760bffd4 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -51,6 +51,26 @@ export async function getCommandFallback( } return null; + case "moveToTarget": + if (destinationIsSelection(action.destination)) { + const text = await getText( + commandRunner, + command.usePrePhraseSnapshot, + action.source, + ); + await remove( + commandRunner, + command.usePrePhraseSnapshot, + action.source, + ); + return { + action: "insert", + scope: getScopeFromDestination(action.destination), + text, + }; + } + return null; + case "callAsFunction": if (targetIsSelection(action.argument)) { return { @@ -84,7 +104,6 @@ export async function getCommandFallback( } : null; - case "moveToTarget": case "swapTargets": case "editNew": case "insertSnippet": @@ -174,3 +193,18 @@ async function getText( const replaceWith = returnValue as string[]; return replaceWith.join("\n"); } + +function remove( + commandRunner: CommandRunner, + usePrePhraseSnapshot: boolean, + target: PartialTargetDescriptor, +): Promise { + return commandRunner.run({ + version: LATEST_VERSION, + usePrePhraseSnapshot, + action: { + name: "remove", + target, + }, + }); +} From 2a6cca5cba93ebc4356b83c07bf92329fe6c4f67 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 11:53:28 +0100 Subject: [PATCH 05/34] Cleanup --- cursorless-talon/src/command.py | 15 +++++++-------- packages/common/src/index.ts | 1 + .../common/src/types/command/CommandV7.types.ts | 8 ++++++++ .../common/src/types/command/command.types.ts | 8 +++++--- .../canonicalizeAndValidateCommand.ts | 4 ++++ .../core/commandVersionUpgrades/upgradeV6ToV7.ts | 5 +++++ .../src/generateSpokenForm/getHatMapCommand.ts | 2 +- packages/cursorless-engine/src/runCommand.ts | 2 +- .../recorded/testCaseRecorder/takeHarp.yml | 2 +- 9 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 packages/common/src/types/command/CommandV7.types.ts create mode 100644 packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV6ToV7.ts diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index 133df815f2..cee63df523 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -8,7 +8,7 @@ @dataclasses.dataclass class CursorlessCommand: - version = 6 + version = 7 spokenForm: str usePrePhraseSnapshot: bool action: dict @@ -36,7 +36,7 @@ def private_cursorless_command_and_wait(action: dict): CURSORLESS_COMMAND_ID, construct_cursorless_command(action), ) - if type(response) is dict and "fallback" in response: + if "fallback" in response: perform_fallback(response["fallback"]) def private_cursorless_command_no_wait(action: dict): @@ -52,12 +52,11 @@ def private_cursorless_command_get(action: dict): CURSORLESS_COMMAND_ID, construct_cursorless_command(action), ) - if type(response) is dict: - if "fallback" in response: - return perform_fallback(response["fallback"]) - if "returnValue" in response: - return response["returnValue"] - return response + 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/packages/common/src/index.ts b/packages/common/src/index.ts index 2770a19025..719ccacce7 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -85,6 +85,7 @@ 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"; 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..8e82ab6893 100644 --- a/packages/common/src/types/command/command.types.ts +++ b/packages/common/src/types/command/command.types.ts @@ -1,4 +1,5 @@ -import { CommandV6 } from "./CommandV6.types"; +import type { CommandV6 } from "./CommandV6.types"; +import type { CommandV7 } from "./CommandV7.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 +8,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 +21,5 @@ export type Command = | CommandV3 | CommandV4 | CommandV5 - | CommandV6; + | CommandV6 + | CommandV7; 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/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 d654c269e7..8bbf029291 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -52,7 +52,7 @@ export async function runCommand( debug.log(JSON.stringify(command, null, 2)); } - const useFallback = command.version >= 6; // TODO: change to 7 + const useFallback = command.version >= 7; const commandComplete = canonicalizeAndValidateCommand(command); const readableHatMap = await hatTokenMap.getReadableMap( 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 From 4577c7b8aa15452392f07de777ee5f2cf08da5b4 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 12:18:06 +0100 Subject: [PATCH 06/34] unwrap return value --- .../src/suite/recorded.vscode.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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..6cce2dbaff 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -34,7 +34,7 @@ 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"; +import { isUndefined, isPlainObject } from "lodash"; function createPosition(position: PositionPlainObject) { return new vscode.Position(position.line, position.character); @@ -118,6 +118,12 @@ async function runTest(file: string, spyIde: SpyIDE) { ...fixture.command, usePrePhraseSnapshot, }); + if (isPlainObject(returnValue)) { + const returnValueObj = returnValue as Record; + if ("returnValue" in returnValueObj) { + returnValue = returnValueObj.returnValue; + } + } } catch (err) { const error = err as Error; From 3064930ecb78d0d4da2e7f27ce121f7a0238cc40 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:29:04 +0000 Subject: [PATCH 07/34] [pre-commit.ci lite] apply automatic fixes --- cursorless-talon/src/fallback.py | 1 - packages/cursorless-engine/src/api/CursorlessEngineApi.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index c761174418..c6955ce070 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -1,6 +1,5 @@ from talon import actions - action_callbacks = { "getText": lambda: [actions.edit.selected_text()], "setSelection": actions.skip, diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 7b79f33860..fca0f1b7e5 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -1,4 +1,4 @@ -import { Command, HatTokenMap, IDE, ReplaceWith } from "@cursorless/common"; +import { Command, HatTokenMap, IDE } from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { StoredTargetMap } from "../core/StoredTargets"; import { ScopeProvider } from "@cursorless/common"; From f2bf6ac6b3354ded07a9f46d1bcc0f3bd2000621 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 10 Feb 2024 14:50:55 +0100 Subject: [PATCH 08/34] remove print --- cursorless-talon/src/fallback.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index c6955ce070..e839d99ce9 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -33,7 +33,6 @@ def callAsFunction(callee: str): def perform_fallback(fallback: dict): try: - print(fallback) scope_callback = get_scope_callback(fallback) action_callback = get_action_callback(fallback) scope_callback() From 3c325d6681a839bd6696901063d38d9dcc1464a0 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 12 Feb 2024 16:55:47 +0100 Subject: [PATCH 09/34] updates --- cursorless-talon/src/fallback.py | 90 ++++++++++++------- .../src/api/CursorlessEngineApi.ts | 18 ++-- .../src/core/getCommandFallback.ts | 74 +++++++-------- 3 files changed, 103 insertions(+), 79 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index e839d99ce9..7f6a4462c4 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -1,3 +1,4 @@ +from typing import Callable from talon import actions action_callbacks = { @@ -8,69 +9,98 @@ "pasteFromClipboard": actions.edit.paste, "clearAndSetSelection": actions.edit.delete, "remove": actions.edit.delete, - "applyFormatter": actions.user.reformat_selection, "editNewLineBefore": actions.edit.line_insert_up, "editNewLineAfter": actions.edit.line_insert_down, "nextHomophone": actions.user.homophones_cycle_selected, - # "replaceWithTarget": replace_with_target, } -scope_callbacks = { - "cursor": actions.skip, +modifier_callbacks = { "extendThroughStartOf.line": actions.user.select_line_start, "extendThroughEndOf.line": actions.user.select_line_end, - "containing.document": actions.edit.select_all, - "containing.paragraph": actions.edit.select_paragraph, - "containing.line": actions.edit.select_line, - "containing.token": actions.edit.select_word, + "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 callAsFunction(callee: str): +def call_as_function(callee: str): actions.insert(f"{callee}()") actions.edit.left() +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: - scope_callback = get_scope_callback(fallback) + modifier_callbacks = get_modifier_callbacks(fallback) action_callback = get_action_callback(fallback) - scope_callback() + 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): +def get_action_callback(fallback: dict) -> Callable: action = fallback["action"] if action in action_callbacks: return action_callbacks[action] - if action == "insert": - return lambda: actions.insert(fallback["text"]) + 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"] + ) - if action == "callAsFunction": - return lambda: callAsFunction(fallback["callee"]) + raise ValueError(f"Unknown Cursorless fallback action: {action}") - if action == "wrapWithPairedDelimiter": - return lambda: actions.user.delimiters_pair_wrap_selection_with( - fallback["left"], fallback["right"] - ) - raise ValueError(f"Unknown Cursorless fallback action: {action}") +def get_modifier_callbacks(fallback: dict) -> list[Callable]: + modifiers = fallback["modifiers"] + callbacks = [] + + for modifier in modifiers: + callbacks.append(get_modifier_callback(modifier)) + + return callbacks -def get_scope_callback(fallback: dict): - if "scope" not in fallback: - return actions.skip +def get_modifier_callback(modifier: dict) -> Callable: + modifier_type = modifier["type"] - scope = fallback["scope"] + 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") - if scope is None: - return actions.skip + raise ValueError(f"Unknown Cursorless fallback modifier: {modifier_type}") - if scope in scope_callbacks: - return scope_callbacks[scope] - raise ValueError(f"Unknown Cursorless fallback scope: {scope}") +def get_simple_modifier_callback(key: str) -> Callable: + if key in modifier_callbacks: + return modifier_callbacks[key] + raise ValueError(f"Unknown Cursorless fallback modifier: {key}") diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index fca0f1b7e5..d7cf3e5ec4 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -1,4 +1,10 @@ -import { Command, HatTokenMap, IDE } from "@cursorless/common"; +import { + ActionDescriptor, + Command, + HatTokenMap, + IDE, + Modifier, +} from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { StoredTargetMap } from "../core/StoredTargets"; import { ScopeProvider } from "@cursorless/common"; @@ -45,13 +51,15 @@ export interface CommandApi { export type CommandResponse = { returnValue: unknown } | { fallback: Fallback }; +export type FallbackModifier = Modifier | { type: "containingTokenIfEmpty" }; + export type Fallback = - | { action: string; scope: string | null } - | { action: "insert"; scope: string | null; text: string } - | { action: "callAsFunction"; scope: string | null; callee: string } + | { action: ActionDescriptor["name"]; modifiers: FallbackModifier[] } + | { action: "insert"; modifiers: FallbackModifier[]; text: string } + | { action: "callAsFunction"; modifiers: FallbackModifier[]; callee: string } | { action: "wrapWithPairedDelimiter" | "rewrapWithPairedDelimiter"; - scope: string | null; + modifiers: FallbackModifier[]; left: string; right: string; }; diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index 4d760bffd4..403106972c 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -2,25 +2,20 @@ import { CommandComplete, CommandServerApi, DestinationDescriptor, - LATEST_VERSION, PartialTargetDescriptor, } from "@cursorless/common"; - -import { CommandRunner } from ".."; -import { Fallback } from "../api/CursorlessEngineApi"; +import { Fallback, FallbackModifier } from "../api/CursorlessEngineApi"; +import { CommandRunner } from "../CommandRunner"; export async function getCommandFallback( commandServerApi: CommandServerApi | null, commandRunner: CommandRunner, command: CommandComplete, ): Promise { - if (commandServerApi == null) { - return null; - } - - const focusedElement = commandServerApi?.getFocusedElementType(); - - if (focusedElement === "textEditor") { + if ( + commandServerApi == null || + commandServerApi.getFocusedElementType() === "textEditor" + ) { return null; } @@ -32,7 +27,7 @@ export async function getCommandFallback( Array.isArray(action.replaceWith) ? { action: "insert", - scope: getScopeFromDestination(action.destination), + modifiers: getModifiersFromDestination(action.destination), text: action.replaceWith.join("\n"), } : null; @@ -41,7 +36,7 @@ export async function getCommandFallback( if (destinationIsSelection(action.destination)) { return { action: "insert", - scope: getScopeFromDestination(action.destination), + modifiers: getModifiersFromDestination(action.destination), text: await getText( commandRunner, command.usePrePhraseSnapshot, @@ -65,7 +60,7 @@ export async function getCommandFallback( ); return { action: "insert", - scope: getScopeFromDestination(action.destination), + modifiers: getModifiersFromDestination(action.destination), text, }; } @@ -75,7 +70,7 @@ export async function getCommandFallback( if (targetIsSelection(action.argument)) { return { action: action.name, - scope: getScopeFromTarget(action.argument), + modifiers: getModifiersFromTarget(action.argument), callee: await getText( commandRunner, command.usePrePhraseSnapshot, @@ -90,7 +85,7 @@ export async function getCommandFallback( return targetIsSelection(action.target) ? { action: action.name, - scope: getScopeFromTarget(action.target), + modifiers: getModifiersFromTarget(action.target), left: action.left, right: action.right, } @@ -100,7 +95,7 @@ export async function getCommandFallback( return destinationIsSelection(action.destination) ? { action: action.name, - scope: getScopeFromDestination(action.destination), + modifiers: getModifiersFromDestination(action.destination), } : null; @@ -113,7 +108,10 @@ export async function getCommandFallback( default: return targetIsSelection(action.target) - ? { action: action.name, scope: getScopeFromTarget(action.target) } + ? { + action: action.name, + modifiers: getModifiersFromTarget(action.target), + } : null; } } @@ -141,40 +139,28 @@ function targetIsSelection(target: PartialTargetDescriptor): boolean { return false; } -function getScopeFromDestination( +function getModifiersFromDestination( destination: DestinationDescriptor, -): string | null { +): FallbackModifier[] { if (destination.type === "primitive") { - return getScopeFromTarget(destination.target); + return getModifiersFromTarget(destination.target); } - return null; + return []; } -function getScopeFromTarget(target: PartialTargetDescriptor): string | null { +function getModifiersFromTarget( + target: PartialTargetDescriptor, +): FallbackModifier[] { if (target.type === "primitive") { if (target.modifiers != null && target.modifiers.length > 0) { - const modifier = target.modifiers[0]; - - switch (modifier.type) { - case "containingScope": - return `containing.${modifier.scopeType.type}`; - case "extendThroughStartOf": - case "extendThroughEndOf": - if (modifier.modifiers == null) { - return `${modifier.type}.line`; - } - } - - throw Error( - `Unknown Cursorless fallback modifier type: ${modifier.type}`, - ); + return target.modifiers; } if (target.mark?.type === "cursor") { - return target.mark.type; + return [{ type: "containingTokenIfEmpty" }]; } } - return null; + return []; } async function getText( @@ -183,15 +169,15 @@ async function getText( target: PartialTargetDescriptor, ): Promise { const returnValue = await commandRunner.run({ - version: LATEST_VERSION, + version: 7, usePrePhraseSnapshot, action: { name: "getText", target, }, }); - const replaceWith = returnValue as string[]; - return replaceWith.join("\n"); + const texts = returnValue as string[]; + return texts.join("\n"); } function remove( @@ -200,7 +186,7 @@ function remove( target: PartialTargetDescriptor, ): Promise { return commandRunner.run({ - version: LATEST_VERSION, + version: 7, usePrePhraseSnapshot, action: { name: "remove", From 407dc945a32ab14ee92c1f8b01a19bc4fc894ea1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:57:12 +0000 Subject: [PATCH 10/34] [pre-commit.ci lite] apply automatic fixes --- cursorless-talon/src/fallback.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 7f6a4462c4..12ad3dc51a 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -1,4 +1,5 @@ from typing import Callable + from talon import actions action_callbacks = { From 3205c10a0106e4e3e7132d5fe0c95e662b76f1f6 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 03:26:28 +0100 Subject: [PATCH 11/34] move fall back into command runner --- .../cursorless-engine/src/CommandRunner.ts | 5 +- .../core/commandRunner/CommandRunnerImpl.ts | 27 +++++++-- .../src/core/getCommandFallback.ts | 57 +++++-------------- packages/cursorless-engine/src/runCommand.ts | 21 ++----- 4 files changed, 42 insertions(+), 68 deletions(-) diff --git a/packages/cursorless-engine/src/CommandRunner.ts b/packages/cursorless-engine/src/CommandRunner.ts index 39942f1226..5d4c9be3ab 100644 --- a/packages/cursorless-engine/src/CommandRunner.ts +++ b/packages/cursorless-engine/src/CommandRunner.ts @@ -1,5 +1,6 @@ -import { CommandComplete } from "@cursorless/common"; +import type { CommandComplete } from "@cursorless/common"; +import type { CommandResponse } from "."; export interface CommandRunner { - run(command: CommandComplete): Promise; + run(command: CommandComplete): Promise; } diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 277eeaff81..c19acdef09 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -1,17 +1,19 @@ import { + ActionDescriptor, CommandComplete, + CommandServerApi, DestinationDescriptor, - ActionDescriptor, PartialTargetDescriptor, } from "@cursorless/common"; import { CommandRunner } from "../../CommandRunner"; import { ActionRecord, ActionReturnValue } from "../../actions/actions.types"; -import { StoredTargetMap } from "../../index"; +import { CommandResponse, StoredTargetMap } from "../../index"; import { TargetPipelineRunner } from "../../processTargets"; 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,6 +23,7 @@ export class CommandRunnerImpl implements CommandRunner { private noAutomaticTokenExpansion: boolean | undefined; constructor( + private commandServerApi: CommandServerApi | null, private debug: Debug, private storedTargets: StoredTargetMap, private pipelineRunner: TargetPipelineRunner, @@ -46,7 +49,21 @@ 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 { + const useFallback = command.version >= 7; + + if (useFallback) { + const fallback = await getCommandFallback( + this.commandServerApi, + this.runAction, + command, + ); + + if (fallback != null) { + return { fallback }; + } + } + const { returnValue, thatSelections: newThatSelections, @@ -55,7 +72,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 +85,7 @@ export class CommandRunnerImpl implements CommandRunner { this.storedTargets.set("instanceReference", newInstanceReferenceTargets); this.storedTargets.set("keyboard", newKeyboardTargets); - return returnValue; + return useFallback ? { returnValue } : returnValue; } private runAction( diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index 403106972c..ce54d792ce 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -1,15 +1,16 @@ import { + ActionDescriptor, CommandComplete, CommandServerApi, DestinationDescriptor, PartialTargetDescriptor, } from "@cursorless/common"; +import { ActionReturnValue } from "../actions/actions.types"; import { Fallback, FallbackModifier } from "../api/CursorlessEngineApi"; -import { CommandRunner } from "../CommandRunner"; export async function getCommandFallback( commandServerApi: CommandServerApi | null, - commandRunner: CommandRunner, + runAction: (actionDescriptor: ActionDescriptor) => Promise, command: CommandComplete, ): Promise { if ( @@ -37,27 +38,15 @@ export async function getCommandFallback( return { action: "insert", modifiers: getModifiersFromDestination(action.destination), - text: await getText( - commandRunner, - command.usePrePhraseSnapshot, - action.source, - ), + text: await getText(runAction, action.source), }; } return null; case "moveToTarget": if (destinationIsSelection(action.destination)) { - const text = await getText( - commandRunner, - command.usePrePhraseSnapshot, - action.source, - ); - await remove( - commandRunner, - command.usePrePhraseSnapshot, - action.source, - ); + const text = await getText(runAction, action.source); + await remove(runAction, action.source); return { action: "insert", modifiers: getModifiersFromDestination(action.destination), @@ -71,11 +60,7 @@ export async function getCommandFallback( return { action: action.name, modifiers: getModifiersFromTarget(action.argument), - callee: await getText( - commandRunner, - command.usePrePhraseSnapshot, - action.callee, - ), + callee: await getText(runAction, action.callee), }; } return null; @@ -164,33 +149,17 @@ function getModifiersFromTarget( } async function getText( - commandRunner: CommandRunner, - usePrePhraseSnapshot: boolean, + runAction: (actionDescriptor: ActionDescriptor) => Promise, target: PartialTargetDescriptor, ): Promise { - const returnValue = await commandRunner.run({ - version: 7, - usePrePhraseSnapshot, - action: { - name: "getText", - target, - }, - }); + const returnValue = await runAction({ name: "getText", target }); const texts = returnValue as string[]; return texts.join("\n"); } -function remove( - commandRunner: CommandRunner, - usePrePhraseSnapshot: boolean, +async function remove( + runAction: (actionDescriptor: ActionDescriptor) => Promise, target: PartialTargetDescriptor, -): Promise { - return commandRunner.run({ - version: 7, - usePrePhraseSnapshot, - action: { - name: "remove", - target, - }, - }); +): Promise { + await runAction({ name: "remove", target }); } diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 8bbf029291..50bba1580f 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -14,7 +14,6 @@ import { Debug } from "./core/Debug"; import { Snippets } from "./core/Snippets"; import { CommandRunnerImpl } from "./core/commandRunner/CommandRunnerImpl"; import { canonicalizeAndValidateCommand } from "./core/commandVersionUpgrades/canonicalizeAndValidateCommand"; -import { getCommandFallback } from "./core/getCommandFallback"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { StoredTargetMap, TreeSitter } from "./index"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; @@ -52,7 +51,6 @@ export async function runCommand( debug.log(JSON.stringify(command, null, 2)); } - const useFallback = command.version >= 7; const commandComplete = canonicalizeAndValidateCommand(command); const readableHatMap = await hatTokenMap.getReadableMap( @@ -61,6 +59,7 @@ export async function runCommand( let commandRunner = createCommandRunner( treeSitter, + commandServerApi, languageDefinitions, debug, storedTargets, @@ -73,25 +72,12 @@ export async function runCommand( commandRunner = decorator.wrapCommandRunner(readableHatMap, commandRunner); } - if (useFallback) { - const fallback = await getCommandFallback( - commandServerApi, - commandRunner, - commandComplete, - ); - - if (fallback != null) { - return { fallback }; - } - } - - const returnValue = await commandRunner.run(commandComplete); - - return useFallback ? { returnValue } : returnValue; + return await commandRunner.run(commandComplete); } function createCommandRunner( treeSitter: TreeSitter, + commandServerApi: CommandServerApi | null, languageDefinitions: LanguageDefinitions, debug: Debug, storedTargets: StoredTargetMap, @@ -115,6 +101,7 @@ function createCommandRunner( ); markStageFactory.setPipelineRunner(targetPipelineRunner); return new CommandRunnerImpl( + commandServerApi, debug, storedTargets, targetPipelineRunner, From f716a58ad5cdc549aeb89565ef219f6267620829 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 04:03:59 +0100 Subject: [PATCH 12/34] refactoring --- .../cursorless-engine/src/CommandRunner.ts | 2 +- .../src/api/CursorlessEngineApi.ts | 2 +- .../core/commandRunner/CommandRunnerImpl.ts | 10 ++++---- .../src/core/getCommandFallback.ts | 9 ++++++-- .../cursorless-engine/src/cursorlessEngine.ts | 23 ++++++++++++++++--- packages/cursorless-engine/src/runCommand.ts | 2 +- .../src/testCaseRecorder/TestCase.ts | 14 ++++++++--- .../src/testCaseRecorder/TestCaseRecorder.ts | 3 ++- 8 files changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/cursorless-engine/src/CommandRunner.ts b/packages/cursorless-engine/src/CommandRunner.ts index 5d4c9be3ab..194a4945b5 100644 --- a/packages/cursorless-engine/src/CommandRunner.ts +++ b/packages/cursorless-engine/src/CommandRunner.ts @@ -2,5 +2,5 @@ import type { CommandComplete } from "@cursorless/common"; import type { CommandResponse } from "."; 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 d7cf3e5ec4..88114ea378 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -40,7 +40,7 @@ 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 diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index c19acdef09..95c8408fc5 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -13,7 +13,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 { getCommandFallback, useFallback } from "../getCommandFallback"; import { inferFullTargetDescriptor } from "../inferFullTargetDescriptor"; import { selectionToStoredTarget } from "./selectionToStoredTarget"; @@ -49,10 +49,8 @@ export class CommandRunnerImpl implements CommandRunner { * action, and returns the desired return value indicated by the action, if * it has one. */ - async run(command: CommandComplete): Promise { - const useFallback = command.version >= 7; - - if (useFallback) { + async run(command: CommandComplete): Promise { + if (useFallback(command)) { const fallback = await getCommandFallback( this.commandServerApi, this.runAction, @@ -85,7 +83,7 @@ export class CommandRunnerImpl implements CommandRunner { this.storedTargets.set("instanceReference", newInstanceReferenceTargets); this.storedTargets.set("keyboard", newKeyboardTargets); - return useFallback ? { returnValue } : returnValue; + return { returnValue }; } private runAction( diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index ce54d792ce..a24b522265 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -1,5 +1,6 @@ import { ActionDescriptor, + Command, CommandComplete, CommandServerApi, DestinationDescriptor, @@ -8,6 +9,10 @@ import { import { ActionReturnValue } from "../actions/actions.types"; import { Fallback, FallbackModifier } from "../api/CursorlessEngineApi"; +export function useFallback(command: Command): boolean { + return command.version >= 7; +} + export async function getCommandFallback( commandServerApi: CommandServerApi | null, runAction: (actionDescriptor: ActionDescriptor) => Promise, @@ -152,8 +157,8 @@ async function getText( runAction: (actionDescriptor: ActionDescriptor) => Promise, target: PartialTargetDescriptor, ): Promise { - const returnValue = await runAction({ name: "getText", target }); - const texts = returnValue as string[]; + const response = await runAction({ name: "getText", target }); + const texts = response.returnValue as string[]; return texts.join("\n"); } diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 2fdb336733..2622832733 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -9,6 +9,7 @@ import { import { StoredTargetMap } from "./core/StoredTargets"; import { TreeSitter } from "./typings/TreeSitter"; import { + CommandResponse, CommandRunnerDecorator, CursorlessEngine, } from "./api/CursorlessEngineApi"; @@ -30,6 +31,7 @@ import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker"; import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher"; import { TalonSpokenFormsJsonReader } from "./nodeCommon/TalonSpokenFormsJsonReader"; import { injectIde } from "./singletons/ide.singleton"; +import { useFallback } from "./core/getCommandFallback"; export function createCursorlessEngine( treeSitter: TreeSitter, @@ -86,8 +88,9 @@ export function createCursorlessEngine( ); }, - runCommandSafe(...args: unknown[]) { - return runCommand( + async runCommandSafe(...args: unknown[]) { + const command = ensureCommandShape(args); + const response = await runCommand( treeSitter, commandServerApi, debug, @@ -97,8 +100,9 @@ export function createCursorlessEngine( languageDefinitions, rangeUpdater, commandRunnerDecorators, - ensureCommandShape(args), + command, ); + return unwrapCommandResponse(command, response); }, }, scopeProvider: createScopeProvider( @@ -119,6 +123,19 @@ export function createCursorlessEngine( }; } +async function unwrapCommandResponse( + command: Command, + response: CommandResponse, +): Promise { + if (useFallback(command)) { + return response; + } + if ("returnValue" in response) { + return response.returnValue; + } + return undefined; +} + function createScopeProvider( languageDefinitions: LanguageDefinitions, storedTargets: StoredTargetMap, diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 50bba1580f..ed58d0edfa 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -45,7 +45,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)); diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts index 04b85fa684..4ac260d87f 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts @@ -18,7 +18,7 @@ import { Token, } from "@cursorless/common"; import { pick } from "lodash"; -import { StoredTargetMap } from ".."; +import { CommandResponse, Fallback, StoredTargetMap } from ".."; import { ide } from "../singletons/ide.singleton"; import { extractTargetKeys } from "../testUtil/extractTargetKeys"; import { takeSnapshot } from "../testUtil/takeSnapshot"; @@ -32,6 +32,7 @@ export class TestCase { private finalState?: TestCaseSnapshot; thrownError?: ThrownError; private returnValue?: unknown; + private fallback?: Fallback; private targetKeys: string[]; private _awaitingFinalMarkInfo: boolean; private marksToCheck?: string[]; @@ -162,9 +163,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..7949a51223 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts @@ -34,6 +34,7 @@ import { CommandRunner } from "../CommandRunner"; import { RecordTestCaseCommandOptions } from "./RecordTestCaseCommandOptions"; import { SpokenFormGenerator } from "../generateSpokenForm"; import { defaultSpokenFormMap } from "../spokenForms/defaultSpokenFormMap"; +import { CommandResponse } from "../api/CursorlessEngineApi"; const CALIBRATION_DISPLAY_DURATION_MS = 50; @@ -321,7 +322,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 From 0742049596c57b6726081d4327568b5bb3f0b7bb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 04:23:25 +0100 Subject: [PATCH 13/34] Recorded test --- .../src/testUtil/serializeTestFixture.ts | 1 + packages/common/src/types/TestCaseFixture.ts | 11 +++++-- .../common/src/types/command/command.types.ts | 17 +++++++++++ .../cursorless-engine/src/CommandRunner.ts | 3 +- .../src/api/CursorlessEngineApi.ts | 30 +++++-------------- .../core/commandRunner/CommandRunnerImpl.ts | 3 +- .../src/core/getCommandFallback.ts | 5 ++-- .../cursorless-engine/src/cursorlessEngine.ts | 10 +++---- packages/cursorless-engine/src/runCommand.ts | 6 ++-- .../src/testCaseRecorder/TestCase.ts | 5 +++- .../src/testCaseRecorder/TestCaseRecorder.ts | 12 ++++---- .../fixtures/recorded/fallback/takeToken.yml | 28 +++++++++++++++++ 12 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml diff --git a/packages/common/src/testUtil/serializeTestFixture.ts b/packages/common/src/testUtil/serializeTestFixture.ts index 4e49c08303..edf04d43fb 100644 --- a/packages/common/src/testUtil/serializeTestFixture.ts +++ b/packages/common/src/testUtil/serializeTestFixture.ts @@ -15,6 +15,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/TestCaseFixture.ts b/packages/common/src/types/TestCaseFixture.ts index 5424b7d0c2..7fceab66d0 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 } from ".."; +import type { TestCaseSnapshot } from "../testUtil/TestCaseSnapshot"; +import type { PlainSpyIDERecordedValues } from "../testUtil/spyToPlainObject"; export type ThrownError = { name: string; @@ -30,6 +30,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/command.types.ts b/packages/common/src/types/command/command.types.ts index 8e82ab6893..1a6f9e260a 100644 --- a/packages/common/src/types/command/command.types.ts +++ b/packages/common/src/types/command/command.types.ts @@ -1,5 +1,7 @@ +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"; @@ -23,3 +25,18 @@ export type Command = | CommandV5 | 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/cursorless-engine/src/CommandRunner.ts b/packages/cursorless-engine/src/CommandRunner.ts index 194a4945b5..d70f19d58b 100644 --- a/packages/cursorless-engine/src/CommandRunner.ts +++ b/packages/cursorless-engine/src/CommandRunner.ts @@ -1,5 +1,4 @@ -import type { CommandComplete } from "@cursorless/common"; -import type { CommandResponse } from "."; +import type { CommandComplete, CommandResponse } from "@cursorless/common"; export interface CommandRunner { run(command: CommandComplete): Promise; diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 88114ea378..84743fbfe5 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -1,15 +1,14 @@ -import { - ActionDescriptor, +import type { Command, + CommandResponse, HatTokenMap, IDE, - Modifier, + ReadOnlyHatMap, + ScopeProvider, } 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 { CommandRunner } from "../CommandRunner"; +import type { Snippets } from "../core/Snippets"; +import type { StoredTargetMap } from "../core/StoredTargets"; export interface CursorlessEngine { commandApi: CommandApi; @@ -49,21 +48,6 @@ export interface CommandApi { runCommandSafe(...args: unknown[]): Promise; } -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; - }; - export interface CommandRunnerDecorator { /** * @param commandRunner: A CommandRunner. diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 95c8408fc5..3da0a57005 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -1,13 +1,14 @@ import { ActionDescriptor, CommandComplete, + CommandResponse, CommandServerApi, DestinationDescriptor, PartialTargetDescriptor, } from "@cursorless/common"; import { CommandRunner } from "../../CommandRunner"; import { ActionRecord, ActionReturnValue } from "../../actions/actions.types"; -import { CommandResponse, StoredTargetMap } from "../../index"; +import { StoredTargetMap } from "../../index"; import { TargetPipelineRunner } from "../../processTargets"; import { ModifierStage } from "../../processTargets/PipelineStages.types"; import { SelectionWithEditor } from "../../typings/Types"; diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index a24b522265..6a973f8eca 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -4,10 +4,11 @@ import { CommandComplete, CommandServerApi, DestinationDescriptor, + Fallback, + FallbackModifier, PartialTargetDescriptor, } from "@cursorless/common"; -import { ActionReturnValue } from "../actions/actions.types"; -import { Fallback, FallbackModifier } from "../api/CursorlessEngineApi"; +import type { ActionReturnValue } from "../actions/actions.types"; export function useFallback(command: Command): boolean { return command.version >= 7; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 2622832733..7f960afe15 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,25 +1,26 @@ import { Command, + CommandResponse, CommandServerApi, FileSystem, Hats, IDE, ScopeProvider, } from "@cursorless/common"; -import { StoredTargetMap } from "./core/StoredTargets"; -import { TreeSitter } from "./typings/TreeSitter"; import { - CommandResponse, CommandRunnerDecorator, CursorlessEngine, } from "./api/CursorlessEngineApi"; 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 { useFallback } from "./core/getCommandFallback"; 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"; @@ -29,9 +30,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 { useFallback } from "./core/getCommandFallback"; +import { TreeSitter } from "./typings/TreeSitter"; export function createCursorlessEngine( treeSitter: TreeSitter, diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index ed58d0edfa..ee42c80c10 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -1,15 +1,13 @@ import { Command, + CommandResponse, CommandServerApi, HatTokenMap, ReadOnlyHatMap, } from "@cursorless/common"; import { CommandRunner } from "./CommandRunner"; import { Actions } from "./actions/Actions"; -import { - CommandResponse, - CommandRunnerDecorator, -} from "./api/CursorlessEngineApi"; +import { CommandRunnerDecorator } from "./api/CursorlessEngineApi"; import { Debug } from "./core/Debug"; import { Snippets } from "./core/Snippets"; import { CommandRunnerImpl } from "./core/commandRunner/CommandRunnerImpl"; diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts index 4ac260d87f..8f365afba9 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts @@ -1,9 +1,11 @@ import { ActionType, CommandLatest, + CommandResponse, EnforceUndefined, extractTargetedMarks, ExtraSnapshotField, + Fallback, marksToPlainObject, PartialTargetDescriptor, PlainSpyIDERecordedValues, @@ -18,7 +20,7 @@ import { Token, } from "@cursorless/common"; import { pick } from "lodash"; -import { CommandResponse, Fallback, StoredTargetMap } from ".."; +import { StoredTargetMap } from ".."; import { ide } from "../singletons/ide.singleton"; import { extractTargetKeys } from "../testUtil/extractTargetKeys"; import { takeSnapshot } from "../testUtil/takeSnapshot"; @@ -144,6 +146,7 @@ export class TestCase { initialState: this.initialState, finalState: this.finalState, returnValue: this.returnValue, + fallback: this.fallback, thrownError: this.thrownError, ide: this.spyIdeValues, }; diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts index 7949a51223..52e9d97924 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts @@ -1,6 +1,7 @@ import { CommandComplete, CommandLatest, + CommandResponse, DecoratedSymbolMark, DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, extractTargetedMarks, @@ -26,15 +27,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 { CommandResponse } from "../api/CursorlessEngineApi"; +import { takeSnapshot } from "../testUtil/takeSnapshot"; +import { RecordTestCaseCommandOptions } from "./RecordTestCaseCommandOptions"; +import { TestCase } from "./TestCase"; const CALIBRATION_DISPLAY_DURATION_MS = 50; 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..65b72cbee9 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml @@ -0,0 +1,28 @@ +languageId: plaintext +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: 3} + active: {line: 0, character: 3} + marks: {} +finalState: + documentContents: foo + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fallback: + action: setSelection + modifiers: + - type: containingScope + scopeType: {type: token} From 2e74d9e1750f9a07f0463cf033fa621203c32b2e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 07:54:33 +0100 Subject: [PATCH 14/34] Mock focused element in command server api --- packages/common/src/FakeCommandServerApi.ts | 24 +++++++++++++++++++ .../common/src/getFakeCommandServerApi.ts | 13 ---------- packages/common/src/index.ts | 3 ++- packages/common/src/util/useFallback.ts | 5 ++++ .../core/commandRunner/CommandRunnerImpl.ts | 3 ++- .../src/core/getCommandFallback.ts | 5 ---- .../cursorless-engine/src/cursorlessEngine.ts | 2 +- .../src/suite/recorded.vscode.test.ts | 21 ++++++++++------ .../src/constructTestHelpers.ts | 6 ++--- packages/cursorless-vscode/src/extension.ts | 14 +++++------ packages/vscode-common/src/TestHelpers.ts | 4 ++-- 11 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 packages/common/src/FakeCommandServerApi.ts delete mode 100644 packages/common/src/getFakeCommandServerApi.ts create mode 100644 packages/common/src/util/useFallback.ts diff --git a/packages/common/src/FakeCommandServerApi.ts b/packages/common/src/FakeCommandServerApi.ts new file mode 100644 index 0000000000..aa80348d67 --- /dev/null +++ b/packages/common/src/FakeCommandServerApi.ts @@ -0,0 +1,24 @@ +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 } }; + } + + 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 f5183c9745..0000000000 --- a/packages/common/src/getFakeCommandServerApi.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommandServerApi } from "./types/CommandServerApi"; - -export function getFakeCommandServerApi(): CommandServerApi { - return { - getFocusedElementType: () => "textEditor", - - signals: { - prePhrase: { - getVersion: async () => null, - }, - }, - }; -} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 719ccacce7..dd4143b0fa 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -91,11 +91,12 @@ 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/useFallback"; export * from "./scopeSupportFacets/scopeSupportFacets.types"; export * from "./scopeSupportFacets/scopeSupportFacetInfos"; export * from "./scopeSupportFacets/textualScopeSupportFacetInfos"; diff --git a/packages/common/src/util/useFallback.ts b/packages/common/src/util/useFallback.ts new file mode 100644 index 0000000000..a293e756df --- /dev/null +++ b/packages/common/src/util/useFallback.ts @@ -0,0 +1,5 @@ +import type { Command } from "../types/command/command.types"; + +export function useFallback(command: Command): boolean { + return command.version >= 7; +} diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 3da0a57005..a8041e7c72 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -5,6 +5,7 @@ import { CommandServerApi, DestinationDescriptor, PartialTargetDescriptor, + useFallback, } from "@cursorless/common"; import { CommandRunner } from "../../CommandRunner"; import { ActionRecord, ActionReturnValue } from "../../actions/actions.types"; @@ -14,7 +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, useFallback } from "../getCommandFallback"; +import { getCommandFallback } from "../getCommandFallback"; import { inferFullTargetDescriptor } from "../inferFullTargetDescriptor"; import { selectionToStoredTarget } from "./selectionToStoredTarget"; diff --git a/packages/cursorless-engine/src/core/getCommandFallback.ts b/packages/cursorless-engine/src/core/getCommandFallback.ts index 6a973f8eca..a1383227b0 100644 --- a/packages/cursorless-engine/src/core/getCommandFallback.ts +++ b/packages/cursorless-engine/src/core/getCommandFallback.ts @@ -1,6 +1,5 @@ import { ActionDescriptor, - Command, CommandComplete, CommandServerApi, DestinationDescriptor, @@ -10,10 +9,6 @@ import { } from "@cursorless/common"; import type { ActionReturnValue } from "../actions/actions.types"; -export function useFallback(command: Command): boolean { - return command.version >= 7; -} - export async function getCommandFallback( commandServerApi: CommandServerApi | null, runAction: (actionDescriptor: ActionDescriptor) => Promise, diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 7f960afe15..b16bea874d 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -6,6 +6,7 @@ import { Hats, IDE, ScopeProvider, + useFallback, } from "@cursorless/common"; import { CommandRunnerDecorator, @@ -16,7 +17,6 @@ import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; import { StoredTargetMap } from "./core/StoredTargets"; import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape"; -import { useFallback } from "./core/getCommandFallback"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; import { CustomSpokenFormGeneratorImpl } from "./generateSpokenForm/CustomSpokenFormGeneratorImpl"; import { LanguageDefinitions } from "./languages/LanguageDefinitions"; 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 6cce2dbaff..bc915e0a07 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -1,5 +1,6 @@ import { asyncSafety, + CommandResponse, DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, ExcludableSnapshotField, extractTargetedMarks, @@ -22,6 +23,7 @@ import { TestCaseFixtureLegacy, TextEditor, TokenHat, + useFallback, } from "@cursorless/common"; import { getCursorlessApi, @@ -30,11 +32,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, isPlainObject } from "lodash"; function createPosition(position: PositionPlainObject) { return new vscode.Position(position.line, position.character); @@ -74,7 +76,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 +103,10 @@ async function runTest(file: string, spyIde: SpyIDE) { // spyIde.clipboard.writeText(fixture.initialState.clipboard); } + commandServerApi.setFocusedElementType( + fixture.fallback == null ? "textEditor" : undefined, + ); + // Ensure that the expected hats are present await hatTokenMap.allocateHats( getTokenHats(fixture.initialState.marks, spyIde.activeTextEditor!), @@ -118,11 +124,12 @@ async function runTest(file: string, spyIde: SpyIDE) { ...fixture.command, usePrePhraseSnapshot, }); - if (isPlainObject(returnValue)) { - const returnValueObj = returnValue as Record; - if ("returnValue" in returnValueObj) { - returnValue = returnValueObj.returnValue; - } + if (useFallback(fixture.command)) { + const returnValueObj = returnValue as CommandResponse; + returnValue = + "returnValue" in returnValueObj + ? returnValueObj.returnValue + : undefined; } } catch (err) { const error = err as Error; 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..38c5d3a9b9 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); @@ -145,7 +145,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 96be54c5a713f9aa25389aee20c9e82a83c18320 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 08:00:50 +0100 Subject: [PATCH 15/34] assert fallback --- .../src/suite/recorded.vscode.test.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) 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 bc915e0a07..71409b33dd 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -4,6 +4,7 @@ import { DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, ExcludableSnapshotField, extractTargetedMarks, + Fallback, getRecordedTestPaths, HatStability, marksToPlainObject, @@ -118,18 +119,21 @@ async function runTest(file: string, spyIde: SpyIDE) { checkMarks(fixture.initialState.marks, readableHatMap); let returnValue: unknown; + let fallback: Fallback | undefined; try { - returnValue = await runCursorlessCommand({ + const commandReturnValue = await runCursorlessCommand({ ...fixture.command, usePrePhraseSnapshot, }); if (useFallback(fixture.command)) { - const returnValueObj = returnValue as CommandResponse; - returnValue = - "returnValue" in returnValueObj - ? returnValueObj.returnValue - : undefined; + const commandResponse = commandReturnValue as CommandResponse; + if ("returnValue" in commandResponse) { + returnValue = commandResponse.returnValue; + } + if ("fallback" in commandResponse) { + fallback = commandResponse.fallback; + } } } catch (err) { const error = err as Error; @@ -201,6 +205,7 @@ async function runTest(file: string, spyIde: SpyIDE) { ...fixture, finalState: resultState, returnValue, + fallback, ide: actualSpyIdeValues, thrownError: undefined, }; @@ -225,6 +230,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, From 71303d6d3a7fffb8c312330db3c194b0c320d1e6 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 08:10:30 +0100 Subject: [PATCH 16/34] Added more tests --- .../core/commandRunner/CommandRunnerImpl.ts | 1 + .../fixtures/recorded/fallback/bringFine.yml | 29 +++++++++++++++++++ .../fixtures/recorded/fallback/moveFine.yml | 29 +++++++++++++++++++ .../fixtures/recorded/fallback/takeThis.yml | 25 ++++++++++++++++ .../fixtures/recorded/fallback/takeToken.yml | 8 ++--- 5 files changed, 88 insertions(+), 4 deletions(-) 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/moveFine.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index a8041e7c72..672b7b8a4b 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -31,6 +31,7 @@ export class CommandRunnerImpl implements CommandRunner { private pipelineRunner: TargetPipelineRunner, private actions: ActionRecord, ) { + this.runAction = this.runAction.bind(this); this.inferenceContext = new InferenceContext(this.debug); } 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..c2edc066b9 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml @@ -0,0 +1,29 @@ +languageId: plaintext +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/moveFine.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml new file mode 100644 index 0000000000..4522b35e13 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml @@ -0,0 +1,29 @@ +languageId: plaintext +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..d1020fe6ca --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml @@ -0,0 +1,25 @@ +languageId: plaintext +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 index 65b72cbee9..4aa0779335 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml @@ -13,14 +13,14 @@ command: initialState: documentContents: foo selections: - - anchor: {line: 0, character: 3} - active: {line: 0, character: 3} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} marks: {} finalState: documentContents: foo selections: - - anchor: {line: 0, character: 3} - active: {line: 0, character: 3} + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} fallback: action: setSelection modifiers: From 0de126aea464d58439d7242ba48967ab851736fa Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 08:34:26 +0100 Subject: [PATCH 17/34] Fix return value in tests --- .../cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 71409b33dd..2af0754343 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -122,12 +122,12 @@ async function runTest(file: string, spyIde: SpyIDE) { let fallback: Fallback | undefined; try { - const commandReturnValue = await runCursorlessCommand({ + returnValue = await runCursorlessCommand({ ...fixture.command, usePrePhraseSnapshot, }); if (useFallback(fixture.command)) { - const commandResponse = commandReturnValue as CommandResponse; + const commandResponse = returnValue as CommandResponse; if ("returnValue" in commandResponse) { returnValue = commandResponse.returnValue; } From bb9a20b125f0a67bee4e7251f014ab78fd23cd55 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 08:50:07 +0100 Subject: [PATCH 18/34] cleanup --- .../src/suite/recorded.vscode.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 2af0754343..4ee0d8ff6a 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -128,12 +128,12 @@ async function runTest(file: string, spyIde: SpyIDE) { }); if (useFallback(fixture.command)) { const commandResponse = returnValue as CommandResponse; - if ("returnValue" in commandResponse) { - returnValue = commandResponse.returnValue; - } - if ("fallback" in commandResponse) { - fallback = commandResponse.fallback; - } + returnValue = + "returnValue" in commandResponse + ? commandResponse.returnValue + : undefined; + fallback = + "fallback" in commandResponse ? commandResponse.fallback : undefined; } } catch (err) { const error = err as Error; From 98e9529844dc5b6561bb9ab7db7f681669fe2640 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 18 Feb 2024 09:00:17 +0100 Subject: [PATCH 19/34] set default focused element --- packages/common/src/FakeCommandServerApi.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/src/FakeCommandServerApi.ts b/packages/common/src/FakeCommandServerApi.ts index aa80348d67..b37500d407 100644 --- a/packages/common/src/FakeCommandServerApi.ts +++ b/packages/common/src/FakeCommandServerApi.ts @@ -10,6 +10,7 @@ export class FakeCommandServerApi implements CommandServerApi { constructor() { this.signals = { prePhrase: { getVersion: async () => null } }; + this.focusedElementType = "textEditor"; } getFocusedElementType(): FocusedElementType | undefined { From 5b7371e831ce1414afdb1b928f769b729b5c27b0 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 19 Feb 2024 08:55:39 +0100 Subject: [PATCH 20/34] move and rename fallback --- packages/common/src/index.ts | 2 +- ...eFallback.ts => clientSupportsFallBack.ts} | 2 +- .../src/api/CursorlessEngineApi.ts | 2 +- .../core/commandRunner/CommandRunnerImpl.ts | 4 ++-- .../cursorless-engine/src/cursorlessEngine.ts | 21 ++----------------- packages/cursorless-engine/src/runCommand.ts | 20 ++++++++++++++++-- .../src/suite/recorded.vscode.test.ts | 4 ++-- 7 files changed, 27 insertions(+), 28 deletions(-) rename packages/common/src/util/{useFallback.ts => clientSupportsFallBack.ts} (58%) diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index dd4143b0fa..7a663b6d77 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -96,7 +96,7 @@ export * from "./types/TestCaseFixture"; export * from "./util/getEnvironmentVariableStrict"; export * from "./util/CompositeKeyDefaultMap"; export * from "./util/toPlainObject"; -export * from "./util/useFallback"; +export * from "./util/clientSupportsFallBack"; export * from "./scopeSupportFacets/scopeSupportFacets.types"; export * from "./scopeSupportFacets/scopeSupportFacetInfos"; export * from "./scopeSupportFacets/textualScopeSupportFacetInfos"; diff --git a/packages/common/src/util/useFallback.ts b/packages/common/src/util/clientSupportsFallBack.ts similarity index 58% rename from packages/common/src/util/useFallback.ts rename to packages/common/src/util/clientSupportsFallBack.ts index a293e756df..beb07f13a9 100644 --- a/packages/common/src/util/useFallback.ts +++ b/packages/common/src/util/clientSupportsFallBack.ts @@ -1,5 +1,5 @@ import type { Command } from "../types/command/command.types"; -export function useFallback(command: Command): boolean { +export function clientSupportsFallback(command: Command): boolean { return command.version >= 7; } diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 84743fbfe5..63e4b8d591 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -39,7 +39,7 @@ 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 diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 672b7b8a4b..9e030348d1 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -5,7 +5,7 @@ import { CommandServerApi, DestinationDescriptor, PartialTargetDescriptor, - useFallback, + clientSupportsFallback, } from "@cursorless/common"; import { CommandRunner } from "../../CommandRunner"; import { ActionRecord, ActionReturnValue } from "../../actions/actions.types"; @@ -53,7 +53,7 @@ export class CommandRunnerImpl implements CommandRunner { * it has one. */ async run(command: CommandComplete): Promise { - if (useFallback(command)) { + if (clientSupportsFallback(command)) { const fallback = await getCommandFallback( this.commandServerApi, this.runAction, diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index b16bea874d..71dd1b1d4e 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -1,12 +1,10 @@ import { Command, - CommandResponse, CommandServerApi, FileSystem, Hats, IDE, ScopeProvider, - useFallback, } from "@cursorless/common"; import { CommandRunnerDecorator, @@ -89,8 +87,7 @@ export function createCursorlessEngine( }, async runCommandSafe(...args: unknown[]) { - const command = ensureCommandShape(args); - const response = await runCommand( + return runCommand( treeSitter, commandServerApi, debug, @@ -100,9 +97,8 @@ export function createCursorlessEngine( languageDefinitions, rangeUpdater, commandRunnerDecorators, - command, + ensureCommandShape(args), ); - return unwrapCommandResponse(command, response); }, }, scopeProvider: createScopeProvider( @@ -123,19 +119,6 @@ export function createCursorlessEngine( }; } -async function unwrapCommandResponse( - command: Command, - response: CommandResponse, -): Promise { - if (useFallback(command)) { - return response; - } - if ("returnValue" in response) { - return response.returnValue; - } - return undefined; -} - function createScopeProvider( languageDefinitions: LanguageDefinitions, storedTargets: StoredTargetMap, diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index ee42c80c10..ac1bcdc22f 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -4,6 +4,7 @@ import { CommandServerApi, HatTokenMap, ReadOnlyHatMap, + clientSupportsFallback, } from "@cursorless/common"; import { CommandRunner } from "./CommandRunner"; import { Actions } from "./actions/Actions"; @@ -43,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)); @@ -70,7 +71,22 @@ export async function runCommand( commandRunner = decorator.wrapCommandRunner(readableHatMap, commandRunner); } - return await commandRunner.run(commandComplete); + const response = await commandRunner.run(commandComplete); + + return await unwrapCommandResponse(command, response); +} + +async function unwrapCommandResponse( + command: Command, + response: CommandResponse, +): Promise { + if (clientSupportsFallback(command)) { + return response; + } + if ("returnValue" in response) { + return response.returnValue; + } + return undefined; } function createCommandRunner( 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 4ee0d8ff6a..9158290e80 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -24,7 +24,7 @@ import { TestCaseFixtureLegacy, TextEditor, TokenHat, - useFallback, + clientSupportsFallback, } from "@cursorless/common"; import { getCursorlessApi, @@ -126,7 +126,7 @@ async function runTest(file: string, spyIde: SpyIDE) { ...fixture.command, usePrePhraseSnapshot, }); - if (useFallback(fixture.command)) { + if (clientSupportsFallback(fixture.command)) { const commandResponse = returnValue as CommandResponse; returnValue = "returnValue" in commandResponse From 300511c685ccfd732b50545289f67cb0b78eb760 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 19 Feb 2024 09:26:46 +0100 Subject: [PATCH 21/34] more tests --- cursorless-talon/src/command.py | 4 ++- cursorless-talon/src/fallback.py | 5 ++++ packages/common/src/index.ts | 2 +- .../src/testUtil/serializeTestFixture.ts | 1 + packages/common/src/types/TestCaseFixture.ts | 7 ++++- .../src/testCaseRecorder/TestCase.ts | 6 ++++ .../src/testCaseRecorder/TestCaseRecorder.ts | 3 ++ .../fixtures/recorded/fallback/bringFine.yml | 1 + .../fixtures/recorded/fallback/bringFine2.yml | 30 +++++++++++++++++++ .../fixtures/recorded/fallback/chuckFine.yml | 25 ++++++++++++++++ .../fixtures/recorded/fallback/chuckFine2.yml | 25 ++++++++++++++++ .../fixtures/recorded/fallback/moveFine.yml | 1 + .../fixtures/recorded/fallback/takeThis.yml | 1 + .../fixtures/recorded/fallback/takeToken.yml | 1 + .../src/suite/recorded.vscode.test.ts | 4 ++- packages/cursorless-vscode/src/extension.ts | 6 +++- .../populateDist/transformPackageJson.ts | 2 +- 17 files changed, 118 insertions(+), 6 deletions(-) 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 diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index cee63df523..13d5033983 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -5,10 +5,12 @@ from .fallback import perform_fallback +COMMAND_VERSION = 7 + @dataclasses.dataclass class CursorlessCommand: - version = 7 + version = COMMAND_VERSION spokenForm: str usePrePhraseSnapshot: bool action: dict diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 12ad3dc51a..ea476ecb6d 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -2,6 +2,11 @@ from talon import actions +from .command 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, diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 7a663b6d77..61badf4813 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -96,7 +96,7 @@ export * from "./types/TestCaseFixture"; export * from "./util/getEnvironmentVariableStrict"; export * from "./util/CompositeKeyDefaultMap"; export * from "./util/toPlainObject"; -export * from "./util/clientSupportsFallBack"; +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 edf04d43fb..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, diff --git a/packages/common/src/types/TestCaseFixture.ts b/packages/common/src/types/TestCaseFixture.ts index 7fceab66d0..7b366e694a 100644 --- a/packages/common/src/types/TestCaseFixture.ts +++ b/packages/common/src/types/TestCaseFixture.ts @@ -1,4 +1,4 @@ -import type { Command, CommandLatest, Fallback } from ".."; +import type { Command, CommandLatest, Fallback, FocusedElementType } from ".."; import type { TestCaseSnapshot } from "../testUtil/TestCaseSnapshot"; import type { PlainSpyIDERecordedValues } from "../testUtil/spyToPlainObject"; @@ -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 */ diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts index 8f365afba9..0dd2dad532 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts @@ -6,6 +6,7 @@ import { extractTargetedMarks, ExtraSnapshotField, Fallback, + FocusedElementType, marksToPlainObject, PartialTargetDescriptor, PlainSpyIDERecordedValues, @@ -43,6 +44,7 @@ export class TestCase { constructor( command: CommandLatest, + private focusedElementType: FocusedElementType | undefined, private hatTokenMap: ReadOnlyHatMap, private storedTargets: StoredTargetMap, private spyIde: SpyIDE, @@ -138,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, diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts index 52e9d97924..8be2e5bb06 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts @@ -2,6 +2,7 @@ import { CommandComplete, CommandLatest, CommandResponse, + CommandServerApi, DecoratedSymbolMark, DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, extractTargetedMarks, @@ -64,6 +65,7 @@ export class TestCaseRecorder { private spokenFormGenerator = new SpokenFormGenerator(defaultSpokenFormMap); constructor( + private commandServerApi: CommandServerApi | null, private hatTokenMap: HatTokenMap, private storedTargets: StoredTargetMap, ) { @@ -293,6 +295,7 @@ export class TestCaseRecorder { ? spokenForm.spokenForms[0] : command.spokenForm, }, + this.commandServerApi?.getFocusedElementType(), hatTokenMap, this.storedTargets, this.spyIde, 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 index c2edc066b9..ea198925ff 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/bringFine.yml @@ -1,4 +1,5 @@ languageId: plaintext +focusedElementType: other command: version: 7 spokenForm: bring fine 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 index 4522b35e13..7bf0f45f6b 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/moveFine.yml @@ -1,4 +1,5 @@ languageId: plaintext +focusedElementType: other command: version: 7 spokenForm: move fine 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 index d1020fe6ca..3ede086e32 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeThis.yml @@ -1,4 +1,5 @@ languageId: plaintext +focusedElementType: other command: version: 7 spokenForm: take this 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 index 4aa0779335..e42b5c19c3 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/fallback/takeToken.yml @@ -1,4 +1,5 @@ languageId: plaintext +focusedElementType: other command: version: 7 spokenForm: take token 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 9158290e80..22258d170c 100644 --- a/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts @@ -105,7 +105,9 @@ async function runTest(file: string, spyIde: SpyIDE) { } commandServerApi.setFocusedElementType( - fixture.fallback == null ? "textEditor" : undefined, + fixture.focusedElementType === "other" + ? undefined + : fixture.focusedElementType ?? "textEditor", ); // Ensure that the expected hats are present diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index 38c5d3a9b9..fcc7e2334d 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -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"); diff --git a/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts b/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts index d33c31dbdc..92572383d4 100644 --- a/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts +++ b/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts @@ -28,7 +28,7 @@ export async function transformPackageJson( json.version = `${major}.${minor}.${commitCount}`; } else { const gitSha = (await runCommand("git rev-parse --short HEAD")).trim(); - json.version = `${json.version}-${gitSha}`; + // json.version = `${json.version}-${gitSha}`; } return json; From 817c02aa788023c4021bb538b8d712a5b508aeee Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 19 Feb 2024 09:30:31 +0100 Subject: [PATCH 22/34] test --- .../{clientSupportsFallBack.ts => clientSupportsFallback2.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/common/src/util/{clientSupportsFallBack.ts => clientSupportsFallback2.ts} (100%) diff --git a/packages/common/src/util/clientSupportsFallBack.ts b/packages/common/src/util/clientSupportsFallback2.ts similarity index 100% rename from packages/common/src/util/clientSupportsFallBack.ts rename to packages/common/src/util/clientSupportsFallback2.ts From cde86b0fba57959cae30e81520cc67e289854dda Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 19 Feb 2024 09:31:04 +0100 Subject: [PATCH 23/34] rename --- .../{clientSupportsFallback2.ts => clientSupportsFallBack.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/common/src/util/{clientSupportsFallback2.ts => clientSupportsFallBack.ts} (100%) diff --git a/packages/common/src/util/clientSupportsFallback2.ts b/packages/common/src/util/clientSupportsFallBack.ts similarity index 100% rename from packages/common/src/util/clientSupportsFallback2.ts rename to packages/common/src/util/clientSupportsFallBack.ts From 8e604b4873715c68466e2896490ea0b706da3970 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 19 Feb 2024 09:31:53 +0100 Subject: [PATCH 24/34] rename --- .../util/{clientSupportsFallBack.ts => clientSupportsFalllack.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/common/src/util/{clientSupportsFallBack.ts => clientSupportsFalllack.ts} (100%) diff --git a/packages/common/src/util/clientSupportsFallBack.ts b/packages/common/src/util/clientSupportsFalllack.ts similarity index 100% rename from packages/common/src/util/clientSupportsFallBack.ts rename to packages/common/src/util/clientSupportsFalllack.ts From b32439f6b2b393258846849037d63bbf36738aeb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 19 Feb 2024 09:32:22 +0100 Subject: [PATCH 25/34] rename --- .../util/{clientSupportsFalllack.ts => clientSupportsFallback.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/common/src/util/{clientSupportsFalllack.ts => clientSupportsFallback.ts} (100%) diff --git a/packages/common/src/util/clientSupportsFalllack.ts b/packages/common/src/util/clientSupportsFallback.ts similarity index 100% rename from packages/common/src/util/clientSupportsFalllack.ts rename to packages/common/src/util/clientSupportsFallback.ts From 7225594bbddd5317a99b9380d59f12cacfe882c2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 20 Feb 2024 12:00:30 +0100 Subject: [PATCH 26/34] Restore sha --- .../src/scripts/populateDist/transformPackageJson.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts b/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts index 92572383d4..d33c31dbdc 100644 --- a/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts +++ b/packages/cursorless-vscode/src/scripts/populateDist/transformPackageJson.ts @@ -28,7 +28,7 @@ export async function transformPackageJson( json.version = `${major}.${minor}.${commitCount}`; } else { const gitSha = (await runCommand("git rev-parse --short HEAD")).trim(); - // json.version = `${json.version}-${gitSha}`; + json.version = `${json.version}-${gitSha}`; } return json; From b02f33427db5ce8733b5586385460a28a170c3d6 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:06:04 +0000 Subject: [PATCH 27/34] Fix circular import error --- cursorless-talon/src/command.py | 3 +-- cursorless-talon/src/fallback.py | 2 +- cursorless-talon/src/versions.py | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 cursorless-talon/src/versions.py diff --git a/cursorless-talon/src/command.py b/cursorless-talon/src/command.py index 13d5033983..329678649c 100644 --- a/cursorless-talon/src/command.py +++ b/cursorless-talon/src/command.py @@ -4,8 +4,7 @@ from talon import Module, actions, speech_system from .fallback import perform_fallback - -COMMAND_VERSION = 7 +from .versions import COMMAND_VERSION @dataclasses.dataclass diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index ea476ecb6d..fc2699d7d1 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -2,7 +2,7 @@ from talon import actions -from .command import COMMAND_VERSION +from .versions import COMMAND_VERSION # This ensures that we remember to update fallback if the response payload changes assert COMMAND_VERSION == 7 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 From 4237c6bcb79b0965b7823d8c0fa2e8b5c3fc5d4e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:19:52 +0000 Subject: [PATCH 28/34] Fix call_as_function with arg --- cursorless-talon/src/fallback.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index fc2699d7d1..5a7d9f5ab7 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -31,8 +31,7 @@ def call_as_function(callee: str): - actions.insert(f"{callee}()") - actions.edit.left() + wrap_with_paired_delimiter(f"{callee}(", ")") def wrap_with_paired_delimiter(left: str, right: str): From bfeef407206f00fe30f242f7b31e9a619b60a948 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:22:19 +0000 Subject: [PATCH 29/34] minor cleanup --- cursorless-talon/src/fallback.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 5a7d9f5ab7..4a2d95c14f 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -77,13 +77,7 @@ def get_action_callback(fallback: dict) -> Callable: def get_modifier_callbacks(fallback: dict) -> list[Callable]: - modifiers = fallback["modifiers"] - callbacks = [] - - for modifier in modifiers: - callbacks.append(get_modifier_callback(modifier)) - - return callbacks + return [get_modifier_callback(modifier) for modifier in fallback["modifiers"]] def get_modifier_callback(modifier: dict) -> Callable: From 88740e921055e97eaa231b2171724dfe54486f06 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:24:40 +0000 Subject: [PATCH 30/34] more pythonification --- cursorless-talon/src/fallback.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 4a2d95c14f..5a9d4c468a 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -100,6 +100,7 @@ def get_modifier_callback(modifier: dict) -> Callable: def get_simple_modifier_callback(key: str) -> Callable: - if key in modifier_callbacks: + try: return modifier_callbacks[key] - raise ValueError(f"Unknown Cursorless fallback modifier: {key}") + except KeyError: + raise ValueError(f"Unknown Cursorless fallback modifier: {key}") From 06edc07eecca200d9548cb483f84e347c6f5c0dd Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:40:17 +0000 Subject: [PATCH 31/34] Rename --- packages/cursorless-engine/src/runCommand.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index ac1bcdc22f..fb2a4b2abc 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -73,10 +73,10 @@ export async function runCommand( const response = await commandRunner.run(commandComplete); - return await unwrapCommandResponse(command, response); + return await unwrapLegacyCommandResponse(command, response); } -async function unwrapCommandResponse( +async function unwrapLegacyCommandResponse( command: Command, response: CommandResponse, ): Promise { From 4a9386cc0dc7120b106ee4e2b917a8c39a4b190a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 21 Feb 2024 11:33:14 +0100 Subject: [PATCH 32/34] Remove superfluous next homophone action --- cursorless-talon/src/fallback.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index 5a9d4c468a..d57758a078 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -17,7 +17,6 @@ "remove": actions.edit.delete, "editNewLineBefore": actions.edit.line_insert_up, "editNewLineAfter": actions.edit.line_insert_down, - "nextHomophone": actions.user.homophones_cycle_selected, } modifier_callbacks = { From 5f79f3e0198b6e4ad66a07245ad27309bf750e00 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 26 Feb 2024 07:03:51 +0100 Subject: [PATCH 33/34] Added pre and post fallback actions --- cursorless-talon/src/fallback.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cursorless-talon/src/fallback.py b/cursorless-talon/src/fallback.py index d57758a078..55d86ab8bd 100644 --- a/cursorless-talon/src/fallback.py +++ b/cursorless-talon/src/fallback.py @@ -10,6 +10,8 @@ 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, From d74175fa4231536020bf4b3cce4dd2402fc52a12 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:20:29 +0000 Subject: [PATCH 34/34] add release notes --- changelog/2024-03-fallBackToTalonActions.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/2024-03-fallBackToTalonActions.md 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.