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.
- qcodesitter-0.1.0/LICENSE.txt +7 -0
- qcodesitter-0.1.0/PKG-INFO +255 -0
- qcodesitter-0.1.0/README.md +219 -0
- qcodesitter-0.1.0/pyproject.toml +67 -0
- qcodesitter-0.1.0/setup.cfg +4 -0
- qcodesitter-0.1.0/src/QCodeSitter/__init__.py +38 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/__init__.py +115 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/auto_bracket.py +141 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/code_folding.py +530 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/comment_toggle.py +71 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/line_numbers.py +49 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/overscroll.py +104 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/smart_indent.py +191 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/syntax_highlighting.py +181 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/popup.py +123 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/providers.py +66 -0
- qcodesitter-0.1.0/src/QCodeSitter/behaviors/tab_completion/types.py +55 -0
- qcodesitter-0.1.0/src/QCodeSitter/code_editor.py +540 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/__init__.py +8 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/clipboard.py +51 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/iterator.py +141 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/line_edits.py +128 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/multi.py +240 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/state.py +55 -0
- qcodesitter-0.1.0/src/QCodeSitter/cursors/tracked.py +97 -0
- qcodesitter-0.1.0/src/QCodeSitter/defaults.py +93 -0
- qcodesitter-0.1.0/src/QCodeSitter/editing_keys.py +150 -0
- qcodesitter-0.1.0/src/QCodeSitter/editor_options.py +246 -0
- qcodesitter-0.1.0/src/QCodeSitter/gui_colors.py +51 -0
- qcodesitter-0.1.0/src/QCodeSitter/gutters.py +141 -0
- qcodesitter-0.1.0/src/QCodeSitter/integrations/__init__.py +1 -0
- qcodesitter-0.1.0/src/QCodeSitter/integrations/preditor_interface.py +390 -0
- qcodesitter-0.1.0/src/QCodeSitter/languages/__init__.py +99 -0
- qcodesitter-0.1.0/src/QCodeSitter/languages/base.py +60 -0
- qcodesitter-0.1.0/src/QCodeSitter/languages/python.py +146 -0
- qcodesitter-0.1.0/src/QCodeSitter/languages/python_highlights.py +444 -0
- qcodesitter-0.1.0/src/QCodeSitter/py.typed +0 -0
- qcodesitter-0.1.0/src/QCodeSitter/queries/__init__.py +73 -0
- qcodesitter-0.1.0/src/QCodeSitter/queries/predicates.py +172 -0
- qcodesitter-0.1.0/src/QCodeSitter/syntax_specs.py +65 -0
- qcodesitter-0.1.0/src/QCodeSitter/themes.py +271 -0
- qcodesitter-0.1.0/src/QCodeSitter/tracked_document.py +107 -0
- qcodesitter-0.1.0/src/QCodeSitter/tree_manager.py +180 -0
- qcodesitter-0.1.0/src/QCodeSitter/utils.py +84 -0
- qcodesitter-0.1.0/src/QCodeSitter.egg-info/PKG-INFO +255 -0
- qcodesitter-0.1.0/src/QCodeSitter.egg-info/SOURCES.txt +52 -0
- qcodesitter-0.1.0/src/QCodeSitter.egg-info/dependency_links.txt +1 -0
- qcodesitter-0.1.0/src/QCodeSitter.egg-info/entry_points.txt +2 -0
- qcodesitter-0.1.0/src/QCodeSitter.egg-info/requires.txt +27 -0
- 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,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
|
+
]
|