pyqt-code-editor 0.0.2__tar.gz → 0.0.4__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 (74) hide show
  1. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/PKG-INFO +2 -1
  2. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyproject.toml +1 -0
  3. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/__init__.py +1 -1
  4. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/_settings.py +1 -0
  5. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/app.py +36 -24
  6. pyqt_code_editor-0.0.4/pyqt_code_editor/code_editors/languages/generic.py +9 -0
  7. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/editor_panel.py +5 -1
  8. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/sigmund.py +39 -9
  9. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/__init__.py +1 -0
  10. pyqt_code_editor-0.0.4/pyqt_code_editor/mixins/auto_indent.py +267 -0
  11. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/symbols.py +1 -1
  12. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/python_utils.py +4 -4
  13. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +10 -2
  14. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -1
  15. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/tabbed_editor.py +7 -2
  16. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/process.py +3 -3
  17. pyqt_code_editor-0.0.2/pyqt_code_editor/code_editors/languages/generic.py +0 -11
  18. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/COPYING +0 -0
  19. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/code_editors/__init__.py +0 -0
  20. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  21. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  22. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  23. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/code_editors/languages/python.py +0 -0
  24. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  25. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/__init__.py +0 -0
  26. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/find_in_files.py +0 -0
  27. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/jupyter_console.py +0 -0
  28. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/project_explorer.py +0 -0
  29. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/settings_panel.py +0 -0
  30. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  31. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  32. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/base.py +0 -0
  33. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/check.py +0 -0
  34. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/comment.py +0 -0
  35. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/complete.py +0 -0
  36. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/execute.py +0 -0
  37. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/file_link.py +0 -0
  38. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/line_number.py +0 -0
  39. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
  40. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/search_replace.py +0 -0
  41. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  42. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  43. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/theme.py +0 -0
  44. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  45. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/mixins/zoom.py +0 -0
  46. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/signal_router.py +0 -0
  47. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  48. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  49. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  50. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  51. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/themes.py +0 -0
  52. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/utils.py +0 -0
  53. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/watchdog.py +0 -0
  54. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/__init__.py +0 -0
  55. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  56. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  57. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/dock.py +0 -0
  58. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  59. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  60. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  61. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  62. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/__init__.py +0 -0
  63. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  64. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/languages/generic.py +0 -0
  65. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/languages/python.py +0 -0
  66. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/manager.py +0 -0
  67. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  68. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  69. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/providers/jedi.py +0 -0
  70. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/providers/ruff.py +0 -0
  71. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  72. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  73. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/pyqt_code_editor/worker/settings.py +0 -0
  74. {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.4}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqt_code_editor
3
- Version: 0.0.2
3
+ Version: 0.0.4
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>
@@ -8,6 +8,7 @@ Requires-Python: >=3.10
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: COPYING
10
10
  Requires-Dist: chardet
11
+ Requires-Dist: detect-indent
11
12
  Requires-Dist: jedi
12
13
  Requires-Dist: mistralai
13
14
  Requires-Dist: pathspec
@@ -13,6 +13,7 @@ requires-python = ">=3.10"
13
13
  dynamic = ["version", "description"]
