Skip to content

Commit 6a14ac0

Browse files
cursorless everywhere: add the ability to asynchronous values from Python (#2649)
## Checklist NB: haven't tested this yet - [ ] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet - [x] Test format action - [x] Test homophones action --------- Co-authored-by: Andreas Arvidsson <[email protected]>
1 parent 8829946 commit 6a14ac0

File tree

5 files changed

+122
-36
lines changed

5 files changed

+122
-36
lines changed
Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,43 @@
1-
from typing import TypedDict
1+
import json
2+
from typing import Any
23

3-
from talon import Module
4+
from talon import Context, Module, actions
45

6+
from .cursorless_everywhere_types import EditorEdit, EditorState, SelectionOffsets
57

6-
class SelectionOffsets(TypedDict):
7-
anchor: int
8-
active: int
9-
10-
11-
class EditorState(TypedDict):
12-
text: str
13-
selections: list[SelectionOffsets]
14-
15-
16-
class EditorChange(TypedDict):
17-
text: str
18-
rangeOffset: int
19-
rangeLength: int
8+
mod = Module()
209

10+
mod.tag("cursorless_everywhere_talon", desc="Enable cursorless everywhere in Talon")
2111

22-
class EditorEdit(TypedDict):
23-
# The new document content after the edit
24-
text: str
12+
ctx = Context()
13+
ctx.matches = r"""
14+
tag: user.cursorless_everywhere_talon
15+
"""
2516

26-
# A list of changes that were made to the document. If you can not handle
27-
# this, you can ignore it and just replace the entire document with the
28-
# value of the `text` field above.
29-
changes: list[EditorChange]
3017

18+
@ctx.action_class("user")
19+
class UserActions:
20+
def private_cursorless_run_rpc_command_and_wait(
21+
command_id: str, # pyright: ignore [reportGeneralTypeIssues]
22+
arg1: Any = None,
23+
arg2: Any = None,
24+
):
25+
actions.user.private_cursorless_talonjs_run_and_wait(command_id, arg1, arg2)
3126

32-
mod = Module()
27+
def private_cursorless_run_rpc_command_no_wait(
28+
command_id: str, # pyright: ignore [reportGeneralTypeIssues]
29+
arg1: Any = None,
30+
arg2: Any = None,
31+
):
32+
actions.user.private_cursorless_talonjs_run_no_wait(command_id, arg1, arg2)
3333

34-
mod.tag("cursorless_everywhere_talon", desc="Enable cursorless everywhere in Talon")
34+
def private_cursorless_run_rpc_command_get(
35+
command_id: str, # pyright: ignore [reportGeneralTypeIssues]
36+
arg1: Any = None,
37+
arg2: Any = None,
38+
) -> Any:
39+
actions.user.private_cursorless_talonjs_run_and_wait(command_id, arg1, arg2)
40+
return json.loads(actions.user.private_cursorless_talonjs_get_response_json())
3541

3642

3743
@mod.action_class
@@ -48,3 +54,20 @@ def cursorless_everywhere_edit_text(
4854
edit: EditorEdit, # pyright: ignore [reportGeneralTypeIssues]
4955
):
5056
"""Edit focused element text"""
57+
58+
def private_cursorless_talonjs_run_and_wait(
59+
command_id: str, # pyright: ignore [reportGeneralTypeIssues]
60+
arg1: Any = None,
61+
arg2: Any = None,
62+
):
63+
"""Executes a Cursorless command, waits for its completion, but does not return the response"""
64+
65+
def private_cursorless_talonjs_run_no_wait(
66+
command_id: str, # pyright: ignore [reportGeneralTypeIssues]
67+
arg1: Any = None,
68+
arg2: Any = None,
69+
):
70+
"""Executes a Cursorless command, but does not wait for it to finish, nor return the response"""
71+
72+
def private_cursorless_talonjs_get_response_json() -> str:
73+
"""Returns the response from the last Cursorless command"""
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import TypedDict
2+
3+
4+
class SelectionOffsets(TypedDict):
5+
anchor: int
6+
active: int
7+
8+
9+
class EditorState(TypedDict):
10+
text: str
11+
selections: list[SelectionOffsets]
12+
13+
14+
class EditorChange(TypedDict):
15+
text: str
16+
rangeOffset: int
17+
rangeLength: int
18+
19+
20+
class EditorEdit(TypedDict):
21+
# The new document content after the edit
22+
text: str
23+
24+
# A list of changes that were made to the document. If you can not handle
25+
# this, you can ignore it and just replace the entire document with the
26+
# value of the `text` field above.
27+
changes: list[EditorChange]

packages/cursorless-everywhere-talon-core/src/registerCommands.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,28 @@ export function registerCommands(
1111
const ctx = new talon.Context();
1212
ctx.matches = "tag: user.cursorless_everywhere_talon";
1313

14+
let lastCommandResponse: unknown = null;
15+
1416
ctx.action_class("user", {
15-
private_cursorless_run_rpc_command_no_wait(
17+
async private_cursorless_talonjs_run_and_wait(
1618
commandId: string,
1719
command: unknown,
18-
): void {
19-
void runCommand(commandId, command);
20+
): Promise<void> {
21+
lastCommandResponse = await runCommand(commandId, command);
2022
},
2123

22-
async private_cursorless_run_rpc_command_get(
24+
private_cursorless_talonjs_run_no_wait(
2325
commandId: string,
2426
command: unknown,
25-
): Promise<unknown> {
26-
return await runCommand(commandId, command);
27+
): void {
28+
void runCommand(commandId, command);
29+
lastCommandResponse = null;
30+
},
31+
32+
private_cursorless_talonjs_get_response_json(): string {
33+
// Talon JS doesn't convert JS objects to Python objects, and also doesn't provide
34+
// a way to inspect types, so just serialize the response to JSON
35+
return JSON.stringify(lastCommandResponse);
2736
},
2837
});
2938

packages/cursorless-everywhere-talon-core/src/types/talon.types.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,41 @@ export interface TalonActions {
2121
}
2222

2323
export interface TalonContextActions {
24-
private_cursorless_run_rpc_command_no_wait(
24+
/**
25+
* Executes an RPC command and waits for the result.
26+
* This function is useful when the result of the command is needed
27+
* immediately after execution.
28+
*
29+
* @param commandId - The identifier of the command to be executed.
30+
* @param command - The command object containing necessary parameters.
31+
* @returns A Promise that resolves with the result of the command execution.
32+
*/
33+
private_cursorless_talonjs_run_and_wait(
2534
commandId: string,
2635
command: unknown,
27-
): void;
28-
private_cursorless_run_rpc_command_get(
36+
): Promise<void>;
37+
/**
38+
* Executes an RPC command without waiting for the result.
39+
* This function is useful for fire-and-forget operations where
40+
* the result is not immediately needed.
41+
*
42+
* @param commandId - The identifier of the command to be executed.
43+
* @param command - The command object containing necessary parameters.
44+
*/
45+
private_cursorless_talonjs_run_no_wait(
2946
commandId: string,
3047
command: unknown,
31-
): Promise<unknown>;
48+
): void;
49+
/**
50+
* Retrieves the response json from the last RPC command execution.
51+
*
52+
* This is useful because TalonJS doesn't have a way to read the responses from promises,
53+
* but it does wait for them, so we store the response in a global variable and let it be
54+
* read by this action.
55+
*
56+
* @returns The most recent response from an RPC command (JSON stringified).
57+
*/
58+
private_cursorless_talonjs_get_response_json(): string;
3259
}
3360

3461
export interface TalonContext {

packages/cursorless-everywhere-talon-e2e/src/quickjsTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function runAction(action: ActionDescriptor) {
7474
};
7575
return talonMock
7676
.getTestHelpers()
77-
.contextActions.private_cursorless_run_rpc_command_get(
77+
.contextActions.private_cursorless_talonjs_run_and_wait(
7878
"cursorless.command",
7979
command,
8080
);

0 commit comments

Comments
 (0)