Skip to content

Commit 5eaf08c

Browse files
committed
Initial working version
1 parent 99d58b5 commit 5eaf08c

File tree

5 files changed

+121
-52
lines changed

5 files changed

+121
-52
lines changed

Diff for: cursorless-talon/src/csv_overrides.py

+52-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import csv
22
from collections.abc import Container
3+
from dataclasses import dataclass
34
from datetime import datetime
45
from pathlib import Path
5-
from typing import Callable, Optional
6+
from typing import Callable, Optional, TypedDict
67

78
from talon import Context, Module, actions, app, fs
89

@@ -35,13 +36,21 @@
3536
"""
3637

3738

39+
# Maps from Talon list name to a map from spoken form to value
3840
ListToSpokenForms = dict[str, dict[str, str]]
3941

4042

43+
@dataclass
44+
class SpokenFormEntry:
45+
list_name: str
46+
id: str
47+
spoken_forms: list[str]
48+
49+
4150
def init_csv_and_watch_changes(
4251
filename: str,
4352
default_values: ListToSpokenForms,
44-
handle_new_values: Optional[Callable[[ListToSpokenForms], None]] = None,
53+
handle_new_values: Optional[Callable[[list[SpokenFormEntry]], None]] = None,
4554
extra_ignored_values: Optional[list[str]] = None,
4655
allow_unknown_values: bool = False,
4756
default_list_name: Optional[str] = None,
@@ -69,7 +78,7 @@ def init_csv_and_watch_changes(
6978
`cursorles-settings` dir
7079
default_values (ListToSpokenForms): The default values for the lists to
7180
be customized in the given csv
72-
handle_new_values (Optional[Callable[[ListToSpokenForms], None]]): A
81+
handle_new_values (Optional[Callable[[list[SpokenFormEntry]], None]]): A
7382
callback to be called when the lists are updated
7483
extra_ignored_values (Optional[list[str]]): Don't throw an exception if
7584
any of these appear as values; just ignore them and don't add them
@@ -185,18 +194,18 @@ def create_default_vocabulary_dicts(
185194

186195

187196
def update_dicts(
188-
default_values: dict[str, dict],
189-
current_values: dict,
197+
default_values: ListToSpokenForms,
198+
current_values: dict[str, str],
190199
extra_ignored_values: list[str],
191200
allow_unknown_values: bool,
192201
default_list_name: Optional[str],
193202
pluralize_lists: list[str],
194-
handle_new_values: Optional[Callable[[ListToSpokenForms], None]],
203+
handle_new_values: Optional[Callable[[list[SpokenFormEntry]], None]],
195204
):
196205
# Create map with all default values
197-
results_map = {}
198-
for list_name, dict in default_values.items():
199-
for key, value in dict.items():
206+
results_map: dict[str, ResultsListEntry] = {}
207+
for list_name, obj in default_values.items():
208+
for key, value in obj.items():
200209
results_map[value] = {"key": key, "value": value, "list": list_name}
201210

202211
# Update result with current values
@@ -206,7 +215,7 @@ def update_dicts(
206215
except KeyError:
207216
if value in extra_ignored_values:
208217
pass
209-
elif allow_unknown_values:
218+
elif allow_unknown_values and default_list_name is not None:
210219
results_map[value] = {
211220
"key": key,
212221
"value": value,
@@ -217,9 +226,35 @@ def update_dicts(
217226

218227
# Convert result map back to result list
219228
results = {res["list"]: {} for res in results_map.values()}
220-
for obj in results_map.values():
229+
values: list[SpokenFormEntry] = []
230+
for list_name, id, spoken_forms in generate_spoken_forms(
231+
list(results_map.values())
232+
):
233+
for spoken_form in spoken_forms:
234+
results[list_name][spoken_form] = id
235+
values.append(
236+
SpokenFormEntry(list_name=list_name, id=id, spoken_forms=spoken_forms)
237+
)
238+
239+
# Assign result to talon context list
240+
assign_lists_to_context(ctx, results, pluralize_lists)
241+
242+
if handle_new_values is not None:
243+
handle_new_values(values)
244+
245+
246+
class ResultsListEntry(TypedDict):
247+
key: str
248+
value: str
249+
list: str
250+
251+
252+
def generate_spoken_forms(results_list: list[ResultsListEntry]):
253+
for obj in results_list:
221254
value = obj["value"]
222255
key = obj["key"]
256+
257+
spoken = []
223258
if not is_removed(key):
224259
for k in key.split("|"):
225260
if value == "pasteFromClipboard" and k.endswith(" to"):
@@ -230,13 +265,13 @@ def update_dicts(
230265
# cursorless before this change would have "paste to" as
231266
# their spoken form and so would need to say "paste to to".
232267
k = k[:-3]
233-
results[obj["list"]][k.strip()] = value
268+
spoken.append(k.strip())
234269

235-
# Assign result to talon context list
236-
assign_lists_to_context(ctx, results, pluralize_lists)
237-
238-
if handle_new_values is not None:
239-
handle_new_values(results)
270+
yield (
271+
obj["list"],
272+
value,
273+
spoken,
274+
)
240275

241276

242277
def assign_lists_to_context(

Diff for: cursorless-talon/src/spoken_forms.py

+17-17
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import json
2-
from itertools import groupby
32
from pathlib import Path
43
from typing import Callable, Concatenate, ParamSpec, TypeVar
54

65
from talon import app, fs
76

8-
from .csv_overrides import SPOKEN_FORM_HEADER, init_csv_and_watch_changes
7+
from .csv_overrides import (
8+
SPOKEN_FORM_HEADER,
9+
ListToSpokenForms,
10+
SpokenFormEntry,
11+
init_csv_and_watch_changes,
12+
)
913
from .marks.decorated_mark import init_hats
1014
from .spoken_forms_output import SpokenFormsOutput
1115

@@ -16,15 +20,13 @@
1620
P = ParamSpec("P")
1721
R = TypeVar("R")
1822

19-
# Maps from Talon list name to a map from spoken form to value
20-
ListToSpokenForms = dict[str, dict[str, str]]
21-
2223

2324
def auto_construct_defaults(
2425
spoken_forms: dict[str, ListToSpokenForms],
25-
handle_new_values: Callable[[ListToSpokenForms], None],
26+
handle_new_values: Callable[[list[SpokenFormEntry]], None],
2627
f: Callable[
27-
Concatenate[str, ListToSpokenForms, Callable[[ListToSpokenForms], None], P], R
28+
Concatenate[str, ListToSpokenForms, Callable[[list[SpokenFormEntry]], None], P],
29+
R,
2830
],
2931
):
3032
"""
@@ -74,27 +76,25 @@ def update():
7476
spoken_forms = json.load(file)
7577

