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,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())