QCodeSitter 0.1.0__tar.gz

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 (54) hide show
  1. qcodesitter-0.1.0/LICENSE.txt +7 -0
  2. qcodesitter-0.1.0/PKG-INFO +255 -0
  3. qcodesitter-0.1.0/README.md +219 -0
  4. qcodesitter-0.1.0/pyproject.toml +67 -0
  5. qcodesitter-0.1.0/setup.cfg +4 -0
  6. qcodesitter-0.1.0/src/QCodeSitter/__init__.py +38 -0
  7. qcodesitter-0.1.0/src/QCodeSitter/behaviors/__init__.py +115 -0
  8. qcodesitter-0.1.0/src/QCodeSitter/behaviors/auto_bracket.py +141 -0
  9. qcodesitter-0.1.0/src/QCodeSitter/behaviors/code_folding.py +530 -0
  10. qcodesitter-0.1.0/src/QCodeSitter/behaviors/comment_toggle.py +71 -0
  11. qcodesitter-0.1.0/src/QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
  12. qcodesitter-0.1.0/src/QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
  13. qcodesitter-0.1.0/src/QCodeSitter/behaviors/line_numbers.py +49 -0
  14. qcodesitter-0.1.0/src/QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
  15. qcodesitter-0.1.0/src/QCodeSitter/behaviors/overscroll.py +104 -0
  16. qcodesitter-0.1.0/src/QCodeSitter/behaviors/smart_indent.py +191 -0
  17. qcodesitter-0.1.0/src/QCodeSitter/behaviors/syntax_highlighting.py +181 -0
  18. qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
  19. qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/popup.py +123 -0
  20. qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/providers.py +66 -0
  21. qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/types.py +55 -0
  22. qcodesitter-0.1.0/src/QCodeSitter/code_editor.py +540 -0
  23. qcodesitter-0.1.0/src/QCodeSitter/cursors/__init__.py +8 -0
  24. qcodesitter-0.1.0/src/QCodeSitter/cursors/clipboard.py +51 -0
  25. qcodesitter-0.1.0/src/QCodeSitter/cursors/iterator.py +141 -0
  26. qcodesitter-0.1.0/src/QCodeSitter/cursors/line_edits.py +128 -0
  27. qcodesitter-0.1.0/src/QCodeSitter/cursors/multi.py +240 -0
  28. qcodesitter-0.1.0/src/QCodeSitter/cursors/state.py +55 -0
  29. qcodesitter-0.1.0/src/QCodeSitter/cursors/tracked.py +97 -0
  30. qcodesitter-0.1.0/src/QCodeSitter/defaults.py +93 -0
  31. qcodesitter-0.1.0/src/QCodeSitter/editing_keys.py +150 -0
  32. qcodesitter-0.1.0/src/QCodeSitter/editor_options.py +246 -0
  33. qcodesitter-0.1.0/src/QCodeSitter/gui_colors.py +51 -0
  34. qcodesitter-0.1.0/src/QCodeSitter/gutters.py +141 -0
  35. qcodesitter-0.1.0/src/QCodeSitter/integrations/__init__.py +1 -0
  36. qcodesitter-0.1.0/src/QCodeSitter/integrations/preditor_interface.py +390 -0
  37. qcodesitter-0.1.0/src/QCodeSitter/languages/__init__.py +99 -0
  38. qcodesitter-0.1.0/src/QCodeSitter/languages/base.py +60 -0
  39. qcodesitter-0.1.0/src/QCodeSitter/languages/python.py +146 -0
  40. qcodesitter-0.1.0/src/QCodeSitter/languages/python_highlights.py +444 -0
  41. qcodesitter-0.1.0/src/QCodeSitter/py.typed +0 -0
  42. qcodesitter-0.1.0/src/QCodeSitter/queries/__init__.py +73 -0
  43. qcodesitter-0.1.0/src/QCodeSitter/queries/predicates.py +172 -0
  44. qcodesitter-0.1.0/src/QCodeSitter/syntax_specs.py +65 -0
  45. qcodesitter-0.1.0/src/QCodeSitter/themes.py +271 -0
  46. qcodesitter-0.1.0/src/QCodeSitter/tracked_document.py +107 -0
  47. qcodesitter-0.1.0/src/QCodeSitter/tree_manager.py +180 -0
  48. qcodesitter-0.1.0/src/QCodeSitter/utils.py +84 -0
  49. qcodesitter-0.1.0/src/QCodeSitter.egg-info/PKG-INFO +255 -0
  50. qcodesitter-0.1.0/src/QCodeSitter.egg-info/SOURCES.txt +52 -0
  51. qcodesitter-0.1.0/src/QCodeSitter.egg-info/dependency_links.txt +1 -0
  52. qcodesitter-0.1.0/src/QCodeSitter.egg-info/entry_points.txt +2 -0
  53. qcodesitter-0.1.0/src/QCodeSitter.egg-info/requires.txt +27 -0
  54. qcodesitter-0.1.0/src/QCodeSitter.egg-info/top_level.txt +1 -0
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Tyler Fox
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: QCodeSitter
3
+ Version: 0.1.0
4
+ Summary: A tree-sitter powered code editor widget for Qt
5
+ Author-email: Tyler Fox <tyler@blur.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tbttfox/qcodesitter
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE.txt
13
+ Requires-Dist: Qt.py>=2.0
14
+ Requires-Dist: tree-sitter<0.26,>=0.25.2
15
+ Requires-Dist: tree-sitter-python<0.26,>=0.23
16
+ Provides-Extra: pyside6
17
+ Requires-Dist: PySide6; extra == "pyside6"
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7.0; extra == "test"
20
+ Requires-Dist: pytest-cov; extra == "test"
21
+ Requires-Dist: pytest-qt>=4.0; extra == "test"
22
+ Requires-Dist: PySide6; extra == "test"
23
+ Provides-Extra: fuzz
24
+ Requires-Dist: pytest>=7.0; extra == "fuzz"
25
+ Requires-Dist: pytest-qt>=4.0; extra == "fuzz"
26
+ Requires-Dist: pytest-timeout; extra == "fuzz"
27
+ Requires-Dist: hypothesis; extra == "fuzz"
28
+ Requires-Dist: PySide6; extra == "fuzz"
29
+ Provides-Extra: dev
30
+ Requires-Dist: tox>=4.0; extra == "dev"
31
+ Requires-Dist: mypy; extra == "dev"
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Provides-Extra: preditor
34
+ Requires-Dist: preditor; extra == "preditor"
35
+ Dynamic: license-file
36
+
37
+ # QCodeSitter
38
+
39
+ A reusable, tree-sitter-powered code editor widget for Qt applications. QCodeSitter provides a `QPlainTextEdit` subclass with syntax highlighting, multi-cursor editing, smart indentation, code folding, and more -- all driven by a modular plugin system that makes it easy to add, remove, or customize features.
40
+
41
+ This is not an IDE. It's a drop-in editor widget you can embed in your own Qt application.
42
+
43
+ ## Quick Start
44
+
45
+ The quickest way to a full editor is the default setup:
46
+
47
+ ```python
48
+ from Qt import QtWidgets
49
+
50
+ from QCodeSitter import CodeEditor, default_options, install_default_behaviors
51
+
52
+ app = QtWidgets.QApplication([])
53
+ editor = CodeEditor(default_options())
54
+ install_default_behaviors(editor)
55
+ editor.show()
56
+ app.exec()
57
+ ```
58
+
59
+ `install_default_behaviors(editor, optional=True)` also adds the behaviors that are off by
60
+ default, like `AutoBracket`.
61
+
62
+ Or pick the options and behaviors yourself:
63
+
64
+ ```python
65
+ from tree_sitter import Language
66
+ import tree_sitter_python as tspython
67
+ from Qt import QtGui
68
+
69
+ from QCodeSitter import CodeEditor, EditorOptions
70
+ from QCodeSitter.themes import DARK_THEME, read_theme
71
+ from QCodeSitter.behaviors.syntax_highlighting import SyntaxHighlighting
72
+ from QCodeSitter.behaviors.smart_indent import SmartIndent
73
+ from QCodeSitter.behaviors.line_numbers import LineNumbers
74
+ from QCodeSitter.behaviors.auto_bracket import AutoBracket
75
+
76
+ language = Language(tspython.language())
77
+ colors, syntax = read_theme(DARK_THEME)
78
+ options = EditorOptions({
79
+ "space_indent_width": 4,
80
+ "indent_using_tabs": False,
81
+ "language": language,
82
+ "syntax": syntax,
83
+ "colors": colors,
84
+ "font": QtGui.QFont("Consolas", pointSize=11),
85
+ })
86
+
87
+ editor = CodeEditor(options)
88
+ editor.add_behavior(SyntaxHighlighting)
89
+ editor.add_behavior(SmartIndent)
90
+ editor.add_behavior(LineNumbers)
91
+ editor.add_behavior(AutoBracket)
92
+ ```
93
+
94
+ You only pay for what you use. If you don't need code folding, don't add it. If you want custom behavior, write your own.
95
+
96
+ Every option is listed in `OptionsDict` in `editor_options.py`, and the options you don't
97
+ give take their value from `OPTION_DEFAULTS`. Setting an option that isn't listed raises a
98
+ `KeyError`, and a value of the wrong type raises a `TypeError`, so mistakes don't silently
99
+ do nothing. The highlight query comes from the language, so changing `"language"` is all it
100
+ takes to highlight another language. `"syntax"` only holds the format of each capture.
101
+
102
+ The `"colors"` option holds the gui colors, listed with their defaults in `gui_colors.py`.
103
+
104
+ Setting an option to the value it already has does nothing, so change a list option by
105
+ setting a new list rather than editing it in place. A behavior from outside the package
106
+ adds its own options with `register_option("my_option", int, default=3)`.
107
+
108
+ ## The Behavior System
109
+
110
+ Every editor feature beyond basic text editing is implemented as a **Behavior**. Behaviors are essentially just plugins. They are modular, self-contained classes that plug into the editor through a well-defined interface.
111
+
112
+ ### Base Class
113
+
114
+ All behaviors inherit from `Behavior`:
115
+
116
+ ```python
117
+ from QCodeSitter.behaviors import Behavior
118
+
119
+ class MyBehavior(Behavior):
120
+ def __init__(self, editor):
121
+ super().__init__(editor)
122
+ # self.editor - the CodeEditor instance
123
+ # self.options - the EditorOptions dict-like object
124
+ # Signals connected with self.connect are disconnected when it's removed
125
+ self.connect(editor.textChanged, self.on_text_changed)
126
+
127
+ def on_text_changed(self):
128
+ ...
129
+
130
+ def on_remove(self):
131
+ # Undo anything else done in __init__. This runs after the
132
+ # signals connected with self.connect are disconnected
133
+ ```
134
+
135
+ Behaviors are added and removed at runtime:
136
+
137
+ ```python
138
+ behavior = editor.add_behavior(MyBehavior) # Replaces any existing MyBehavior
139
+ editor.remove_behavior(MyBehavior) # Returns the removed instance
140
+ editor.get_behavior(MyBehavior) # Returns the instance or None
141
+ ```
142
+
143
+ ### Reacting to Option Changes
144
+
145
+ Every option except `"language"` and `"font"` always has a value, so a behavior can read a
146
+ setting from `self.options` when it needs it. For a setting that needs work when it changes,
147
+ register a handler. Handlers are called with the option's value right away, and again
148
+ whenever it changes. The handlers of `"language"` and `"font"` wait until they are set:
149
+
150
+ ```python
151
+ class MyBehavior(Behavior):
152
+ def __init__(self, editor):
153
+ super().__init__(editor)
154
+ self.font = QtGui.QFont() # The default until the option is set
155
+ self.watch_options({"font": self.set_font})
156
+
157
+ def set_font(self, font):
158
+ self.font = font
159
+ ```
160
+
161
+ Parsing and highlighting are the expensive part of an editor, so the editor only sets its
162
+ language when it is first shown. Call `editor.load()` to set it for an editor you don't show.
163
+
164
+ ### Keys, Painting and Signals
165
+
166
+ - Override `handle_key_press(event)` to get key presses before the editor's own key
167
+ handling, and return `True` from it to consume the key. Behaviors with a higher
168
+ `KEY_PRIORITY` get the keys first, like `TabCompletion`, so Tab accepts a completion
169
+ before `SmartIndent` indents.
170
+ - Return actions from `get_hotkeys` to add keyboard shortcuts to the editor.
171
+ - Connect to `editor.behaviorPaint(event, painter)` to paint over the text.
172
+ - Connect to `editor.cursors.changed`, `editor.tree_manager.reparsed`, or
173
+ `editor.undoRedoFinished` to react to the editor.
174
+ - Subclass `GutterWidget` (in `gutters.py`) for a column beside the text, like line numbers.
175
+
176
+ ### Languages
177
+
178
+ Language-specific knowledge (comment prefix, indent rules, foldable node types, and the
179
+ highlight and identifier queries) lives in a `LanguageSpec`, looked up from the editor's
180
+ tree-sitter language. Add a language by registering a spec for its grammar name:
181
+
182
+ ```python
183
+ from QCodeSitter.languages import LanguageSpec, register_language
184
+
185
+ register_language(LanguageSpec("lua", comment_prefix="-- "))
186
+ ```
187
+
188
+ ## Built-in Behaviors
189
+
190
+ | Behavior | Description |
191
+ |---|---|
192
+ | `SyntaxHighlighting` | Tree-sitter-based syntax highlighting with incremental updates |
193
+ | `SmartIndent` | Language-aware auto-indentation on Enter, Tab, Backspace |
194
+ | `LineNumbers` | Line number gutter |
195
+ | `AutoBracket` | Auto-close brackets, quotes, and parens; selection wrapping |
196
+ | `TabCompletion` | Autocomplete popup with pluggable providers |
197
+ | `CodeFolding` | Tree-sitter-based code folding with gutter indicators |
198
+ | `CommentToggle` | Toggle line comments (Ctrl+/) with language detection |
199
+ | `HighlightMatchingBrackets` | Highlight matching bracket/paren/brace pairs |
200
+ | `HighlightMatchingSelection` | Highlight all occurrences of selected text |
201
+ | `MultiCursorPaint` | Render primary and secondary cursors |
202
+ | `Overscroll` | Allow scrolling past end of document |
203
+
204
+ ## Multi-Cursor Editing
205
+
206
+ The editor has built-in multi-cursor support. All text operations (insert, delete, indent, comment, etc.) apply to every cursor simultaneously.
207
+
208
+ **Default bindings:**
209
+ - **Ctrl+D** -- Select next occurrence of current selection
210
+ - **Ctrl+Alt+Up/Down** -- Add cursor above/below
211
+ - **Ctrl+Shift+L** -- Add cursors to all line ends in selection
212
+ - **Alt+Click** -- Add cursor at mouse position
213
+ - **Escape** -- Exit multi-cursor mode
214
+
215
+ Behaviors that modify text across multiple cursors use the editor's `CursorIterator`.
216
+ It moves one cursor to each cursor in turn, and shifts the later cursors by however much
217
+ each edit changed the document:
218
+
219
+ ```python
220
+ def my_multi_cursor_operation(self):
221
+ citer = self.editor.citer
222
+ with self.editor.edit_session(): # One undo step, and one reparse
223
+ for cursor in citer.iterate_cursors(complete_all=True):
224
+ cursor.insertText("hello")
225
+ ```
226
+
227
+ `complete_all=True` marks every cursor completed, which stops later key handlers in the
228
+ same edit session from editing them. A pass that handles only some cursors calls
229
+ `citer.mark_cursor_completed()` inside the loop for each one it handled instead.
230
+
231
+ ## Tree-Sitter Integration
232
+
233
+ QCodeSitter uses tree-sitter for parsing, with UTF-16 encoding throughout to match Qt's internal string representation. The `TreeManager` provides:
234
+
235
+ - Incremental parsing (only re-parses changed regions)
236
+ - A `reparsed` signal for loose coupling between components
237
+ - Pause/unpause to batch edits (e.g., during multi-cursor operations)
238
+
239
+ The `TrackedDocument` (a `QTextDocument` subclass) and `TrackedCursor` (a `QTextCursor` subclass) automatically track UTF-16 byte offsets and emit change signals that keep the parse tree in sync.
240
+
241
+ ### Dependencies
242
+
243
+ - Python 3.10+
244
+
245
+ - [Qt.py](https://github.com/mottosso/Qt.py), and a Qt binding for it. The tests run on
246
+ PySide6 (`tox`) and PyQt6 (`tox -e pyqt6`); PySide2 and PyQt5 are untested.
247
+ `pip install QCodeSitter[pyside6]` installs PySide6
248
+ - [tree-sitter](https://github.com/tree-sitter/py-tree-sitter)
249
+ - A tree-sitter language grammar (e.g., `tree-sitter-python`)
250
+
251
+ ### Running Tests
252
+
253
+ Tests are set up to be run with `tox`, and `tox -e fuzz` runs the slower Hypothesis fuzz
254
+ tests. `python examples/demo.py` opens a window with the default editor.
255
+
@@ -0,0 +1,219 @@
1
+ # QCodeSitter
2
+
3
+ A reusable, tree-sitter-powered code editor widget for Qt applications. QCodeSitter provides a `QPlainTextEdit` subclass with syntax highlighting, multi-cursor editing, smart indentation, code folding, and more -- all driven by a modular plugin system that makes it easy to add, remove, or customize features.
4
+
5
+ This is not an IDE. It's a drop-in editor widget you can embed in your own Qt application.
6
+
7
+ ## Quick Start
8
+
9
+ The quickest way to a full editor is the default setup:
10
+
11
+ ```python
12
+ from Qt import QtWidgets
13
+
14
+ from QCodeSitter import CodeEditor, default_options, install_default_behaviors
15
+
16
+ app = QtWidgets.QApplication([])
17
+ editor = CodeEditor(default_options())
18
+ install_default_behaviors(editor)
19
+ editor.show()
20
+ app.exec()
21
+ ```
22
+
23
+ `install_default_behaviors(editor, optional=True)` also adds the behaviors that are off by
24
+ default, like `AutoBracket`.
25
+
26
+ Or pick the options and behaviors yourself:
27
+
28
+ ```python
29
+ from tree_sitter import Language
30
+ import tree_sitter_python as tspython
31
+ from Qt import QtGui
32
+
33
+ from QCodeSitter import CodeEditor, EditorOptions
34
+ from QCodeSitter.themes import DARK_THEME, read_theme
35
+ from QCodeSitter.behaviors.syntax_highlighting import SyntaxHighlighting
36
+ from QCodeSitter.behaviors.smart_indent import SmartIndent
37
+ from QCodeSitter.behaviors.line_numbers import LineNumbers
38
+ from QCodeSitter.behaviors.auto_bracket import AutoBracket
39
+
40
+ language = Language(tspython.language())
41
+ colors, syntax = read_theme(DARK_THEME)
42
+ options = EditorOptions({
43
+ "space_indent_width": 4,
44
+ "indent_using_tabs": False,
45
+ "language": language,
46
+ "syntax": syntax,
47
+ "colors": colors,
48
+ "font": QtGui.QFont("Consolas", pointSize=11),
49
+ })
50
+
51
+ editor = CodeEditor(options)
52
+ editor.add_behavior(SyntaxHighlighting)
53
+ editor.add_behavior(SmartIndent)
54
+ editor.add_behavior(LineNumbers)
55
+ editor.add_behavior(AutoBracket)
56
+ ```
57
+
58
+ You only pay for what you use. If you don't need code folding, don't add it. If you want custom behavior, write your own.
59
+
60
+ Every option is listed in `OptionsDict` in `editor_options.py`, and the options you don't
61
+ give take their value from `OPTION_DEFAULTS`. Setting an option that isn't listed raises a
62
+ `KeyError`, and a value of the wrong type raises a `TypeError`, so mistakes don't silently
63
+ do nothing. The highlight query comes from the language, so changing `"language"` is all it
64
+ takes to highlight another language. `"syntax"` only holds the format of each capture.
65
+
66
+ The `"colors"` option holds the gui colors, listed with their defaults in `gui_colors.py`.
67
+
68
+ Setting an option to the value it already has does nothing, so change a list option by
69
+ setting a new list rather than editing it in place. A behavior from outside the package
70
+ adds its own options with `register_option("my_option", int, default=3)`.
71
+
72
+ ## The Behavior System
73
+
74
+ Every editor feature beyond basic text editing is implemented as a **Behavior**. Behaviors are essentially just plugins. They are modular, self-contained classes that plug into the editor through a well-defined interface.
75
+
76
+ ### Base Class
77
+
78
+ All behaviors inherit from `Behavior`:
79
+
80
+ ```python
81
+ from QCodeSitter.behaviors import Behavior
82
+
83
+ class MyBehavior(Behavior):
84
+ def __init__(self, editor):
85
+ super().__init__(editor)
86
+ # self.editor - the CodeEditor instance
87
+ # self.options - the EditorOptions dict-like object
88
+ # Signals connected with self.connect are disconnected when it's removed
89
+ self.connect(editor.textChanged, self.on_text_changed)
90
+
91
+ def on_text_changed(self):
92
+ ...
93
+
94
+ def on_remove(self):
95
+ # Undo anything else done in __init__. This runs after the
96
+ # signals connected with self.connect are disconnected
97
+ ```
98
+
99
+ Behaviors are added and removed at runtime:
100
+
101
+ ```python
102
+ behavior = editor.add_behavior(MyBehavior) # Replaces any existing MyBehavior
103
+ editor.remove_behavior(MyBehavior) # Returns the removed instance
104
+ editor.get_behavior(MyBehavior) # Returns the instance or None
105
+ ```
106
+
107
+ ### Reacting to Option Changes
108
+
109
+ Every option except `"language"` and `"font"` always has a value, so a behavior can read a
110
+ setting from `self.options` when it needs it. For a setting that needs work when it changes,
111
+ register a handler. Handlers are called with the option's value right away, and again
112
+ whenever it changes. The handlers of `"language"` and `"font"` wait until they are set:
113
+
114
+ ```python
115
+ class MyBehavior(Behavior):
116
+ def __init__(self, editor):
117
+ super().__init__(editor)
118
+ self.font = QtGui.QFont() # The default until the option is set
119
+ self.watch_options({"font": self.set_font})
120
+
121
+ def set_font(self, font):
122
+ self.font = font
123
+ ```
124
+
125
+ Parsing and highlighting are the expensive part of an editor, so the editor only sets its
126
+ language when it is first shown. Call `editor.load()` to set it for an editor you don't show.
127
+
128
+ ### Keys, Painting and Signals
129
+
130
+ - Override `handle_key_press(event)` to get key presses before the editor's own key
131
+ handling, and return `True` from it to consume the key. Behaviors with a higher
132
+ `KEY_PRIORITY` get the keys first, like `TabCompletion`, so Tab accepts a completion
133
+ before `SmartIndent` indents.
134
+ - Return actions from `get_hotkeys` to add keyboard shortcuts to the editor.
135
+ - Connect to `editor.behaviorPaint(event, painter)` to paint over the text.
136
+ - Connect to `editor.cursors.changed`, `editor.tree_manager.reparsed`, or
137
+ `editor.undoRedoFinished` to react to the editor.
138
+ - Subclass `GutterWidget` (in `gutters.py`) for a column beside the text, like line numbers.
139
+
140
+ ### Languages
141
+
142
+ Language-specific knowledge (comment prefix, indent rules, foldable node types, and the
143
+ highlight and identifier queries) lives in a `LanguageSpec`, looked up from the editor's
144
+ tree-sitter language. Add a language by registering a spec for its grammar name:
145
+
146
+ ```python
147
+ from QCodeSitter.languages import LanguageSpec, register_language
148
+
149
+ register_language(LanguageSpec("lua", comment_prefix="-- "))
150
+ ```
151
+
152
+ ## Built-in Behaviors
153
+
154
+ | Behavior | Description |
155
+ |---|---|
156
+ | `SyntaxHighlighting` | Tree-sitter-based syntax highlighting with incremental updates |
157
+ | `SmartIndent` | Language-aware auto-indentation on Enter, Tab, Backspace |
158
+ | `LineNumbers` | Line number gutter |
159
+ | `AutoBracket` | Auto-close brackets, quotes, and parens; selection wrapping |
160
+ | `TabCompletion` | Autocomplete popup with pluggable providers |
161
+ | `CodeFolding` | Tree-sitter-based code folding with gutter indicators |
162
+ | `CommentToggle` | Toggle line comments (Ctrl+/) with language detection |
163
+ | `HighlightMatchingBrackets` | Highlight matching bracket/paren/brace pairs |
164
+ | `HighlightMatchingSelection` | Highlight all occurrences of selected text |
165
+ | `MultiCursorPaint` | Render primary and secondary cursors |
166
+ | `Overscroll` | Allow scrolling past end of document |
167
+
168
+ ## Multi-Cursor Editing
169
+
170
+ The editor has built-in multi-cursor support. All text operations (insert, delete, indent, comment, etc.) apply to every cursor simultaneously.
171
+
172
+ **Default bindings:**
173
+ - **Ctrl+D** -- Select next occurrence of current selection
174
+ - **Ctrl+Alt+Up/Down** -- Add cursor above/below
175
+ - **Ctrl+Shift+L** -- Add cursors to all line ends in selection
176
+ - **Alt+Click** -- Add cursor at mouse position
177
+ - **Escape** -- Exit multi-cursor mode
178
+
179
+ Behaviors that modify text across multiple cursors use the editor's `CursorIterator`.
180
+ It moves one cursor to each cursor in turn, and shifts the later cursors by however much
181
+ each edit changed the document:
182
+
183
+ ```python
184
+ def my_multi_cursor_operation(self):
185
+ citer = self.editor.citer
186
+ with self.editor.edit_session(): # One undo step, and one reparse
187
+ for cursor in citer.iterate_cursors(complete_all=True):
188
+ cursor.insertText("hello")
189
+ ```
190
+
191
+ `complete_all=True` marks every cursor completed, which stops later key handlers in the
192
+ same edit session from editing them. A pass that handles only some cursors calls
193
+ `citer.mark_cursor_completed()` inside the loop for each one it handled instead.
194
+
195
+ ## Tree-Sitter Integration
196
+
197
+ QCodeSitter uses tree-sitter for parsing, with UTF-16 encoding throughout to match Qt's internal string representation. The `TreeManager` provides:
198
+
199
+ - Incremental parsing (only re-parses changed regions)
200
+ - A `reparsed` signal for loose coupling between components
201
+ - Pause/unpause to batch edits (e.g., during multi-cursor operations)
202
+
203
+ The `TrackedDocument` (a `QTextDocument` subclass) and `TrackedCursor` (a `QTextCursor` subclass) automatically track UTF-16 byte offsets and emit change signals that keep the parse tree in sync.
204
+
205
+ ### Dependencies
206
+
207
+ - Python 3.10+
208
+
209
+ - [Qt.py](https://github.com/mottosso/Qt.py), and a Qt binding for it. The tests run on
210
+ PySide6 (`tox`) and PyQt6 (`tox -e pyqt6`); PySide2 and PyQt5 are untested.
211
+ `pip install QCodeSitter[pyside6]` installs PySide6
212
+ - [tree-sitter](https://github.com/tree-sitter/py-tree-sitter)
213
+ - A tree-sitter language grammar (e.g., `tree-sitter-python`)
214
+
215
+ ### Running Tests
216
+
217
+ Tests are set up to be run with `tox`, and `tox -e fuzz` runs the slower Hypothesis fuzz
218
+ tests. `python examples/demo.py` opens a window with the default editor.
219
+
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "QCodeSitter"
7
+ version = "0.1.0"
8
+ description = "A tree-sitter powered code editor widget for Qt"
9
+ requires-python = ">=3.10"
10
+ # Qt.py needs a Qt binding: install one yourself, or use the pyside6 extra.
11
+ # The grammar has to be built for an ABI that the tree-sitter core can load
12
+ dependencies = [
13
+ "Qt.py>=2.0",
14
+ "tree-sitter>=0.25.2,<0.26",
15
+ "tree-sitter-python>=0.23,<0.26",
16
+ ]
17
+ authors = [
18
+ { name="Tyler Fox", email="tyler@blur.com" },
19
+ ]
20
+ readme = "README.md"
21
+ license = "MIT"
22
+ license-files = ["LICENSE.txt"]
23
+ classifiers = [
24
+ "Programming Language :: Python :: 3",
25
+ "Operating System :: OS Independent",
26
+ ]
27
+
28
+
29
+ [project.optional-dependencies]
30
+ pyside6 = ["PySide6"]
31
+ test = ["pytest>=7.0", "pytest-cov", "pytest-qt>=4.0", "PySide6"]
32
+ fuzz = ["pytest>=7.0", "pytest-qt>=4.0", "pytest-timeout", "hypothesis", "PySide6"]
33
+ dev = ["tox>=4.0", "mypy", "ruff"]
34
+ preditor = ["preditor"]
35
+
36
+ [project.entry-points."preditor.plug.editors"]
37
+ QCodeSitter = "QCodeSitter.integrations.preditor_interface:CodeSitterTextEdit"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/tbttfox/qcodesitter"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+
45
+ [tool.setuptools.package-data]
46
+ QCodeSitter = ["py.typed"]
47
+
48
+ [tool.mypy]
49
+ python_version = "3.10"
50
+
51
+ [[tool.mypy.overrides]]
52
+ # PrEditor ships no stubs or py.typed marker
53
+ module = ["preditor.*"]
54
+ ignore_missing_imports = true
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ python_files = ["test_*.py"]
59
+ python_classes = ["Test*"]
60
+ python_functions = ["test_*"]
61
+ addopts = "-v --ignore=tests/fuzz"
62
+ markers = [
63
+ "fuzz: slow fuzz tests (run with: tox -e fuzz)",
64
+ ]
65
+
66
+ [tool.ruff.lint]
67
+ ignore = ["SIM", "RUF012", "N999"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """A tree-sitter powered code editor widget for Qt
2
+
3
+ `default_options` and `install_default_behaviors` come from the defaults module,
4
+ which is only imported when one of them is used, because it imports every
5
+ built-in behavior. Using the editor with your own behaviors doesn't need them
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from .code_editor import CodeEditor
13
+ from .editor_options import EditorOptions
14
+ from .tracked_document import TrackedDocument
15
+ from .tree_manager import TreeManager
16
+
17
+ if TYPE_CHECKING:
18
+ from .defaults import default_options, install_default_behaviors
19
+
20
+ _FROM_DEFAULTS = ("default_options", "install_default_behaviors")
21
+
22
+
23
+ def __getattr__(name: str) -> Any:
24
+ if name in _FROM_DEFAULTS:
25
+ from . import defaults
26
+
27
+ return getattr(defaults, name)
28
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
29
+
30
+
31
+ __all__ = [
32
+ "CodeEditor",
33
+ "EditorOptions",
34
+ "TrackedDocument",
35
+ "TreeManager",
36
+ "default_options",
37
+ "install_default_behaviors",
38
+ ]