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,181 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Collection
4
+ from typing import TYPE_CHECKING
5
+
6
+ from Qt import QtGui, QtWidgets
7
+
8
+ from ..gui_colors import GuiColors, gui_color
9
+ from ..languages import language_spec
10
+ from ..tracked_document import len16, utf16_to_index
11
+ from ..utils import MM
12
+ from . import Behavior
13
+
14
+ if TYPE_CHECKING:
15
+ from tree_sitter import Node
16
+
17
+ from ..code_editor import CodeEditor
18
+
19
+
20
+ class HighlightMatchingBrackets(Behavior):
21
+ """Highlight a bracket or quote next to the cursor, and the one it pairs with
22
+
23
+ The pairs come from the tree-sitter tree, so brackets in strings and
24
+ comments don't match the code around them. The brackets and quotes come
25
+ from the "highlight_bracket_pairs" and "highlight_quote_chars" options
26
+ """
27
+
28
+ SOURCE = "bracket_matching"
29
+
30
+ def __init__(self, editor: CodeEditor) -> None:
31
+ super().__init__(editor)
32
+ self.watch_options(
33
+ {
34
+ "colors": self._set_colors,
35
+ "highlight_bracket_pairs": self._set_bracket_pairs,
36
+ }
37
+ )
38
+ self.connect(
39
+ self.editor.cursorPositionChanged, self.highlight_matching_brackets
40
+ )
41
+
42
+ def _set_colors(self, colors: GuiColors) -> None:
43
+ self._format = QtGui.QTextCharFormat()
44
+ self._format.setBackground(gui_color(colors, "pair_hl"))
45
+
46
+ def _set_bracket_pairs(self, ordered_pairs: Collection[str]) -> None:
47
+ """Set the brackets from strings of an opening and closing character, ie "()" """
48
+ self.opening_brackets = "".join(p[0] for p in ordered_pairs)
49
+ self.bracket_chars = "".join(ordered_pairs)
50
+ self.bracket_pairs = {p[0]: p[1] for p in ordered_pairs}
51
+ self.bracket_pairs.update({p[1]: p[0] for p in ordered_pairs})
52
+
53
+ @property
54
+ def _quote_chars(self) -> str:
55
+ return self.options["highlight_quote_chars"]
56
+
57
+ def on_remove(self) -> None:
58
+ self.editor.clear_selections(self.SOURCE)
59
+
60
+ def highlight_matching_brackets(self) -> None:
61
+ self.editor.set_selections(self.SOURCE, self._matching_selections())
62
+
63
+ def _matching_selections(self) -> list[QtWidgets.QTextEdit.ExtraSelection]:
64
+ match_info = self._find_character_to_match()
65
+ if match_info is None:
66
+ return []
67
+ match_char, match_pos = match_info
68
+ node = self.editor.tree_manager.node_at_char(match_pos)
69
+ if node is None:
70
+ return []
71
+
72
+ if match_char in self._quote_chars:
73
+ ranges = self._matching_quote_ranges(node, match_pos)
74
+ else:
75
+ ranges = self._matching_bracket_ranges(node, match_char)
76
+ return [self._create_selection(start, end) for start, end in ranges]
77
+
78
+ def _find_character_to_match(self) -> tuple[str, int] | None:
79
+ """Find the bracket or quote next to the cursor, preferring the one before it
80
+
81
+ Returns:
82
+ The character and its position, or None if neither side matches
83
+ """
84
+ cursor = self.editor.textCursor()
85
+ pos = cursor.position()
86
+ text = cursor.block().text()
87
+ index = utf16_to_index(text, cursor.positionInBlock())
88
+ matchable = self._quote_chars + self.bracket_chars
89
+ if index > 0 and text[index - 1] in matchable:
90
+ char = text[index - 1]
91
+ return char, pos - len16(char)
92
+ if index < len(text) and text[index] in matchable:
93
+ return text[index], pos
94
+ return None
95
+
96
+ def _matching_quote_ranges(
97
+ self, node: Node, match_pos: int
98
+ ) -> list[tuple[int, int]]:
99
+ """Get the ranges of the quote at match_pos, and the other end of its string"""
100
+ spec = language_spec(self.editor.tree_manager.language)
101
+ string_node: Node | None = node
102
+ while string_node is not None and string_node.type != spec.string_type:
103
+ string_node = string_node.parent
104
+ if string_node is None:
105
+ return []
106
+
107
+ # The delimiter nodes include triple quotes and prefixes like f"
108
+ delimiters = {child.type: child for child in string_node.children}
109
+ start_type, end_type = spec.string_delimiter_types
110
+ start_node = delimiters.get(start_type)
111
+ end_node = delimiters.get(end_type)
112
+ if start_node is None or end_node is None:
113
+ return []
114
+
115
+ doc = self.editor.document()
116
+ opening, closing = (
117
+ (doc.byte_to_char(n.start_byte), doc.byte_to_char(n.end_byte))
118
+ for n in (start_node, end_node)
119
+ )
120
+ if opening[0] <= match_pos < opening[1]:
121
+ return [opening, closing]
122
+ if closing[0] <= match_pos < closing[1]:
123
+ return [closing, opening]
124
+ # A quote inside the string, like the apostrophe in "don't"
125
+ return []
126
+
127
+ def _matching_bracket_ranges(
128
+ self, node: Node, match_char: str
129
+ ) -> list[tuple[int, int]]:
130
+ """Get the ranges of a bracket node, and its sibling it pairs with
131
+
132
+ The partner is the nearest balanced one, which matters in error nodes
133
+ that hold several unmatched brackets
134
+ """
135
+ if node.type not in self.bracket_pairs or node.parent is None:
136
+ return []
137
+
138
+ partner_type = self.bracket_pairs[match_char]
139
+ siblings = node.parent.children
140
+ index = siblings.index(node)
141
+ # Opening brackets match forwards, and closing brackets match backwards
142
+ if match_char in self.opening_brackets:
143
+ candidates = siblings[index + 1 :]
144
+ else:
145
+ candidates = siblings[index - 1 :: -1] if index else []
146
+ partner = None
147
+ depth = 0
148
+ for sibling in candidates:
149
+ if sibling.type == node.type:
150
+ depth += 1
151
+ elif sibling.type == partner_type:
152
+ if depth == 0:
153
+ partner = sibling
154
+ break
155
+ depth -= 1
156
+ if partner is None:
157
+ return []
158
+
159
+ doc = self.editor.document()
160
+ return [
161
+ (doc.byte_to_char(n.start_byte), doc.byte_to_char(n.end_byte))
162
+ for n in (node, partner)
163
+ ]
164
+
165
+ def _create_selection(
166
+ self, start: int, end: int
167
+ ) -> QtWidgets.QTextEdit.ExtraSelection:
168
+ """Create an ExtraSelection for a range of the document
169
+
170
+ The range is clamped to the document, which it can pass
171
+ if the tree hasn't caught up with an edit yet
172
+ """
173
+ # -1 for the paragraph separator at the end of the document
174
+ doc_length = self.editor.document().characterCount() - 1
175
+ cursor = self.editor.textCursor()
176
+ cursor.setPosition(min(start, doc_length))
177
+ cursor.setPosition(min(end, doc_length), MM.KeepAnchor)
178
+ selection = QtWidgets.QTextEdit.ExtraSelection()
179
+ selection.cursor = cursor
180
+ selection.format = self._format
181
+ return selection
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from Qt import QtGui, QtWidgets
6
+
7
+ from ..gui_colors import GuiColors, gui_color
8
+ from . import Behavior
9
+
10
+ if TYPE_CHECKING:
11
+ from ..code_editor import CodeEditor
12
+
13
+
14
+ class HighlightMatchingSelection(Behavior):
15
+ """Highlight the other occurrences of the selected text"""
16
+
17
+ SOURCE = "selection_highlight"
18
+ MIN_LENGTH = 2
19
+ MAX_LENGTH = 100
20
+
21
+ def __init__(self, editor: CodeEditor) -> None:
22
+ super().__init__(editor)
23
+ self.watch_options({"colors": self._set_colors})
24
+ self.connect(self.editor.selectionChanged, self.highlight_occurrences)
25
+
26
+ def _set_colors(self, colors: GuiColors) -> None:
27
+ self._match_hl = gui_color(colors, "match_hl")
28
+
29
+ def on_remove(self) -> None:
30
+ self.editor.clear_selections(self.SOURCE)
31
+
32
+ def highlight_occurrences(self) -> None:
33
+ """Highlight all occurrences of the selected text"""
34
+ extra_selections = []
35
+ cursor = self.editor.textCursor()
36
+ selected_text = cursor.selectedText()
37
+
38
+ # Single characters would match everywhere
39
+ if self.MIN_LENGTH <= len(selected_text) <= self.MAX_LENGTH:
40
+ fmt = QtGui.QTextCharFormat()
41
+ fmt.setBackground(self._match_hl)
42
+ doc = self.editor.document()
43
+ search_cursor = QtGui.QTextCursor(doc)
44
+ case_sensitive = QtGui.QTextDocument.FindFlag.FindCaseSensitively
45
+
46
+ while True:
47
+ search_cursor = doc.find(selected_text, search_cursor, case_sensitive)
48
+ if search_cursor.isNull():
49
+ break
50
+
51
+ # Qt already highlights the selection itself
52
+ found = (search_cursor.selectionStart(), search_cursor.selectionEnd())
53
+ if found != (cursor.selectionStart(), cursor.selectionEnd()):
54
+ selection = QtWidgets.QTextEdit.ExtraSelection()
55
+ selection.cursor = search_cursor
56
+ selection.format = fmt
57
+ extra_selections.append(selection)
58
+
59
+ self.editor.set_selections(self.SOURCE, extra_selections)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from Qt import QtCore, QtGui
6
+
7
+ from ..gutters import GutterWidget
8
+ from . import Behavior
9
+
10
+ if TYPE_CHECKING:
11
+ from ..code_editor import CodeEditor
12
+
13
+
14
+ class LineNumberArea(GutterWidget):
15
+ def width_hint(self) -> int:
16
+ digits = len(str(max(1, self.editor.blockCount())))
17
+ return 10 + self.fontMetrics().horizontalAdvance("9") * digits
18
+
19
+ def paintEvent(self, event: QtGui.QPaintEvent) -> None:
20
+ painter = QtGui.QPainter(self)
21
+ painter.fillRect(event.rect(), self.bg_color)
22
+ painter.setPen(self.fg_color)
23
+ for block, top, bottom in self.visible_blocks(event.rect().bottom()):
24
+ if bottom >= event.rect().top():
25
+ painter.drawText(
26
+ 0,
27
+ int(top),
28
+ self.width() - 5,
29
+ self.fontMetrics().height(),
30
+ QtCore.Qt.AlignmentFlag.AlignRight,
31
+ str(block.blockNumber() + 1),
32
+ )
33
+
34
+
35
+ class LineNumbers(Behavior):
36
+ """A gutter showing the line numbers"""
37
+
38
+ def __init__(self, editor: CodeEditor) -> None:
39
+ super().__init__(editor)
40
+ self.line_number_area = LineNumberArea(self.editor)
41
+ self.watch_options(
42
+ {
43
+ "font": self.line_number_area.setFont,
44
+ "colors": self.line_number_area.set_gutter_colors,
45
+ }
46
+ )
47
+
48
+ def on_remove(self) -> None:
49
+ self.line_number_area.remove()
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from Qt import QtCore, QtGui, QtWidgets
6
+
7
+ from ..gui_colors import GuiColors, gui_color
8
+ from . import Behavior
9
+
10
+ if TYPE_CHECKING:
11
+ from ..code_editor import CodeEditor
12
+
13
+
14
+ class MultiCursorPaint(Behavior):
15
+ """Draws every cursor, including the secondary cursors of multi-cursor mode
16
+
17
+ Qt's own cursor is hidden while this behavior is installed, and the primary
18
+ cursor is drawn the same way as the others. The selections of the secondary
19
+ cursors are shown as extra selections.
20
+ """
21
+
22
+ SOURCE = "multi_cursor"
23
+
24
+ def __init__(self, editor: CodeEditor) -> None:
25
+ super().__init__(editor)
26
+ self.cursor_width = 2
27
+ self.watch_options({"colors": self._set_colors})
28
+ self._qt_cursor_width = self.editor.cursorWidth()
29
+ self.editor.setCursorWidth(0)
30
+
31
+ self._blink_state = True
32
+ self.blink_timer = QtCore.QTimer(self.editor)
33
+ self.blink_timer.timeout.connect(self._update_blink)
34
+ self.blink_timer.start(QtWidgets.QApplication.instance().cursorFlashTime() // 2)
35
+ self.connect(
36
+ self.editor.behaviorPaint,
37
+ self.paint_cursors,
38
+ QtCore.Qt.ConnectionType.DirectConnection,
39
+ )
40
+ self.connect(self.editor.cursors.changed, self.on_cursors_changed)
41
+
42
+ def _set_colors(self, colors: GuiColors) -> None:
43
+ self.primary_cursor_color = gui_color(colors, "primary_cursor")
44
+ self.secondary_cursor_color = gui_color(colors, "secondary_cursor")
45
+ self.secondary_selection_color = gui_color(colors, "secondary_selection")
46
+
47
+ @property
48
+ def cursors_visible(self) -> bool:
49
+ """Whether the cursors are drawn right now, which toggles as they blink"""
50
+ return self._blink_state
51
+
52
+ @cursors_visible.setter
53
+ def cursors_visible(self, visible: bool) -> None:
54
+ self._blink_state = visible
55
+ self.editor.viewport().update()
56
+
57
+ def on_cursors_changed(self) -> None:
58
+ if self.editor.cursors.secondary:
59
+ self._update_selections()
60
+ else:
61
+ self.editor.clear_selections(self.SOURCE)
62
+ self.editor.viewport().update()
63
+
64
+ def _update_selections(self) -> None:
65
+ """Show the selections of every cursor as extra selections"""
66
+ # In multi-cursor mode the primary selection is drawn like the others
67
+ states = [*self.editor.cursors.secondary, self.editor.cursors.primary()]
68
+
69
+ fmt = QtGui.QTextCharFormat()
70
+ fmt.setBackground(self.secondary_selection_color.lighter(150))
71
+ doc = self.editor.document()
72
+ selections = []
73
+ for state in states:
74
+ if state.hasSelection():
75
+ selection = QtWidgets.QTextEdit.ExtraSelection()
76
+ selection.format = fmt
77
+ selection.cursor = state.build_cursor(doc)
78
+ selections.append(selection)
79
+
80
+ self.editor.set_selections(self.SOURCE, selections)
81
+
82
+ def paint_cursors(self, _event: QtGui.QPaintEvent, painter: QtGui.QPainter) -> None:
83
+ """Paint thin cursor lines for all cursors without selections"""
84
+ if not self._blink_state:
85
+ return
86
+ painter.save()
87
+ try:
88
+ cursor = QtGui.QTextCursor(self.editor.document())
89
+ for state in self.editor.cursors.secondary:
90
+ if not state.hasSelection():
91
+ self._draw_cursor_at_position(
92
+ painter, cursor, state.position, self.secondary_cursor_color
93
+ )
94
+ primary = self.editor.cursors.primary()
95
+ if not primary.hasSelection():
96
+ self._draw_cursor_at_position(
97
+ painter, cursor, primary.position, self.primary_cursor_color
98
+ )
99
+ finally:
100
+ painter.restore()
101
+
102
+ def _update_blink(self) -> None:
103
+ if not self.editor.hasFocus():
104
+ if self._blink_state:
105
+ self.cursors_visible = False
106
+ return
107
+ self.cursors_visible = not self._blink_state
108
+
109
+ def _draw_cursor_at_position(
110
+ self,
111
+ painter: QtGui.QPainter,
112
+ cursor: QtGui.QTextCursor,
113
+ position: int,
114
+ color: QtGui.QColor,
115
+ ) -> None:
116
+ """Draw a cursor line at a document position, if it's in the viewport"""
117
+ cursor.setPosition(position)
118
+ cursor_rect = self.editor.cursorRect(cursor)
119
+ if not self.editor.viewport().rect().intersects(cursor_rect):
120
+ return
121
+
122
+ pen = QtGui.QPen(color)
123
+ pen.setWidth(self.cursor_width)
124
+ painter.setPen(pen)
125
+ painter.drawLine(cursor_rect.topLeft(), cursor_rect.bottomLeft())
126
+
127
+ def on_remove(self) -> None:
128
+ self.blink_timer.stop()
129
+ self.blink_timer.deleteLater()
130
+ self.editor.clear_selections(self.SOURCE)
131
+ self.editor.setCursorWidth(self._qt_cursor_width)
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from Qt import QtCore, QtGui
6
+
7
+ from ..gui_colors import GuiColors, gui_color
8
+ from . import Behavior
9
+
10
+ if TYPE_CHECKING:
11
+ from ..code_editor import CodeEditor
12
+
13
+
14
+ class Overscroll(Behavior):
15
+ """Behavior that allows scrolling past the end of the document"""
16
+
17
+ def __init__(self, editor: CodeEditor) -> None:
18
+ super().__init__(editor)
19
+ self._brush = QtGui.QBrush(QtCore.Qt.BrushStyle.SolidPattern)
20
+ self._pen = QtGui.QPen(QtCore.Qt.PenStyle.NoPen)
21
+
22
+ # Track the scroll position if the scroll position gets clamped
23
+ # to the "normal" scroll range, so we can restore it afterwards
24
+ self._cur_scroll = 0
25
+ self._was_clamped = False
26
+
27
+ sb = self.editor.verticalScrollBar()
28
+ self.connect(sb.valueChanged, self.track_scroll)
29
+ self.connect(sb.rangeChanged, self.on_range_changed)
30
+ self.connect(self.editor.document().contentsChanged, self.update_extra_scroll)
31
+
32
+ self.update_extra_scroll()
33
+ self.connect(
34
+ self.editor.behaviorPaint,
35
+ self.paint_outside_document,
36
+ QtCore.Qt.ConnectionType.DirectConnection,
37
+ )
38
+ self.watch_options({"colors": self._set_colors})
39
+
40
+ def track_scroll(self, value: int) -> None:
41
+ sb = self.editor.verticalScrollBar()
42
+ self._was_clamped = value == sb.maximum() and self._cur_scroll > sb.maximum()
43
+ self._cur_scroll = value
44
+
45
+ def update_extra_scroll(self) -> None:
46
+ """Let the last line scroll up to the top of the viewport"""
47
+ sb = self.editor.verticalScrollBar()
48
+ newmax = self.editor.document().blockCount() - 1
49
+ if sb.maximum() != newmax:
50
+ sb.setMaximum(newmax)
51
+
52
+ def on_range_changed(self) -> None:
53
+ """Put back the extra scroll range, and a scroll position Qt clamped"""
54
+ saved_value = self._cur_scroll
55
+ update = self._was_clamped
56
+ self.update_extra_scroll()
57
+ if update:
58
+ sb = self.editor.verticalScrollBar()
59
+ sb.setValue(saved_value)
60
+
61
+ def _set_colors(self, colors: GuiColors) -> None:
62
+ self._brush.setColor(gui_color(colors, "outside_bg"))
63
+ self.editor.viewport().update()
64
+
65
+ def paint_outside_document(
66
+ self, _event: QtGui.QPaintEvent, painter: QtGui.QPainter
67
+ ) -> None:
68
+ """Shade the space below the last line"""
69
+ doc = self.editor.document()
70
+ block = doc.lastBlock()
71
+
72
+ if not block.isValid():
73
+ return
74
+
75
+ last_rect = self.editor.blockBoundingGeometry(block)
76
+ last_rect.translate(self.editor.contentOffset())
77
+
78
+ viewport_rect = QtCore.QRectF(self.editor.viewport().rect())
79
+ if not viewport_rect.intersects(last_rect):
80
+ return
81
+
82
+ painter.save()
83
+ try:
84
+ painter.setBrush(self._brush)
85
+ painter.setPen(self._pen)
86
+
87
+ oob_rect = QtCore.QRect(
88
+ 0,
89
+ int(last_rect.bottom()),
90
+ int(viewport_rect.width()),
91
+ int(viewport_rect.height() - last_rect.bottom()),
92
+ )
93
+ painter.drawRect(oob_rect)
94
+ finally:
95
+ painter.restore()
96
+
97
+ def on_remove(self) -> None:
98
+ """Set the scroll range back to the line count"""
99
+ sb = self.editor.verticalScrollBar()
100
+ viewport_height = self.editor.viewport().height()
101
+ line_height = self.editor.fontMetrics().lineSpacing()
102
+ visible_line_count = viewport_height // line_height
103
+ total_line_count = self.editor.document().blockCount()
104
+ sb.setMaximum(max(0, total_line_count - visible_line_count))