14
14
  dependencies = [
15
15
  "chardet",
16
+ "detect-indent",
16
17
  "jedi",
17
18
  "mistralai",
18
19
  "pathspec",
@@ -2,4 +2,4 @@
2
2
 
3
3
  from ._settings import settings
4
4
 
5
- __version__ = '0.0.2'
5
+ __version__ = '0.0.4'
@@ -82,6 +82,7 @@ class Settings(QObject):
82
82
  font_family = SettingProperty('Ubuntu Mono', "Appearance")
83
83
  color_scheme = SettingProperty('monokai', "Appearance")
84
84
  tab_width = SettingProperty(4, "Appearance")
85
+ default_indent = SettingProperty('\t', "Appearance")
85
86
  word_wrap = SettingProperty(False, "Appearance")
86
87
  character_ruler = SettingProperty(80, "Appearance") # None to disable
87
88
  search_replace_background = SettingProperty("#fdff74", "Appearance")
@@ -3,7 +3,7 @@ import sys
3
3
  import logging
4
4
  from qtpy.QtWidgets import QMainWindow, QShortcut, QMessageBox, \
5
5
  QDockWidget, QToolBar, QAction, QApplication
6
- from qtpy.QtCore import Qt, QByteArray, QSize
6
+ from qtpy.QtCore import Qt, QSize
7
7
  from qtpy.QtGui import QKeySequence
8
8
  import qtawesome as qta
9
9
  from .widgets import QuickOpenFileDialog
@@ -32,6 +32,7 @@ class MainWindow(QMainWindow):
32
32
  self.setWindowTitle("Sigmund Analyst")
33
33
  # The editor panel provides splittable editor tabs
34
34
  self._editor_panel = EditorPanel()
35
+ self._editor_panel.open_folder_requested.connect(self._open_folder)
35
36
  self.setCentralWidget(self._editor_panel)
36
37
  self._project_explorers = []
37
38
  # Track if project explorers are hidden as a group
@@ -110,28 +111,35 @@ class MainWindow(QMainWindow):
110
111
 
111
112
  # Connect to dynamically created signals
112
113
  signal_router.connect_to_signal("execute_code", self._execute_code)
113
- signal_router.connect_to_signal("execute_file", self._execute_file)
114
+ signal_router.connect_to_signal("execute_file", self._execute_file)
114
115
 
115
116
  self._setup_toolbar()
116
117
 
117
- # If we have no saved geometry/state, hide the docks by default
118
- if not settings.window_geometry or not settings.window_state:
119
- self._workspace_explorer.hide()
120
- self._jupyter_console.hide()
121
- self._settings_panel.hide()
122
- else:
123
- # Restore geometry and state
124
- try:
125
- geom = QByteArray.fromBase64(
126
- settings.window_geometry.encode('utf-8')
127
- )
128
- state = QByteArray.fromBase64(
129
- settings.window_state.encode('utf-8')
130
- )
131
- self.restoreGeometry(geom)
132
- self.restoreState(state)
133
- except Exception as e:
134
- logger.warning(f"Could not restore window geometry/state: {e}")
118
+ # Restoring the geometry often crashes with a segfault, so for now we
119
+ # just use this default
120
+ # just use this default
121
+ self._workspace_explorer.hide()
122
+ self._jupyter_console.hide()
123
+ self._sigmund.hide()
124
+ self._settings_panel.hide()
125
+ # # If we have no saved geometry/state, hide the docks by default
126
+ # if not settings.window_geometry or not settings.window_state:
127
+ # self._workspace_explorer.hide()
128
+ # self._jupyter_console.hide()
129
+ # self._settings_panel.hide()
130
+ # else:
131
+ # # Restore geometry and state
132
+ # try:
133
+ # geom = QByteArray.fromBase64(
134
+ # settings.window_geometry.encode('utf-8')
135
+ # )
136
+ # state = QByteArray.fromBase64(
137
+ # settings.window_state.encode('utf-8')
138
+ # )
139
+ # self.restoreGeometry(geom)
140
+ # self.restoreState(state)
141
+ # except Exception as e:
142
+ # logger.warning(f"Could not restore window geometry/state: {e}")
135
143
 
136
144
  def _setup_toolbar(self):
137
145
  """Setup the main toolbar with Material Design icons."""
@@ -374,10 +382,14 @@ class MainWindow(QMainWindow):
374
382
  def _close_project_explorer(self, project_explorer):
375
383
  self._project_explorers.remove(project_explorer)
376
384
 
377
- def _open_folder(self):
378
- project_explorer = ProjectExplorer.open_folder(
379
- self._editor_panel, parent=self
380
- )
385
+ def _open_folder(self, path=None):
386
+ # The event handler may pass a bool argument
387
+ if path is None or isinstance(path, bool):
388
+ project_explorer = ProjectExplorer.open_folder(
389
+ self._editor_panel, parent=self)
390
+ else:
391
+ project_explorer = ProjectExplorer(self._editor_panel, path,
392
+ parent=self)
381
393
  if project_explorer is None:
382
394
  return
383
395
  project_explorer.closed.connect(self._close_project_explorer)
@@ -0,0 +1,9 @@
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,7 +1,7 @@
1
1
  import os
2
2
  from qtpy.QtWidgets import QApplication, QShortcut, QWidget, QHBoxLayout, QFileDialog
3
3
  from qtpy.QtGui import QKeySequence
4
- from qtpy.QtCore import Qt
4
+ from qtpy.QtCore import Qt, Signal
5
5
  from ..widgets import TabbedEditor, TabSplitter
6
6
  from .. import settings, utils
7
7
  import logging
@@ -10,6 +10,9 @@ logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
12
  class EditorPanel(QWidget):
13
+
14
+ open_folder_requested = Signal(str)
15
+
13
16
  def __init__(self, parent=None, initial_path=None):
14
17
  super().__init__(parent)
15
18
  logger.info("EditorPanel initialized")
@@ -55,6 +58,7 @@ class EditorPanel(QWidget):
55
58
  # Connect the lastTabClosed signal so that we can remove empty panels
56
59
  t.lastTabClosed.connect(self.handle_tabbed_editor_empty)
57
60
  t.tabCloseRequested.connect(self._relabel_tabs)
61
+ t.open_folder_requested.connect(self.open_folder_requested)
58
62
  return t
59
63
 
60
64
  def unsaved_changes(self):
@@ -1,13 +1,40 @@
1
1
  from sigmund_qtwidget.sigmund_widget import SigmundWidget
2
2
  from ..widgets import Dock
3
3
  from .. import watchdog
4
+ import textwrap
4
5
  import logging
5
- logging.basicConfig(level=logging.debug)
6
+ logging.basicConfig(level=logging.INFO, force=True)
7
+ logger = logging.getLogger(__name__)
6
8
 
7
9
 
8
10
  class EditorWorkspace:
9
11
  def __init__(self, editor_panel):
10
12
  self._editor_panel = editor_panel
13
+ self._indentation = ''
14
+
15
+ def _get_indentation(self, content):
16
+ dedented = textwrap.dedent(content)
17
+ lines = content.splitlines()
18
+ dedented_lines = dedented.splitlines()
19
+
20
+ for original_line, dedented_line in zip(lines, dedented_lines):
21
+ # Skip empty lines
22
+ if original_line.strip():
23
+ # Count leading whitespace
24
+ orig_leading = len(original_line) - len(original_line.lstrip(' \t'))
25
+ ded_leading = len(dedented_line) - len(dedented_line.lstrip(' \t'))
26
+ # The difference should be the indentation sequence
27
+ indentation_length = orig_leading - ded_leading
28
+ if indentation_length > 0:
29
+ return original_line[:indentation_length]
30
+ return ""
31
+ # If no non-empty lines, return no indentation
32
+ return ""
33
+
34
+ def prepare(self, content):
35
+ if content is None:
36
+ return content
37
+ return textwrap.indent(content, self._indentation)
11
38
 
12
39
  @property
13
40
  def _editor(self):
@@ -33,8 +60,12 @@ class EditorWorkspace:
33
60
  def get(self):
34
61
  text_cursor = self._editor.textCursor()
35
62
  if text_cursor.hasSelection():
36
- return self._normalize_line_breaks(text_cursor.selectedText()), self._editor.code_editor_language
37
- return self._editor.toPlainText(), self._editor.code_editor_language
63
+ content = self._normalize_line_breaks(text_cursor.selectedText())
64
+ else:
65
+ content = self._editor.toPlainText()
66
+ self._indentation = self._get_indentation(content)
67
+ logger.info(f'content was indented by "{self._indentation}"')
68
+ return content, self._editor.code_editor_language
38
69
 
39
70
  def set(self, content, language):
40
71
  text_cursor = self._editor.textCursor()
@@ -51,8 +82,6 @@ class EditorWorkspace:
51
82
  else:
52
83
  editor_content = self._editor.toPlainText()
53
84
 
54
- if not content:
55
- return False
56
85
  if content in (editor_content, self.strip_content(editor_content)):
57
86
  return False
58
87
  return True
@@ -70,13 +99,14 @@ class Sigmund(Dock):
70
99
  workspace = EditorWorkspace(editor_panel)
71
100
  self.sigmund_widget = SigmundWidget(self)
72
101
  self.sigmund_widget.set_workspace_manager(workspace)
73
- self.sigmund_widget.start_server()
74
- watchdog.register_subprocess(self.sigmund_widget.server_pid)
75
102
  self.setWidget(self.sigmund_widget)
76
- self.visibilityChanged.connect(self._on_visibility_changed)
77
103
 
78
- def _on_visibility_changed(self, visible):
104
+ def setVisible(self, visible):
79
105
  if visible:
106
+ logger.info('starting sigmund connector')
80
107
  self.sigmund_widget.start_server()
108
+ watchdog.register_subprocess(self.sigmund_widget.server_pid)
81
109
  else:
110
+ logger.info('stopping sigmund connector')
82
111
  self.sigmund_widget.stop_server()
112
+ super().setVisible(visible)
@@ -1,3 +1,4 @@
1
+ from .auto_indent import AutoIndent
1
2
  from .auto_pair import AutoPair, PythonAutoPair
2
3
  from .base import Base
3
4
  from .check import Check
@@ -0,0 +1,267 @@
1
+ from qtpy.QtCore import Qt
2
+ from .. import settings
3
+ import logging
4
+ logging.basicConfig(level=logging.INFO, force=True)
5
+
6
+
7
+ class AutoIndent:
8
+ """
9
+ A generic mixin class to provide auto-indentation features for a QPlainTextEdit.
10
+ Implements the following specs:
11
+
12
+ 1. Indentation Style Detection
13
+ - detect_indentation_style() returns a string representing a single indentation
14
+ (e.g. '\t' or ' ').
15
+ - Currently, we return just '\t' by default (dummy implementation).
16
+
17
+ 2. Multiline Indent/Dedent
18
+ - Tab in multi-line selection indents those lines by one chunk.
19
+ - Shift+Tab (or Backtab) dedents those lines.
20
+
21
+ 3. Preserving Indentation on New Lines
22
+ - Pressing Enter creates a new line with the same leading indentation
23
+ (no language-specific logic).
24
+
25
+ 4. Single-Line Insert or Remove Indentation
26
+ - Within a single line, Tab inserts one indentation chunk at the cursor location.
27
+ - Shift+Tab removes up to one indentation chunk from the start of the line if possible.
28
+
29
+ 5. Handling Backspace and Delete in Leading Whitespace
30
+ - Backspace/Delete in leading whitespace removes one indentation chunk worth of characters
31
+ if they match the indentation style (e.g., one tab or up to len(indent_chunk) spaces).
32
+
33
+ 6. Completely Generic (No Language-Specific Rules)
34
+ - No indentation changes based on braces, parentheses, or keywords.
35
+ """
36
+
37
+ def detect_indentation_style(self):
38
+ """
39
+ Dummy function that always returns a single-tab string for now.
40
+ Later, this can analyze the document to decide if it should
41
+ use tabs or spaces (e.g. ' ' for four spaces).
42
+ """
43
+ from detect_indent import detect_indent
44
+
45
+ indent = detect_indent(self.toPlainText())
46
+ if indent['type'] is None:
47
+ return settings.default_indent
48
+ return indent['indent']
49
+
50
+ def keyPressEvent(self, event):
51
+ key = event.key()
52
+ modifiers = event.modifiers()
53
+ logging.info("keyPressEvent: key=%s, modifiers=%s", key, modifiers)
54
+
55
+ # Check for Tab/Shift+Tab indentation
56
+ if key == Qt.Key_Tab and modifiers == Qt.NoModifier:
57
+ logging.info("Tab pressed -> indent code")
58
+ self.indent_code()
59
+ return
60
+ elif (key == Qt.Key_Tab and modifiers == Qt.ShiftModifier) or key == Qt.Key_Backtab:
61
+ logging.info("Shift+Tab pressed -> dedent code")
62
+ self.dedent_code()
63
+ return
64
+
65
+ # Handle backspace in leading indentation
66
+ if key == Qt.Key_Backspace and modifiers == Qt.NoModifier:
67
+ if self._handle_backspace():
68
+ return
69
+
70
+ # Handle delete in leading indentation
71
+ if key == Qt.Key_Delete and modifiers == Qt.NoModifier:
72
+ if self._handle_delete():
73
+ return
74
+
75
+ # Check for Enter/Return (preserve indentation)
76
+ if key in (Qt.Key_Enter, Qt.Key_Return):
77
+ logging.info("Enter/Return pressed -> preserve indentation on new line")
78
+ cursor = self.textCursor()
79
+
80
+ # Get leading whitespace of the current line
81
+ block_text = cursor.block().text()
82
+ leading_ws = len(block_text) - len(block_text.lstrip(' \t'))
83
+ preserve_ws = block_text[:leading_ws]
84
+
85
+ # Perform normal newline
86
+ super().keyPressEvent(event)
87
+
88
+ # Insert the same leading whitespace on the new line
89
+ cursor.insertText(preserve_ws)
90
+ self.setTextCursor(cursor)
91
+ return
92
+
93
+ # Default behavior for any other keys
94
+ super().keyPressEvent(event)
95
+
96
+ def _get_indent_string(self):
97
+ """
98
+ Returns the indentation chunk based on detect_indentation_style().
99
+ By default, this is a single tab ('\t') but could be ' ' for spaces, etc.
100
+ """
101
+ return self.detect_indentation_style()
102
+
103
+ def _is_multiline_selection(self):
104
+ """
105
+ Returns True if selection spans more than one line, else False.
106
+ """
107
+ cursor = self.textCursor()
108
+ start = cursor.selectionStart()
109
+ end = cursor.selectionEnd()
110
+ start_block = self.document().findBlock(start).blockNumber()
111
+ end_block = self.document().findBlock(end).blockNumber()
112
+ return end_block > start_block
113
+
114
+ def indent_code(self):
115
+ """
116
+ Indent either the selected lines (if multi-line selection)
117
+ or just the current cursor position (if single-line).
118
+ """
119
+ logging.info("indent_code triggered")
120
+ cursor = self.textCursor()
121
+ cursor.beginEditBlock()
122
+
123
+ if self._is_multiline_selection():
124
+ logging.info("Multi-line selection -> indent each line")
125
+ self._indent_selection()
126
+ else:
127
+ logging.info("Single line -> insert indent at cursor")
128
+ indent_str = self._get_indent_string()
129
+ cursor.insertText(indent_str)
130
+
131
+ cursor.endEditBlock()
132
+
133
+ def dedent_code(self):
134
+ """
135
+ Dedent either the selected lines (if multi-line selection)
136
+ or just the current line (if single-line).
137
+ """
138
+ logging.info("dedent_code triggered")
139
+ cursor = self.textCursor()
140
+ cursor.beginEditBlock()
141
+
142
+ if self._is_multiline_selection():
143
+ logging.info("Multi-line selection -> dedent each line")
144
+ self._dedent_selection()
145
+ else:
146
+ logging.info("Single line -> remove up to one indent chunk from start")
147
+ line_start = cursor.block().position()
148
+ line_text = cursor.block().text()
149
+ leading_ws = len(line_text) - len(line_text.lstrip(' \t'))
150
+
151
+ chunk_size = self._compute_chunk_size()
152
+ remove_chars = min(chunk_size, leading_ws)
153
+ logging.info("Removing up to %d chars from leading whitespace", remove_chars)
154
+
155
+ cursor.setPosition(line_start)
156
+ for _ in range(remove_chars):
157
+ self._delete_forward_if_tab_or_space(cursor)
158
+
159
+ cursor.endEditBlock()
160
+
161
+ def _indent_selection(self):
162
+ """
163
+ Increase indent for each selected line by one chunk.
164
+ """
165
+ logging.info("indent_selection (multi-line)")
166
+ indent_str = self._get_indent_string()
167
+
168
+ cursor = self.textCursor()
169
+ start = cursor.selectionStart()
170
+ end = cursor.selectionEnd()
171
+
172
+ start_block = self.document().findBlock(start).blockNumber()
173
+ end_block = self.document().findBlock(end).blockNumber()
174
+
175
+ for block_num in range(start_block, end_block + 1):
176
+ block = self.document().findBlockByNumber(block_num)
177
+ tmp_cursor = self.textCursor()
178
+ tmp_cursor.setPosition(block.position())
179
+ tmp_cursor.insertText(indent_str)
180
+
181
+ def _dedent_selection(self):
182
+ """
183
+ Decrease indent for each selected line by one chunk,
184
+ ensuring we don't go below zero indentation.
185
+ """
186
+ logging.info("dedent_selection (multi-line)")
187
+ cursor = self.textCursor()
188
+ start = cursor.selectionStart()
189
+ end = cursor.selectionEnd()
190
+
191
+ start_block = self.document().findBlock(start).blockNumber()
192
+ end_block = self.document().findBlock(end).blockNumber()
193
+
194
+ for block_num in range(start_block, end_block + 1):
195
+ block = self.document().findBlockByNumber(block_num)
196
+ line_text = block.text()
197
+ leading_ws = len(line_text) - len(line_text.lstrip(' \t'))
198
+
199
+ chunk_size = self._compute_chunk_size()
200
+ remove_chars = min(chunk_size, leading_ws)
201
+
202
+ tmp_cursor = self.textCursor()
203
+ tmp_cursor.setPosition(block.position())
204
+ for _ in range(remove_chars):
205
+ self._delete_forward_if_tab_or_space(tmp_cursor)
206
+
207
+ def _handle_backspace(self):
208
+ """
209
+ If the cursor is in leading indentation, remove one indentation chunk
210
+ (based on the length of detect_indentation_style()).
211
+ Returns True if handled, False otherwise.
212
+ """
213
+ cursor = self.textCursor()
214
+ if not cursor.hasSelection():
215
+ block_text = cursor.block().text()
216
+ pos_in_block = cursor.positionInBlock()
217
+ leading_ws = len(block_text) - len(block_text.lstrip(' \t'))
218
+
219
+ # Only if we're in the leading whitespace region:
220
+ if 0 < pos_in_block <= leading_ws:
221
+ chunk_size = self._compute_chunk_size()
222
+ remove_count = min(chunk_size, pos_in_block)
223
+ logging.info("Backspace in leading whitespace -> removing %d chars", remove_count)
224
+
225
+ for _ in range(remove_count):
226
+ cursor.deletePreviousChar()
227
+ return True
228
+ return False
229
+
230
+ def _handle_delete(self):
231
+ """
232
+ If the cursor is in leading indentation, remove one indentation chunk going forward.
233
+ Returns True if handled, False otherwise.
234
+ """
235
+ cursor = self.textCursor()
236
+ if not cursor.hasSelection():
237
+ block_text = cursor.block().text()
238
+ pos_in_block = cursor.positionInBlock()
239
+ leading_ws = len(block_text) - len(block_text.lstrip(' \t'))
240
+
241
+ if pos_in_block < leading_ws:
242
+ chunk_size = self._compute_chunk_size()
243
+ remove_count = min(chunk_size, leading_ws - pos_in_block)
244
+ logging.info("Delete in leading whitespace -> removing %d chars", remove_count)
245
+
246
+ for _ in range(remove_count):
247
+ cursor.deleteChar()
248
+ return True
249
+ return False
250
+
251
+ def _compute_chunk_size(self):
252
+ """
253
+ Returns the length of the indentation chunk returned by detect_indentation_style().
254
+ For example, if detect_indentation_style() returns '\t', the size is 1;
255
+ if it returns ' ', the size is 4, etc.
256
+ """
257
+ chunk = self.detect_indentation_style()
258
+ return len(chunk)
259
+
260
+ def _delete_forward_if_tab_or_space(self, cursor):
261
+ """
262
+ Deletes one character if it is a space or a tab in leading whitespace.
263
+ (We remove characters one by one for partial or exact chunk removal.)
264
+ """
265
+ ch = cursor.document().characterAt(cursor.position())
266
+ if ch in (' ', '\t'):
267
+ cursor.deleteChar()
@@ -31,7 +31,7 @@ class Symbols:
31
31
  def handle_worker_result(self, action, result):
32
32
  """Check for completion or calltip results from the external worker."""
33
33
  super().handle_worker_result(action, result)
34
- if action != 'symbols':
34
+ if action != 'symbols' or not result.get('symbols', False):
35
35
  return
36
36
  QuickSymbolDialog(self, result['symbols'], self.jump_to_symbol).exec()
37
37
 
@@ -821,12 +821,12 @@ def extract_cells_from_code(code: str) -> list:
821
821
  # Store all detected cells
822
822
  cells = []
823
823
 
824
- # Track the current position
825
- current_pos = 0
826
-
827
824
  # Define regex patterns for cell markers
828
825
  marker_pattern = re.compile(r'^(# %%|#%%|# In\[\]:?).*?$', re.MULTILINE)
829
- triple_quotes_pattern = re.compile(r'(""".*?"""|\'\'\'.*?\'\'\')', re.DOTALL)
826
+ triple_quotes_pattern = re.compile(
827
+ r'(?m)^(""".*?"""|\'\'\'.*?\'\'\')',
828
+ re.DOTALL
829
+ )
830
830
 
831
831
  # Find all marker-based cells
832
832
  marker_matches = list(marker_pattern.finditer(code))
@@ -5,11 +5,19 @@ logging.basicConfig(level=logging.INFO, force=True)
5
5
  logger = logging.getLogger(__name__)
6
6
  module_cache = {}
7
7
 
8
+ # Some lexers have weird names that are not recognized by get_lexer_by_name().
9
+ # Often this seems to be a matter of discarding a suffix after a space or /
10
+ # This may not be a foolproof solution.
11
+ LANGUAGE_SEPARATORS = ' ', '/'
8
12
 
9
13
  def create_syntax_highlighter(language, *args, **kwargs):
10
- try:
14
+ for ch in LANGUAGE_SEPARATORS:
15
+ if ch in language:
16
+ logger.info(f'mapping {language} to {language[:language.find(ch)]}')
17
+ language = language[:language.find(ch)]
18
+ try:
11
19
  lexer = get_lexer_by_name(language)
12
- except:
20
+ except Exception:
13
21
  lexer = get_lexer_by_name('markdown')
14
22
  if language not in module_cache:
15
23
  try:
@@ -1,4 +1,3 @@
1
- import os
2
1
  import logging
3
2
  from . import QuickOpenDialog
4
3
  logger = logging.getLogger(__name__)
@@ -110,6 +110,7 @@ class DraggableTabBar(QTabBar):
110
110
  class TabbedEditor(QTabWidget):
111
111
  """A tab widget that can hold multiple CodeEditor instances."""
112
112
  lastTabClosed = Signal(QTabWidget)
113
+ open_folder_requested = Signal(str)
113
114
 
114
115
  def __init__(self, parent=None):
115
116
  super().__init__(parent)
@@ -312,7 +313,12 @@ class TabbedEditor(QTabWidget):
312
313
  if event.mimeData().hasUrls():
313
314
  for url in event.mimeData().urls():
314
315
  path = url.toLocalFile()
315
- self.add_code_editor(path)
316
+ # If path is a folder, open a code editor, otherwise emit the
317
+ # open_folder signal
318
+ if os.path.isdir(path):
319
+ self.open_folder_requested.emit(path)
320
+ else:
321
+ self.add_code_editor(path)
316
322
 
317
323
  if not event.mimeData().hasFormat("application/x-tabdata"):
318
324
  event.ignore()
@@ -325,7 +331,6 @@ class TabbedEditor(QTabWidget):
325
331
  dataByteArray = event.mimeData().data("application/x-tabdata")
326
332
  stream = QDataStream(dataByteArray, QDataStream.ReadOnly)
327
333
  tabText = stream.readQString()
328
- hasIcon = stream.readBool()
329
334
  # (We ignore the icon detail in this example or simply set a blank icon.)
330
335
  # If you’re handing around pixmaps, you could store them more explicitly.
331
336
 
@@ -49,7 +49,7 @@ def main_worker_process_function(request_queue, result_queue):
49
49
  try:
50
50
  worker_functions = importlib.import_module(
51
51
  f".languages.{language}", package=__package__)
52
- except ImportError as e:
52
+ except ImportError:
53
53
  from .languages import generic as worker_functions
54
54
  logger.info(f'failed to load worker functions for {language}, falling back to generic')
55
55
  else:
@@ -84,11 +84,11 @@ def main_worker_process_function(request_queue, result_queue):
84
84
  })
