Skip to content

Commit 4563f38

Browse files
committed
Add tree view to indicate scope support
1 parent 5347894 commit 4563f38

38 files changed

+1803
-703
lines changed

cursorless-talon/src/csv_overrides.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections.abc import Container
33
from datetime import datetime
44
from pathlib import Path
5-
from typing import Optional
5+
from typing import Callable, Optional
66

77
from talon import Context, Module, actions, app, fs
88

@@ -25,49 +25,65 @@
2525
desc="The directory to use for cursorless settings csvs relative to talon user directory",
2626
)
2727

28-
default_ctx = Context()
29-
default_ctx.matches = r"""
28+
# The global context we use for our lists
29+
ctx = Context()
30+
31+
# A context that contains default vocabulary, for use in testing
32+
normalized_ctx = Context()
33+
normalized_ctx.matches = r"""
3034
tag: user.cursorless_default_vocabulary
3135
"""
3236

3337

38+
ListToSpokenForms = dict[str, dict[str, str]]
39+
40+
3441
def init_csv_and_watch_changes(
3542
filename: str,
36-
default_values: dict[str, dict[str, str]],
43+
default_values: ListToSpokenForms,
44+
handle_new_values: Optional[Callable[[ListToSpokenForms], None]] = None,
3745
extra_ignored_values: Optional[list[str]] = None,
3846
allow_unknown_values: bool = False,
3947
default_list_name: Optional[str] = None,
4048
headers: list[str] = [SPOKEN_FORM_HEADER, CURSORLESS_IDENTIFIER_HEADER],
41-
ctx: Context = Context(),
4249
no_update_file: bool = False,
43-
pluralize_lists: Optional[list[str]] = [],
50+
pluralize_lists: list[str] = [],
4451
):
4552
"""
4653
Initialize a cursorless settings csv, creating it if necessary, and watch
4754
for changes to the csv. Talon lists will be generated based on the keys of
4855
`default_values`. For example, if there is a key `foo`, there will be a
49-
list created called `user.cursorless_foo` that will contain entries from
50-
the original dict at the key `foo`, updated according to customization in
51-
the csv at
56+
list created called `user.cursorless_foo` that will contain entries from the
57+
original dict at the key `foo`, updated according to customization in the
58+
csv at
5259
53-
actions.path.talon_user() / "cursorless-settings" / filename
60+
```
61+
actions.path.talon_user() / "cursorless-settings" / filename
62+
```
5463
5564
Note that the settings directory location can be customized using the
5665
`user.cursorless_settings_directory` setting.
5766
5867
Args:
5968
filename (str): The name of the csv file to be placed in
60-
`cursorles-settings` dir
61-
default_values (dict[str, dict]): The default values for the lists to
62-
be customized in the given csv
63-
extra_ignored_values list[str]: Don't throw an exception if any of
64-
these appear as values; just ignore them and don't add them to any list
65-
allow_unknown_values bool: If unknown values appear, just put them in the list
66-
default_list_name Optional[str]: If unknown values are allowed, put any
67-
unknown values in this list
68-
no_update_file Optional[bool]: Set this to `TRUE` to indicate that we should
69-
not update the csv. This is used generally in case there was an issue coming up with the default set of values so we don't want to persist those to disk
70-
pluralize_lists: Create plural version of given lists
69+
`cursorles-settings` dir
70+
default_values (ListToSpokenForms): The default values for the lists to
71+
be customized in the given csv
72+
handle_new_values (Optional[Callable[[ListToSpokenForms], None]]): A
73+
callback to be called when the lists are updated
74+
extra_ignored_values (Optional[list[str]]): Don't throw an exception if
75+
any of these appear as values; just ignore them and don't add them
76+
to any list
77+
allow_unknown_values (bool): If unknown values appear, just put them in
78+
the list
79+
default_list_name (Optional[str]): If unknown values are
80+
allowed, put any unknown values in this list
81+
headers (list[str]): The headers to use for the csv
82+
no_update_file (bool): Set this to `True` to indicate that we should not
83+
update the csv. This is used generally in case there was an issue
84+
coming up with the default set of values so we don't want to persist
85+
those to disk
86+
pluralize_lists (list[str]): Create plural version of given lists
7187
"""
7288
if extra_ignored_values is None:
7389
extra_ignored_values = []
@@ -96,7 +112,7 @@ def on_watch(path, flags):
96112
allow_unknown_values,
97113
default_list_name,
98114
pluralize_lists,
99-
ctx,
115+
handle_new_values,
100116
)
101117

102118
fs.watch(str(file_path.parent), on_watch)
@@ -117,7 +133,7 @@ def on_watch(path, flags):
117133
allow_unknown_values,
118134
default_list_name,
119135
pluralize_lists,
120-
ctx,
136+
handle_new_values,
121137
)
122138
else:
123139
if not no_update_file:
@@ -129,7 +145,7 @@ def on_watch(path, flags):
129145
allow_unknown_values,
130146
default_list_name,
131147
pluralize_lists,
132-
ctx,
148+
handle_new_values,
133149
)
134150

135151
def unsubscribe():
@@ -165,7 +181,7 @@ def create_default_vocabulary_dicts(
165181
if active_key:
166182
updated_dict[active_key] = value2
167183
default_values_updated[key] = updated_dict
168-
assign_lists_to_context(default_ctx, default_values_updated, pluralize_lists)
184+
assign_lists_to_context(normalized_ctx, default_values_updated, pluralize_lists)
169185

170186

171187
def update_dicts(
@@ -175,7 +191,7 @@ def update_dicts(
175191
allow_unknown_values: bool,
176192
default_list_name: Optional[str],
177193
pluralize_lists: list[str],
178-
ctx: Context,
194+
handle_new_values: Optional[Callable[[ListToSpokenForms], None]],
179195
):
180196
# Create map with all default values
181197
results_map = {}
@@ -219,6 +235,9 @@ def update_dicts(
219235
# Assign result to talon context list
220236
assign_lists_to_context(ctx, results, pluralize_lists)
221237

238+
if handle_new_values is not None:
239+
handle_new_values(results)
240+
222241

223242
def assign_lists_to_context(
224243
ctx: Context,
@@ -386,7 +405,7 @@ def get_full_path(filename: str):
386405
return (settings_directory / filename).resolve()
387406

388407

389-
def get_super_values(values: dict[str, dict[str, str]]):
408+
def get_super_values(values: ListToSpokenForms):
390409
result: dict[str, str] = {}
391410
for value_dict in values.values():
392411
result.update(value_dict)

cursorless-talon/src/marks/decorated_mark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def setup_hat_styles_csv(hat_colors: dict[str, str], hat_shapes: dict[str, str])
138138
"hat_color": active_hat_colors,
139139
"hat_shape": active_hat_shapes,
140140
},
141-
[*hat_colors.values(), *hat_shapes.values()],
141+
extra_ignored_values=[*hat_colors.values(), *hat_shapes.values()],
142142
no_update_file=is_shape_error or is_color_error,
143143
)
144144

cursorless-talon/src/spoken_forms.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
import json
2+
from itertools import groupby
23
from pathlib import Path
34
from typing import Callable, Concatenate, ParamSpec, TypeVar
45

56
from talon import app, fs
67

78
from .csv_overrides import SPOKEN_FORM_HEADER, init_csv_and_watch_changes
89
from .marks.decorated_mark import init_hats
10+
from .spoken_forms_output import SpokenFormsOutput
911

1012
JSON_FILE = Path(__file__).parent / "spoken_forms.json"
1113
disposables: list[Callable] = []
1214

1315

14-
def watch_file(spoken_forms: dict, filename: str) -> Callable:
15-
return init_csv_and_watch_changes(
16-
filename,
17-
spoken_forms[filename],
18-
)
19-
20-
2116
P = ParamSpec("P")
2217
R = TypeVar("R")
2318

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

2523
def auto_construct_defaults(
26-
spoken_forms: dict[str, dict[str, dict[str, str]]],
27-
f: Callable[Concatenate[str, dict[str, dict[str, str]], P], R],
24+
spoken_forms: dict[str, ListToSpokenForms],
25+
handle_new_values: Callable[[ListToSpokenForms], None],
26+
f: Callable[
27+
Concatenate[str, ListToSpokenForms, Callable[[ListToSpokenForms], None], P], R
28+
],
2829
):
2930
"""
3031
Decorator that automatically constructs the default values for the
@@ -37,17 +38,32 @@ def auto_construct_defaults(
3738
of `init_csv_and_watch_changes` to remove the `default_values` parameter.
3839
3940
Args:
40-
spoken_forms (dict[str, dict[str, dict[str, str]]]): The spoken forms
41-
f (Callable[Concatenate[str, dict[str, dict[str, str]], P], R]): Will always be `init_csv_and_watch_changes`
41+
spoken_forms (dict[str, ListToSpokenForms]): The spoken forms
42+
handle_new_values (Callable[[ListToSpokenForms], None]): A callback to be called when the lists are updated
43+
f (Callable[Concatenate[str, ListToSpokenForms, P], R]): Will always be `init_csv_and_watch_changes`
4244
"""
4345

4446
def ret(filename: str, *args: P.args, **kwargs: P.kwargs) -> R:
4547
default_values = spoken_forms[filename]
46-
return f(filename, default_values, *args, **kwargs)
48+
return f(filename, default_values, handle_new_values, *args, **kwargs)
4749

4850
return ret
4951

5052

53+
# Maps from Talon list name to the type of the value in that list, e.g.
54+
# `pairedDelimiter` or `simpleScopeTypeType`
55+
# FIXME: This is a hack until we generate spoken_forms.json from Typescript side
56+
# At that point we can just include its type as part of that file
57+
LIST_TO_TYPE_MAP = {
58+
"wrapper_selectable_paired_delimiter": "pairedDelimiter",
59+
"selectable_only_paired_delimiter": "pairedDelimiter",
60+
"wrapper_only_paired_delimiter": "pairedDelimiter",
61+
"surrounding_pair_scope_type": "pairedDelimiter",
62+
"scope_type": "simpleScopeTypeType",
63+
"custom_regex_scope_type": "customRegex",
64+
}
65+
66+
5167
def update():
5268
global disposables
5369

@@ -57,7 +73,36 @@ def update():
5773
with open(JSON_FILE, encoding="utf-8") as file:
5874
spoken_forms = json.load(file)
5975

60-
handle_csv = auto_construct_defaults(spoken_forms, init_csv_and_watch_changes)
76+
initialized = False
77+
custom_spoken_forms: ListToSpokenForms = {}
78+
spoken_forms_output = SpokenFormsOutput()
79+
spoken_forms_output.init()
80+
81+
def update_spoken_forms_output():
82+
spoken_forms_output.write(
83+
[
84+
{
85+
"type": entry_type,
86+
"id": value,
87+
"spokenForms": [spoken_form[0] for spoken_form in spoken_forms],
88+
}
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+
)
93+
]
94+
)
95+
96+
def handle_new_values(values: ListToSpokenForms):
97+
custom_spoken_forms.update(values)
98+
if initialized:
99+
# On first run, we just do one update at the end, so we suppress
100+
# writing until we get there
101+
update_spoken_forms_output()
102+
103+
handle_csv = auto_construct_defaults(
104+
spoken_forms, handle_new_values, init_csv_and_watch_changes
105+
)
61106

62107
disposables = [
63108
handle_csv("actions.csv"),
@@ -107,6 +152,9 @@ def update():
107152
),
108153
]
109154

155+
update_spoken_forms_output()
156+
initialized = True
157+
110158

111159
def on_watch(path, flags):
112160
if JSON_FILE.match(path):
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
from pathlib import Path
3+
from typing import TypedDict
4+
5+
from talon import app
6+
7+
SPOKEN_FORMS_OUTPUT_PATH = Path.home() / ".cursorless" / "spokenForms.json"
8+
9+
10+
class SpokenFormEntry(TypedDict):
11+
type: str
12+
id: str
13+
spokenForms: list[str]
14+
15+
16+
class SpokenFormsOutput:
17+
"""
18+
Writes spoken forms to a json file for use by the Cursorless vscode extension
19+
"""
20+
21+
def init(self):
22+
try:
23+
SPOKEN_FORMS_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
24+
except Exception:
25+
error_message = (
26+
f"Error creating spoken form dir {SPOKEN_FORMS_OUTPUT_PATH.parent}"
27+
)
28+
print(error_message)
29+
app.notify(error_message)
30+
31+
def write(self, spoken_forms: list[SpokenFormEntry]):
32+
with open(SPOKEN_FORMS_OUTPUT_PATH, "w") as out:
33+
try:
34+
out.write(json.dumps({"version": 0, "entries": spoken_forms}))
35+
except Exception:
36+
error_message = (
37+
f"Error writing spoken form json {SPOKEN_FORMS_OUTPUT_PATH}"
38+
)
39+
print(error_message)
40+
app.notify(error_message)

packages/common/src/ide/types/FileSystem.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export interface FileSystem {
99
* @param onDidChange A function to call on changes
1010
* @returns A disposable to cancel the watcher
1111
*/
12-
watchDir(path: string, onDidChange: PathChangeListener): Disposable;
12+
watch(path: string, onDidChange: PathChangeListener): Disposable;
1313
}

packages/common/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export { getKey, splitKey } from "./util/splitKey";
1111
export { hrtimeBigintToSeconds } from "./util/timeUtils";
1212
export * from "./util/walkSync";
1313
export * from "./util/walkAsync";
14+
export * from "./util/Disposer";
15+
export * from "./util/camelCaseToAllDown";
1416
export { Notifier } from "./util/Notifier";
1517
export type { Listener } from "./util/Notifier";
1618
export type { TokenHatSplittingMode } from "./ide/types/Configuration";

0 commit comments

Comments
 (0)