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.
Files changed (49) hide show
  1. QCodeSitter/__init__.py +38 -0
  2. QCodeSitter/behaviors/__init__.py +115 -0
  3. QCodeSitter/behaviors/auto_bracket.py +141 -0
  4. QCodeSitter/behaviors/code_folding.py +530 -0
  5. QCodeSitter/behaviors/comment_toggle.py +71 -0
  6. QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
  7. QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
  8. QCodeSitter/behaviors/line_numbers.py +49 -0
  9. QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
  10. QCodeSitter/behaviors/overscroll.py +104 -0
  11. QCodeSitter/behaviors/smart_indent.py +191 -0
  12. QCodeSitter/behaviors/syntax_highlighting.py +181 -0
  13. QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
  14. QCodeSitter/behaviors/tab_completion/popup.py +123 -0
  15. QCodeSitter/behaviors/tab_completion/providers.py +66 -0
  16. QCodeSitter/behaviors/tab_completion/types.py +55 -0
  17. QCodeSitter/code_editor.py +540 -0
  18. QCodeSitter/cursors/__init__.py +8 -0
  19. QCodeSitter/cursors/clipboard.py +51 -0
  20. QCodeSitter/cursors/iterator.py +141 -0
  21. QCodeSitter/cursors/line_edits.py +128 -0
  22. QCodeSitter/cursors/multi.py +240 -0
  23. QCodeSitter/cursors/state.py +55 -0
  24. QCodeSitter/cursors/tracked.py +97 -0
  25. QCodeSitter/defaults.py +93 -0
  26. QCodeSitter/editing_keys.py +150 -0
  27. QCodeSitter/editor_options.py +246 -0
  28. QCodeSitter/gui_colors.py +51 -0
  29. QCodeSitter/gutters.py +141 -0
  30. QCodeSitter/integrations/__init__.py +1 -0
  31. QCodeSitter/integrations/preditor_interface.py +390 -0
  32. QCodeSitter/languages/__init__.py +99 -0
  33. QCodeSitter/languages/base.py +60 -0
  34. QCodeSitter/languages/python.py +146 -0
  35. QCodeSitter/languages/python_highlights.py +444 -0
  36. QCodeSitter/py.typed +0 -0
  37. QCodeSitter/queries/__init__.py +73 -0
  38. QCodeSitter/queries/predicates.py +172 -0
  39. QCodeSitter/syntax_specs.py +65 -0
  40. QCodeSitter/themes.py +271 -0
  41. QCodeSitter/tracked_document.py +107 -0
  42. QCodeSitter/tree_manager.py +180 -0
  43. QCodeSitter/utils.py +84 -0
  44. qcodesitter-0.1.0.dist-info/METADATA +255 -0
  45. qcodesitter-0.1.0.dist-info/RECORD +49 -0
  46. qcodesitter-0.1.0.dist-info/WHEEL +5 -0
  47. qcodesitter-0.1.0.dist-info/entry_points.txt +2 -0
  48. qcodesitter-0.1.0.dist-info/licenses/LICENSE.txt +7 -0
  49. 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