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,180 @@
1
+ from __future__ import annotations
2
+
3
+ from Qt import QtCore
4
+ from tree_sitter import Language, Node, Parser, Point, Tree
5
+
6
+ from .tracked_document import ENC, TrackedDocument
7
+
8
+
9
+ class TreeManager(QtCore.QObject):
10
+ """Owns the tree-sitter parse tree of a document
11
+
12
+ Edits are applied incrementally, and every reparse emits `reparsed`.
13
+ Offsets and Point columns are UTF-16LE bytes, see TrackedDocument.
14
+ """
15
+
16
+ reparsed = QtCore.Signal()
17
+ # Emitted after the language changes, before the new tree is parsed
18
+ languageChanged = QtCore.Signal()
19
+
20
+ def __init__(self, document: TrackedDocument) -> None:
21
+ super().__init__()
22
+ self.document = document
23
+ self.parser: Parser | None = None
24
+ self.tree: Tree | None = None
25
+ self._pause_depth = 0
26
+ # Whether an edit was made while paused, so unpausing has to reparse
27
+ self._reparse_pending = False
28
+
29
+ # The (first, last) rows touched by edits since the last parse
30
+ self._edit_rows: tuple[int, int] | None = None
31
+ # The (first, last) row ranges that changed in the last parse
32
+ # None means the whole document changed
33
+ self.changed_rows: list[tuple[int, int]] | None = None
34
+
35
+ self._set_parser(None)
36
+
37
+ @property
38
+ def language(self) -> Language | None:
39
+ return self.parser.language if self.parser is not None else None
40
+
41
+ @property
42
+ def is_paused(self) -> bool:
43
+ return self._pause_depth > 0
44
+
45
+ def set_language(self, language: Language | None) -> None:
46
+ """Change the language, and parse the document again"""
47
+ self._set_parser(language)
48
+ self.languageChanged.emit()
49
+ self.parse_from_scratch()
50
+
51
+ def _set_parser(self, language: Language | None) -> None:
52
+ self.parser = Parser(language) if language is not None else None
53
+ self.tree = None
54
+ self._edit_rows = None
55
+ self.changed_rows = None
56
+
57
+ def treesitter_source_callback(self, _byte_offset: int, ts_point: Point) -> bytes:
58
+ """Give tree-sitter the UTF-16LE bytes from a Point to the end of its line"""
59
+ curblock = self.document.findBlockByNumber(ts_point.row)
60
+ # The block can be invalid after an undo
61
+ if not curblock.isValid():
62
+ return b""
63
+
64
+ suffix = "\n" if curblock.next().isValid() else ""
65
+ linetext = curblock.text() + suffix
66
+ return linetext.encode(ENC)[ts_point.column :]
67
+
68
+ def _parse(self, parser: Parser, old_tree: Tree | None = None) -> Tree:
69
+ if old_tree is None:
70
+ return parser.parse(self.treesitter_source_callback, encoding="utf16")
71
+ return parser.parse(self.treesitter_source_callback, old_tree, encoding="utf16")
72
+
73
+ def parse_from_scratch(self) -> None:
74
+ """Parse the whole document from scratch. Without a language there is no tree"""
75
+ self.tree = self._parse(self.parser) if self.parser is not None else None
76
+ self._edit_rows = None
77
+ self.changed_rows = None
78
+ self.reparsed.emit()
79
+
80
+ def pause(self) -> None:
81
+ """Hold reparses until `unpause`, so a group of edits is parsed once
82
+
83
+ Pauses nest, and only the unpause matching the first pause reparses
84
+ """
85
+ if self._pause_depth == 0:
86
+ self._reparse_pending = False
87
+ self._pause_depth += 1
88
+
89
+ def unpause(self) -> None:
90
+ if self._pause_depth == 0:
91
+ return
92
+ self._pause_depth -= 1
93
+ if self._pause_depth == 0 and self._reparse_pending:
94
+ self._reparse_pending = False
95
+ self.reparse()
96
+
97
+ def _track_edit_rows(
98
+ self, start_row: int, old_end_row: int, new_end_row: int
99
+ ) -> None:
100
+ """Grow the span of edited rows to include this edit
101
+
102
+ The span is kept in the current document's rows, so rows after
103
+ the edit shift by the number of lines it added or removed
104
+ """
105
+ if self._edit_rows is None:
106
+ self._edit_rows = (start_row, new_end_row)
107
+ return
108
+ lo, hi = self._edit_rows
109
+ if hi > old_end_row:
110
+ hi += new_end_row - old_end_row
111
+ else:
112
+ hi = new_end_row
113
+ self._edit_rows = (min(lo, start_row), hi)
114
+
115
+ def apply_edit(
116
+ self,
117
+ start_byte: int,
118
+ old_end_byte: int,
119
+ new_end_byte: int,
120
+ start_point: Point,
121
+ old_end_point: Point,
122
+ new_end_point: Point,
123
+ ) -> None:
124
+ """Apply a document edit to the tree and reparse
125
+
126
+ The arguments match TrackedDocument.byteContentsChange: UTF-16LE byte
127
+ offsets, and Points whose columns are in bytes
128
+ """
129
+ if self.tree is not None:
130
+ self.tree.edit(
131
+ start_byte=start_byte,
132
+ old_end_byte=old_end_byte,
133
+ new_end_byte=new_end_byte,
134
+ start_point=start_point,
135
+ old_end_point=old_end_point,
136
+ new_end_point=new_end_point,
137
+ )
138
+ self._track_edit_rows(start_point.row, old_end_point.row, new_end_point.row)
139
+ self.reparse()
140
+
141
+ def reparse(self) -> None:
142
+ """Reparse incrementally after edits, recording which rows changed"""
143
+ if self._pause_depth:
144
+ self._reparse_pending = True
145
+ return
146
+
147
+ if self.parser is None:
148
+ self._edit_rows = None
149
+ return
150
+
151
+ old_tree = self.tree
152
+ self.tree = self._parse(self.parser, old_tree)
153
+ if old_tree is None:
154
+ self.changed_rows = None
155
+ else:
156
+ # The rows that need rehighlighting are the ones whose syntax changed
157
+ # plus the ones that were edited, because predicates check the text
158
+ rows = [
159
+ (r.start_point.row, r.end_point.row)
160
+ for r in old_tree.changed_ranges(self.tree)
161
+ ]
162
+ if self._edit_rows is not None:
163
+ rows.append(self._edit_rows)
164
+ self.changed_rows = rows
165
+ self._edit_rows = None
166
+ self.reparsed.emit()
167
+
168
+ def node_at_byte(self, byte_offset: int) -> Node | None:
169
+ """Get the smallest node at a UTF-16LE byte offset"""
170
+ if self.tree is None:
171
+ return None
172
+ return self.tree.root_node.descendant_for_byte_range(byte_offset, byte_offset)
173
+
174
+ def node_at_char(self, position: int) -> Node | None:
175
+ """Get the smallest node at a Qt position"""
176
+ return self.node_at_byte(TrackedDocument.char_to_byte(position))
177
+
178
+ @property
179
+ def root_node(self) -> Node | None:
180
+ return self.tree.root_node if self.tree is not None else None
QCodeSitter/utils.py ADDED
@@ -0,0 +1,84 @@
1
+ """Short names for the Qt enums, indentation string helpers, and Qt5/Qt6 event and
2
+ action shims
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable
8
+
9
+ from Qt import QtCore, QtGui, QtWidgets
10
+
11
+ MO = QtGui.QTextCursor.MoveOperation
12
+ MM = QtGui.QTextCursor.MoveMode
13
+ KEY = QtCore.Qt.Key
14
+ MOD = QtCore.Qt.KeyboardModifier
15
+
16
+
17
+ def leading_whitespace(text: str) -> str:
18
+ """The indentation at the start of a line"""
19
+ return text[: len(text) - len(text.lstrip())]
20
+
21
+
22
+ def dedent_string(indent: str, indent_using_tabs: bool, space_indent_width: int) -> str:
23
+ """Remove one level of indentation from the indent string"""
24
+ if indent_using_tabs:
25
+ return indent.removesuffix("\t")
26
+ trailing_spaces = len(indent) - len(indent.rstrip(" "))
27
+ remove = min(space_indent_width, trailing_spaces)
28
+ return indent[:-remove] if remove else indent
29
+
30
+
31
+ def event_pos(event: QtGui.QMouseEvent | QtGui.QDropEvent) -> QtCore.QPoint:
32
+ """The event position as a QPoint in both Qt5 and Qt6"""
33
+ position = getattr(event, "position", None)
34
+ return position().toPoint() if position is not None else event.pos()
35
+
36
+
37
+ def make_shortcut_action(
38
+ parent: QtWidgets.QWidget,
39
+ shortcut: str,
40
+ slot: Callable[..., object],
41
+ name: str = "",
42
+ ) -> QtGui.QAction:
43
+ """Create an action for a keyboard shortcut that only fires in the parent widget"""
44
+ action = QtGui.QAction(name, parent)
45
+ action.setShortcut(QtGui.QKeySequence(shortcut))
46
+ action.setShortcutContext(QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut)
47
+ action.triggered.connect(slot)
48
+ return action
49
+
50
+
51
+ def add_menu_action(
52
+ menu: QtWidgets.QMenu,
53
+ text: str,
54
+ slot: Callable[[], object],
55
+ shortcut: str = "",
56
+ enabled: bool = True,
57
+ ) -> QtGui.QAction:
58
+ """Add an action to a menu, showing the shortcut that runs it elsewhere
59
+
60
+ The shortcut is only shown, not bound, so it doesn't clash with the editor's
61
+ own shortcut action while the menu is open
62
+ """
63
+ if shortcut:
64
+ native = QtGui.QKeySequence(shortcut).toString(
65
+ QtGui.QKeySequence.SequenceFormat.NativeText
66
+ )
67
+ text = f"{text}\t{native}"
68
+ action = menu.addAction(text)
69
+ action.setEnabled(enabled)
70
+ # triggered passes a `checked` bool, which the slot doesn't take
71
+ action.triggered.connect(lambda _checked=False: slot())
72
+ return action
73
+
74
+
75
+ def tidy_separators(menu: QtWidgets.QMenu) -> None:
76
+ """Remove the separators at the start or end of a menu, or next to another"""
77
+ previous_was_separator = True
78
+ for action in menu.actions():
79
+ if action.isSeparator() and previous_was_separator:
80
+ menu.removeAction(action)
81
+ previous_was_separator = action.isSeparator()
82
+ actions = menu.actions()
83
+ if actions and actions[-1].isSeparator():
84
+ menu.removeAction(actions[-1])
@@ -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,49 @@
1
+ QCodeSitter/__init__.py,sha256=u2ZKpX2OaFRJy50392y0H4Kz-qLzFO-_zHQ8ZUEHkLw,1049
2
+ QCodeSitter/code_editor.py,sha256=Q2lmfNSpfDsozWl30RXMJaeKVK-32UUHgh-7mVLp5A8,20722
3
+ QCodeSitter/defaults.py,sha256=c4XsoqzxjjHQCX20euJOOialchP1PFPr4dVS_2BJRjo,3189
4
+ QCodeSitter/editing_keys.py,sha256=ymfBrv6_cIajqQhYZClhEiNbUE3ULBEutWNKV0jnAYU,5479
5
+ QCodeSitter/editor_options.py,sha256=YzWz9Ash2xhGUMzYwma6rsIhc8hVSZITxBLgVyIvnzo,9155
6
+ QCodeSitter/gui_colors.py,sha256=7zDN8rCSLGfvG-p5G-RhsgF2qeT07pScPNSFxgRiYi8,1632
7
+ QCodeSitter/gutters.py,sha256=lSANpJ_jV5IjAwWU1AD76ApbFOpIGQo9c11eNba-A9g,5149
8
+ QCodeSitter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ QCodeSitter/syntax_specs.py,sha256=kKj8devqE7A35v5V-Zz2B9IR-aAUejF5cTokAsfpCxo,1863
10
+ QCodeSitter/themes.py,sha256=zSmY_96Y9tbCBjJcLC91_3VRz8DyfcLzmLEMUXJECT4,10553
11
+ QCodeSitter/tracked_document.py,sha256=LeT3MQV7POmu3ciWFH8NOClxtvP3WDPN1TBDlxXI6hQ,4185
12
+ QCodeSitter/tree_manager.py,sha256=ByJshn4OpG0-nm2cgRFMaNhpKrKb45PD4j1vElWf-vw,6566
13
+ QCodeSitter/utils.py,sha256=WSUJS0J-jpAl10C6BDM1GUC8K8TIPa3ZMHjqDMi7Bcw,2836
14
+ QCodeSitter/behaviors/__init__.py,sha256=3aYqispH47Nm0FCWKpG-VECO54nhr2Oz7ji7IoTIug8,4435
15
+ QCodeSitter/behaviors/auto_bracket.py,sha256=3harFJwMbGZ2T0MDfa3gyi8bfRIe4YKzMdAOSRaXdk4,6000
16
+ QCodeSitter/behaviors/code_folding.py,sha256=DAxWaikUjvMa0BNEXKUDng_vLeVx4NmLi4R72SIqMxg,20993
17
+ QCodeSitter/behaviors/comment_toggle.py,sha256=XUek0SMBQr2g2v_wevSO2arOQAURmUAs9xITKEa638Q,2651
18
+ QCodeSitter/behaviors/highlight_matching_brackets.py,sha256=tIEGGEC9asyLsJbnR1A_WJuIbQeESCLmLI5QcVwUBkQ,6843
19
+ QCodeSitter/behaviors/highlight_matching_selection.py,sha256=pu02vImwgOIhccT2cNaq_AjNSkiZs4Z41aldbpj8NhY,2132
20
+ QCodeSitter/behaviors/line_numbers.py,sha256=lJAdtKYOnQ95qWZob04LxNTNZ9OUiF07oWE4K4sXIvw,1515
21
+ QCodeSitter/behaviors/multi_cursor_paint.py,sha256=yXIoqsQcvc3cIt_9PszUPr8FI72XX-uL0SGZDOKyA0U,4882
22
+ QCodeSitter/behaviors/overscroll.py,sha256=HR5UnGsUAZ5G9K328Kcs2d10tMFNdlSC9rnVkBFc65Q,3668
23
+ QCodeSitter/behaviors/smart_indent.py,sha256=nFSt6DLgs5moCrqR1IP-GFyFRubiuqa3Ts89y6Ug5GE,7318
24
+ QCodeSitter/behaviors/syntax_highlighting.py,sha256=4XHqQV2sNPgejvD3udpQcrmG2rIhx7I6XoAatkk_ImA,6912
25
+ QCodeSitter/behaviors/tab_completion/__init__.py,sha256=UHTxuDE4bKSYHe4Rv0oWkETQQNh541qC0YJIHO5jOds,9727
26
+ QCodeSitter/behaviors/tab_completion/popup.py,sha256=9VYrVY7qcvb0Nwl94xvT8PF8UozFskGtaAatct8Kjo4,4736
27
+ QCodeSitter/behaviors/tab_completion/providers.py,sha256=ZzhKWSObN26SZeDiA4EA3e-h7Hj6NZA6wHH_y5BJLZg,1939
28
+ QCodeSitter/behaviors/tab_completion/types.py,sha256=otKxLkbTQ_4wQW-aNJZFSXR8kMuEU8ILVC47DFYE5ss,1596
29
+ QCodeSitter/cursors/__init__.py,sha256=d8phNVVxae75vWmZqXDWPc3E3pXZqna3jH1Hv_iMnLQ,402
30
+ QCodeSitter/cursors/clipboard.py,sha256=598AvQup3Nfoxc9RRgh4mnIDSbcfmQLJLqCQrlLwUmU,1664
31
+ QCodeSitter/cursors/iterator.py,sha256=pwcOY3sL1aeuxlCuRx7FrDbRVWVsEJskQdHH1EPfy8g,5413
32
+ QCodeSitter/cursors/line_edits.py,sha256=NboxU7VC1rRJ1o79ZMjyqQdWPWHCl3jlq-spmzBdsXs,4758
33
+ QCodeSitter/cursors/multi.py,sha256=UCOOcxD2vnzb260Gu9Z4sxw0nsTI5Fw-_rKdOdkHtlA,9390
34
+ QCodeSitter/cursors/state.py,sha256=ZGXHaSD5TtzPS3TrlRCYHVLeSWEPLvsBuoyZq1ZgHSI,1627
35
+ QCodeSitter/cursors/tracked.py,sha256=KDpwbE1EdYnSQSnhWSHX3WjKgkcECBGLQsH42vQBkhI,3504
36
+ QCodeSitter/integrations/__init__.py,sha256=Vp-GRSU_mu_GcyvUUZElZdkuHNfKKlRJclHwRfZwRJY,58
37
+ QCodeSitter/integrations/preditor_interface.py,sha256=0hFyXmfOjjsiM3S4joSc57ClxzV7KZ5iwkVXYyZg4a4,16882
38
+ QCodeSitter/languages/__init__.py,sha256=tLNi-mc32oeiw-mg0UFlQC92bWnMaVzQZvnMZgQ00cE,3227
39
+ QCodeSitter/languages/base.py,sha256=S9Q4AzzBV4sBRBHciXoPkup6T4jeyLZ58QMJ_sy0ImI,2317
40
+ QCodeSitter/languages/python.py,sha256=yvqekgHuMp2N4gior7PhUu41jeb84qgRzkMLWgS4ENw,4877
41
+ QCodeSitter/languages/python_highlights.py,sha256=CYPisXS8sKxwc0SHESa_tnfJrOqcpK3P0DkqjwnGUoo,8942
42
+ QCodeSitter/queries/__init__.py,sha256=rR-qVuwzxQXWvtOaS6WQ6gOpBLClEPFg3Sk1XX5j_4s,2448
43
+ QCodeSitter/queries/predicates.py,sha256=L5Upo7ckUUDc5Hf6-z2Ow7lI9TyI_ue-MfOBnYxAdKw,5551
44
+ qcodesitter-0.1.0.dist-info/licenses/LICENSE.txt,sha256=XXQgakqrJQxGWQswReIWcVZtUCuvnymgmuqPCZSL57w,1057
45
+ qcodesitter-0.1.0.dist-info/METADATA,sha256=n3moGn42Et59TdET92-7jP_e_D4wo6Hm_rJMM4mLVzo,10316
46
+ qcodesitter-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
47
+ qcodesitter-0.1.0.dist-info/entry_points.txt,sha256=7hsyL7EWMH_L4Za4fJxpeiDT-XXrmVAIhDl8a496EpQ,101
48
+ qcodesitter-0.1.0.dist-info/top_level.txt,sha256=nDTbAjVeu-WCu_x7FIIs_tqR1jtl2lm_9yFDhtMJ8v4,12
49
+ qcodesitter-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [preditor.plug.editors]
2
+ QCodeSitter = QCodeSitter.integrations.preditor_interface:CodeSitterTextEdit
@@ -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 @@
1
+ QCodeSitter