riched 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- riched/__init__.py +2 -0
- riched/app.py +433 -0
- riched/bindings.yaml +76 -0
- riched/cli.py +24 -0
- riched/editor.py +96 -0
- riched/keybindings.py +123 -0
- riched/screens.py +284 -0
- riched/settings.py +47 -0
- riched/syntax.py +241 -0
- riched-0.1.0.dist-info/METADATA +72 -0
- riched-0.1.0.dist-info/RECORD +13 -0
- riched-0.1.0.dist-info/WHEEL +4 -0
- riched-0.1.0.dist-info/entry_points.txt +2 -0
riched/keybindings.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import resources
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class KeyBindingHelpGroup:
|
|
13
|
+
title: str
|
|
14
|
+
rows: tuple[tuple[str, str], ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_binding_spec() -> dict[str, Any]:
|
|
18
|
+
text = resources.files("riched").joinpath("bindings.yaml").read_text()
|
|
19
|
+
data = yaml.safe_load(text)
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
raise ValueError("bindings.yaml must contain a mapping")
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
BINDING_SPEC = load_binding_spec()
|
|
26
|
+
APP_COMMANDS = BINDING_SPEC["app"]["commands"]
|
|
27
|
+
COMMANDS: list[tuple[str, str, str]] = [
|
|
28
|
+
(item["name"], item["description"], item["key"]) for item in APP_COMMANDS
|
|
29
|
+
]
|
|
30
|
+
DEFAULT_BINDINGS: dict[str, str] = {name: key for name, _, key in COMMANDS}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_bindings() -> list[Binding]:
|
|
34
|
+
# priority=True so configured shortcuts override TextArea built-ins.
|
|
35
|
+
return [
|
|
36
|
+
Binding(default, name, desc, priority=True)
|
|
37
|
+
for name, desc, default in COMMANDS
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_static_bindings(section: str) -> list[Binding]:
|
|
42
|
+
items = BINDING_SPEC[section]
|
|
43
|
+
return [
|
|
44
|
+
Binding(
|
|
45
|
+
item["key"],
|
|
46
|
+
item["action"],
|
|
47
|
+
item.get("description", ""),
|
|
48
|
+
show=item.get("show", True),
|
|
49
|
+
)
|
|
50
|
+
for item in items
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_screen_bindings(screen: str) -> list[Binding]:
|
|
55
|
+
items = BINDING_SPEC["screens"][screen]
|
|
56
|
+
return [
|
|
57
|
+
Binding(
|
|
58
|
+
item["key"],
|
|
59
|
+
item["action"],
|
|
60
|
+
item.get("description", ""),
|
|
61
|
+
show=item.get("show", True),
|
|
62
|
+
)
|
|
63
|
+
for item in items
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def binding_help_groups() -> list[KeyBindingHelpGroup]:
|
|
68
|
+
groups: list[KeyBindingHelpGroup] = []
|
|
69
|
+
|
|
70
|
+
app_rows = tuple(
|
|
71
|
+
(_display_key(item["key"]), item.get("description") or item["name"])
|
|
72
|
+
for item in BINDING_SPEC["app"]["commands"]
|
|
73
|
+
)
|
|
74
|
+
groups.append(KeyBindingHelpGroup("App", app_rows))
|
|
75
|
+
|
|
76
|
+
editor_rows = tuple(
|
|
77
|
+
(_display_key(item["key"]), item.get("description") or item["action"])
|
|
78
|
+
for item in BINDING_SPEC["editor"]
|
|
79
|
+
)
|
|
80
|
+
groups.append(KeyBindingHelpGroup("Editor", editor_rows))
|
|
81
|
+
|
|
82
|
+
for screen, items in BINDING_SPEC["screens"].items():
|
|
83
|
+
rows = tuple(
|
|
84
|
+
(_display_key(item["key"]), item.get("description") or item["action"])
|
|
85
|
+
for item in items
|
|
86
|
+
)
|
|
87
|
+
groups.append(KeyBindingHelpGroup(f"Screens / {screen}", rows))
|
|
88
|
+
|
|
89
|
+
return groups
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _display_key(key: str) -> str:
|
|
93
|
+
return display_key(key)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def display_key(key: str) -> str:
|
|
97
|
+
keys = [part.strip() for part in key.split(",") if part.strip()]
|
|
98
|
+
if not keys:
|
|
99
|
+
return key
|
|
100
|
+
|
|
101
|
+
without_super = [candidate for candidate in keys if "super" not in candidate]
|
|
102
|
+
candidates = without_super or keys
|
|
103
|
+
|
|
104
|
+
def score(candidate: str) -> tuple[int, int, int]:
|
|
105
|
+
parts = candidate.split("+")
|
|
106
|
+
has_cmd = "cmd" in parts
|
|
107
|
+
preferred_cmd_order = not (
|
|
108
|
+
"cmd" in parts
|
|
109
|
+
and "shift" in parts
|
|
110
|
+
and parts.index("shift") < parts.index("cmd")
|
|
111
|
+
)
|
|
112
|
+
preferred_alt_order = not (
|
|
113
|
+
"alt" in parts
|
|
114
|
+
and "shift" in parts
|
|
115
|
+
and parts.index("shift") < parts.index("alt")
|
|
116
|
+
)
|
|
117
|
+
return (
|
|
118
|
+
0 if has_cmd else 1,
|
|
119
|
+
0 if preferred_cmd_order else 1,
|
|
120
|
+
0 if preferred_alt_order else 1,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return min(candidates, key=score)
|
riched/screens.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual import events
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Horizontal, Vertical
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Input, Label, OptionList, Static
|
|
10
|
+
from textual.widgets.option_list import Option
|
|
11
|
+
|
|
12
|
+
from .keybindings import binding_help_groups, build_screen_bindings
|
|
13
|
+
|
|
14
|
+
MAX_QUICK_OPEN_RESULTS = 100
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnsavedChangesScreen(ModalScreen[str]):
|
|
18
|
+
"""Prompt asking whether to save, discard, or cancel."""
|
|
19
|
+
|
|
20
|
+
BINDINGS = build_screen_bindings("unsaved_changes")
|
|
21
|
+
|
|
22
|
+
CSS = """
|
|
23
|
+
UnsavedChangesScreen {
|
|
24
|
+
align: center middle;
|
|
25
|
+
}
|
|
26
|
+
UnsavedChangesScreen > #dialog {
|
|
27
|
+
width: 64;
|
|
28
|
+
height: auto;
|
|
29
|
+
padding: 1 2;
|
|
30
|
+
background: $panel;
|
|
31
|
+
border: tall $accent;
|
|
32
|
+
}
|
|
33
|
+
UnsavedChangesScreen Label {
|
|
34
|
+
width: 100%;
|
|
35
|
+
content-align: center middle;
|
|
36
|
+
padding-bottom: 1;
|
|
37
|
+
}
|
|
38
|
+
UnsavedChangesScreen #buttons {
|
|
39
|
+
height: auto;
|
|
40
|
+
align: center middle;
|
|
41
|
+
}
|
|
42
|
+
UnsavedChangesScreen Button {
|
|
43
|
+
margin: 0 1;
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def compose(self) -> ComposeResult:
|
|
48
|
+
with Vertical(id="dialog"):
|
|
49
|
+
yield Label("Unsaved changes.\nSave before continuing?")
|
|
50
|
+
with Horizontal(id="buttons"):
|
|
51
|
+
yield Button("Save", variant="primary", id="save")
|
|
52
|
+
yield Button("Discard", variant="error", id="discard")
|
|
53
|
+
yield Button("Cancel", id="cancel")
|
|
54
|
+
|
|
55
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
56
|
+
self.dismiss(event.button.id or "cancel")
|
|
57
|
+
|
|
58
|
+
def on_key(self, event: events.Key) -> None:
|
|
59
|
+
focused = self.focused
|
|
60
|
+
if event.key == "space" and isinstance(focused, Button):
|
|
61
|
+
event.stop()
|
|
62
|
+
event.prevent_default()
|
|
63
|
+
focused.action_press()
|
|
64
|
+
|
|
65
|
+
def action_cancel(self) -> None:
|
|
66
|
+
self.dismiss("cancel")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class QuickOpenScreen(ModalScreen[Path | None]):
|
|
70
|
+
"""Fuzzy file picker."""
|
|
71
|
+
|
|
72
|
+
BINDINGS = build_screen_bindings("quick_open")
|
|
73
|
+
|
|
74
|
+
CSS = """
|
|
75
|
+
QuickOpenScreen {
|
|
76
|
+
align: center top;
|
|
77
|
+
}
|
|
78
|
+
QuickOpenScreen > #dialog {
|
|
79
|
+
offset-y: 2;
|
|
80
|
+
width: 76;
|
|
81
|
+
height: auto;
|
|
82
|
+
max-height: 24;
|
|
83
|
+
padding: 1 2;
|
|
84
|
+
background: $panel;
|
|
85
|
+
border: tall $accent;
|
|
86
|
+
}
|
|
87
|
+
QuickOpenScreen #query {
|
|
88
|
+
margin-bottom: 1;
|
|
89
|
+
}
|
|
90
|
+
QuickOpenScreen #results {
|
|
91
|
+
height: auto;
|
|
92
|
+
max-height: 18;
|
|
93
|
+
}
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, root: Path, files: list[Path]) -> None:
|
|
97
|
+
super().__init__()
|
|
98
|
+
self.root = root
|
|
99
|
+
self.files = files
|
|
100
|
+
self._relative_paths = {
|
|
101
|
+
path: path.relative_to(root).as_posix() for path in files
|
|
102
|
+
}
|
|
103
|
+
self._option_paths: dict[str, Path] = {}
|
|
104
|
+
|
|
105
|
+
def compose(self) -> ComposeResult:
|
|
106
|
+
with Vertical(id="dialog"):
|
|
107
|
+
yield Input(placeholder="Quick open", id="query")
|
|
108
|
+
yield OptionList(id="results")
|
|
109
|
+
|
|
110
|
+
def on_mount(self) -> None:
|
|
111
|
+
self._refresh_results("")
|
|
112
|
+
self.query_one("#query", Input).focus()
|
|
113
|
+
|
|
114
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
115
|
+
if event.input.id == "query":
|
|
116
|
+
self._refresh_results(event.value)
|
|
117
|
+
|
|
118
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
119
|
+
if event.input.id == "query":
|
|
120
|
+
self._open_highlighted()
|
|
121
|
+
|
|
122
|
+
def on_option_list_option_selected(
|
|
123
|
+
self, event: OptionList.OptionSelected
|
|
124
|
+
) -> None:
|
|
125
|
+
path = self._option_paths.get(event.option.id or "")
|
|
126
|
+
if path is not None:
|
|
127
|
+
self.dismiss(path)
|
|
128
|
+
|
|
129
|
+
def on_key(self, event: events.Key) -> None:
|
|
130
|
+
results = self.query_one("#results", OptionList)
|
|
131
|
+
if event.key == "up":
|
|
132
|
+
event.stop()
|
|
133
|
+
event.prevent_default()
|
|
134
|
+
results.action_cursor_up()
|
|
135
|
+
elif event.key == "down":
|
|
136
|
+
event.stop()
|
|
137
|
+
event.prevent_default()
|
|
138
|
+
results.action_cursor_down()
|
|
139
|
+
elif event.key == "enter":
|
|
140
|
+
event.stop()
|
|
141
|
+
event.prevent_default()
|
|
142
|
+
self._open_highlighted()
|
|
143
|
+
|
|
144
|
+
def action_close(self) -> None:
|
|
145
|
+
self.dismiss(None)
|
|
146
|
+
|
|
147
|
+
def _refresh_results(self, query: str) -> None:
|
|
148
|
+
results = self.query_one("#results", OptionList)
|
|
149
|
+
results.clear_options()
|
|
150
|
+
self._option_paths.clear()
|
|
151
|
+
|
|
152
|
+
matches = self._ranked_matches(query)
|
|
153
|
+
if not matches:
|
|
154
|
+
results.add_option(Option("No matches", id="no-matches", disabled=True))
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
options: list[Option] = []
|
|
158
|
+
for index, path in enumerate(matches[:MAX_QUICK_OPEN_RESULTS]):
|
|
159
|
+
option_id = str(index)
|
|
160
|
+
self._option_paths[option_id] = path
|
|
161
|
+
options.append(Option(self._relative_paths[path], id=option_id))
|
|
162
|
+
results.add_options(options)
|
|
163
|
+
results.highlighted = 0
|
|
164
|
+
|
|
165
|
+
def _ranked_matches(self, query: str) -> list[Path]:
|
|
166
|
+
stripped_query = query.strip()
|
|
167
|
+
if not stripped_query:
|
|
168
|
+
return sorted(self.files, key=lambda path: self._relative_paths[path].lower())
|
|
169
|
+
|
|
170
|
+
scored: list[tuple[tuple[int, int, int, int, str], Path]] = []
|
|
171
|
+
for path in self.files:
|
|
172
|
+
relative_path = self._relative_paths[path]
|
|
173
|
+
score = _fuzzy_score(stripped_query, relative_path)
|
|
174
|
+
if score is not None:
|
|
175
|
+
scored.append((score, path))
|
|
176
|
+
scored.sort(key=lambda item: item[0])
|
|
177
|
+
return [path for _score, path in scored]
|
|
178
|
+
|
|
179
|
+
def _open_highlighted(self) -> None:
|
|
180
|
+
results = self.query_one("#results", OptionList)
|
|
181
|
+
highlighted = results.highlighted
|
|
182
|
+
if highlighted is None:
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
option = results.get_option_at_index(highlighted)
|
|
186
|
+
except Exception:
|
|
187
|
+
return
|
|
188
|
+
path = self._option_paths.get(option.id or "")
|
|
189
|
+
if path is not None:
|
|
190
|
+
self.dismiss(path)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class KeysHelpScreen(ModalScreen[None]):
|
|
194
|
+
"""Popup key binding help."""
|
|
195
|
+
|
|
196
|
+
BINDINGS = build_screen_bindings("keys_help")
|
|
197
|
+
|
|
198
|
+
CSS = """
|
|
199
|
+
KeysHelpScreen {
|
|
200
|
+
align: center middle;
|
|
201
|
+
}
|
|
202
|
+
KeysHelpScreen > #dialog {
|
|
203
|
+
width: 80%;
|
|
204
|
+
height: 80%;
|
|
205
|
+
max-width: 96;
|
|
206
|
+
padding: 1 2;
|
|
207
|
+
background: $panel;
|
|
208
|
+
border: tall $accent;
|
|
209
|
+
}
|
|
210
|
+
KeysHelpScreen Label {
|
|
211
|
+
height: auto;
|
|
212
|
+
margin-bottom: 1;
|
|
213
|
+
text-style: bold;
|
|
214
|
+
}
|
|
215
|
+
KeysHelpScreen #bindings-list {
|
|
216
|
+
width: 1fr;
|
|
217
|
+
height: 1fr;
|
|
218
|
+
padding: 0;
|
|
219
|
+
overflow-y: auto;
|
|
220
|
+
}
|
|
221
|
+
KeysHelpScreen .binding-group {
|
|
222
|
+
height: auto;
|
|
223
|
+
margin-bottom: 1;
|
|
224
|
+
}
|
|
225
|
+
KeysHelpScreen .binding-title {
|
|
226
|
+
height: auto;
|
|
227
|
+
color: $text-primary;
|
|
228
|
+
text-style: underline bold;
|
|
229
|
+
}
|
|
230
|
+
KeysHelpScreen .binding-row {
|
|
231
|
+
height: auto;
|
|
232
|
+
layout: horizontal;
|
|
233
|
+
}
|
|
234
|
+
KeysHelpScreen .binding-key {
|
|
235
|
+
width: 38;
|
|
236
|
+
height: auto;
|
|
237
|
+
color: $text-accent;
|
|
238
|
+
text-style: bold;
|
|
239
|
+
padding-right: 1;
|
|
240
|
+
}
|
|
241
|
+
KeysHelpScreen .binding-description {
|
|
242
|
+
width: 1fr;
|
|
243
|
+
height: auto;
|
|
244
|
+
color: $foreground;
|
|
245
|
+
}
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def compose(self) -> ComposeResult:
|
|
249
|
+
with Vertical(id="dialog"):
|
|
250
|
+
yield Label("Key Bindings")
|
|
251
|
+
with Vertical(id="bindings-list"):
|
|
252
|
+
for group in binding_help_groups():
|
|
253
|
+
with Vertical(classes="binding-group"):
|
|
254
|
+
yield Static(group.title, classes="binding-title")
|
|
255
|
+
for key, description in group.rows:
|
|
256
|
+
with Horizontal(classes="binding-row"):
|
|
257
|
+
yield Static(key, classes="binding-key")
|
|
258
|
+
yield Static(description, classes="binding-description")
|
|
259
|
+
|
|
260
|
+
def action_close(self) -> None:
|
|
261
|
+
self.dismiss(None)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _fuzzy_score(query: str, candidate: str) -> tuple[int, int, int, int, str] | None:
|
|
265
|
+
query_lower = query.lower()
|
|
266
|
+
candidate_lower = candidate.lower()
|
|
267
|
+
positions: list[int] = []
|
|
268
|
+
search_from = 0
|
|
269
|
+
for char in query_lower:
|
|
270
|
+
index = candidate_lower.find(char, search_from)
|
|
271
|
+
if index == -1:
|
|
272
|
+
return None
|
|
273
|
+
positions.append(index)
|
|
274
|
+
search_from = index + 1
|
|
275
|
+
|
|
276
|
+
first = positions[0]
|
|
277
|
+
last = positions[-1]
|
|
278
|
+
spread = last - first + 1
|
|
279
|
+
gaps = spread - len(query_lower)
|
|
280
|
+
basename = Path(candidate_lower).name
|
|
281
|
+
basename_index = candidate_lower.rfind(basename)
|
|
282
|
+
basename_bonus = 0 if first >= basename_index else 1
|
|
283
|
+
contiguous_bonus = 0 if query_lower in candidate_lower else 1
|
|
284
|
+
return (basename_bonus, contiguous_bonus, gaps, len(candidate_lower), candidate_lower)
|
riched/settings.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
SETTINGS_FILENAME = "settings.yaml"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def config_dir() -> Path:
|
|
14
|
+
if sys.platform == "darwin":
|
|
15
|
+
return Path("~/Library/Application Support/riched").expanduser()
|
|
16
|
+
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
17
|
+
if xdg_config_home:
|
|
18
|
+
return Path(xdg_config_home).expanduser() / "riched"
|
|
19
|
+
return Path("~/.config/riched").expanduser()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def settings_path() -> Path:
|
|
23
|
+
return config_dir() / SETTINGS_FILENAME
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_settings() -> dict[str, Any]:
|
|
27
|
+
path = settings_path()
|
|
28
|
+
try:
|
|
29
|
+
data = yaml.safe_load(path.read_text())
|
|
30
|
+
except Exception:
|
|
31
|
+
return {}
|
|
32
|
+
if not isinstance(data, dict):
|
|
33
|
+
return {}
|
|
34
|
+
return data
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_theme() -> str | None:
|
|
38
|
+
theme = load_settings().get("theme")
|
|
39
|
+
return theme if isinstance(theme, str) and theme else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def save_theme(theme: str) -> None:
|
|
43
|
+
path = settings_path()
|
|
44
|
+
settings = load_settings()
|
|
45
|
+
settings["theme"] = theme
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
path.write_text(yaml.safe_dump(settings, sort_keys=False))
|
riched/syntax.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual.widgets import TextArea
|
|
4
|
+
|
|
5
|
+
EXT_LANGUAGE = {
|
|
6
|
+
".py": "python",
|
|
7
|
+
".js": "javascript",
|
|
8
|
+
".mjs": "javascript",
|
|
9
|
+
".cjs": "javascript",
|
|
10
|
+
".ts": "typescript",
|
|
11
|
+
".tsx": "tsx",
|
|
12
|
+
".sh": "bash",
|
|
13
|
+
".bash": "bash",
|
|
14
|
+
".zsh": "bash",
|
|
15
|
+
".html": "html",
|
|
16
|
+
".htm": "html",
|
|
17
|
+
".css": "css",
|
|
18
|
+
".json": "json",
|
|
19
|
+
".md": "markdown",
|
|
20
|
+
".markdown": "markdown",
|
|
21
|
+
".yaml": "yaml",
|
|
22
|
+
".yml": "yaml",
|
|
23
|
+
".toml": "toml",
|
|
24
|
+
".xml": "xml",
|
|
25
|
+
".sql": "sql",
|
|
26
|
+
".java": "java",
|
|
27
|
+
".go": "go",
|
|
28
|
+
".rs": "rust",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_TS_FAMILY_REGISTERED = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def reset_ts_registration() -> None:
|
|
35
|
+
global _TS_FAMILY_REGISTERED
|
|
36
|
+
_TS_FAMILY_REGISTERED = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def apply_language(editor: TextArea, suffix: str) -> None:
|
|
40
|
+
editor.language = None
|
|
41
|
+
lang = EXT_LANGUAGE.get(suffix.lower())
|
|
42
|
+
if lang is None:
|
|
43
|
+
return
|
|
44
|
+
if lang in ("typescript", "tsx") and not _register_ts_family(editor):
|
|
45
|
+
lang = "javascript"
|
|
46
|
+
editor.language = lang
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _register_ts_family(editor: TextArea) -> bool:
|
|
50
|
+
"""Register both ``typescript`` and ``tsx`` grammars on the editor."""
|
|
51
|
+
global _TS_FAMILY_REGISTERED
|
|
52
|
+
if _TS_FAMILY_REGISTERED:
|
|
53
|
+
return True
|
|
54
|
+
try:
|
|
55
|
+
import tree_sitter_typescript as tsts # type: ignore
|
|
56
|
+
from tree_sitter import Language # type: ignore
|
|
57
|
+
except Exception:
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
ts_lang = Language(tsts.language_typescript())
|
|
61
|
+
tsx_lang = Language(tsts.language_tsx())
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
64
|
+
try:
|
|
65
|
+
editor.register_language("typescript", ts_lang, _TS_HIGHLIGHTS)
|
|
66
|
+
editor.register_language("tsx", tsx_lang, _TS_HIGHLIGHTS + _JSX_HIGHLIGHTS)
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
69
|
+
_TS_FAMILY_REGISTERED = True
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_TS_HIGHLIGHTS = r"""
|
|
74
|
+
; --- JavaScript base ---------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
(identifier) @variable
|
|
77
|
+
(property_identifier) @property
|
|
78
|
+
|
|
79
|
+
(function_expression
|
|
80
|
+
name: (identifier) @function)
|
|
81
|
+
(function_declaration
|
|
82
|
+
name: (identifier) @function)
|
|
83
|
+
(method_definition
|
|
84
|
+
name: (property_identifier) @function.method)
|
|
85
|
+
|
|
86
|
+
(pair
|
|
87
|
+
key: (property_identifier) @function.method
|
|
88
|
+
value: [(function_expression) (arrow_function)])
|
|
89
|
+
|
|
90
|
+
(assignment_expression
|
|
91
|
+
left: (member_expression
|
|
92
|
+
property: (property_identifier) @function.method)
|
|
93
|
+
right: [(function_expression) (arrow_function)])
|
|
94
|
+
|
|
95
|
+
(variable_declarator
|
|
96
|
+
name: (identifier) @function
|
|
97
|
+
value: [(function_expression) (arrow_function)])
|
|
98
|
+
|
|
99
|
+
(assignment_expression
|
|
100
|
+
left: (identifier) @function
|
|
101
|
+
right: [(function_expression) (arrow_function)])
|
|
102
|
+
|
|
103
|
+
(call_expression
|
|
104
|
+
function: (identifier) @function)
|
|
105
|
+
|
|
106
|
+
(call_expression
|
|
107
|
+
function: (member_expression
|
|
108
|
+
property: (property_identifier) @function.method))
|
|
109
|
+
|
|
110
|
+
((identifier) @constructor
|
|
111
|
+
(#match? @constructor "^[A-Z]"))
|
|
112
|
+
|
|
113
|
+
([
|
|
114
|
+
(identifier)
|
|
115
|
+
(shorthand_property_identifier)
|
|
116
|
+
(shorthand_property_identifier_pattern)
|
|
117
|
+
] @constant
|
|
118
|
+
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
|
|
119
|
+
|
|
120
|
+
((identifier) @variable.builtin
|
|
121
|
+
(#match? @variable.builtin "^(arguments|module|console|window|document)$")
|
|
122
|
+
(#is-not? local))
|
|
123
|
+
|
|
124
|
+
((identifier) @function.builtin
|
|
125
|
+
(#eq? @function.builtin "require")
|
|
126
|
+
(#is-not? local))
|
|
127
|
+
|
|
128
|
+
(this) @variable.builtin
|
|
129
|
+
(super) @variable.builtin
|
|
130
|
+
|
|
131
|
+
[
|
|
132
|
+
(true)
|
|
133
|
+
(false)
|
|
134
|
+
(null)
|
|
135
|
+
(undefined)
|
|
136
|
+
] @constant.builtin
|
|
137
|
+
|
|
138
|
+
(comment) @comment
|
|
139
|
+
|
|
140
|
+
[
|
|
141
|
+
(string)
|
|
142
|
+
(template_string)
|
|
143
|
+
] @string
|
|
144
|
+
|
|
145
|
+
(regex) @string.special
|
|
146
|
+
(number) @number
|
|
147
|
+
|
|
148
|
+
[
|
|
149
|
+
";"
|
|
150
|
+
(optional_chain)
|
|
151
|
+
"."
|
|
152
|
+
","
|
|
153
|
+
] @punctuation.delimiter
|
|
154
|
+
|
|
155
|
+
[
|
|
156
|
+
"-" "--" "-=" "+" "++" "+=" "*" "*=" "**" "**=" "/" "/=" "%" "%="
|
|
157
|
+
"<" "<=" "<<" "<<=" "=" "==" "===" "!" "!=" "!==" "=>" ">" ">="
|
|
158
|
+
">>" ">>=" ">>>" ">>>=" "~" "^" "&" "|" "^=" "&=" "|=" "&&" "||"
|
|
159
|
+
"??" "&&=" "||=" "??="
|
|
160
|
+
] @operator
|
|
161
|
+
|
|
162
|
+
[
|
|
163
|
+
"(" ")" "[" "]" "{" "}"
|
|
164
|
+
] @punctuation.bracket
|
|
165
|
+
|
|
166
|
+
(template_substitution
|
|
167
|
+
"${" @punctuation.special
|
|
168
|
+
"}" @punctuation.special) @embedded
|
|
169
|
+
|
|
170
|
+
[
|
|
171
|
+
"as" "async" "await" "break" "case" "catch" "class" "const" "continue"
|
|
172
|
+
"debugger" "default" "delete" "do" "else" "export" "extends" "finally"
|
|
173
|
+
"for" "from" "function" "get" "if" "import" "in" "instanceof" "let"
|
|
174
|
+
"new" "of" "return" "set" "static" "switch" "target" "throw" "try"
|
|
175
|
+
"typeof" "var" "void" "while" "with" "yield"
|
|
176
|
+
] @keyword
|
|
177
|
+
|
|
178
|
+
; --- TypeScript additions ----------------------------------------------------
|
|
179
|
+
|
|
180
|
+
(type_identifier) @type
|
|
181
|
+
(predefined_type) @type.builtin
|
|
182
|
+
|
|
183
|
+
((identifier) @type
|
|
184
|
+
(#match? @type "^[A-Z]"))
|
|
185
|
+
|
|
186
|
+
(type_arguments
|
|
187
|
+
"<" @punctuation.bracket
|
|
188
|
+
">" @punctuation.bracket)
|
|
189
|
+
|
|
190
|
+
(required_parameter (identifier) @variable.parameter)
|
|
191
|
+
(optional_parameter (identifier) @variable.parameter)
|
|
192
|
+
|
|
193
|
+
[ "abstract" "declare" "enum" "implements" "interface" "keyof"
|
|
194
|
+
"namespace" "private" "protected" "public" "type" "readonly"
|
|
195
|
+
"override" "satisfies"
|
|
196
|
+
] @keyword
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_JSX_HIGHLIGHTS = r"""
|
|
201
|
+
; --- JSX ---------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
(jsx_opening_element
|
|
204
|
+
["<" ">"] @punctuation.bracket)
|
|
205
|
+
|
|
206
|
+
(jsx_closing_element
|
|
207
|
+
["</" ">"] @punctuation.bracket)
|
|
208
|
+
|
|
209
|
+
(jsx_self_closing_element
|
|
210
|
+
["<" "/>"] @punctuation.bracket)
|
|
211
|
+
|
|
212
|
+
(jsx_opening_element
|
|
213
|
+
name: (identifier) @tag)
|
|
214
|
+
|
|
215
|
+
(jsx_closing_element
|
|
216
|
+
name: (identifier) @tag)
|
|
217
|
+
|
|
218
|
+
(jsx_self_closing_element
|
|
219
|
+
name: (identifier) @tag)
|
|
220
|
+
|
|
221
|
+
(jsx_opening_element
|
|
222
|
+
(member_expression
|
|
223
|
+
object: (identifier) @tag
|
|
224
|
+
property: (property_identifier) @tag))
|
|
225
|
+
|
|
226
|
+
(jsx_closing_element
|
|
227
|
+
(member_expression
|
|
228
|
+
object: (identifier) @tag
|
|
229
|
+
property: (property_identifier) @tag))
|
|
230
|
+
|
|
231
|
+
(jsx_self_closing_element
|
|
232
|
+
(member_expression
|
|
233
|
+
object: (identifier) @tag
|
|
234
|
+
property: (property_identifier) @tag))
|
|
235
|
+
|
|
236
|
+
(jsx_attribute
|
|
237
|
+
(property_identifier) @property)
|
|
238
|
+
|
|
239
|
+
(jsx_text) @string
|
|
240
|
+
"""
|
|
241
|
+
|