85
85
 
86
86
  elif action == 'calltip':
87
+ cursor_pos = request.get('cursor_pos', 0)
87
88
  if worker_functions.calltip is None:
88
89
  signatures = None
89
90
  else:
90
91
  code = request.get('code', '')
91
- cursor_pos = request.get('cursor_pos', 0)
92
92
  path = request.get('path', None)
93
93
  logger.info(f"Performing calltip: language='{language}', path={path}")
94
94
  signatures = worker_functions.calltip(
@@ -105,7 +105,7 @@ def main_worker_process_function(request_queue, result_queue):
105
105
 
106
106
  elif action == 'symbols':
107
107
  if worker_functions.symbols is None:
108
- check_results = []
108
+ symbols_results = []
109
109
  else:
110
110
  code = request.get('code', '')
111
111
  symbols_results = worker_functions.symbols(code)
@@ -1,11 +0,0 @@
1
- from qtpy.QtWidgets import QPlainTextEdit
2
- from pyqt_code_editor.mixins import Complete, \
3
- Theme, Zoom, LineNumber, Comment, \
4
- AutoPair, SearchReplace, Base, Check, Shortcuts, FileLink, Symbols
5
-
6
-
7
- class Editor(LineNumber, Zoom, Complete, AutoPair,
8
- Comment, SearchReplace, FileLink,
9
- Theme, Check, Shortcuts, Symbols, Base,
10
- QPlainTextEdit):
11
- pass