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",