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.
Files changed (87) hide show
  1. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/PKG-INFO +10 -3
  2. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyproject.toml +10 -3
  3. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/__init__.py +1 -1
  4. pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/generic.py +13 -0
  5. pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/markdown.py +11 -0
  6. pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/python.py +16 -0
  7. pyqt_code_editor-0.0.51/pyqt_code_editor/code_editors/languages/text.py +11 -0
  8. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/workspace_explorer.py +8 -12
  9. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/__init__.py +2 -0
  10. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/auto_indent.py +0 -6
  11. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/auto_pair.py +87 -64
  12. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/check.py +0 -4
  13. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/comment.py +0 -6
  14. pyqt_code_editor-0.0.51/pyqt_code_editor/mixins/highlight_matching_brackets.py +98 -0
  15. pyqt_code_editor-0.0.51/pyqt_code_editor/mixins/merge_undo_actions.py +32 -0
  16. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/python_auto_indent.py +0 -9
  17. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/shortcuts.py +1 -7
  18. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/generic.py +2 -1
  19. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/python.py +2 -1
  20. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/process.py +17 -1
  21. pyqt_code_editor-0.0.51/pyqt_code_editor/worker/providers/tree_sitter.py +61 -0
  22. pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/generic.py +0 -9
  23. pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/markdown.py +0 -11
  24. pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/python.py +0 -13
  25. pyqt_code_editor-0.0.48/pyqt_code_editor/code_editors/languages/text.py +0 -9
  26. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/COPYING +0 -0
  27. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/_settings.py +0 -0
  28. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/app.py +0 -0
  29. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/__init__.py +0 -0
  30. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  31. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  32. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/code_editors/languages/javascript.py +0 -0
  33. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/__init__.py +0 -0
  34. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/editor_panel.py +0 -0
  35. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/find_in_files.py +0 -0
  36. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/jupyter_console.py +0 -0
  37. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/project_explorer.py +0 -0
  38. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/settings_panel.py +0 -0
  39. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/components/sigmund.py +0 -0
  40. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/environment_manager.py +0 -0
  41. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/base.py +0 -0
  42. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/complete.py +0 -0
  43. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/execute.py +0 -0
  44. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/file_link.py +0 -0
  45. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/line_number.py +0 -0
  46. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/search_replace.py +0 -0
  47. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  48. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/symbols.py +0 -0
  49. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/theme.py +0 -0
  50. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  51. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/mixins/zoom.py +0 -0
  52. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/sigmund-analyst.png +0 -0
  53. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/signal_router.py +0 -0
  54. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  55. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  56. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  57. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  58. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  59. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/themes.py +0 -0
  60. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/__init__.py +0 -0
  61. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/__init__.py +0 -0
  62. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
  63. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
  64. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
  65. {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
  66. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/watchdog.py +0 -0
  67. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/__init__.py +0 -0
  68. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  69. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  70. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/dock.py +0 -0
  71. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  72. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  73. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  74. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  75. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  76. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  77. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/__init__.py +0 -0
  78. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  79. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/manager.py +0 -0
  80. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  81. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  82. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/jedi.py +0 -0
  83. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/ruff.py +0 -0
  84. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  85. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  86. {pyqt_code_editor-0.0.48 → pyqt_code_editor-0.0.51}/pyqt_code_editor/worker/settings.py +0 -0
  87. {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.48
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
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  from ._settings import settings
4
4
 
5
- __version__ = '0.0.48'
5
+ __version__ = '0.0.51'
@@ -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 (QDockWidget, QWidget, QVBoxLayout, QHBoxLayout,
2
- QTableView, QHeaderView, QLineEdit, QLabel,
3
- QAbstractItemView, QMenu, QAction)
4
- from qtpy.QtCore import Qt, QSortFilterProxyModel, QAbstractTableModel, QModelIndex
5
- from qtpy.QtGui import QIcon, QColor, QFont, QBrush
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
 
@@ -14,3 +14,5 @@ from .symbols import Symbols
14
14
  from .shortcuts import Shortcuts
15
15
  from .theme import Theme
16
16
  from .zoom import Zoom
17
+ from .merge_undo_actions import MergeUndoActions
18
+ from .highlight_matching_brackets import HighlightMatchingBrackets
@@ -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
- Generic mixin for auto-pairing brackets/quotes/etc.
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 event.key() == Qt.Key_Backspace:
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
- doc_text = self.toPlainText()
60
- if (new_cursor.position() < len(doc_text) and
61
- doc_text[new_cursor.position()] == typed_char):
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
- Returns True if handled, False if not.
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 pos < 1 or cursor.atEnd():
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(old_pos)
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
- # Insert the close_seq and inbetween_seq, indented to the same level as the block
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, old_pos
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()
@@ -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)
@@ -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
@@ -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