QCodeSitter
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.
This is not an IDE. It's a drop-in editor widget you can embed in your own Qt application.
Quick Start
The quickest way to a full editor is the default setup:
from Qt import QtWidgets
from QCodeSitter import CodeEditor, default_options, install_default_behaviors
app = QtWidgets.QApplication([])
editor = CodeEditor(default_options())
install_default_behaviors(editor)
editor.show()
app.exec()
install_default_behaviors(editor, optional=True) also adds the behaviors that are off by
default, like AutoBracket.
Or pick the options and behaviors yourself:
from tree_sitter import Language
import tree_sitter_python as tspython
from Qt import QtGui
from QCodeSitter import CodeEditor, EditorOptions
from QCodeSitter.themes import DARK_THEME, read_theme
from QCodeSitter.behaviors.syntax_highlighting import SyntaxHighlighting
from QCodeSitter.behaviors.smart_indent import SmartIndent
from QCodeSitter.behaviors.line_numbers import LineNumbers
from QCodeSitter.behaviors.auto_bracket import AutoBracket
language = Language(tspython.language())
colors, syntax = read_theme(DARK_THEME)
options = EditorOptions({
"space_indent_width": 4,
"indent_using_tabs": False,
"language": language,
"syntax": syntax,
"colors": colors,
"font": QtGui.QFont("Consolas", pointSize=11),
})
editor = CodeEditor(options)
editor.add_behavior(SyntaxHighlighting)
editor.add_behavior(SmartIndent)
editor.add_behavior(LineNumbers)
editor.add_behavior(AutoBracket)
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.
Every option is listed in OptionsDict in editor_options.py, and the options you don't
give take their value from OPTION_DEFAULTS. Setting an option that isn't listed raises a
KeyError, and a value of the wrong type raises a TypeError, so mistakes don't silently
do nothing. The highlight query comes from the language, so changing "language" is all it
takes to highlight another language. "syntax" only holds the format of each capture.
The "colors" option holds the gui colors, listed with their defaults in gui_colors.py.
Setting an option to the value it already has does nothing, so change a list option by
setting a new list rather than editing it in place. A behavior from outside the package
adds its own options with register_option("my_option", int, default=3).
The Behavior System
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.
Base Class
All behaviors inherit from Behavior:
from QCodeSitter.behaviors import Behavior
class MyBehavior(Behavior):
def __init__(self, editor):
super().__init__(editor)
# self.editor - the CodeEditor instance
# self.options - the EditorOptions dict-like object
# Signals connected with self.connect are disconnected when it's removed
self.connect(editor.textChanged, self.on_text_changed)
def on_text_changed(self):
...
def on_remove(self):
# Undo anything else done in __init__. This runs after the
# signals connected with self.connect are disconnected
Behaviors are added and removed at runtime:
behavior = editor.add_behavior(MyBehavior) # Replaces any existing MyBehavior
editor.remove_behavior(MyBehavior) # Returns the removed instance
editor.get_behavior(MyBehavior) # Returns the instance or None
Reacting to Option Changes
Every option except "language" and "font" always has a value, so a behavior can read a
setting from self.options when it needs it. For a setting that needs work when it changes,
register a handler. Handlers are called with the option's value right away, and again
whenever it changes. The handlers of "language" and "font" wait until they are set:
class MyBehavior(Behavior):
def __init__(self, editor):
super().__init__(editor)
self.font = QtGui.QFont() # The default until the option is set
self.watch_options({"font": self.set_font})
def set_font(self, font):
self.font = font
Parsing and highlighting are the expensive part of an editor, so the editor only sets its
language when it is first shown. Call editor.load() to set it for an editor you don't show.
Keys, Painting and Signals
- Override
handle_key_press(event)to get key presses before the editor's own key handling, and returnTruefrom it to consume the key. Behaviors with a higherKEY_PRIORITYget the keys first, likeTabCompletion, so Tab accepts a completion beforeSmartIndentindents. - Return actions from
get_hotkeysto add keyboard shortcuts to the editor. - Connect to
editor.behaviorPaint(event, painter)to paint over the text. - Connect to
editor.cursors.changed,editor.tree_manager.reparsed, oreditor.undoRedoFinishedto react to the editor. - Subclass
GutterWidget(ingutters.py) for a column beside the text, like line numbers.
Languages
Language-specific knowledge (comment prefix, indent rules, foldable node types, and the
highlight and identifier queries) lives in a LanguageSpec, looked up from the editor's
tree-sitter language. Add a language by registering a spec for its grammar name:
from QCodeSitter.languages import LanguageSpec, register_language
register_language(LanguageSpec("lua", comment_prefix="-- "))
Built-in Behaviors
| Behavior | Description |
|---|---|
SyntaxHighlighting |
Tree-sitter-based syntax highlighting with incremental updates |
SmartIndent |
Language-aware auto-indentation on Enter, Tab, Backspace |
LineNumbers |
Line number gutter |
AutoBracket |
Auto-close brackets, quotes, and parens; selection wrapping |
TabCompletion |
Autocomplete popup with pluggable providers |
CodeFolding |
Tree-sitter-based code folding with gutter indicators |
CommentToggle |
Toggle line comments (Ctrl+/) with language detection |
HighlightMatchingBrackets |
Highlight matching bracket/paren/brace pairs |
HighlightMatchingSelection |
Highlight all occurrences of selected text |
MultiCursorPaint |
Render primary and secondary cursors |
Overscroll |
Allow scrolling past end of document |
Multi-Cursor Editing
The editor has built-in multi-cursor support. All text operations (insert, delete, indent, comment, etc.) apply to every cursor simultaneously.
Default bindings:
- Ctrl+D -- Select next occurrence of current selection
- Ctrl+Alt+Up/Down -- Add cursor above/below
- Ctrl+Shift+L -- Add cursors to all line ends in selection
- Alt+Click -- Add cursor at mouse position
- Escape -- Exit multi-cursor mode
Behaviors that modify text across multiple cursors use the editor's CursorIterator.
It moves one cursor to each cursor in turn, and shifts the later cursors by however much
each edit changed the document:
def my_multi_cursor_operation(self):
citer = self.editor.citer
with self.editor.edit_session(): # One undo step, and one reparse
for cursor in citer.iterate_cursors(complete_all=True):
cursor.insertText("hello")
complete_all=True marks every cursor completed, which stops later key handlers in the
same edit session from editing them. A pass that handles only some cursors calls
citer.mark_cursor_completed() inside the loop for each one it handled instead.
Tree-Sitter Integration
QCodeSitter uses tree-sitter for parsing, with UTF-16 encoding throughout to match Qt's internal string representation. The TreeManager provides:
- Incremental parsing (only re-parses changed regions)
- A
reparsedsignal for loose coupling between components - Pause/unpause to batch edits (e.g., during multi-cursor operations)
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.
Dependencies
-
Python 3.10+
-
Qt.py, and a Qt binding for it. The tests run on PySide6 (
tox) and PyQt6 (tox -e pyqt6); PySide2 and PyQt5 are untested.pip install QCodeSitter[pyside6]installs PySide6 -
A tree-sitter language grammar (e.g.,
tree-sitter-python)
Running Tests
Tests are set up to be run with tox, and tox -e fuzz runs the slower Hypothesis fuzz
tests. python examples/demo.py opens a window with the default editor.
Metadata
Release files for QCodeSitter 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| qcodesitter-0.1.0.tar.gz | 70.7 kB | With provenance | Details for qcodesitter-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| qcodesitter-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for qcodesitter-0.1.0-py3-none-any.whl |
Total release size: 156.5 kB
Release files / qcodesitter-0.1.0.tar.gz
| Download URL | qcodesitter-0.1.0.tar.gz |
|---|---|
| Size | 70.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b50c76e8f978326c66978d0ceb03f7c7f9ee5d00407f77b55c990e8bc8a9afa
|
|
BLAKE2b-256 checksum How to use checksums |
67572c8008130b0755196edae62cc1c88c66a120cfcaf9fb0e3a0fa7a247d242
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logRelease files / qcodesitter-0.1.0-py3-none-any.whl
| Download URL | qcodesitter-0.1.0-py3-none-any.whl |
|---|---|
| Size | 85.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
faf469fb286b28151983b54e5780db8def62453371975d7e2c6f8c20cab7112a
|
|
BLAKE2b-256 checksum How to use checksums |
2a92b714820997106e691f575a77249bd5e15b4117adfa24dbf5d4ec3c860504
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logSecurity for QCodeSitter 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for qcodesitter-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.