pyqt-code-editor 0.0.48__tar.gz → 0.0.51__tar.gz
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-0.0.48 → pyqt_code_editor-0.0.51}/PKG-INFO +10 -3
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyproject.toml +10 -3
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/__init__.py +1 -1
- pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/generic.py +13 -0
- pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/markdown.py +11 -0
- pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/python.py +16 -0
- pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/text.py +11 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/workspace_explorer.py +8 -12
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/__init__.py +2 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/auto_indent.py +0 -6
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/auto_pair.py +87 -64
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/check.py +0 -4
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/comment.py +0 -6
- pyqt_code_editor-0.0.51/pyqt_code_editor/mixins/highlight_matching_brackets.py +98 -0
- pyqt_code_editor-0.0.51/pyqt_code_editor/mixins/merge_undo_actions.py +32 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/python_auto_indent.py +0 -9
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/shortcuts.py +1 -7
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/generic.py +2 -1
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/python.py +2 -1
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/process.py +17 -1
- pyqt_code_editor-0.0.51/pyqt_code_editor/worker/providers/tree_sitter.py +61 -0
- pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/generic.py +0 -9
- pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/markdown.py +0 -11
- pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/python.py +0 -13
- pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/text.py +0 -9
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/COPYING +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/_settings.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/app.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/code_editors.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/languages/javascript.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/editor_panel.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/find_in_files.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/jupyter_console.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/project_explorer.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/settings_panel.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/sigmund.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/environment_manager.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/base.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/complete.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/execute.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/file_link.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/line_number.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/search_replace.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/symbols.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/theme.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/zoom.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/sigmund-analyst.png +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/signal_router.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/themes.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/watchdog.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/completion_popup.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/dock.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/manager.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/__init__.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/codestral.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/jedi.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/ruff.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/symbol.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/settings.py +0 -0
- {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/readme.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyqt_code_editor
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.51
|
|
4
4
|
Summary: Fully featured code-editor widgets for PyQt
|
|
5
5
|
Keywords: code editor,pyqt,ide
|
|
6
6
|
Author-email: Sebastiaan Mathôt <s.mathot@cogsci.nl>
|
|
@@ -12,15 +12,22 @@ Requires-Dist: detect-indent
|
|
|
12
12
|
Requires-Dist: jedi
|
|
13
13
|
Requires-Dist: httpx<0.29.0
|
|
14
14
|
Requires-Dist: mistralai
|
|
15
|
+
Requires-Dist: opensesame-extension-sigmund
|
|
15
16
|
Requires-Dist: pathspec
|
|
16
17
|
Requires-Dist: psutil
|
|
17
18
|
Requires-Dist: pygments
|
|
18
19
|
Requires-Dist: qtawesome
|
|
19
20
|
Requires-Dist: qtpy
|
|
20
|
-
Requires-Dist: ruff
|
|
21
|
-
Requires-Dist: opensesame-extension-sigmund
|
|
22
21
|
Requires-Dist: pyqt6
|
|
23
22
|
Requires-Dist: qtconsole
|
|
23
|
+
Requires-Dist: tree-sitter
|
|
24
|
+
Requires-Dist: tree-sitter-javascript
|
|
25
|
+
Requires-Dist: tree-sitter-json
|
|
26
|
+
Requires-Dist: tree-sitter-html
|
|
27
|
+
Requires-Dist: tree-sitter-python
|
|
28
|
+
Requires-Dist: tree-sitter-toml
|
|
29
|
+
Requires-Dist: tree-sitter-yaml
|
|
30
|
+
Requires-Dist: ruff
|
|
24
31
|
Project-URL: Source, https://github.com/open-cogsci/sigmund-analyst
|
|
25
32
|
|
|
26
33
|
# PyQt Code Editor / Sigmund Analyst
|
|
@@ -17,15 +17,22 @@ dependencies = [
|
|
|
17
17
|
"jedi",
|
|
18
18
|
"httpx<0.29.0",
|
|
19
19
|
"mistralai",
|
|
20
|
+
"opensesame-extension-sigmund",
|
|
20
21
|
"pathspec",
|
|
21
22
|
"psutil",
|
|
22
23
|
"pygments",
|
|
23
24
|
"qtawesome",
|
|
24
25
|
"qtpy",
|
|
25
|
-
"ruff",
|
|
26
|
-
"opensesame-extension-sigmund",
|
|
27
26
|
"pyqt6",
|
|
28
|
-
"qtconsole"
|
|
27
|
+
"qtconsole",
|
|
28
|
+
"tree-sitter",
|
|
29
|
+
"tree-sitter-javascript",
|
|
30
|
+
"tree-sitter-json",
|
|
31
|
+
"tree-sitter-html",
|
|
32
|
+
"tree-sitter-python",
|
|
33
|
+
"tree-sitter-toml",
|
|
34
|
+
"tree-sitter-yaml",
|
|
35
|
+
"ruff"
|
|
29
36
|
]
|
|
30
37
|
keywords = ["code editor", "pyqt", "ide"]
|
|
31
38
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
+
from pyqt_code_editor.mixins import (MergeUndoActions, Complete, Theme, Zoom,
|
|
3
|
+
LineNumber, Comment, AutoIndent,
|
|
4
|
+
AutoPair, SearchReplace, Base, Check,
|
|
5
|
+
Shortcuts, FileLink, Symbols,
|
|
6
|
+
HighlightMatchingBrackets)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Editor(MergeUndoActions, LineNumber, Zoom, Complete, AutoIndent,
|
|
10
|
+
AutoPair, Comment, SearchReplace, FileLink, Theme, Check,
|
|
11
|
+
Shortcuts, HighlightMatchingBrackets, Symbols, Base,
|
|
12
|
+
QPlainTextEdit):
|
|
13
|
+
pass
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
+
from pyqt_code_editor.mixins import (MergeUndoActions, Complete,
|
|
3
|
+
HighlightSyntax, Zoom, LineNumber,
|
|
4
|
+
Comment, SearchReplace, Base, Check,
|
|
5
|
+
Shortcuts, FileLink, Symbols)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Editor(MergeUndoActions, LineNumber, Zoom, Complete, Comment,
|
|
9
|
+
SearchReplace, FileLink, HighlightSyntax, Check, Shortcuts,
|
|
10
|
+
Symbols, Base, QPlainTextEdit):
|
|
11
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
+
from pyqt_code_editor.mixins import (Complete, PythonAutoIndent,
|
|
3
|
+
PythonAutoPair, Theme, Zoom, LineNumber,
|
|
4
|
+
Comment, SearchReplace, Base, Check,
|
|
5
|
+
Shortcuts, FileLink, Symbols,
|
|
6
|
+
SmartBackspaceDelete, Execute,
|
|
7
|
+
MergeUndoActions,
|
|
8
|
+
HighlightMatchingBrackets)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Editor(MergeUndoActions, LineNumber, Zoom, SmartBackspaceDelete,
|
|
12
|
+
PythonAutoPair, Complete, Comment, PythonAutoIndent,
|
|
13
|
+
SearchReplace, FileLink, Execute, Check, Theme, Shortcuts,
|
|
14
|
+
HighlightMatchingBrackets, Symbols, Base, QPlainTextEdit):
|
|
15
|
+
|
|
16
|
+
code_editor_language = 'python'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
+
from pyqt_code_editor.mixins import (MergeUndoActions, Complete,
|
|
3
|
+
HighlightSyntax, Zoom, LineNumber,
|
|
4
|
+
SearchReplace, Base, Check, Shortcuts,
|
|
5
|
+
FileLink)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Editor(MergeUndoActions, LineNumber, Zoom, Complete, SearchReplace,
|
|
9
|
+
FileLink, HighlightSyntax, Check, Shortcuts, Base,
|
|
10
|
+
QPlainTextEdit):
|
|
11
|
+
pass
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from qtpy.QtWidgets import (
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from qtpy.QtCore import Qt, QSortFilterProxyModel, QAbstractTableModel,
|
|
5
|
-
|
|
1
|
+
from qtpy.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTableView,
|
|
2
|
+
QHeaderView, QLineEdit, QAbstractItemView, QMenu,
|
|
3
|
+
QAction)
|
|
4
|
+
from qtpy.QtCore import (Qt, QSortFilterProxyModel, QAbstractTableModel,
|
|
5
|
+
QModelIndex)
|
|
6
|
+
from qtpy.QtGui import QColor, QFont, QBrush
|
|
6
7
|
from ..widgets import Dock
|
|
7
8
|
|
|
8
9
|
|
|
@@ -77,6 +78,7 @@ class WorkspaceModel(QAbstractTableModel):
|
|
|
77
78
|
return self._headers[section]
|
|
78
79
|
return None
|
|
79
80
|
|
|
81
|
+
|
|
80
82
|
class WorkspaceExplorer(Dock):
|
|
81
83
|
"""Dock widget for exploring variables in the Jupyter workspace"""
|
|
82
84
|
|
|
@@ -150,13 +152,7 @@ class WorkspaceExplorer(Dock):
|
|
|
150
152
|
# Add actions specific to the variable
|
|
151
153
|
inspect_action = QAction(f"Inspect '{var_name}'", self)
|
|
152
154
|
delete_action = QAction(f"Delete '{var_name}'", self)
|
|
153
|
-
|
|
154
155
|
menu.addAction(inspect_action)
|
|
155
156
|
menu.addAction(delete_action)
|
|
156
|
-
|
|
157
|
-
# Show the menu
|
|
158
|
-
action = menu.exec_(self.table_view.mapToGlobal(position))
|
|
159
|
-
|
|
160
|
-
# Handle action selection
|
|
161
|
-
# (In a real implementation, would connect to actual functionality)
|
|
157
|
+
menu.exec_(self.table_view.mapToGlobal(position))
|
|
162
158
|
|
|
@@ -118,7 +118,6 @@ class AutoIndent:
|
|
|
118
118
|
"""
|
|
119
119
|
logger.info("indent_code triggered")
|
|
120
120
|
cursor = self.textCursor()
|
|
121
|
-
cursor.beginEditBlock()
|
|
122
121
|
|
|
123
122
|
if self._is_multiline_selection():
|
|
124
123
|
logger.info("Multi-line selection -> indent each line")
|
|
@@ -128,8 +127,6 @@ class AutoIndent:
|
|
|
128
127
|
indent_str = self._get_indent_string()
|
|
129
128
|
cursor.insertText(indent_str)
|
|
130
129
|
|
|
131
|
-
cursor.endEditBlock()
|
|
132
|
-
|
|
133
130
|
def dedent_code(self):
|
|
134
131
|
"""
|
|
135
132
|
Dedent either the selected lines (if multi-line selection)
|
|
@@ -137,7 +134,6 @@ class AutoIndent:
|
|
|
137
134
|
"""
|
|
138
135
|
logger.info("dedent_code triggered")
|
|
139
136
|
cursor = self.textCursor()
|
|
140
|
-
cursor.beginEditBlock()
|
|
141
137
|
|
|
142
138
|
if self._is_multiline_selection():
|
|
143
139
|
logger.info("Multi-line selection -> dedent each line")
|
|
@@ -156,8 +152,6 @@ class AutoIndent:
|
|
|
156
152
|
for _ in range(remove_chars):
|
|
157
153
|
self._delete_forward_if_tab_or_space(cursor)
|
|
158
154
|
|
|
159
|
-
cursor.endEditBlock()
|
|
160
|
-
|
|
161
155
|
def _indent_selection(self):
|
|
162
156
|
"""
|
|
163
157
|
Increase indent for each selected line by one chunk.
|
|
@@ -4,9 +4,8 @@ logger = logging.getLogger(__name__)
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class AutoPair:
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"""Generic mixin for auto-pairing brackets/quotes/etc.
|
|
8
|
+
|
|
10
9
|
You can override self.PAIRS to add or remove different open/close
|
|
11
10
|
sequences. Each entry is a dict with:
|
|
12
11
|
- open_seq: string that triggers auto-pair (e.g. '(' or '\"\"\"')
|
|
@@ -14,7 +13,7 @@ class AutoPair:
|
|
|
14
13
|
- inbetween_seq: optional string inserted between open_seq and close_seq
|
|
15
14
|
(can be empty). e.g. '\n' for triple quotes.
|
|
16
15
|
"""
|
|
17
|
-
|
|
16
|
+
|
|
18
17
|
PAIRS = [
|
|
19
18
|
{"open_seq": "(", "close_seq": ")", "inbetween_seq": ""},
|
|
20
19
|
{"open_seq": "[", "close_seq": "]", "inbetween_seq": ""},
|
|
@@ -22,155 +21,179 @@ class AutoPair:
|
|
|
22
21
|
{"open_seq": "\"", "close_seq": "\"", "inbetween_seq": ""},
|
|
23
22
|
{"open_seq": "\'", "close_seq": "\'", "inbetween_seq": ""},
|
|
24
23
|
]
|
|
25
|
-
|
|
24
|
+
|
|
26
25
|
def __init__(self, *args, **kwargs):
|
|
27
26
|
super().__init__(*args, **kwargs)
|
|
28
27
|
self.setUndoRedoEnabled(True)
|
|
29
28
|
# For scanning up to the max length of an open_seq
|
|
30
29
|
self._max_open_len = max(len(pair["open_seq"]) for pair in self.PAIRS)
|
|
30
|
+
self._trigger_chars = ''.join([pair['open_seq'] + pair['close_seq']
|
|
31
|
+
for pair in self.PAIRS])
|
|
31
32
|
|
|
32
33
|
def keyPressEvent(self, event):
|
|
34
|
+
# Determine all relevant variables upfront
|
|
35
|
+
typed_char = event.text()
|
|
36
|
+
typed_key = event.key()
|
|
33
37
|
cursor = self.textCursor()
|
|
34
38
|
old_pos = cursor.position()
|
|
35
39
|
selected_text = cursor.selectedText()
|
|
36
|
-
|
|
40
|
+
doc_text = self.toPlainText()
|
|
41
|
+
doc_len = len(doc_text)
|
|
42
|
+
# Get character before cursor
|
|
43
|
+
if old_pos > 0:
|
|
44
|
+
char_before = doc_text[old_pos - 1]
|
|
45
|
+
else:
|
|
46
|
+
char_before = ""
|
|
47
|
+
# Get character after cursor
|
|
48
|
+
if old_pos < doc_len:
|
|
49
|
+
char_after = doc_text[old_pos]
|
|
50
|
+
else:
|
|
51
|
+
char_after = ""
|
|
52
|
+
|
|
53
|
+
# If there is no opening (trigger character or backspace), then we
|
|
54
|
+
# don't need to do anything.
|
|
55
|
+
if typed_char not in self._trigger_chars and typed_key != Qt.Key_Backspace:
|
|
56
|
+
super().keyPressEvent(event)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Don't autopair when there is no selection and the next character is
|
|
60
|
+
# alphanumeric or underscore. This is for example when we're typing the
|
|
61
|
+
# opening bracket of a list that is already completed
|
|
62
|
+
if not selected_text:
|
|
63
|
+
if char_after.isalnum() or char_after == "_":
|
|
64
|
+
super().keyPressEvent(event)
|
|
65
|
+
return
|
|
66
|
+
|
|
37
67
|
# Allows auto-pairing to be disabled for certain positions
|
|
38
68
|
if self._disabled_for_position(old_pos):
|
|
39
69
|
super().keyPressEvent(event)
|
|
40
70
|
return
|
|
41
|
-
|
|
71
|
+
|
|
42
72
|
# 1) Handle backspace for removing empty pairs like (|)
|
|
43
|
-
if
|
|
44
|
-
if self._handle_auto_pair_backspace():
|
|
73
|
+
if typed_key == Qt.Key_Backspace:
|
|
74
|
+
if self._handle_auto_pair_backspace(char_before, char_after, old_pos):
|
|
45
75
|
super().keyPressEvent(event)
|
|
46
76
|
return
|
|
47
|
-
|
|
48
|
-
typed_char = event.text()
|
|
49
|
-
|
|
77
|
+
|
|
50
78
|
# Pass the event up (inserts the typed_char)
|
|
51
79
|
super().keyPressEvent(event)
|
|
52
|
-
|
|
80
|
+
|
|
53
81
|
# 2) Possibly skip duplicate closing bracket/quote
|
|
54
82
|
# when the user manually types it.
|
|
55
83
|
if typed_char and len(typed_char) == 1:
|
|
56
84
|
for pair in self.PAIRS:
|
|
57
85
|
if typed_char == pair["close_seq"]:
|
|
86
|
+
print(typed_char)
|
|
58
87
|
new_cursor = self.textCursor()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
88
|
+
new_pos = new_cursor.position()
|
|
89
|
+
new_doc_text = self.toPlainText()
|
|
90
|
+
new_doc_len = len(new_doc_text)
|
|
91
|
+
if new_pos < new_doc_len and new_doc_text[new_pos] == typed_char:
|
|
62
92
|
# We remove the newly typed character and move cursor forward
|
|
63
|
-
new_cursor.beginEditBlock()
|
|
64
93
|
# Remove the bracket that was just typed
|
|
65
94
|
new_cursor.setPosition(old_pos)
|
|
66
95
|
new_cursor.deleteChar()
|
|
67
96
|
# Move cursor to skip the existing bracket
|
|
68
97
|
new_cursor.setPosition(old_pos + 1)
|
|
69
|
-
new_cursor.endEditBlock()
|
|
70
98
|
self.setTextCursor(new_cursor)
|
|
71
99
|
break
|
|
72
|
-
|
|
100
|
+
|
|
73
101
|
# 3) After insertion, see if the text before the cursor matches an 'open_seq'
|
|
74
102
|
# If it does, insert close_seq + inbetween_seq, then restore cursor.
|
|
75
103
|
new_cursor = self.textCursor()
|
|
76
104
|
new_pos = new_cursor.position()
|
|
77
105
|
text = self.toPlainText()
|
|
78
|
-
|
|
106
|
+
|
|
79
107
|
# We'll scan backward from new_pos for up to _max_open_len chars
|
|
80
108
|
start_search = max(0, new_pos - self._max_open_len)
|
|
81
109
|
just_typed = text[start_search:new_pos]
|
|
82
|
-
|
|
110
|
+
|
|
83
111
|
for pair in self.PAIRS:
|
|
84
112
|
open_seq = pair["open_seq"]
|
|
85
113
|
close_seq = pair["close_seq"]
|
|
86
114
|
inbetween_seq = pair["inbetween_seq"]
|
|
87
|
-
|
|
115
|
+
|
|
88
116
|
if just_typed.endswith(open_seq) and event.text() == text[new_pos - 1: new_pos]:
|
|
89
117
|
self._insert_pair(open_seq, close_seq, inbetween_seq,
|
|
90
|
-
selected_text)
|
|
118
|
+
selected_text, new_pos)
|
|
91
119
|
break
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
def _handle_auto_pair_backspace(self) -> bool:
|
|
121
|
+
def _handle_auto_pair_backspace(self, char_before, char_after, pos) -> bool:
|
|
95
122
|
"""
|
|
96
123
|
If the cursor is between an exact open_seq and close_seq pair (e.g., '(|)'),
|
|
97
124
|
remove the following close_seq. The preceding open_seq will be removed
|
|
98
125
|
by the standard backspace. Only works for pairs of single characters.
|
|
99
|
-
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
char_before: The character before the cursor
|
|
129
|
+
char_after: The character after the cursor
|
|
130
|
+
pos: The cursor position
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if handled, False if not.
|
|
100
134
|
"""
|
|
101
|
-
cursor = self.textCursor()
|
|
102
|
-
pos = cursor.position()
|
|
103
|
-
|
|
104
135
|
# We need at least one character before and after cursor
|
|
105
|
-
if
|
|
136
|
+
if not char_before or not char_after:
|
|
106
137
|
return False
|
|
107
|
-
|
|
108
|
-
# Get the character before the cursor
|
|
109
|
-
cursor.setPosition(pos - 1)
|
|
110
|
-
cursor.setPosition(pos, cursor.KeepAnchor)
|
|
111
|
-
char_before = cursor.selectedText()
|
|
112
|
-
|
|
113
|
-
# Get the character after the cursor
|
|
114
|
-
cursor.setPosition(pos)
|
|
115
|
-
cursor.setPosition(pos + 1, cursor.KeepAnchor)
|
|
116
|
-
char_after = cursor.selectedText()
|
|
117
|
-
|
|
138
|
+
|
|
118
139
|
# Check if they form a pair
|
|
119
140
|
for pair in self.PAIRS:
|
|
120
141
|
open_seq = pair["open_seq"]
|
|
121
142
|
close_seq = pair["close_seq"]
|
|
122
|
-
|
|
143
|
+
|
|
123
144
|
# Only handle single-character pairs
|
|
124
145
|
if len(open_seq) != 1 or len(close_seq) != 1:
|
|
125
146
|
continue
|
|
126
|
-
|
|
147
|
+
|
|
127
148
|
if char_before == open_seq and char_after == close_seq:
|
|
128
149
|
logger.info("deleting closing bracket")
|
|
129
150
|
# Delete only the closing character
|
|
151
|
+
cursor = self.textCursor()
|
|
130
152
|
cursor.setPosition(pos)
|
|
131
153
|
cursor.deleteChar()
|
|
132
|
-
|
|
154
|
+
|
|
133
155
|
# The standard backspace will handle the opening character
|
|
134
156
|
return True
|
|
135
|
-
|
|
136
|
-
return False
|
|
137
157
|
|
|
158
|
+
return False
|
|
138
159
|
|
|
139
|
-
def _insert_pair(self, open_seq, close_seq, inbetween_seq, selected_text):
|
|
160
|
+
def _insert_pair(self, open_seq, close_seq, inbetween_seq, selected_text, cursor_pos):
|
|
140
161
|
"""
|
|
141
162
|
Called when we recognize that the user typed an open_seq in full.
|
|
142
163
|
Insert the close_seq plus any inbetween_seq, and restore the cursor
|
|
143
164
|
to the original position (right after the open_seq).
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
open_seq: The opening sequence that was typed
|
|
168
|
+
close_seq: The closing sequence to insert
|
|
169
|
+
inbetween_seq: Text to insert between open and close
|
|
170
|
+
selected_text: Text that was selected (if any)
|
|
171
|
+
cursor_pos: Current cursor position (after the open_seq was inserted)
|
|
144
172
|
"""
|
|
145
173
|
c = self.textCursor()
|
|
146
|
-
|
|
147
|
-
# We'll remember where the user ended up (right after open_seq).
|
|
148
|
-
old_pos = c.position()
|
|
174
|
+
|
|
149
175
|
if inbetween_seq == '\n':
|
|
150
176
|
# Get indentation level of block after cursor
|
|
151
|
-
block = self.document().findBlock(
|
|
177
|
+
block = self.document().findBlock(cursor_pos)
|
|
152
178
|
block_text = block.text()
|
|
153
179
|
indent = block_text[:len(block_text) - len(block_text.lstrip())]
|
|
154
180
|
inbetween_seq = '\n' + indent
|
|
155
|
-
|
|
156
|
-
c.beginEditBlock()
|
|
157
|
-
|
|
181
|
+
|
|
158
182
|
# Insert in-between text, then the closing
|
|
159
183
|
c.insertText(selected_text + inbetween_seq + close_seq)
|
|
160
|
-
|
|
184
|
+
|
|
161
185
|
# Move the cursor back so that it's right after the open_seq and (if any)
|
|
162
186
|
# the selected text
|
|
163
187
|
c.movePosition(c.Left, c.MoveAnchor, len(close_seq) + len(inbetween_seq))
|
|
164
|
-
|
|
165
|
-
c.endEditBlock()
|
|
188
|
+
|
|
166
189
|
self.setTextCursor(c)
|
|
167
|
-
|
|
190
|
+
|
|
168
191
|
logger.info(
|
|
169
192
|
"Auto-paired '%s' with '%s', inserted in-between '%s'. "
|
|
170
193
|
"Cursor restored to position %d",
|
|
171
|
-
open_seq, close_seq, inbetween_seq,
|
|
194
|
+
open_seq, close_seq, inbetween_seq, cursor_pos
|
|
172
195
|
)
|
|
173
|
-
|
|
196
|
+
|
|
174
197
|
def _disabled_for_position(self, pos):
|
|
175
198
|
return False
|
|
176
199
|
|
|
@@ -186,7 +209,7 @@ class PythonAutoPair(AutoPair):
|
|
|
186
209
|
{"open_seq": "\"", "close_seq": "\"", "inbetween_seq": ""},
|
|
187
210
|
{"open_seq": "\'", "close_seq": "\'", "inbetween_seq": ""},
|
|
188
211
|
]
|
|
189
|
-
|
|
212
|
+
|
|
190
213
|
def _disabled_for_position(self, pos):
|
|
191
214
|
"""
|
|
192
215
|
Returns True if 'pos' (a QTextCursor.position() in self)
|
|
@@ -197,12 +220,12 @@ class PythonAutoPair(AutoPair):
|
|
|
197
220
|
block = self.document().findBlock(pos)
|
|
198
221
|
line_text = block.text()
|
|
199
222
|
column = pos - block.position()
|
|
200
|
-
|
|
223
|
+
|
|
201
224
|
# Find the first '#' in line_text
|
|
202
225
|
hash_index = line_text.find('#')
|
|
203
|
-
|
|
226
|
+
|
|
204
227
|
# If a '#' is found and it's to the left of the cursor, assume comment
|
|
205
228
|
if hash_index != -1 and hash_index < column:
|
|
206
229
|
return True
|
|
207
|
-
|
|
230
|
+
|
|
208
231
|
return False
|
|
@@ -23,10 +23,6 @@ class Check:
|
|
|
23
23
|
self._check_debounce_timer.setSingleShot(True)
|
|
24
24
|
self._check_debounce_timer.setInterval(500)
|
|
25
25
|
self._check_debounce_timer.timeout.connect(self._execute_check)
|
|
26
|
-
# Periodic timer: also periodically run a check
|
|
27
|
-
self._check_interval_timer = QTimer(self)
|
|
28
|
-
self._check_interval_timer.setInterval(5000)
|
|
29
|
-
self._check_interval_timer.timeout.connect(self._execute_check)
|
|
30
26
|
|
|
31
27
|
# Connect textChanged signal to a debounced method
|
|
32
28
|
self.textChanged.connect(self._on_code_changed)
|
|
@@ -35,10 +35,8 @@ class Comment:
|
|
|
35
35
|
# Extract the indentation from the current line
|
|
36
36
|
indentation = line_text[:len(line_text) - len(stripped)]
|
|
37
37
|
# Insert newline with same indentation plus comment marker
|
|
38
|
-
cursor.beginEditBlock()
|
|
39
38
|
cursor.insertText(
|
|
40
39
|
'\n' + indentation + self.code_editor_comment_string)
|
|
41
|
-
cursor.endEditBlock()
|
|
42
40
|
event.accept()
|
|
43
41
|
return
|
|
44
42
|
super().keyPressEvent(event)
|
|
@@ -76,7 +74,6 @@ class Comment:
|
|
|
76
74
|
def _comment_blocks(self, start_block: int, end_block: int):
|
|
77
75
|
"""Comment all lines from start_block to end_block."""
|
|
78
76
|
cursor = self.textCursor()
|
|
79
|
-
cursor.beginEditBlock() # group undo steps
|
|
80
77
|
for block_num in range(start_block, end_block + 1):
|
|
81
78
|
block = self.document().findBlockByNumber(block_num)
|
|
82
79
|
if not block.isValid():
|
|
@@ -87,12 +84,10 @@ class Comment:
|
|
|
87
84
|
insert_position = block.position() + leading_spaces
|
|
88
85
|
cursor.setPosition(insert_position)
|
|
89
86
|
cursor.insertText(self.code_editor_comment_string)
|
|
90
|
-
cursor.endEditBlock()
|
|
91
87
|
|
|
92
88
|
def _uncomment_blocks(self, start_block: int, end_block: int):
|
|
93
89
|
"""Uncomment all lines from start_block to end_block."""
|
|
94
90
|
cursor = self.textCursor()
|
|
95
|
-
cursor.beginEditBlock()
|
|
96
91
|
for block_num in range(start_block, end_block + 1):
|
|
97
92
|
block = self.document().findBlockByNumber(block_num)
|
|
98
93
|
if not block.isValid():
|
|
@@ -107,4 +102,3 @@ class Comment:
|
|
|
107
102
|
cursor.setPosition(remove_position + strip_len, cursor.KeepAnchor)
|
|
108
103
|
if cursor.selectedText() == self.code_editor_comment_string.rstrip('\r\n'):
|
|
109
104
|
cursor.removeSelectedText()
|
|
110
|
-
cursor.endEditBlock()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from qtpy.QtCore import QTimer
|
|
3
|
+
from qtpy.QtGui import QTextCursor, QColor, QTextCharFormat
|
|
4
|
+
from qtpy.QtWidgets import QTextEdit
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HighlightMatchingBrackets:
|
|
10
|
+
"""Highlights matching brackets that match the current cursor position.
|
|
11
|
+
The actual matching is done in the backend.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self._bracket_request_pending = False
|
|
16
|
+
self._current_bracket_pairs = []
|
|
17
|
+
|
|
18
|
+
# Connect to text changes
|
|
19
|
+
self.textChanged.connect(self._on_text_changed)
|
|
20
|
+
|
|
21
|
+
# Connect to cursor position changes
|
|
22
|
+
self.cursorPositionChanged.connect(self._on_cursor_changed)
|
|
23
|
+
|
|
24
|
+
# Set up debounce timer (500ms)
|
|
25
|
+
self._debounce_timer = QTimer()
|
|
26
|
+
self._debounce_timer.setSingleShot(True)
|
|
27
|
+
self._debounce_timer.timeout.connect(self._on_debounce_timeout)
|
|
28
|
+
|
|
29
|
+
def _on_text_changed(self):
|
|
30
|
+
"""Called when text changes - restart debounce timer."""
|
|
31
|
+
# Restart the debounce timer
|
|
32
|
+
self._debounce_timer.stop()
|
|
33
|
+
self._debounce_timer.start(500)
|
|
34
|
+
|
|
35
|
+
def _on_debounce_timeout(self):
|
|
36
|
+
"""Called after 500ms of no text changes - request bracket update."""
|
|
37
|
+
if not self._bracket_request_pending:
|
|
38
|
+
self._request_matching_brackets()
|
|
39
|
+
|
|
40
|
+
def _on_cursor_changed(self):
|
|
41
|
+
"""Called when cursor position changes - update highlighting."""
|
|
42
|
+
self._highlight_brackets()
|
|
43
|
+
|
|
44
|
+
def _request_matching_brackets(self):
|
|
45
|
+
self._bracket_request_pending = True
|
|
46
|
+
self.send_worker_request(action='matching_brackets',
|
|
47
|
+
language=getattr(self, "code_editor_language", "python"),
|
|
48
|
+
code=self.toPlainText())
|
|
49
|
+
|
|
50
|
+
def _handle_matching_brackets(self, pairs):
|
|
51
|
+
"""Pairs is a list of (open_pos, close_pos) positions relative to the
|
|
52
|
+
start of the document.
|
|
53
|
+
"""
|
|
54
|
+
self._bracket_request_pending = False
|
|
55
|
+
self._current_bracket_pairs = pairs
|
|
56
|
+
self._highlight_brackets()
|
|
57
|
+
|
|
58
|
+
def _highlight_brackets(self):
|
|
59
|
+
"""Highlight matching bracket pairs where cursor is on one of the brackets."""
|
|
60
|
+
extra_selections = []
|
|
61
|
+
cursor_pos = self.textCursor().position()
|
|
62
|
+
for open_pos, close_pos in self._current_bracket_pairs:
|
|
63
|
+
# Check if cursor is on either bracket
|
|
64
|
+
if cursor_pos in (open_pos, open_pos + 1,
|
|
65
|
+
close_pos, close_pos + 1):
|
|
66
|
+
# Highlight opening bracket
|
|
67
|
+
open_selection = self._create_bracket_selection(open_pos)
|
|
68
|
+
if open_selection:
|
|
69
|
+
extra_selections.append(open_selection)
|
|
70
|
+
|
|
71
|
+
# Highlight closing bracket
|
|
72
|
+
close_selection = self._create_bracket_selection(close_pos)
|
|
73
|
+
if close_selection:
|
|
74
|
+
extra_selections.append(close_selection)
|
|
75
|
+
|
|
76
|
+
self.setExtraSelections(extra_selections)
|
|
77
|
+
|
|
78
|
+
def _create_bracket_selection(self, pos):
|
|
79
|
+
"""Create an extra selection for a single bracket at position."""
|
|
80
|
+
cursor = self.textCursor()
|
|
81
|
+
cursor.setPosition(pos)
|
|
82
|
+
cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 1)
|
|
83
|
+
|
|
84
|
+
# Create yellow background format
|
|
85
|
+
fmt = QTextCharFormat()
|
|
86
|
+
fmt.setBackground(QColor(255, 255, 0, 50)) # Yellow with some transparency
|
|
87
|
+
|
|
88
|
+
selection = QTextEdit.ExtraSelection()
|
|
89
|
+
selection.cursor = cursor
|
|
90
|
+
selection.format = fmt
|
|
91
|
+
|
|
92
|
+
return selection
|
|
93
|
+
|
|
94
|
+
def handle_worker_result(self, action, result):
|
|
95
|
+
super().handle_worker_result(action, result)
|
|
96
|
+
if action == 'matching_brackets':
|
|
97
|
+
logger.info("Handling matching brackets result")
|
|
98
|
+
self._handle_matching_brackets(**result)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from qtpy.QtCore import Qt
|
|
2
|
+
from qtpy.QtGui import QTextCursor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MergeUndoActions:
|
|
6
|
+
"""Ensures that all changes made as part of a key press result in a
|
|
7
|
+
single edit block for undo purposes. This class should be the first
|
|
8
|
+
mixin of the list.
|
|
9
|
+
"""
|
|
10
|
+
def keyPressEvent(self, event):
|
|
11
|
+
# Navigation keys that should NOT be wrapped in edit blocks. This
|
|
12
|
+
# causes a specific issue with the cursor jumping to the start of a
|
|
13
|
+
# line when navigating vertically.
|
|
14
|
+
navigation_keys = {
|
|
15
|
+
Qt.Key.Key_Up, Qt.Key.Key_Down,
|
|
16
|
+
Qt.Key.Key_Left, Qt.Key.Key_Right,
|
|
17
|
+
Qt.Key.Key_Home, Qt.Key.Key_End,
|
|
18
|
+
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
|
|
19
|
+
}
|
|
20
|
+
is_navigation = (event.key() in navigation_keys and
|
|
21
|
+
not (event.modifiers() & Qt.KeyboardModifier.ShiftModifier))
|
|
22
|
+
if is_navigation:
|
|
23
|
+
super().keyPressEvent(event)
|
|
24
|
+
return
|
|
25
|
+
# Wrap all other keys (editing operations) in a single edit block to
|
|
26
|
+
# unify changes into a single undo action
|
|
27
|
+
cursor = QTextCursor(self.document())
|
|
28
|
+
cursor.beginEditBlock()
|
|
29
|
+
try:
|
|
30
|
+
super().keyPressEvent(event)
|
|
31
|
+
finally:
|
|
32
|
+
cursor.endEditBlock()
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/python_auto_indent.py
RENAMED
|
@@ -51,7 +51,6 @@ class PythonAutoIndent:
|
|
|
51
51
|
if key in (Qt.Key_Enter, Qt.Key_Return):
|
|
52
52
|
logger.info("Enter/Return pressed")
|
|
53
53
|
cursor = self.textCursor()
|
|
54
|
-
cursor.beginEditBlock()
|
|
55
54
|
|
|
56
55
|
# Use QTextCursor to get text up to cursor position
|
|
57
56
|
# This handles UTF-16 encoding correctly
|
|
@@ -66,7 +65,6 @@ class PythonAutoIndent:
|
|
|
66
65
|
if new_indent > 0:
|
|
67
66
|
logger.info("Inserting %d spaces of indentation", new_indent)
|
|
68
67
|
self._insert_indentation(new_indent)
|
|
69
|
-
cursor.endEditBlock()
|
|
70
68
|
self.setTextCursor(cursor)
|
|
71
69
|
return
|
|
72
70
|
# Otherwise, default behavior
|
|
@@ -144,7 +142,6 @@ class PythonAutoIndent:
|
|
|
144
142
|
"""
|
|
145
143
|
logger.info("Indent code triggered")
|
|
146
144
|
cursor = self.textCursor()
|
|
147
|
-
cursor.beginEditBlock()
|
|
148
145
|
|
|
149
146
|
if self._is_multiline_selection():
|
|
150
147
|
logger.info("Multi-line selection detected -> Indenting each line")
|
|
@@ -154,8 +151,6 @@ class PythonAutoIndent:
|
|
|
154
151
|
# Insert spacing at the current cursor position
|
|
155
152
|
cursor.insertText(' ' * settings.tab_width)
|
|
156
153
|
|
|
157
|
-
cursor.endEditBlock()
|
|
158
|
-
|
|
159
154
|
def dedent_code(self):
|
|
160
155
|
"""
|
|
161
156
|
Dedent either the selected lines (if multi-line selection)
|
|
@@ -164,8 +159,6 @@ class PythonAutoIndent:
|
|
|
164
159
|
"""
|
|
165
160
|
logger.info("Dedent code triggered")
|
|
166
161
|
cursor = self.textCursor()
|
|
167
|
-
cursor.beginEditBlock()
|
|
168
|
-
|
|
169
162
|
if self._is_multiline_selection():
|
|
170
163
|
logger.info("Multi-line selection detected -> Dedenting each line")
|
|
171
164
|
self._dedent_selection()
|
|
@@ -185,8 +178,6 @@ class PythonAutoIndent:
|
|
|
185
178
|
for _ in range(remove_spaces):
|
|
186
179
|
self._delete_forward_if_space(cursor)
|
|
187
180
|
|
|
188
|
-
cursor.endEditBlock()
|
|
189
|
-
|
|
190
181
|
def _indent_selection(self):
|
|
191
182
|
"""
|
|
192
183
|
Increase indent for each selected line by settings.tab_width spaces.
|
|
@@ -47,7 +47,6 @@ class Shortcuts:
|
|
|
47
47
|
start, end = self._current_line_bounds(cursor)
|
|
48
48
|
doc_length = self.document().characterCount()
|
|
49
49
|
|
|
50
|
-
cursor.beginEditBlock()
|
|
51
50
|
# We want to remove the trailing or preceding newline, but not both.
|
|
52
51
|
if end < doc_length - 1:
|
|
53
52
|
end += 1
|
|
@@ -56,7 +55,6 @@ class Shortcuts:
|
|
|
56
55
|
cursor.setPosition(start)
|
|
57
56
|
cursor.setPosition(end, QTextCursor.KeepAnchor)
|
|
58
57
|
cursor.removeSelectedText()
|
|
59
|
-
cursor.endEditBlock()
|
|
60
58
|
|
|
61
59
|
def _current_line_bounds(self, cursor=None):
|
|
62
60
|
"""
|
|
@@ -85,7 +83,6 @@ class Shortcuts:
|
|
|
85
83
|
"""
|
|
86
84
|
doc = self.document()
|
|
87
85
|
cursor = self.textCursor()
|
|
88
|
-
cursor.beginEditBlock()
|
|
89
86
|
|
|
90
87
|
anchor = cursor.anchor()
|
|
91
88
|
pos = cursor.position()
|
|
@@ -164,7 +161,6 @@ class Shortcuts:
|
|
|
164
161
|
cursor.setPosition(anchor)
|
|
165
162
|
cursor.setPosition(pos, QTextCursor.KeepAnchor)
|
|
166
163
|
self.setTextCursor(cursor)
|
|
167
|
-
cursor.endEditBlock()
|
|
168
164
|
self.refresh()
|
|
169
165
|
|
|
170
166
|
def _move_line_up(self):
|
|
@@ -183,7 +179,6 @@ class Shortcuts:
|
|
|
183
179
|
In other words, it should appear as if the duplication is inserted above.
|
|
184
180
|
"""
|
|
185
181
|
cursor = self.textCursor()
|
|
186
|
-
cursor.beginEditBlock()
|
|
187
182
|
|
|
188
183
|
# 1) Remember original anchor and cursor positions
|
|
189
184
|
original_anchor = cursor.anchor()
|
|
@@ -223,5 +218,4 @@ class Shortcuts:
|
|
|
223
218
|
cursor.setPosition(original_anchor + inserted_length, cursor.MoveAnchor)
|
|
224
219
|
cursor.setPosition(original_position + inserted_length, cursor.KeepAnchor)
|
|
225
220
|
|
|
226
|
-
self.setTextCursor(cursor)
|
|
227
|
-
cursor.endEditBlock()
|
|
221
|
+
self.setTextCursor(cursor)
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/generic.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ..providers import symbol, codestral
|
|
1
|
+
from ..providers import symbol, codestral, tree_sitter
|
|
2
2
|
|
|
3
3
|
def complete(code, cursor_pos, path, multiline, full, env_path, prefix):
|
|
4
4
|
if full or multiline:
|
|
@@ -13,3 +13,4 @@ def complete(code, cursor_pos, path, multiline, full, env_path, prefix):
|
|
|
13
13
|
calltip = None
|
|
14
14
|
check = None
|
|
15
15
|
symbols = None
|
|
16
|
+
matching_brackets = tree_sitter.tree_sitter_matching_brackets
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/python.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ..providers import jedi, codestral, ruff
|
|
1
|
+
from ..providers import jedi, codestral, ruff, tree_sitter
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def complete(code, cursor_pos, path, multiline, full, env_path, prefix):
|
|
@@ -24,3 +24,4 @@ def complete(code, cursor_pos, path, multiline, full, env_path, prefix):
|
|
|
24
24
|
calltip = jedi.jedi_signatures
|
|
25
25
|
check = ruff.ruff_check
|
|
26
26
|
symbols = jedi.jedi_symbols
|
|
27
|
+
matching_brackets = tree_sitter.tree_sitter_matching_brackets
|
|
@@ -28,7 +28,7 @@ def main_worker_process_function(request_queue, result_queue):
|
|
|
28
28
|
Required fields:
|
|
29
29
|
action : str
|
|
30
30
|
The action to perform. One of: 'complete', 'calltip', 'symbols',
|
|
31
|
-
'check', 'set_settings', 'quit'
|
|
31
|
+
'check', 'set_settings', 'quit', 'matching_brackets'
|
|
32
32
|
|
|
33
33
|
Optional fields (depending on action):
|
|
34
34
|
language : str, default 'text'
|
|
@@ -81,6 +81,12 @@ def main_worker_process_function(request_queue, result_queue):
|
|
|
81
81
|
'action': 'check',
|
|
82
82
|
'messages': dict
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
For 'matching_brackets' action:
|
|
86
|
+
{
|
|
87
|
+
'action': 'matching_brackets',
|
|
88
|
+
'pairs': list[(int, int)]
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
Behavior
|
|
86
92
|
--------
|
|
@@ -211,5 +217,15 @@ def main_worker_process_function(request_queue, result_queue):
|
|
|
211
217
|
'action': 'check',
|
|
212
218
|
'messages': check_results
|
|
213
219
|
})
|
|
220
|
+
|
|
221
|
+
elif action == 'matching_brackets':
|
|
222
|
+
if worker_functions.matching_brackets is None:
|
|
223
|
+
pairs = []
|
|
224
|
+
else:
|
|
225
|
+
pairs = worker_functions.matching_brackets(code, language)
|
|
226
|
+
result_queue.put({
|
|
227
|
+
'action': 'matching_brackets',
|
|
228
|
+
'pairs': pairs
|
|
229
|
+
})
|
|
214
230
|
|
|
215
231
|
logger.info("Completion worker has shut down.")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from tree_sitter import Language, Parser
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def tree_sitter_matching_brackets(code, language):
|
|
6
|
+
"""Find matching brackets using tree-sitter parsing. Handles
|
|
7
|
+
incomplete/invalid code gracefully.
|
|
8
|
+
|
|
9
|
+
Returns
|
|
10
|
+
-------
|
|
11
|
+
List of tuples (open_pos, close_pos) for matching brackets
|
|
12
|
+
"""
|
|
13
|
+
module_name = language.lower().split('+')[0]
|
|
14
|
+
ts_module_name = f'tree_sitter_{module_name}'
|
|
15
|
+
try:
|
|
16
|
+
# Dynamically import the tree-sitter language module
|
|
17
|
+
ts_module = importlib.import_module(ts_module_name)
|
|
18
|
+
except (ImportError, AttributeError):
|
|
19
|
+
# Language not available, return empty list
|
|
20
|
+
return []
|
|
21
|
+
# Set up parser
|
|
22
|
+
lang = Language(ts_module.language())
|
|
23
|
+
parser = Parser(lang)
|
|
24
|
+
# Parse the code
|
|
25
|
+
tree = parser.parse(bytes(code, "utf8"))
|
|
26
|
+
|
|
27
|
+
# Find all bracket positions in the source
|
|
28
|
+
bracket_map = {'(': ')', '[': ']', '{': '}'}
|
|
29
|
+
opening = []
|
|
30
|
+
closing = []
|
|
31
|
+
|
|
32
|
+
for i, char in enumerate(code):
|
|
33
|
+
if char in bracket_map:
|
|
34
|
+
opening.append((i, char))
|
|
35
|
+
elif char in bracket_map.values():
|
|
36
|
+
closing.append((i, char))
|
|
37
|
+
|
|
38
|
+
# Match brackets using a stack-based approach
|
|
39
|
+
# But verify positions are within valid syntax nodes
|
|
40
|
+
matches = []
|
|
41
|
+
stack = []
|
|
42
|
+
|
|
43
|
+
# Merge and sort all bracket positions
|
|
44
|
+
all_brackets = [(pos, char, True) for pos, char in opening]
|
|
45
|
+
all_brackets += [(pos, char, False) for pos, char in closing]
|
|
46
|
+
all_brackets.sort()
|
|
47
|
+
|
|
48
|
+
for pos, char, is_opening in all_brackets:
|
|
49
|
+
if is_opening:
|
|
50
|
+
stack.append((pos, char))
|
|
51
|
+
else:
|
|
52
|
+
# Find matching opening bracket
|
|
53
|
+
if stack:
|
|
54
|
+
expected_close = bracket_map.get(stack[-1][1])
|
|
55
|
+
if expected_close == char:
|
|
56
|
+
open_pos = stack.pop()[0]
|
|
57
|
+
matches.append((open_pos, pos))
|
|
58
|
+
|
|
59
|
+
# Sort by opening position
|
|
60
|
+
matches.sort()
|
|
61
|
+
return matches
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
-
from pyqt_code_editor.mixins import Complete, Theme, Zoom, LineNumber, Comment, \
|
|
3
|
-
AutoIndent, AutoPair, SearchReplace, Base, Check, Shortcuts, FileLink, Symbols
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Editor(LineNumber, Zoom, Complete, AutoIndent, AutoPair, Comment,
|
|
7
|
-
SearchReplace, FileLink, Theme, Check, Shortcuts, Symbols, Base,
|
|
8
|
-
QPlainTextEdit):
|
|
9
|
-
pass
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
-
from pyqt_code_editor.mixins import Complete, \
|
|
3
|
-
HighlightSyntax, Zoom, LineNumber, Comment, \
|
|
4
|
-
SearchReplace, Base, Check, Shortcuts, FileLink, Symbols
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Editor(LineNumber, Zoom, Complete,
|
|
8
|
-
Comment, SearchReplace, FileLink,
|
|
9
|
-
HighlightSyntax, Check, Shortcuts, Symbols, Base,
|
|
10
|
-
QPlainTextEdit):
|
|
11
|
-
pass
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
-
from pyqt_code_editor.mixins import Complete, PythonAutoIndent, \
|
|
3
|
-
PythonAutoPair, Theme, Zoom, LineNumber, Comment, \
|
|
4
|
-
SearchReplace, Base, Check, Shortcuts, FileLink, Symbols, \
|
|
5
|
-
SmartBackspaceDelete, Execute
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Editor(LineNumber, Zoom, SmartBackspaceDelete, PythonAutoPair, Complete,
|
|
9
|
-
Comment, PythonAutoIndent, SearchReplace, FileLink, Execute,
|
|
10
|
-
Check, Theme, Shortcuts, Symbols, Base,
|
|
11
|
-
QPlainTextEdit):
|
|
12
|
-
|
|
13
|
-
code_editor_language = 'python'
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
-
from pyqt_code_editor.mixins import Complete, \
|
|
3
|
-
HighlightSyntax, Zoom, LineNumber, \
|
|
4
|
-
SearchReplace, Base, Check, Shortcuts, FileLink, Symbols
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Editor(LineNumber, Zoom, Complete, SearchReplace, FileLink,
|
|
8
|
-
HighlightSyntax, Check, Shortcuts, Base, QPlainTextEdit):
|
|
9
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/__init__.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/code_editors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/editor_panel.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/find_in_files.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/jupyter_console.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/project_explorer.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/settings_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/search_replace.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/widgets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/calltip_widget.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/completion_popup.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_open_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_symbol_dialog.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/search_replace_frame.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/tab_splitter.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/tabbed_editor.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/__init__.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/codestral.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/jedi.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/ruff.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/sigmund.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/symbol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|