pyqt-code-editor 0.0.1__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.
- pyqt_code_editor/__init__.py +5 -0
- pyqt_code_editor/_settings.py +232 -0
- pyqt_code_editor/app.py +438 -0
- pyqt_code_editor/code_editors/__init__.py +1 -0
- pyqt_code_editor/code_editors/code_editors.py +33 -0
- pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- pyqt_code_editor/code_editors/languages/generic.py +11 -0
- pyqt_code_editor/code_editors/languages/markdown.py +11 -0
- pyqt_code_editor/code_editors/languages/python.py +13 -0
- pyqt_code_editor/code_editors/languages/text.py +9 -0
- pyqt_code_editor/components/__init__.py +0 -0
- pyqt_code_editor/components/editor_panel.py +399 -0
- pyqt_code_editor/components/find_in_files.py +199 -0
- pyqt_code_editor/components/jupyter_console.py +487 -0
- pyqt_code_editor/components/project_explorer.py +515 -0
- pyqt_code_editor/components/settings_panel.py +162 -0
- pyqt_code_editor/components/sigmund.py +75 -0
- pyqt_code_editor/components/workspace_explorer.py +162 -0
- pyqt_code_editor/mixins/__init__.py +15 -0
- pyqt_code_editor/mixins/auto_pair.py +201 -0
- pyqt_code_editor/mixins/base.py +142 -0
- pyqt_code_editor/mixins/check.py +94 -0
- pyqt_code_editor/mixins/comment.py +91 -0
- pyqt_code_editor/mixins/complete.py +482 -0
- pyqt_code_editor/mixins/execute.py +182 -0
- pyqt_code_editor/mixins/file_link.py +166 -0
- pyqt_code_editor/mixins/line_number.py +206 -0
- pyqt_code_editor/mixins/python_auto_indent.py +239 -0
- pyqt_code_editor/mixins/search_replace.py +396 -0
- pyqt_code_editor/mixins/shortcuts.py +228 -0
- pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
- pyqt_code_editor/mixins/symbols.py +45 -0
- pyqt_code_editor/mixins/theme.py +99 -0
- pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- pyqt_code_editor/mixins/zoom.py +51 -0
- pyqt_code_editor/python_utils.py +936 -0
- pyqt_code_editor/signal_router.py +116 -0
- pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
- pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
- pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
- pyqt_code_editor/themes.py +36 -0
- pyqt_code_editor/utils.py +96 -0
- pyqt_code_editor/watchdog.py +141 -0
- pyqt_code_editor/widgets/__init__.py +6 -0
- pyqt_code_editor/widgets/calltip_widget.py +44 -0
- pyqt_code_editor/widgets/completion_popup.py +89 -0
- pyqt_code_editor/widgets/dock.py +15 -0
- pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
- pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
- pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
- pyqt_code_editor/widgets/search_replace_frame.py +128 -0
- pyqt_code_editor/widgets/tab_splitter.py +22 -0
- pyqt_code_editor/widgets/tabbed_editor.py +351 -0
- pyqt_code_editor/worker/__init__.py +0 -0
- pyqt_code_editor/worker/languages/__init__.py +0 -0
- pyqt_code_editor/worker/languages/generic.py +15 -0
- pyqt_code_editor/worker/languages/python.py +17 -0
- pyqt_code_editor/worker/manager.py +113 -0
- pyqt_code_editor/worker/process.py +128 -0
- pyqt_code_editor/worker/providers/__init__.py +1 -0
- pyqt_code_editor/worker/providers/codestral.py +56 -0
- pyqt_code_editor/worker/providers/jedi.py +115 -0
- pyqt_code_editor/worker/providers/ruff.py +45 -0
- pyqt_code_editor/worker/providers/sigmund.py +29 -0
- pyqt_code_editor/worker/providers/symbol.py +78 -0
- pyqt_code_editor/worker/settings.py +1 -0
- pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
- pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
- pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
- pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
- pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from qtpy.QtWidgets import QShortcut
|
|
3
|
+
from qtpy.QtGui import QTextCursor, QKeySequence
|
|
4
|
+
from qtpy.QtCore import Qt, QEvent
|
|
5
|
+
from .. import settings
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Shortcuts:
|
|
11
|
+
"""
|
|
12
|
+
Mixin that adds common editor shortcuts to a QPlainTextEdit.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
# Move line up
|
|
17
|
+
self._shortcut_move_line_up = QShortcut(
|
|
18
|
+
QKeySequence(settings.shortcut_move_line_up), self)
|
|
19
|
+
self._shortcut_move_line_up.setContext(Qt.WidgetShortcut)
|
|
20
|
+
self._shortcut_move_line_up.activated.connect(self._move_line_up)
|
|
21
|
+
|
|
22
|
+
# Move line down
|
|
23
|
+
self._shortcut_move_line_down = QShortcut(
|
|
24
|
+
QKeySequence(settings.shortcut_move_line_down), self)
|
|
25
|
+
self._shortcut_move_line_down.setContext(Qt.WidgetShortcut)
|
|
26
|
+
self._shortcut_move_line_down.activated.connect(self._move_line_down)
|
|
27
|
+
|
|
28
|
+
# Duplicate line
|
|
29
|
+
self._shortcut_duplicate_line = QShortcut(
|
|
30
|
+
QKeySequence(settings.shortcut_duplicate_line), self)
|
|
31
|
+
self._shortcut_duplicate_line.setContext(Qt.WidgetShortcut)
|
|
32
|
+
self._shortcut_duplicate_line.activated.connect(self._duplicate_line)
|
|
33
|
+
|
|
34
|
+
def eventFilter(self, obj, event):
|
|
35
|
+
"""Filters out and handles the 'Cut' shortcut, cross-platform."""
|
|
36
|
+
if obj == self and event.type() == QEvent.KeyPress:
|
|
37
|
+
if event.matches(QKeySequence.Cut):
|
|
38
|
+
if not self.textCursor().hasSelection():
|
|
39
|
+
self._delete_current_line()
|
|
40
|
+
return False
|
|
41
|
+
return super().eventFilter(obj, event)
|
|
42
|
+
|
|
43
|
+
def _delete_current_line(self):
|
|
44
|
+
"""
|
|
45
|
+
Removes the current line, preserving undo/redo.
|
|
46
|
+
"""
|
|
47
|
+
cursor = self.textCursor()
|
|
48
|
+
start, end = self._current_line_bounds(cursor)
|
|
49
|
+
doc_length = self.document().characterCount()
|
|
50
|
+
|
|
51
|
+
cursor.beginEditBlock()
|
|
52
|
+
# We want to remove the trailing or preceding newline, but not both.
|
|
53
|
+
if end < doc_length - 1:
|
|
54
|
+
end += 1
|
|
55
|
+
elif start > 0:
|
|
56
|
+
start -= 1
|
|
57
|
+
cursor.setPosition(start)
|
|
58
|
+
cursor.setPosition(end, QTextCursor.KeepAnchor)
|
|
59
|
+
cursor.removeSelectedText()
|
|
60
|
+
cursor.endEditBlock()
|
|
61
|
+
|
|
62
|
+
def _current_line_bounds(self, cursor=None):
|
|
63
|
+
"""
|
|
64
|
+
Return (start, end) positions of the current line in the document.
|
|
65
|
+
"""
|
|
66
|
+
if cursor is None:
|
|
67
|
+
cursor = self.textCursor()
|
|
68
|
+
original_pos = cursor.position()
|
|
69
|
+
|
|
70
|
+
cursor.movePosition(QTextCursor.StartOfBlock)
|
|
71
|
+
start = cursor.position()
|
|
72
|
+
cursor.movePosition(QTextCursor.EndOfBlock)
|
|
73
|
+
end = cursor.position()
|
|
74
|
+
|
|
75
|
+
# Restore cursor
|
|
76
|
+
cursor.setPosition(original_pos)
|
|
77
|
+
self.setTextCursor(cursor)
|
|
78
|
+
return start, end
|
|
79
|
+
|
|
80
|
+
def _move_lines_impl(self, direction: int):
|
|
81
|
+
"""
|
|
82
|
+
Step 1 of the plan:
|
|
83
|
+
- Expand selection to cover the line before and the line after the
|
|
84
|
+
current selection (or the current line if nothing is selected).
|
|
85
|
+
- Ignore 'direction' for now.
|
|
86
|
+
"""
|
|
87
|
+
doc = self.document()
|
|
88
|
+
cursor = self.textCursor()
|
|
89
|
+
cursor.beginEditBlock()
|
|
90
|
+
|
|
91
|
+
anchor = cursor.anchor()
|
|
92
|
+
pos = cursor.position()
|
|
93
|
+
|
|
94
|
+
# Identify which line is top vs bottom
|
|
95
|
+
anchor_block = doc.findBlock(anchor)
|
|
96
|
+
pos_block = doc.findBlock(pos)
|
|
97
|
+
if anchor_block.firstLineNumber() > pos_block.firstLineNumber():
|
|
98
|
+
anchor_block, pos_block = pos_block, anchor_block
|
|
99
|
+
|
|
100
|
+
# Expand upward if possible
|
|
101
|
+
prev_block = anchor_block.previous()
|
|
102
|
+
if prev_block.isValid():
|
|
103
|
+
has_preceding_line = True
|
|
104
|
+
anchor_block = prev_block
|
|
105
|
+
else:
|
|
106
|
+
has_preceding_line = False
|
|
107
|
+
|
|
108
|
+
# Expand downward if possible
|
|
109
|
+
next_block = pos_block.next()
|
|
110
|
+
if next_block.isValid():
|
|
111
|
+
has_following_line = True
|
|
112
|
+
pos_block = next_block
|
|
113
|
+
else:
|
|
114
|
+
has_following_line = False
|
|
115
|
+
|
|
116
|
+
# Don't move lines up or down if they're already at the start or end
|
|
117
|
+
if not has_preceding_line and direction < 0:
|
|
118
|
+
logger.info('cannot move line up')
|
|
119
|
+
return
|
|
120
|
+
if not has_following_line and direction > 0:
|
|
121
|
+
logger.info('cannot move line down')
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Build new selection from start of anchor_block to end of pos_block
|
|
125
|
+
start_pos = anchor_block.position()
|
|
126
|
+
end_pos = pos_block.position() + pos_block.length()
|
|
127
|
+
|
|
128
|
+
# Set the new selection
|
|
129
|
+
cursor.setPosition(start_pos)
|
|
130
|
+
if end_pos >= doc.characterCount():
|
|
131
|
+
# If the end of the selection is the end of the document, then we
|
|
132
|
+
# need to set it one character sooner, and manually add a newline
|
|
133
|
+
# to the end to compensate for this, but then not pad with a
|
|
134
|
+
# newline to in turn compensate for the fact that we already padded
|
|
135
|
+
# the selection
|
|
136
|
+
cursor.setPosition(end_pos - 1, QTextCursor.KeepAnchor)
|
|
137
|
+
selection = cursor.selectedText() + '\n'
|
|
138
|
+
insert_padding = ''
|
|
139
|
+
else:
|
|
140
|
+
cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
|
|
141
|
+
selection = cursor.selectedText()
|
|
142
|
+
insert_padding = '\n'
|
|
143
|
+
|
|
144
|
+
# Get the current selection, and swap the lines according to the
|
|
145
|
+
# direction in which we're moving. We have special cases for when there
|
|
146
|
+
# is no preceding or following line.
|
|
147
|
+
lines = selection.splitlines()
|
|
148
|
+
if direction < 0:
|
|
149
|
+
anchor -= len(lines[0]) + 1
|
|
150
|
+
pos -= len(lines[0]) + 1
|
|
151
|
+
if has_following_line:
|
|
152
|
+
lines = lines[1:-1] + [lines[0], lines[-1]]
|
|
153
|
+
else:
|
|
154
|
+
lines = lines[1:] + [lines[0]]
|
|
155
|
+
else:
|
|
156
|
+
anchor += len(lines[-1]) + 1
|
|
157
|
+
pos += len(lines[-1]) + 1
|
|
158
|
+
if has_preceding_line:
|
|
159
|
+
lines = [lines[0], lines[-1]] + lines[1:-1]
|
|
160
|
+
else:
|
|
161
|
+
lines = [lines[-1]] + lines[:-1]
|
|
162
|
+
cursor.removeSelectedText()
|
|
163
|
+
cursor.insertText('\n'.join(lines) + insert_padding)
|
|
164
|
+
# Restore the original selection
|
|
165
|
+
cursor.setPosition(anchor)
|
|
166
|
+
cursor.setPosition(pos, QTextCursor.KeepAnchor)
|
|
167
|
+
self.setTextCursor(cursor)
|
|
168
|
+
cursor.endEditBlock()
|
|
169
|
+
self.refresh()
|
|
170
|
+
|
|
171
|
+
def _move_line_up(self):
|
|
172
|
+
"""Move the currently selected lines (or single line) up by one line."""
|
|
173
|
+
self._move_lines_impl(direction=-1)
|
|
174
|
+
|
|
175
|
+
def _move_line_down(self):
|
|
176
|
+
"""Move the currently selected lines (or single line) down by one line."""
|
|
177
|
+
self._move_lines_impl(direction=1)
|
|
178
|
+
|
|
179
|
+
def _duplicate_line(self):
|
|
180
|
+
"""
|
|
181
|
+
If there is no selection, duplicate the current block. If there is
|
|
182
|
+
a selection, all blocks that are part of the selection should be duplicated.
|
|
183
|
+
The anchor and cursor should be preserved on the lower of the duplicated blocks.
|
|
184
|
+
In other words, it should appear as if the duplication is inserted above.
|
|
185
|
+
"""
|
|
186
|
+
cursor = self.textCursor()
|
|
187
|
+
cursor.beginEditBlock()
|
|
188
|
+
|
|
189
|
+
# 1) Remember original anchor and cursor positions
|
|
190
|
+
original_anchor = cursor.anchor()
|
|
191
|
+
original_position = cursor.position()
|
|
192
|
+
|
|
193
|
+
# 2) Determine start_pos and end_pos based on anchor / cursor
|
|
194
|
+
start_pos = min(original_anchor, original_position)
|
|
195
|
+
end_pos = max(original_anchor, original_position)
|
|
196
|
+
|
|
197
|
+
# 3) Move the cursor to the start of the block that contains start_pos
|
|
198
|
+
cursor.setPosition(start_pos)
|
|
199
|
+
cursor.movePosition(cursor.StartOfBlock)
|
|
200
|
+
start_block_pos = cursor.position()
|
|
201
|
+
|
|
202
|
+
# 4) Move the cursor to the end of the block containing end_pos (keeping anchor)
|
|
203
|
+
cursor.setPosition(end_pos, cursor.KeepAnchor)
|
|
204
|
+
cursor.movePosition(cursor.EndOfBlock, cursor.KeepAnchor)
|
|
205
|
+
end_block_pos = cursor.position()
|
|
206
|
+
|
|
207
|
+
# 5) The text that is now selected should be duplicated
|
|
208
|
+
duplication_text = cursor.selectedText()
|
|
209
|
+
# QPlainTextEdit often uses U+2029 for newlines
|
|
210
|
+
duplication_text = duplication_text.replace(u'\u2029', '\n')
|
|
211
|
+
|
|
212
|
+
# 6) Insert '\n' + duplication_text just after the selection
|
|
213
|
+
# But first, move cursor to the end of the selected blocks without a selection
|
|
214
|
+
cursor.setPosition(end_block_pos, cursor.MoveAnchor)
|
|
215
|
+
# Now insert a new line plus the duplicated text
|
|
216
|
+
cursor.insertText('\n' + duplication_text)
|
|
217
|
+
|
|
218
|
+
# Figure out how many characters were inserted
|
|
219
|
+
inserted_length = 1 + len(duplication_text)
|
|
220
|
+
|
|
221
|
+
# 7) Restore anchor/cursor so that they point to the *new* lines (the lower portion).
|
|
222
|
+
# According to your docstring, we want the final “visual” anchor/cursor to effectively
|
|
223
|
+
# be as if the duplication is inserted above, leaving anchor/cursor on the newly inserted lines.
|
|
224
|
+
cursor.setPosition(original_anchor + inserted_length, cursor.MoveAnchor)
|
|
225
|
+
cursor.setPosition(original_position + inserted_length, cursor.KeepAnchor)
|
|
226
|
+
|
|
227
|
+
self.setTextCursor(cursor)
|
|
228
|
+
cursor.endEditBlock()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from qtpy.QtCore import Qt
|
|
2
|
+
from qtpy.QtGui import QKeyEvent, QTextCursor
|
|
3
|
+
import logging
|
|
4
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SmartBackspaceDelete:
|
|
9
|
+
"""
|
|
10
|
+
This is meant to be used as a mixin for QPlainTextEdit to enhance the
|
|
11
|
+
functionality of backspace and delete in a code editor. It re-implements
|
|
12
|
+
keyPressEvent(), supporting the following behavior:
|
|
13
|
+
|
|
14
|
+
1) Backspace anywhere in the whitespace at the end of a line:
|
|
15
|
+
- Full right-trim of the whitespace for that line (including whitespace
|
|
16
|
+
before the cursor).
|
|
17
|
+
- Place the cursor at the end of the trimmed line.
|
|
18
|
+
2) Backspace at the very start of a line:
|
|
19
|
+
- Delete the preceding newline and trailing whitespace on the previous
|
|
20
|
+
line (up to the first non-whitespace or newline).
|
|
21
|
+
- Place the cursor at the end of the trimmed preceding line.
|
|
22
|
+
3) Delete anywhere in the whitespace at the end of a line (except the
|
|
23
|
+
very last position):
|
|
24
|
+
- Right-trim from the cursor position onward.
|
|
25
|
+
- The cursor does not move, so it can remain at the same block/column
|
|
26
|
+
index once the whitespace is removed.
|
|
27
|
+
4) Delete at the very end of a line:
|
|
28
|
+
- Delete the next newline and the leading whitespace on the next line
|
|
29
|
+
up to the first non-whitespace or newline.
|
|
30
|
+
- Keep the cursor where it was, effectively pulling up the next line.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Handle the specific 'backspace anywhere in the whitespace at the
|
|
36
|
+
end of a line' logic. If not applicable, fall back to default behavior.
|
|
37
|
+
"""
|
|
38
|
+
c = self.textCursor()
|
|
39
|
+
# We don't handle special cases if there is a selection
|
|
40
|
+
if c.hasSelection():
|
|
41
|
+
super().keyPressEvent(event)
|
|
42
|
+
return
|
|
43
|
+
key = event.key()
|
|
44
|
+
block = c.block()
|
|
45
|
+
pos = c.positionInBlock()
|
|
46
|
+
text = block.text()
|
|
47
|
+
block_length = len(text)
|
|
48
|
+
stripped_block_length = len(text.rstrip())
|
|
49
|
+
in_trailing_whitespace = text[pos:].isspace()
|
|
50
|
+
has_trailing_whitespace = block_length > stripped_block_length
|
|
51
|
+
at_block_end = pos == block_length
|
|
52
|
+
# We don't do anything with empty lines unless we're at the start of the
|
|
53
|
+
# block
|
|
54
|
+
if text.isspace() and pos > 0:
|
|
55
|
+
super().keyPressEvent(event)
|
|
56
|
+
return
|
|
57
|
+
# If we're at the very start of the line, then we're also eating up the
|
|
58
|
+
# trailing whitespace of the previous line. We do this by first moving to
|
|
59
|
+
# the end of the previous line, then handling this case recursively, and
|
|
60
|
+
# finally deleting the newline.
|
|
61
|
+
if key == Qt.Key_Backspace and pos == 0 and block.previous().isValid():
|
|
62
|
+
logger.info('moving to end of previous block')
|
|
63
|
+
c.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor)
|
|
64
|
+
c.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor)
|
|
65
|
+
self.setTextCursor(c)
|
|
66
|
+
# We only delete trailing whitespace if the previous line has
|
|
67
|
+
# trailing whitespace
|
|
68
|
+
prev_text = c.block().text()
|
|
69
|
+
if prev_text.rstrip() != prev_text:
|
|
70
|
+
logger.info('removing trailing whitespace from previous block')
|
|
71
|
+
self.keyPressEvent(event)
|
|
72
|
+
else:
|
|
73
|
+
logger.info('previous block doesn\'t have trailing whitespace')
|
|
74
|
+
logger.info('removing neweline')
|
|
75
|
+
c = self.textCursor()
|
|
76
|
+
c.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
|
|
77
|
+
c.removeSelectedText()
|
|
78
|
+
self.setTextCursor(c)
|
|
79
|
+
return
|
|
80
|
+
# If we're in the whitespace at the end of the block, we delete all
|
|
81
|
+
# trailing whitespace.
|
|
82
|
+
if key == Qt.Key_Backspace and has_trailing_whitespace and \
|
|
83
|
+
(in_trailing_whitespace or at_block_end):
|
|
84
|
+
n_trim = block_length - stripped_block_length
|
|
85
|
+
logger.info(f'trimming {n_trim} whitespace from end of block')
|
|
86
|
+
c.movePosition(QTextCursor.EndOfBlock)
|
|
87
|
+
c.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, n_trim)
|
|
88
|
+
c.removeSelectedText()
|
|
89
|
+
self.setTextCursor(c)
|
|
90
|
+
event.accept()
|
|
91
|
+
return
|
|
92
|
+
# If we're in the whitespace at the end of the block, we delete all
|
|
93
|
+
# trailing whitespace that follows the cursor.
|
|
94
|
+
if key == Qt.Key_Delete and in_trailing_whitespace:
|
|
95
|
+
n_trim = block_length - pos
|
|
96
|
+
logger.info(f'trimming {n_trim} whitespace from end of block')
|
|
97
|
+
c.movePosition(QTextCursor.EndOfBlock)
|
|
98
|
+
c.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, n_trim)
|
|
99
|
+
c.removeSelectedText()
|
|
100
|
+
self.setTextCursor(c)
|
|
101
|
+
event.accept()
|
|
102
|
+
return
|
|
103
|
+
# If we're at the end of a line, then delete removes the next line break
|
|
104
|
+
# and the leading whitespace on the next line. We do this by first moving
|
|
105
|
+
# to the next block, calculating how much leading whitespace there is,
|
|
106
|
+
# then getting a fresh cursor and removing this leading whitespace plus
|
|
107
|
+
# one character for the newline.
|
|
108
|
+
if key == Qt.Key_Delete and at_block_end and block.next().isValid():
|
|
109
|
+
c.movePosition(QTextCursor.NextBlock, QTextCursor.MoveAnchor)
|
|
110
|
+
next_text = c.block().text()
|
|
111
|
+
n_trim = len(next_text) - len(next_text.lstrip()) + 1
|
|
112
|
+
c = self.textCursor()
|
|
113
|
+
c.movePosition(
|
|
114
|
+
QTextCursor.NextCharacter, QTextCursor.KeepAnchor, n_trim)
|
|
115
|
+
c.removeSelectedText()
|
|
116
|
+
self.setTextCursor(c)
|
|
117
|
+
event.accept()
|
|
118
|
+
return
|
|
119
|
+
# In any situation, fall back
|
|
120
|
+
super().keyPressEvent(event)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from qtpy.QtGui import QShortcut
|
|
3
|
+
from qtpy.QtCore import Qt, Signal
|
|
4
|
+
from .. import settings
|
|
5
|
+
from ..widgets import QuickSymbolDialog
|
|
6
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Symbols:
|
|
11
|
+
"""
|
|
12
|
+
A mixin for QPlainTextEdit that provides all symbols (functions, classes, etc.)
|
|
13
|
+
for the current file.
|
|
14
|
+
"""
|
|
15
|
+
symbols_available = Signal(list)
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
logger.info("Initializing symbols")
|
|
20
|
+
self.symbol_shortcut = QShortcut(settings.shortcut_symbols, self)
|
|
21
|
+
self.symbol_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
|
|
22
|
+
self.symbol_shortcut.activated.connect(self.request_symbols)
|
|
23
|
+
|
|
24
|
+
def request_symbols(self):
|
|
25
|
+
logger.info("Requesting symbols")
|
|
26
|
+
code = self.toPlainText()
|
|
27
|
+
self.send_worker_request(action='symbols', code=code,
|
|
28
|
+
path=self.code_editor_file_path,
|
|
29
|
+
language=self.code_editor_language)
|
|
30
|
+
|
|
31
|
+
def handle_worker_result(self, action, result):
|
|
32
|
+
"""Check for completion or calltip results from the external worker."""
|
|
33
|
+
super().handle_worker_result(action, result)
|
|
34
|
+
if action != 'symbols':
|
|
35
|
+
return
|
|
36
|
+
QuickSymbolDialog(self, result['symbols'], self.jump_to_symbol).exec()
|
|
37
|
+
|
|
38
|
+
def jump_to_symbol(self, symbol):
|
|
39
|
+
block_nr = symbol['line'] - 1
|
|
40
|
+
cursor = self.textCursor()
|
|
41
|
+
# Jump to block number
|
|
42
|
+
cursor.setPosition(self.document().findBlockByNumber(block_nr).position())
|
|
43
|
+
self.setTextCursor(cursor)
|
|
44
|
+
self.centerCursor()
|
|
45
|
+
self.ensureCursorVisible()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from pygments.token import Token
|
|
2
|
+
from .. import settings
|
|
3
|
+
from ..syntax_highlighters.syntax_highlighter import create_syntax_highlighter
|
|
4
|
+
import logging
|
|
5
|
+
from qtpy.QtGui import QPainter, QColor, QFont, QFontMetrics
|
|
6
|
+
from qtpy.QtWidgets import QPlainTextEdit
|
|
7
|
+
|
|
8
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class Theme:
|
|
12
|
+
"""
|
|
13
|
+
Mixin for QPlainTextEdit that provides theming. It handles the following:
|
|
14
|
+
|
|
15
|
+
- Apply syntax highlighting
|
|
16
|
+
- Set a stylesheet
|
|
17
|
+
- Set code_editor_colors property so that other mixins can use it
|
|
18
|
+
- Toggle visibility of character ruler (based on settings.character_ruler)
|
|
19
|
+
- Toggle word wrap (based on settings.word_wrap)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
self._highlighter = create_syntax_highlighter(
|
|
25
|
+
self.code_editor_language, self.document(),
|
|
26
|
+
color_scheme=settings.color_scheme)
|
|
27
|
+
style = self._highlighter._style
|
|
28
|
+
self.code_editor_colors = {
|
|
29
|
+
'background': style.background_color,
|
|
30
|
+
'highlight': style.highlight_color,
|
|
31
|
+
'text': '#' + style.style_for_token(Token.Text)['color'],
|
|
32
|
+
'line-number': '#' + style.style_for_token(Token.Comment)['color'],
|
|
33
|
+
'border': '#' + style.style_for_token(Token.Comment)['color']
|
|
34
|
+
}
|
|
35
|
+
self._apply_stylesheet()
|
|
36
|
+
self._apply_word_wrap()
|
|
37
|
+
self._apply_tab_width()
|
|
38
|
+
|
|
39
|
+
def refresh(self):
|
|
40
|
+
super().refresh()
|
|
41
|
+
self._highlighter.rehighlight()
|
|
42
|
+
|
|
43
|
+
def update_theme(self):
|
|
44
|
+
super().update_theme()
|
|
45
|
+
self._apply_stylesheet()
|
|
46
|
+
self._apply_word_wrap()
|
|
47
|
+
self._apply_tab_width
|
|
48
|
+
self.viewport().update()
|
|
49
|
+
|
|
50
|
+
def _apply_stylesheet(self):
|
|
51
|
+
stylesheet = f"""
|
|
52
|
+
QPlainTextEdit {{
|
|
53
|
+
background-color: {self.code_editor_colors['background']};
|
|
54
|
+
font: {settings.font_size}pt '{settings.font_family}';
|
|
55
|
+
color: {self.code_editor_colors['text']};
|
|
56
|
+
}}
|
|
57
|
+
QToolTip {{
|
|
58
|
+
color: {self.code_editor_colors['text']};
|
|
59
|
+
background-color: {self.code_editor_colors['background']};
|
|
60
|
+
font: {settings.font_size}pt '{settings.font_family}';
|
|
61
|
+
border-color: {self.code_editor_colors['border']};
|
|
62
|
+
border-width: 1px;
|
|
63
|
+
border-style: solid;
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
padding: 4px;
|
|
66
|
+
}}
|
|
67
|
+
"""
|
|
68
|
+
self.setStyleSheet(stylesheet)
|
|
69
|
+
|
|
70
|
+
def paintEvent(self, event):
|
|
71
|
+
super().paintEvent(event)
|
|
72
|
+
if settings.character_ruler:
|
|
73
|
+
char_width = self.fontMetrics().width("x")
|
|
74
|
+
x_pos = int(char_width * settings.character_ruler + self.contentOffset().x())
|
|
75
|
+
y_pos = self.viewport().height()
|
|
76
|
+
painter = QPainter(self.viewport())
|
|
77
|
+
painter.setPen(QColor(self.code_editor_colors['line-number']))
|
|
78
|
+
painter.drawLine(x_pos, 0, x_pos, y_pos)
|
|
79
|
+
|
|
80
|
+
def _apply_word_wrap(self):
|
|
81
|
+
"""
|
|
82
|
+
Toggles word wrap mode based on settings.word_wrap
|
|
83
|
+
"""
|
|
84
|
+
if settings.word_wrap:
|
|
85
|
+
self.setLineWrapMode(QPlainTextEdit.WidgetWidth)
|
|
86
|
+
else:
|
|
87
|
+
self.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
88
|
+
|
|
89
|
+
def _apply_tab_width(self):
|
|
90
|
+
"""Sets the tab-stop distance. This uses a QFont rather than relying on
|
|
91
|
+
the standard font metrics, because these are not immediately applied on
|
|
92
|
+
initialization.
|
|
93
|
+
"""
|
|
94
|
+
font = QFont()
|
|
95
|
+
font.setFamily(settings.font_family)
|
|
96
|
+
font.setPointSize(settings.font_size)
|
|
97
|
+
self.setTabStopDistance(
|
|
98
|
+
settings.tab_width * QFontMetrics(font).horizontalAdvance(' '))
|
|
99
|
+
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from qtpy.QtCore import Qt
|
|
2
|
+
from .. import settings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Zoom:
|
|
6
|
+
"""
|
|
7
|
+
A mixin that implements zoom in/out with:
|
|
8
|
+
- Ctrl+Plus
|
|
9
|
+
- Ctrl+Minus
|
|
10
|
+
- Ctrl+ScrollUp/Down
|
|
11
|
+
It expects the QPlainTextEdit to have a 'code_editor_font_size'
|
|
12
|
+
attribute and a 'setFont(...)' method.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def keyPressEvent(self, event):
|
|
16
|
+
if (event.modifiers() & Qt.ControlModifier):
|
|
17
|
+
if event.key() == Qt.Key_Plus:
|
|
18
|
+
self.zoom_in()
|
|
19
|
+
return
|
|
20
|
+
elif event.key() == Qt.Key_Minus:
|
|
21
|
+
self.zoom_out()
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
super().keyPressEvent(event)
|
|
25
|
+
|
|
26
|
+
def wheelEvent(self, event):
|
|
27
|
+
"""
|
|
28
|
+
Zoom in/out when Ctrl is pressed and the user scrolls the wheel.
|
|
29
|
+
"""
|
|
30
|
+
if event.modifiers() & Qt.ControlModifier:
|
|
31
|
+
# angleDelta().y() is typically 120 per step on many systems.
|
|
32
|
+
delta = event.angleDelta().y()
|
|
33
|
+
if delta > 0:
|
|
34
|
+
self.zoom_in()
|
|
35
|
+
else:
|
|
36
|
+
self.zoom_out()
|
|
37
|
+
# Do not pass event along so normal scrolling doesn't occur
|
|
38
|
+
event.accept()
|
|
39
|
+
else:
|
|
40
|
+
super().wheelEvent(event)
|
|
41
|
+
|
|
42
|
+
def zoom_in(self):
|
|
43
|
+
""" Increase the editor's font size by 1 point. """
|
|
44
|
+
settings.font_size += 1
|
|
45
|
+
self.update_theme()
|
|
46
|
+
|
|
47
|
+
def zoom_out(self):
|
|
48
|
+
""" Decrease the editor's font size by 1 point, down to a minimum of 1. """
|
|
49
|
+
if settings.font_size > 1:
|
|
50
|
+
settings.font_size -= 1
|
|
51
|
+
self.update_theme()
|