QCodeSitter 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.
- QCodeSitter/__init__.py +38 -0
- QCodeSitter/behaviors/__init__.py +115 -0
- QCodeSitter/behaviors/auto_bracket.py +141 -0
- QCodeSitter/behaviors/code_folding.py +530 -0
- QCodeSitter/behaviors/comment_toggle.py +71 -0
- QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
- QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
- QCodeSitter/behaviors/line_numbers.py +49 -0
- QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
- QCodeSitter/behaviors/overscroll.py +104 -0
- QCodeSitter/behaviors/smart_indent.py +191 -0
- QCodeSitter/behaviors/syntax_highlighting.py +181 -0
- QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
- QCodeSitter/behaviors/tab_completion/popup.py +123 -0
- QCodeSitter/behaviors/tab_completion/providers.py +66 -0
- QCodeSitter/behaviors/tab_completion/types.py +55 -0
- QCodeSitter/code_editor.py +540 -0
- QCodeSitter/cursors/__init__.py +8 -0
- QCodeSitter/cursors/clipboard.py +51 -0
- QCodeSitter/cursors/iterator.py +141 -0
- QCodeSitter/cursors/line_edits.py +128 -0
- QCodeSitter/cursors/multi.py +240 -0
- QCodeSitter/cursors/state.py +55 -0
- QCodeSitter/cursors/tracked.py +97 -0
- QCodeSitter/defaults.py +93 -0
- QCodeSitter/editing_keys.py +150 -0
- QCodeSitter/editor_options.py +246 -0
- QCodeSitter/gui_colors.py +51 -0
- QCodeSitter/gutters.py +141 -0
- QCodeSitter/integrations/__init__.py +1 -0
- QCodeSitter/integrations/preditor_interface.py +390 -0
- QCodeSitter/languages/__init__.py +99 -0
- QCodeSitter/languages/base.py +60 -0
- QCodeSitter/languages/python.py +146 -0
- QCodeSitter/languages/python_highlights.py +444 -0
- QCodeSitter/py.typed +0 -0
- QCodeSitter/queries/__init__.py +73 -0
- QCodeSitter/queries/predicates.py +172 -0
- QCodeSitter/syntax_specs.py +65 -0
- QCodeSitter/themes.py +271 -0
- QCodeSitter/tracked_document.py +107 -0
- QCodeSitter/tree_manager.py +180 -0
- QCodeSitter/utils.py +84 -0
- qcodesitter-0.1.0.dist-info/METADATA +255 -0
- qcodesitter-0.1.0.dist-info/RECORD +49 -0
- qcodesitter-0.1.0.dist-info/WHEEL +5 -0
- qcodesitter-0.1.0.dist-info/entry_points.txt +2 -0
- qcodesitter-0.1.0.dist-info/licenses/LICENSE.txt +7 -0
- qcodesitter-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from preditor.gui.workbox_mixin import WorkboxMixin
|
|
6
|
+
from Qt import QtCore, QtGui, QtWidgets
|
|
7
|
+
|
|
8
|
+
from .. import CodeEditor
|
|
9
|
+
from ..behaviors.tab_completion import TabCompletion
|
|
10
|
+
from ..defaults import (
|
|
11
|
+
default_options,
|
|
12
|
+
install_default_behaviors,
|
|
13
|
+
install_tab_completion,
|
|
14
|
+
)
|
|
15
|
+
from ..gui_colors import GuiColors, gui_color
|
|
16
|
+
from ..syntax_specs import SyntaxSpecs, overlay_syntax, resolve_capture
|
|
17
|
+
from ..themes import DARK_THEME, LIGHT_THEME, Theme, read_theme
|
|
18
|
+
from ..utils import MM, MO
|
|
19
|
+
|
|
20
|
+
ThemeSection = Literal["gui", "syntax", "style"]
|
|
21
|
+
# A theme value is identified by its section and key
|
|
22
|
+
ThemeKey = tuple[ThemeSection, str]
|
|
23
|
+
# Colors for the "gui" and "syntax" sections, and capture name lists for "style"
|
|
24
|
+
ThemeValue = QtGui.QColor | str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _theme_property(
|
|
28
|
+
section: ThemeSection, key: str, default: ThemeValue | None = None
|
|
29
|
+
) -> QtCore.Property:
|
|
30
|
+
"""Make a Qt property that overrides one value of the theme
|
|
31
|
+
|
|
32
|
+
These properties are meant to be set from a stylesheet like
|
|
33
|
+
`CodeSitterTextEdit { qproperty-hlKeyword: rgb(250, 24, 110); }`
|
|
34
|
+
Color properties default to an invalid QColor, which means "use the built-in theme"
|
|
35
|
+
"""
|
|
36
|
+
if default is None:
|
|
37
|
+
default = QtGui.QColor()
|
|
38
|
+
|
|
39
|
+
def fget(self: CodeSitterTextEdit) -> ThemeValue:
|
|
40
|
+
return self._theme_values.get((section, key), default)
|
|
41
|
+
|
|
42
|
+
def fset(self: CodeSitterTextEdit, value: ThemeValue) -> None:
|
|
43
|
+
self._set_theme_value((section, key), value)
|
|
44
|
+
|
|
45
|
+
return QtCore.Property(type(default), fget=fget, fset=fset)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _color_to_hex(color: QtGui.QColor) -> str:
|
|
49
|
+
if color.alpha() == 255:
|
|
50
|
+
return color.name(QtGui.QColor.NameFormat.HexRgb)
|
|
51
|
+
return color.name(QtGui.QColor.NameFormat.HexArgb)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _color_to_css(color: QtGui.QColor) -> str:
|
|
55
|
+
return f"rgba({color.red()}, {color.green()}, {color.blue()}, {color.alpha()})"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CodeSitterTextEdit(WorkboxMixin, CodeEditor):
|
|
59
|
+
"""A full-featured multi-cursor editor powered by treesitter"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
console: object | None = None,
|
|
64
|
+
core_name: str | None = None,
|
|
65
|
+
delayable_engine: str = "default",
|
|
66
|
+
parent: QtWidgets.QWidget | None = None,
|
|
67
|
+
**kwargs: object,
|
|
68
|
+
) -> None:
|
|
69
|
+
# Stylesheets set the theme properties one at a time, so collect them all
|
|
70
|
+
# and rebuild the theme once they're done. This exists before the widget
|
|
71
|
+
# does, so the property setters work whenever Qt calls them
|
|
72
|
+
self._theme_values: dict[ThemeKey, ThemeValue] = {}
|
|
73
|
+
self._theme_update_timer = QtCore.QTimer()
|
|
74
|
+
self._theme_update_timer.setSingleShot(True)
|
|
75
|
+
self._theme_update_timer.setInterval(0)
|
|
76
|
+
|
|
77
|
+
# WorkboxMixin takes console, core_name and delayable_engine. PrEditor has
|
|
78
|
+
# no type information, so mypy only sees CodeEditor's __init__
|
|
79
|
+
super().__init__( # type: ignore[call-arg]
|
|
80
|
+
parent=parent,
|
|
81
|
+
console=console,
|
|
82
|
+
core_name=core_name,
|
|
83
|
+
delayable_engine=delayable_engine,
|
|
84
|
+
options=default_options(LIGHT_THEME),
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
87
|
+
self._theme_update_timer.setParent(self)
|
|
88
|
+
self._theme_update_timer.timeout.connect(self.rebuild_theme)
|
|
89
|
+
|
|
90
|
+
self._encoding: str | None = None
|
|
91
|
+
self.__set_console__(console)
|
|
92
|
+
install_default_behaviors(self)
|
|
93
|
+
|
|
94
|
+
self._window_stylesheet = "Bright"
|
|
95
|
+
window = self.window()
|
|
96
|
+
if hasattr(window, "styleSheetChanged"):
|
|
97
|
+
window.styleSheetChanged.connect(self.update_color_scheme)
|
|
98
|
+
if hasattr(window, "_stylesheet"):
|
|
99
|
+
self.update_color_scheme(window._stylesheet)
|
|
100
|
+
|
|
101
|
+
def update_color_scheme(self, stylesheet: str) -> None:
|
|
102
|
+
if stylesheet == self._window_stylesheet:
|
|
103
|
+
return
|
|
104
|
+
self._window_stylesheet = stylesheet
|
|
105
|
+
self._theme_update_timer.start()
|
|
106
|
+
|
|
107
|
+
def _base_theme(self) -> Theme:
|
|
108
|
+
"""Pick the built-in theme that stylesheet properties are layered on top of"""
|
|
109
|
+
bg = self._theme_values.get(("gui", "bg"))
|
|
110
|
+
if isinstance(bg, QtGui.QColor) and bg.isValid():
|
|
111
|
+
return DARK_THEME if bg.lightness() < 128 else LIGHT_THEME
|
|
112
|
+
if str(self._window_stylesheet).lower() == "dark":
|
|
113
|
+
return DARK_THEME
|
|
114
|
+
return LIGHT_THEME
|
|
115
|
+
|
|
116
|
+
def build_theme(self) -> tuple[GuiColors, SyntaxSpecs]:
|
|
117
|
+
"""Build the gui colors and syntax specs from the built-in theme and any
|
|
118
|
+
theme properties set by the stylesheet
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
colors: The gui color dictionary
|
|
122
|
+
syntax: The syntax spec dictionary
|
|
123
|
+
"""
|
|
124
|
+
col, syn = read_theme(self._base_theme())
|
|
125
|
+
|
|
126
|
+
syn_colors: SyntaxSpecs = {}
|
|
127
|
+
styles: dict[str, set[str]] = {}
|
|
128
|
+
for (section, key), value in self._theme_values.items():
|
|
129
|
+
if section == "style":
|
|
130
|
+
styles[key] = set(str(value).replace(",", " ").split())
|
|
131
|
+
elif not isinstance(value, QtGui.QColor) or not value.isValid():
|
|
132
|
+
# The color sections only hold colors, and invalid means unset
|
|
133
|
+
continue
|
|
134
|
+
elif section == "gui":
|
|
135
|
+
col[key] = _color_to_hex(value)
|
|
136
|
+
else:
|
|
137
|
+
syn_colors[key] = {"color": _color_to_hex(value)}
|
|
138
|
+
|
|
139
|
+
syn = overlay_syntax(syn, syn_colors)
|
|
140
|
+
|
|
141
|
+
# If the stylesheet defines the style lists, they take precedence
|
|
142
|
+
for style, names in styles.items():
|
|
143
|
+
lookup = {n: True for n in names}
|
|
144
|
+
for name, spec in syn.items():
|
|
145
|
+
matched = resolve_capture(name, lookup) is not None
|
|
146
|
+
if style == "italic":
|
|
147
|
+
spec["italic"] = matched
|
|
148
|
+
else:
|
|
149
|
+
spec["bold"] = matched
|
|
150
|
+
|
|
151
|
+
return col, syn
|
|
152
|
+
|
|
153
|
+
def rebuild_theme(self) -> None:
|
|
154
|
+
col, syn = self.build_theme()
|
|
155
|
+
# This only redraws the editor if the theme changed
|
|
156
|
+
self.options.update({"syntax": syn, "colors": col})
|
|
157
|
+
|
|
158
|
+
# Make sure that preditor doesn't override the colors with its stylesheet
|
|
159
|
+
bg, fg, sel = (
|
|
160
|
+
_color_to_css(gui_color(col, name)) for name in ("bg", "fg", "selection")
|
|
161
|
+
)
|
|
162
|
+
ss = (
|
|
163
|
+
f"QWidget {{background-color: {bg};}}\n"
|
|
164
|
+
f"QPlainTextEdit {{color: {fg}; selection-background-color: {sel};}}"
|
|
165
|
+
)
|
|
166
|
+
# Setting the stylesheet re-polishes the widget, which re-applies the
|
|
167
|
+
# theme properties. Avoid doing that unless something actually changed
|
|
168
|
+
if self.styleSheet() != ss:
|
|
169
|
+
self.setStyleSheet(ss)
|
|
170
|
+
|
|
171
|
+
def setText(self, text: str) -> None:
|
|
172
|
+
"""Replace the text. WorkboxMixin calls the QTextEdit method name"""
|
|
173
|
+
self.setPlainText(text)
|
|
174
|
+
|
|
175
|
+
def __num_lines__(self) -> int:
|
|
176
|
+
return self.document().blockCount()
|
|
177
|
+
|
|
178
|
+
def __auto_complete_enabled__(self) -> bool:
|
|
179
|
+
return self.get_behavior(TabCompletion) is not None
|
|
180
|
+
|
|
181
|
+
def __set_auto_complete_enabled__(self, state: bool) -> None:
|
|
182
|
+
if state == self.__auto_complete_enabled__():
|
|
183
|
+
return
|
|
184
|
+
if state:
|
|
185
|
+
install_tab_completion(self)
|
|
186
|
+
else:
|
|
187
|
+
self.remove_behavior(TabCompletion)
|
|
188
|
+
|
|
189
|
+
def __clear__(self) -> None:
|
|
190
|
+
self.clear()
|
|
191
|
+
self.__set_last_saved_text__(self.__text__())
|
|
192
|
+
|
|
193
|
+
def __populate_edit_menu__(self, menu: QtWidgets.QMenu) -> None:
|
|
194
|
+
"""Fill PrEditor's Workbox menu, which it clears before every time it's shown
|
|
195
|
+
|
|
196
|
+
The menu isn't opened at a point in the text, so the actions that work
|
|
197
|
+
on a place in the document use the text cursor
|
|
198
|
+
"""
|
|
199
|
+
self.populate_edit_menu(menu)
|
|
200
|
+
|
|
201
|
+
def __copy_indents_as_spaces__(self) -> bool:
|
|
202
|
+
"""Whether copying code converts its leading tabs to spaces"""
|
|
203
|
+
return self.options["copy_indents_as_spaces"]
|
|
204
|
+
|
|
205
|
+
def __set_copy_indents_as_spaces__(self, state: bool) -> None:
|
|
206
|
+
self.options["copy_indents_as_spaces"] = state
|
|
207
|
+
|
|
208
|
+
def __cursor_position__(self) -> tuple[int, int]:
|
|
209
|
+
"""Returns the line and index of the cursor."""
|
|
210
|
+
cursor = self.textCursor()
|
|
211
|
+
sc = QtGui.QTextCursor(self.document())
|
|
212
|
+
sc.setPosition(cursor.selectionStart())
|
|
213
|
+
return sc.blockNumber(), sc.positionInBlock()
|
|
214
|
+
|
|
215
|
+
def _clamped_block(self, line: int) -> QtGui.QTextBlock:
|
|
216
|
+
"""Get the block of a 0-based line, clamped to the document's lines"""
|
|
217
|
+
doc = self.document()
|
|
218
|
+
return doc.findBlockByNumber(max(0, min(line, doc.blockCount() - 1)))
|
|
219
|
+
|
|
220
|
+
def __set_cursor_position__(self, line: int, index: int) -> None:
|
|
221
|
+
"""Put a single cursor at an index into a 0-based line
|
|
222
|
+
|
|
223
|
+
The line is clamped to the document, and the index to the line
|
|
224
|
+
"""
|
|
225
|
+
self.cursors.clear_secondary()
|
|
226
|
+
block = self._clamped_block(line)
|
|
227
|
+
cursor = QtGui.QTextCursor(block)
|
|
228
|
+
cursor.setPosition(block.position() + max(0, min(index, block.length() - 1)))
|
|
229
|
+
self.setTextCursor(cursor)
|
|
230
|
+
|
|
231
|
+
def __insert_text__(self, txt: str) -> None:
|
|
232
|
+
self.insert_text(txt)
|
|
233
|
+
|
|
234
|
+
def __remove_selected_text__(self) -> None:
|
|
235
|
+
self.insert_text("")
|
|
236
|
+
|
|
237
|
+
def __font__(self) -> QtGui.QFont:
|
|
238
|
+
return self.options["font"]
|
|
239
|
+
|
|
240
|
+
def __set_font__(self, font: QtGui.QFont) -> None:
|
|
241
|
+
self.options["font"] = font
|
|
242
|
+
|
|
243
|
+
def __goto_line__(self, line: int) -> None:
|
|
244
|
+
"""Put a single cursor at the start of a 1-based line, clamped to the document"""
|
|
245
|
+
self.cursors.clear_secondary()
|
|
246
|
+
self.setTextCursor(QtGui.QTextCursor(self._clamped_block(line - 1)))
|
|
247
|
+
|
|
248
|
+
def __indentations_use_tabs__(self) -> bool:
|
|
249
|
+
return self.options["indent_using_tabs"]
|
|
250
|
+
|
|
251
|
+
def __set_indentations_use_tabs__(self, state: bool) -> None:
|
|
252
|
+
self.options["indent_using_tabs"] = state
|
|
253
|
+
|
|
254
|
+
def __margins_font__(self) -> QtGui.QFont:
|
|
255
|
+
"""The gutters use the editor font"""
|
|
256
|
+
return self.options.get("font", self.font())
|
|
257
|
+
|
|
258
|
+
def __set_margins_font__(self, font: QtGui.QFont) -> None:
|
|
259
|
+
"""The gutters use the editor font, so there is no separate margin font"""
|
|
260
|
+
|
|
261
|
+
def __tab_width__(self) -> int:
|
|
262
|
+
return self.options["space_indent_width"]
|
|
263
|
+
|
|
264
|
+
def __set_tab_width__(self, width: int) -> None:
|
|
265
|
+
self.options["space_indent_width"] = width
|
|
266
|
+
|
|
267
|
+
def __text__(self) -> str:
|
|
268
|
+
return self.toPlainText()
|
|
269
|
+
|
|
270
|
+
def __selected_text__(
|
|
271
|
+
self, start_of_line: bool = False, selectText: bool = False
|
|
272
|
+
) -> tuple[str, int]:
|
|
273
|
+
"""The selected text, or the cursor's line if nothing is selected
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
start_of_line: Extend the selection back to the start of its first line
|
|
277
|
+
selectText: Select the cursor's line, when nothing is selected
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The text, and the line it starts on
|
|
281
|
+
"""
|
|
282
|
+
cursor = self.textCursor()
|
|
283
|
+
|
|
284
|
+
# The line the selection starts on, counting wrapped and folded lines as one
|
|
285
|
+
line = self.document().findBlock(cursor.selectionStart()).blockNumber()
|
|
286
|
+
|
|
287
|
+
if cursor.selection().isEmpty():
|
|
288
|
+
text = cursor.block().text()
|
|
289
|
+
|
|
290
|
+
if selectText:
|
|
291
|
+
cursor.select(QtGui.QTextCursor.SelectionType.LineUnderCursor)
|
|
292
|
+
self.setTextCursor(cursor)
|
|
293
|
+
|
|
294
|
+
return text, line
|
|
295
|
+
|
|
296
|
+
if start_of_line:
|
|
297
|
+
sc = QtGui.QTextCursor(self.document())
|
|
298
|
+
sc.setPosition(cursor.selectionStart())
|
|
299
|
+
sc.movePosition(MO.StartOfLine, MM.MoveAnchor)
|
|
300
|
+
sc.setPosition(cursor.selectionEnd(), MM.KeepAnchor)
|
|
301
|
+
|
|
302
|
+
return sc.selection().toPlainText(), line
|
|
303
|
+
|
|
304
|
+
return cursor.selection().toPlainText(), line
|
|
305
|
+
|
|
306
|
+
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
|
307
|
+
# PrEditor's own shortcuts come before the editor's keys
|
|
308
|
+
if not self.process_shortcut(event):
|
|
309
|
+
super().keyPressEvent(event)
|
|
310
|
+
|
|
311
|
+
def _set_theme_value(self, theme_key: ThemeKey, value: ThemeValue) -> None:
|
|
312
|
+
if self._theme_values.get(theme_key) == value:
|
|
313
|
+
return
|
|
314
|
+
self._theme_values[theme_key] = value
|
|
315
|
+
self._theme_update_timer.start()
|
|
316
|
+
|
|
317
|
+
# fmt: off
|
|
318
|
+
# GUI colors, named after GUI_COLOR_DEFAULTS and DERIVED_GUI_COLORS in
|
|
319
|
+
# gui_colors.py
|
|
320
|
+
guiBg = _theme_property("gui", "bg")
|
|
321
|
+
guiBgDim = _theme_property("gui", "bg_dim")
|
|
322
|
+
guiFg = _theme_property("gui", "fg")
|
|
323
|
+
guiFgDim = _theme_property("gui", "fg_dim")
|
|
324
|
+
guiGutter = _theme_property("gui", "gutter")
|
|
325
|
+
guiGutterFg = _theme_property("gui", "gutter_fg")
|
|
326
|
+
guiHlDim = _theme_property("gui", "hl_dim")
|
|
327
|
+
guiManualFold = _theme_property("gui", "manual_fold")
|
|
328
|
+
guiMatchHl = _theme_property("gui", "match_hl")
|
|
329
|
+
guiMenu = _theme_property("gui", "menu")
|
|
330
|
+
guiOutsideBg = _theme_property("gui", "outside_bg")
|
|
331
|
+
guiPairHl = _theme_property("gui", "pair_hl")
|
|
332
|
+
guiPrimaryCursor = _theme_property("gui", "primary_cursor")
|
|
333
|
+
guiSecondaryCursor = _theme_property("gui", "secondary_cursor")
|
|
334
|
+
guiSelection = _theme_property("gui", "selection")
|
|
335
|
+
guiSecondarySelection = _theme_property("gui", "secondary_selection")
|
|
336
|
+
|
|
337
|
+
# Syntax colors named after the treesitter captures. Unset captures fall back to
|
|
338
|
+
# their parent group, so hlKeywordReturn falls back to hlKeyword
|
|
339
|
+
hlAttribute = _theme_property("syntax", "attribute")
|
|
340
|
+
hlAttributeBuiltin = _theme_property("syntax", "attribute.builtin")
|
|
341
|
+
hlBoolean = _theme_property("syntax", "boolean")
|
|
342
|
+
hlCharacterSpecial = _theme_property("syntax", "character.special")
|
|
343
|
+
hlComment = _theme_property("syntax", "comment")
|
|
344
|
+
hlConstant = _theme_property("syntax", "constant")
|
|
345
|
+
hlConstantBuiltin = _theme_property("syntax", "constant.builtin")
|
|
346
|
+
hlConstructor = _theme_property("syntax", "constructor")
|
|
347
|
+
hlFunction = _theme_property("syntax", "function")
|
|
348
|
+
hlFunctionBuiltin = _theme_property("syntax", "function.builtin")
|
|
349
|
+
hlFunctionCall = _theme_property("syntax", "function.call")
|
|
350
|
+
hlFunctionMacro = _theme_property("syntax", "function.macro")
|
|
351
|
+
hlFunctionMethod = _theme_property("syntax", "function.method")
|
|
352
|
+
hlFunctionMethodCall = _theme_property("syntax", "function.method.call")
|
|
353
|
+
hlKeyword = _theme_property("syntax", "keyword")
|
|
354
|
+
hlKeywordConditional = _theme_property("syntax", "keyword.conditional")
|
|
355
|
+
hlKeywordCoroutine = _theme_property("syntax", "keyword.coroutine")
|
|
356
|
+
hlKeywordDirective = _theme_property("syntax", "keyword.directive")
|
|
357
|
+
hlKeywordException = _theme_property("syntax", "keyword.exception")
|
|
358
|
+
hlKeywordFunction = _theme_property("syntax", "keyword.function")
|
|
359
|
+
hlKeywordImport = _theme_property("syntax", "keyword.import")
|
|
360
|
+
hlKeywordOperator = _theme_property("syntax", "keyword.operator")
|
|
361
|
+
hlKeywordRepeat = _theme_property("syntax", "keyword.repeat")
|
|
362
|
+
hlKeywordReturn = _theme_property("syntax", "keyword.return")
|
|
363
|
+
hlKeywordType = _theme_property("syntax", "keyword.type")
|
|
364
|
+
hlModule = _theme_property("syntax", "module")
|
|
365
|
+
hlModuleBuiltin = _theme_property("syntax", "module.builtin")
|
|
366
|
+
hlNone = _theme_property("syntax", "none")
|
|
367
|
+
hlNumber = _theme_property("syntax", "number")
|
|
368
|
+
hlNumberFloat = _theme_property("syntax", "number.float")
|
|
369
|
+
hlOperator = _theme_property("syntax", "operator")
|
|
370
|
+
hlPunctuation = _theme_property("syntax", "punctuation")
|
|
371
|
+
hlPunctuationBracket = _theme_property("syntax", "punctuation.bracket")
|
|
372
|
+
hlPunctuationDelimiter = _theme_property("syntax", "punctuation.delimiter")
|
|
373
|
+
hlPunctuationSpecial = _theme_property("syntax", "punctuation.special")
|
|
374
|
+
hlString = _theme_property("syntax", "string")
|
|
375
|
+
hlStringDocumentation = _theme_property("syntax", "string.documentation")
|
|
376
|
+
hlStringEscape = _theme_property("syntax", "string.escape")
|
|
377
|
+
hlStringRegexp = _theme_property("syntax", "string.regexp")
|
|
378
|
+
hlType = _theme_property("syntax", "type")
|
|
379
|
+
hlTypeBuiltin = _theme_property("syntax", "type.builtin")
|
|
380
|
+
hlTypeDefinition = _theme_property("syntax", "type.definition")
|
|
381
|
+
hlVariable = _theme_property("syntax", "variable")
|
|
382
|
+
hlVariableBuiltin = _theme_property("syntax", "variable.builtin")
|
|
383
|
+
hlVariableMember = _theme_property("syntax", "variable.member")
|
|
384
|
+
hlVariableParameter = _theme_property("syntax", "variable.parameter")
|
|
385
|
+
|
|
386
|
+
# Space or comma separated lists of captures to draw italic or bold.
|
|
387
|
+
# Like the colors, these apply to child captures as well
|
|
388
|
+
hlItalic = _theme_property("style", "italic", "")
|
|
389
|
+
hlBold = _theme_property("style", "bold", "")
|
|
390
|
+
# fmt: on
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Per-language knowledge, looked up from the editor's tree-sitter language
|
|
2
|
+
|
|
3
|
+
Add support for a language by registering a LanguageSpec for its grammar name.
|
|
4
|
+
The query functions compile the queries of the language's current spec, so
|
|
5
|
+
registering a new spec takes effect the next time a behavior asks for a query.
|
|
6
|
+
Syntax highlighting asks when the language or the "syntax" option changes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from ..queries import compile_query
|
|
14
|
+
from .base import IndentRules, LanguageSpec
|
|
15
|
+
from .python import PYTHON
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from tree_sitter import Language, Query
|
|
19
|
+
|
|
20
|
+
# The languages that are registered when QCodeSitter is imported
|
|
21
|
+
BUILTIN_LANGUAGES = (
|
|
22
|
+
PYTHON,
|
|
23
|
+
LanguageSpec("bash", comment_prefix="# "),
|
|
24
|
+
LanguageSpec("c", comment_prefix="// "),
|
|
25
|
+
LanguageSpec("cpp", comment_prefix="// "),
|
|
26
|
+
LanguageSpec("lua", comment_prefix="-- "),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LanguageRegistry:
|
|
31
|
+
"""The LanguageSpecs the behaviors use, by tree-sitter grammar name
|
|
32
|
+
|
|
33
|
+
There is one registry for the process, LANGUAGES, so registering a
|
|
34
|
+
language adds it to every editor
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, specs: tuple[LanguageSpec, ...] = ()) -> None:
|
|
38
|
+
self._initial = specs
|
|
39
|
+
self._specs = {spec.name: spec for spec in specs}
|
|
40
|
+
|
|
41
|
+
def reset(self) -> None:
|
|
42
|
+
"""Go back to the specs the registry was made with"""
|
|
43
|
+
self._specs = {spec.name: spec for spec in self._initial}
|
|
44
|
+
|
|
45
|
+
def register(self, spec: LanguageSpec) -> None:
|
|
46
|
+
"""Add a language, or replace the spec of one with the same name"""
|
|
47
|
+
self._specs[spec.name] = spec
|
|
48
|
+
|
|
49
|
+
def spec_for(self, language: Language | None) -> LanguageSpec:
|
|
50
|
+
"""Get the spec for a language, or a plain spec if it isn't registered"""
|
|
51
|
+
name = language.name if language is not None else None
|
|
52
|
+
if not name:
|
|
53
|
+
return LanguageSpec("")
|
|
54
|
+
spec = self._specs.get(name)
|
|
55
|
+
return spec if spec is not None else LanguageSpec(name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
LANGUAGES = LanguageRegistry(BUILTIN_LANGUAGES)
|
|
59
|
+
register_language = LANGUAGES.register
|
|
60
|
+
language_spec = LANGUAGES.spec_for
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def highlight_query(language: Language) -> Query | None:
|
|
64
|
+
"""The language's highlight query"""
|
|
65
|
+
source = language_spec(language).highlight_query
|
|
66
|
+
return compile_query(language, source) if source else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def identifier_query(language: Language) -> Query | None:
|
|
70
|
+
"""The query that captures the names defined in a document"""
|
|
71
|
+
source = language_spec(language).identifier_query
|
|
72
|
+
return compile_query(language, source) if source else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def foldable_query(language: Language) -> Query | None:
|
|
76
|
+
"""A query that captures every foldable node in the language as @fold"""
|
|
77
|
+
types = [
|
|
78
|
+
t
|
|
79
|
+
for t in sorted(language_spec(language).foldable_types)
|
|
80
|
+
if language.id_for_node_kind(t, True)
|
|
81
|
+
]
|
|
82
|
+
if not types:
|
|
83
|
+
return None
|
|
84
|
+
patterns = " ".join(f"({t})" for t in types)
|
|
85
|
+
return compile_query(language, f"[{patterns}] @fold")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"BUILTIN_LANGUAGES",
|
|
90
|
+
"LANGUAGES",
|
|
91
|
+
"IndentRules",
|
|
92
|
+
"LanguageRegistry",
|
|
93
|
+
"LanguageSpec",
|
|
94
|
+
"foldable_query",
|
|
95
|
+
"highlight_query",
|
|
96
|
+
"identifier_query",
|
|
97
|
+
"language_spec",
|
|
98
|
+
"register_language",
|
|
99
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from tree_sitter import Node
|
|
8
|
+
|
|
9
|
+
from ..tree_manager import TreeManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IndentRules:
|
|
13
|
+
"""Syntax-aware indentation decisions for SmartIndent
|
|
14
|
+
|
|
15
|
+
The base rules never change the indent, so the new line copies the current one.
|
|
16
|
+
Rows and columns are Qt rows and UTF-16 code unit columns. Both hooks take
|
|
17
|
+
a row and column, and can read the line from self.document.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tree_manager: TreeManager) -> None:
|
|
21
|
+
self.tree_manager = tree_manager
|
|
22
|
+
self.document = tree_manager.document
|
|
23
|
+
|
|
24
|
+
def node_at(self, row: int, col: int) -> Node | None:
|
|
25
|
+
return self.tree_manager.node_at_char(self.document.row_col_to_char(row, col))
|
|
26
|
+
|
|
27
|
+
def should_indent_after(self, row: int, col: int) -> bool:
|
|
28
|
+
"""Whether a new line after this column should be indented one more level"""
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def should_dedent_after(self, row: int, col: int) -> bool:
|
|
32
|
+
"""Whether a new line after this column should be indented one less level"""
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class LanguageSpec:
|
|
38
|
+
"""Everything the behaviors need to know about one tree-sitter language"""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
comment_prefix: str = "# "
|
|
42
|
+
# The default highlight query for the language. Every query can use the
|
|
43
|
+
# text predicates (#eq?, #match?, #any-of?)
|
|
44
|
+
highlight_query: str = ""
|
|
45
|
+
# Captures the names defined in a document, named after their kind
|
|
46
|
+
identifier_query: str = ""
|
|
47
|
+
# Foldable node types that end with their last line (ie: indented blocks)
|
|
48
|
+
fold_hide_last_line: frozenset[str] = frozenset()
|
|
49
|
+
# Foldable node types whose last line is a closing delimiter to keep showing
|
|
50
|
+
fold_keep_last_line: frozenset[str] = frozenset()
|
|
51
|
+
indent_rules: type[IndentRules] = IndentRules
|
|
52
|
+
# Node types where typing never shows completions, like strings and comments
|
|
53
|
+
no_completion_types: frozenset[str] = frozenset()
|
|
54
|
+
# The string node type, and the types of its opening and closing delimiters
|
|
55
|
+
string_type: str = ""
|
|
56
|
+
string_delimiter_types: tuple[str, str] = ("", "")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def foldable_types(self) -> frozenset[str]:
|
|
60
|
+
return self.fold_hide_last_line | self.fold_keep_last_line
|