7678
initialized = False
77-
custom_spoken_forms: ListToSpokenForms = {}
79+
custom_spoken_forms: list[SpokenFormEntry] = []
7880
spoken_forms_output = SpokenFormsOutput()
7981
spoken_forms_output.init()
8082

8183
def update_spoken_forms_output():
8284
spoken_forms_output.write(
8385
[
8486
{
85-
"type": entry_type,
86-
"id": value,
87-
"spokenForms": [spoken_form[0] for spoken_form in spoken_forms],
87+
"type": LIST_TO_TYPE_MAP[entry.list_name],
88+
"id": entry.id,
89+
"spokenForms": entry.spoken_forms,
8890
}
89-
for list_name, entry_type in LIST_TO_TYPE_MAP.items()
90-
for value, spoken_forms in groupby(
91-
custom_spoken_forms[list_name].items(), lambda item: item[1]
92-
)
91+
for entry in custom_spoken_forms
92+
if entry.list_name in LIST_TO_TYPE_MAP
9393
]
9494
)
9595

96-
def handle_new_values(values: ListToSpokenForms):
97-
custom_spoken_forms.update(values)
96+
def handle_new_values(values: list[SpokenFormEntry]):
97+
custom_spoken_forms.extend(values)
9898
if initialized:
9999
# On first run, we just do one update at the end, so we suppress
100100
# writing until we get there

Diff for: packages/cursorless-engine/src/cursorlessEngine.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import {
66
IDE,
77
} from "@cursorless/common";
88
import { StoredTargetMap, TestCaseRecorder, TreeSitter } from ".";
9-
import { CustomSpokenForms } from "./CustomSpokenForms";
109
import { CursorlessEngine } from "./api/CursorlessEngineApi";
1110
import { ScopeProvider } from "./api/ScopeProvider";
1211
import { Debug } from "./core/Debug";
1312
import { HatTokenMapImpl } from "./core/HatTokenMapImpl";
1413
import { Snippets } from "./core/Snippets";
1514
import { ensureCommandShape } from "./core/commandVersionUpgrades/ensureCommandShape";
1615
import { RangeUpdater } from "./core/updateSelections/RangeUpdater";
16+
import { CustomSpokenFormGenerator } from "./generateSpokenForm/CustomSpokenFormGenerator";
1717
import { LanguageDefinitions } from "./languages/LanguageDefinitions";
1818
import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl";
1919
import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers";
@@ -25,7 +25,6 @@ import { ScopeRangeWatcher } from "./scopeProviders/ScopeRangeWatcher";
2525
import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
2626
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
2727
import { injectIde } from "./singletons/ide.singleton";
28-
import { SpokenFormGenerator } from "./generateSpokenForm";
2928

