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.
- QCodeSitter/__init__.py +38 -0
- QCodeSitter/behaviors/__init__.py +115 -0
- QCodeSitter/behaviors/auto_bracket.py +141 -0
- QCodeSitter/behaviors/code_folding.py +530 -0
- QCodeSitter/behaviors/comment_toggle.py +71 -0
- QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
- QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
- QCodeSitter/behaviors/line_numbers.py +49 -0
- QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
- QCodeSitter/behaviors/overscroll.py +104 -0
- QCodeSitter/behaviors/smart_indent.py +191 -0
- QCodeSitter/behaviors/syntax_highlighting.py +181 -0
- QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
- QCodeSitter/behaviors/tab_completion/popup.py +123 -0
- QCodeSitter/behaviors/tab_completion/providers.py +66 -0
- QCodeSitter/behaviors/tab_completion/types.py +55 -0
- QCodeSitter/code_editor.py +540 -0
- QCodeSitter/cursors/__init__.py +8 -0
- QCodeSitter/cursors/clipboard.py +51 -0
- QCodeSitter/cursors/iterator.py +141 -0
- QCodeSitter/cursors/line_edits.py +128 -0
- QCodeSitter/cursors/multi.py +240 -0
- QCodeSitter/cursors/state.py +55 -0
- QCodeSitter/cursors/tracked.py +97 -0
- QCodeSitter/defaults.py +93 -0
- QCodeSitter/editing_keys.py +150 -0
- QCodeSitter/editor_options.py +246 -0
- QCodeSitter/gui_colors.py +51 -0
- QCodeSitter/gutters.py +141 -0
- QCodeSitter/integrations/__init__.py +1 -0
- QCodeSitter/integrations/preditor_interface.py +390 -0
- QCodeSitter/languages/__init__.py +99 -0
- QCodeSitter/languages/base.py +60 -0
- QCodeSitter/languages/python.py +146 -0
- QCodeSitter/languages/python_highlights.py +444 -0
- QCodeSitter/py.typed +0 -0
- QCodeSitter/queries/__init__.py +73 -0
- QCodeSitter/queries/predicates.py +172 -0
- QCodeSitter/syntax_specs.py +65 -0
- QCodeSitter/themes.py +271 -0
- QCodeSitter/tracked_document.py +107 -0
- QCodeSitter/tree_manager.py +180 -0
- QCodeSitter/utils.py +84 -0
- qcodesitter-0.1.0.dist-info/METADATA +255 -0
- qcodesitter-0.1.0.dist-info/RECORD +49 -0
- qcodesitter-0.1.0.dist-info/WHEEL +5 -0
- qcodesitter-0.1.0.dist-info/entry_points.txt +2 -0
- qcodesitter-0.1.0.dist-info/licenses/LICENSE.txt +7 -0
- qcodesitter-0.1.0.dist-info/top_level.txt +1 -0
QCodeSitter/__init__.py
ADDED
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Collection
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from ..editor_options import check_option_keys
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from Qt import QtCore, QtGui, QtWidgets
|
|
10
|
+
|
|
11
|
+
from ..code_editor import CodeEditor
|
|
12
|
+
from ..editor_options import EditorOptions, OptionHandlers
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Behavior:
|
|
16
|
+
"""A feature that plugs into a CodeEditor
|
|
17
|
+
|
|
18
|
+
Subclasses get their options with `watch_options`, and connect to the
|
|
19
|
+
editor's signals with `connect` so that `remove` disconnects them, and undo
|
|
20
|
+
anything else they did to the editor in `on_remove`.
|
|
21
|
+
Behaviors that handle keys override `handle_key_press`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# The editor passes key presses to the behaviors with the highest priority first.
|
|
25
|
+
# Behaviors of the same priority get them in the order they were added
|
|
26
|
+
KEY_PRIORITY = 0
|
|
27
|
+
|
|
28
|
+
def __init__(self, editor: CodeEditor) -> None:
|
|
29
|
+
self.editor: CodeEditor = editor
|
|
30
|
+
self.options: EditorOptions = editor.options
|
|
31
|
+
self._option_handlers: dict[str, Callable[[Any], object]] = {}
|
|
32
|
+
self._connections: list[tuple[QtCore.SignalInstance, Callable[..., Any]]] = []
|
|
33
|
+
self.shortcut_actions: list[QtGui.QAction] = []
|
|
34
|
+
self.connect(self.options.optionsUpdated, self._on_options_updated)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def handles_key_presses(cls) -> bool:
|
|
38
|
+
"""Whether the editor should pass key presses to `handle_key_press`"""
|
|
39
|
+
return cls.handle_key_press is not Behavior.handle_key_press
|
|
40
|
+
|
|
41
|
+
def connect(
|
|
42
|
+
self,
|
|
43
|
+
signal: QtCore.SignalInstance,
|
|
44
|
+
slot: Callable[..., Any],
|
|
45
|
+
conn_type: QtCore.Qt.ConnectionType | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Connect a signal to a slot until the behavior is removed"""
|
|
48
|
+
if conn_type is None:
|
|
49
|
+
signal.connect(slot)
|
|
50
|
+
else:
|
|
51
|
+
signal.connect(slot, conn_type)
|
|
52
|
+
self._connections.append((signal, slot))
|
|
53
|
+
|
|
54
|
+
def watch_options(self, handlers: OptionHandlers) -> None:
|
|
55
|
+
"""Call these handlers with their option's value now, and whenever it changes
|
|
56
|
+
|
|
57
|
+
Every option has a value except the language and font, whose handlers
|
|
58
|
+
wait until they are set. Raises a KeyError for unknown option keys
|
|
59
|
+
"""
|
|
60
|
+
check_option_keys(handlers)
|
|
61
|
+
self._option_handlers.update(handlers)
|
|
62
|
+
self.options.apply(handlers, handlers)
|
|
63
|
+
|
|
64
|
+
def _on_options_updated(self, keys: Collection[str]) -> None:
|
|
65
|
+
self.options.apply(self._option_handlers, keys)
|
|
66
|
+
|
|
67
|
+
def remove(self) -> None:
|
|
68
|
+
"""Disconnect from the editor, then call `on_remove`
|
|
69
|
+
|
|
70
|
+
Subclasses override `on_remove` instead of this
|
|
71
|
+
"""
|
|
72
|
+
for signal, slot in reversed(self._connections):
|
|
73
|
+
signal.disconnect(slot)
|
|
74
|
+
self._connections.clear()
|
|
75
|
+
self.on_remove()
|
|
76
|
+
|
|
77
|
+
def on_remove(self) -> None:
|
|
78
|
+
"""Undo what the behavior did to the editor, besides connecting signals
|
|
79
|
+
|
|
80
|
+
This runs after the signals connected with `connect` are
|
|
81
|
+
disconnected, so the cleanup can't trigger the behavior's own handlers
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def get_hotkeys(self) -> list[QtGui.QAction]:
|
|
85
|
+
"""The shortcut actions to add to the editor"""
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
def add_menu_actions(
|
|
89
|
+
self, menu: QtWidgets.QMenu, pos: QtCore.QPoint | None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Add this behavior's actions to the editor's right click menu, or to a
|
|
92
|
+
menu outside the editor like a window's menu bar
|
|
93
|
+
|
|
94
|
+
The editor puts a separator before each behavior's actions. Use
|
|
95
|
+
`utils.add_menu_action` to show the hotkey that runs each one without
|
|
96
|
+
binding it. Actions created here should be parented to the menu
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
menu: The menu being built
|
|
100
|
+
pos: Where a right click opened the menu, in viewport coordinates.
|
|
101
|
+
None when it wasn't opened at a point in the text, so act on the
|
|
102
|
+
text cursor instead
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def handle_key_press(self, event: QtGui.QKeyEvent) -> bool:
|
|
106
|
+
"""Handle a key press, before the editor's own key handling
|
|
107
|
+
|
|
108
|
+
This runs inside an edit session. A behavior can handle the key for
|
|
109
|
+
some cursors and not others: calling `editor.citer.mark_cursor_completed()`
|
|
110
|
+
for a cursor stops the handlers after this one from editing it.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True to consume the key, so the handlers after this one never see it
|
|
114
|
+
"""
|
|
115
|
+
return False
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from Qt import QtGui
|
|
7
|
+
|
|
8
|
+
from ..tracked_document import len16, utf16_to_index
|
|
9
|
+
from ..utils import KEY, MM, MO, MOD
|
|
10
|
+
from . import Behavior
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..code_editor import CodeEditor
|
|
14
|
+
|
|
15
|
+
# Python string prefixes, which still get a pair of quotes when typed before one
|
|
16
|
+
_STRING_PREFIXES = frozenset({"r", "u", "b", "f", "br", "rb", "fr", "rf"})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AutoBracket(Behavior):
|
|
20
|
+
"""Automatically inserts closing brackets, quotes, and other paired characters
|
|
21
|
+
|
|
22
|
+
The pairs come from the "auto_bracket_pairs" option
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, editor: CodeEditor) -> None:
|
|
26
|
+
super().__init__(editor)
|
|
27
|
+
self.watch_options({"auto_bracket_pairs": self._set_pairs})
|
|
28
|
+
|
|
29
|
+
def _set_pairs(self, pairs: list[str]) -> None:
|
|
30
|
+
"""Set the pairs from strings of an opening and closing character, ie "()" """
|
|
31
|
+
self.pairs = {pair[0]: pair[1] for pair in pairs}
|
|
32
|
+
self._closing = set(self.pairs.values())
|
|
33
|
+
# Characters that open and close themselves, like quotes
|
|
34
|
+
self._self_closing = {k for k, v in self.pairs.items() if k == v}
|
|
35
|
+
|
|
36
|
+
def handle_key_press(self, event: QtGui.QKeyEvent) -> bool:
|
|
37
|
+
"""Handle the cursors next to pairs, and leave the rest to the editor"""
|
|
38
|
+
text = event.text()
|
|
39
|
+
if event.key() == KEY.Key_Backspace:
|
|
40
|
+
if event.modifiers() & MOD.ControlModifier:
|
|
41
|
+
# Ctrl+Backspace deletes a word, which EditingKeys does
|
|
42
|
+
return False
|
|
43
|
+
self._delete_pair()
|
|
44
|
+
elif text in self._closing or text in self.pairs:
|
|
45
|
+
if text in self._closing:
|
|
46
|
+
self._skip_closing(text)
|
|
47
|
+
if text in self.pairs:
|
|
48
|
+
self._insert_pair(text)
|
|
49
|
+
else:
|
|
50
|
+
return False
|
|
51
|
+
return self.editor.citer.all_completed()
|
|
52
|
+
|
|
53
|
+
def _pair_around(self, text: str, index: int) -> str | None:
|
|
54
|
+
"""Get the empty pair, or pair of triple quotes, that a str index is inside"""
|
|
55
|
+
before = text[max(0, index - 3) : index]
|
|
56
|
+
quote = before[:1]
|
|
57
|
+
if len(before) == 3 and quote in self._self_closing and before == quote * 3:
|
|
58
|
+
if text[index : index + 3] == before:
|
|
59
|
+
return before * 2
|
|
60
|
+
if 0 < index < len(text) and self.pairs.get(text[index - 1]) == text[index]:
|
|
61
|
+
return text[index - 1 : index + 1]
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def _delete_pair(self) -> None:
|
|
65
|
+
"""Backspace both halves of an empty pair"""
|
|
66
|
+
citer = self.editor.citer
|
|
67
|
+
for cursor in citer.iterate_cursors():
|
|
68
|
+
if cursor.hasSelection():
|
|
69
|
+
continue
|
|
70
|
+
text = cursor.block().text()
|
|
71
|
+
pair = self._pair_around(
|
|
72
|
+
text, utf16_to_index(text, cursor.positionInBlock())
|
|
73
|
+
)
|
|
74
|
+
if pair is None:
|
|
75
|
+
continue
|
|
76
|
+
half = len16(pair) // 2
|
|
77
|
+
pos = cursor.position()
|
|
78
|
+
cursor.setPosition(pos - half)
|
|
79
|
+
cursor.setPosition(pos + half, MM.KeepAnchor)
|
|
80
|
+
cursor.removeSelectedText()
|
|
81
|
+
citer.mark_cursor_completed()
|
|
82
|
+
|
|
83
|
+
def _insert_pair(self, open_char: str) -> None:
|
|
84
|
+
"""Insert an opening character and its closing pair
|
|
85
|
+
|
|
86
|
+
This runs after _skip_closing, so any cursor right before a matching
|
|
87
|
+
closing character has already been handled
|
|
88
|
+
"""
|
|
89
|
+
close_char = self.pairs[open_char]
|
|
90
|
+
citer = self.editor.citer
|
|
91
|
+
for cursor in citer.iterate_cursors(complete_all=True):
|
|
92
|
+
if cursor.hasSelection():
|
|
93
|
+
# Wrap the selection, and keep it selected inside the pair
|
|
94
|
+
selected_text = cursor.selectedText()
|
|
95
|
+
start = cursor.selectionStart() + len16(open_char)
|
|
96
|
+
cursor.insertText(open_char + selected_text + close_char)
|
|
97
|
+
cursor.setPosition(start)
|
|
98
|
+
cursor.setPosition(start + len16(selected_text), MM.KeepAnchor)
|
|
99
|
+
elif open_char in self._self_closing and self._typing_triple_quote(
|
|
100
|
+
cursor, open_char
|
|
101
|
+
):
|
|
102
|
+
# Close the docstring, with the cursor inside it
|
|
103
|
+
cursor.insertText(open_char * 4)
|
|
104
|
+
cursor.movePosition(MO.Left, MM.MoveAnchor, 3)
|
|
105
|
+
elif open_char == "'" and self._after_word(cursor):
|
|
106
|
+
# An apostrophe inside a word, like don't
|
|
107
|
+
cursor.insertText(open_char)
|
|
108
|
+
else:
|
|
109
|
+
cursor.insertText(open_char + close_char)
|
|
110
|
+
cursor.movePosition(MO.Left, MM.MoveAnchor, 1)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _typing_triple_quote(cursor: QtGui.QTextCursor, quote: str) -> bool:
|
|
114
|
+
"""Whether typing this quote makes three in a row"""
|
|
115
|
+
text = cursor.block().text()
|
|
116
|
+
index = utf16_to_index(text, cursor.positionInBlock())
|
|
117
|
+
return index >= 2 and text[index - 2 : index] == quote * 2
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _after_word(cursor: QtGui.QTextCursor) -> bool:
|
|
121
|
+
"""Whether the cursor follows a word that is not a string prefix"""
|
|
122
|
+
text = cursor.block().text()
|
|
123
|
+
index = utf16_to_index(text, cursor.positionInBlock())
|
|
124
|
+
match = re.search(r"\w*$", text[:index])
|
|
125
|
+
word = match.group() if match else ""
|
|
126
|
+
return bool(word) and word.lower() not in _STRING_PREFIXES
|
|
127
|
+
|
|
128
|
+
def _skip_closing(self, close_char: str) -> None:
|
|
129
|
+
"""Step over a closing character instead of typing a second one"""
|
|
130
|
+
citer = self.editor.citer
|
|
131
|
+
for cursor in citer.iterate_cursors():
|
|
132
|
+
# Typing over a selection replaces or wraps it instead
|
|
133
|
+
if cursor.hasSelection():
|
|
134
|
+
continue
|
|
135
|
+
cursor.movePosition(MO.Right, MM.KeepAnchor, 1)
|
|
136
|
+
if cursor.selectedText() == close_char:
|
|
137
|
+
cursor.setPosition(cursor.position())
|
|
138
|
+
citer.mark_cursor_completed()
|
|
139
|
+
else:
|
|
140
|
+
# Go back to where it started. At the end of the document it never moved
|
|
141
|
+
cursor.setPosition(cursor.anchor())
|