3029
export function createCursorlessEngine(
3130
treeSitter: TreeSitter,
@@ -57,7 +56,7 @@ export function createCursorlessEngine(
5756

5857
const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);
5958

60-
const customSpokenForms = new CustomSpokenForms(fileSystem);
59+
const customSpokenFormGenerator = new CustomSpokenFormGenerator(fileSystem);
6160

6261
ide.disposeOnExit(rangeUpdater, languageDefinitions, hatTokenMap, debug);
6362

@@ -94,7 +93,7 @@ export function createCursorlessEngine(
9493
scopeProvider: createScopeProvider(
9594
languageDefinitions,
9695
storedTargets,
97-
customSpokenForms,
96+
customSpokenFormGenerator,
9897
),
9998
testCaseRecorder,
10099
storedTargets,
@@ -109,7 +108,7 @@ export function createCursorlessEngine(
109108
function createScopeProvider(
110109
languageDefinitions: LanguageDefinitions,
111110
storedTargets: StoredTargetMap,
112-
customSpokenForms: CustomSpokenForms,
111+
customSpokenFormGenerator: CustomSpokenFormGenerator,
113112
): ScopeProvider {
114113
const scopeHandlerFactory = new ScopeHandlerFactoryImpl(languageDefinitions);
115114

@@ -127,10 +126,7 @@ function createScopeProvider(
127126
rangeProvider,
128127
);
129128
const supportChecker = new ScopeSupportChecker(scopeHandlerFactory);
130-
const infoProvider = new ScopeInfoProvider(
131-
customSpokenForms,
132-
new SpokenFormGenerator(customSpokenForms),
133-
);
129+
const infoProvider = new ScopeInfoProvider(customSpokenFormGenerator);
134130
const supportWatcher = new ScopeSupportWatcher(
135131
languageDefinitions,
136132
supportChecker,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
CommandComplete,
3+
Disposer,
4+
FileSystem,
5+
Listener,
6+
ScopeType,
7+
} from "@cursorless/common";
8+
import { SpokenFormGenerator } from ".";
9+
import { CustomSpokenForms } from "../CustomSpokenForms";
10+
11+
export class CustomSpokenFormGenerator {
12+
private customSpokenForms: CustomSpokenForms;
13+
private spokenFormGenerator: SpokenFormGenerator;
14+
private disposer = new Disposer();
15+
16+
constructor(fileSystem: FileSystem) {
17+
this.customSpokenForms = new CustomSpokenForms(fileSystem);
18+
this.spokenFormGenerator = new SpokenFormGenerator(this.customSpokenForms);
19+
this.disposer.push(
20+
this.customSpokenForms.onDidChangeCustomSpokenForms(() => {
21+
this.spokenFormGenerator = new SpokenFormGenerator(
22+
this.customSpokenForms,
23+
);
24+
}),
25+
);
26+
}
27+
28+
onDidChangeCustomSpokenForms = (listener: Listener<[]>) =>
29+
this.customSpokenForms.onDidChangeCustomSpokenForms(listener);
30+
31+
commandToSpokenForm = (command: CommandComplete) =>
32+
this.spokenFormGenerator.command(command);
33+
34+
scopeTypeToSpokenForm = (scopeType: ScopeType) =>
35+
this.spokenFormGenerator.scopeType(scopeType);
36+
37+
getCustomRegexScopeTypes = () =>
38+
this.customSpokenForms.getCustomRegexScopeTypes();
39+
}

Diff for: packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import { pull } from "lodash";
1010
import { homedir } from "os";
1111
import * as path from "path";
1212
import { ScopeTypeInfo, ScopeTypeInfoEventCallback } from "..";
13-
import { CustomSpokenForms } from "../CustomSpokenForms";
1413

15-
import { SpokenFormGenerator } from "../generateSpokenForm";
14+
import { CustomSpokenFormGenerator } from "../generateSpokenForm/CustomSpokenFormGenerator";
1615
import { scopeTypeToString } from "./scopeTypeToString";
1716

1817
export const spokenFormsPath = path.join(
@@ -29,12 +28,11 @@ export class ScopeInfoProvider {
2928
private listeners: ScopeTypeInfoEventCallback[] = [];
3029
private scopeInfos!: ScopeTypeInfo[];
3130

32-
constructor(
33-
private customSpokenForms: CustomSpokenForms,
34-
private spokenFormGenerator: SpokenFormGenerator,
35-
) {
31+
constructor(private customSpokenFormGenerator: CustomSpokenFormGenerator) {
3632
this.disposer.push(
37-
customSpokenForms.onDidChangeCustomSpokenForms(() => this.onChange()),
33+
customSpokenFormGenerator.onDidChangeCustomSpokenForms(() =>
34+
this.onChange(),
35+
),
3836
);
3937

4038
this.onDidChangeScopeInfo = this.onDidChangeScopeInfo.bind(this);
@@ -87,7 +85,7 @@ export class ScopeInfoProvider {
8785
}),
8886
),
8987

90-
...this.customSpokenForms.getCustomRegexScopeTypes(),
88+
...this.customSpokenFormGenerator.getCustomRegexScopeTypes(),
9189
];
9290

9391
this.scopeInfos = scopeTypes.map((scopeType) =>
@@ -102,7 +100,8 @@ export class ScopeInfoProvider {
102100
getScopeTypeInfo(scopeType: ScopeType): ScopeTypeInfo {
103101
return {
104102
scopeType,
105-
spokenForm: this.spokenFormGenerator.scopeType(scopeType),
103+
spokenForm:
104+
this.customSpokenFormGenerator.scopeTypeToSpokenForm(scopeType),
106105
humanReadableName: scopeTypeToString(scopeType),
107106
isLanguageSpecific: isLanguageSpecific(scopeType),
108107
};

0 commit comments

Comments
 (0)