pyqt-code-editor 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. pyqt_code_editor/__init__.py +5 -0
  2. pyqt_code_editor/_settings.py +232 -0
  3. pyqt_code_editor/app.py +438 -0
  4. pyqt_code_editor/code_editors/__init__.py +1 -0
  5. pyqt_code_editor/code_editors/code_editors.py +33 -0
  6. pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  7. pyqt_code_editor/code_editors/languages/generic.py +11 -0
  8. pyqt_code_editor/code_editors/languages/markdown.py +11 -0
  9. pyqt_code_editor/code_editors/languages/python.py +13 -0
  10. pyqt_code_editor/code_editors/languages/text.py +9 -0
  11. pyqt_code_editor/components/__init__.py +0 -0
  12. pyqt_code_editor/components/editor_panel.py +399 -0
  13. pyqt_code_editor/components/find_in_files.py +199 -0
  14. pyqt_code_editor/components/jupyter_console.py +487 -0
  15. pyqt_code_editor/components/project_explorer.py +515 -0
  16. pyqt_code_editor/components/settings_panel.py +162 -0
  17. pyqt_code_editor/components/sigmund.py +75 -0
  18. pyqt_code_editor/components/workspace_explorer.py +162 -0
  19. pyqt_code_editor/mixins/__init__.py +15 -0
  20. pyqt_code_editor/mixins/auto_pair.py +201 -0
  21. pyqt_code_editor/mixins/base.py +142 -0
  22. pyqt_code_editor/mixins/check.py +94 -0
  23. pyqt_code_editor/mixins/comment.py +91 -0
  24. pyqt_code_editor/mixins/complete.py +482 -0
  25. pyqt_code_editor/mixins/execute.py +182 -0
  26. pyqt_code_editor/mixins/file_link.py +166 -0
  27. pyqt_code_editor/mixins/line_number.py +206 -0
  28. pyqt_code_editor/mixins/python_auto_indent.py +239 -0
  29. pyqt_code_editor/mixins/search_replace.py +396 -0
  30. pyqt_code_editor/mixins/shortcuts.py +228 -0
  31. pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
  32. pyqt_code_editor/mixins/symbols.py +45 -0
  33. pyqt_code_editor/mixins/theme.py +99 -0
  34. pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  35. pyqt_code_editor/mixins/zoom.py +51 -0
  36. pyqt_code_editor/python_utils.py +936 -0
  37. pyqt_code_editor/signal_router.py +116 -0
  38. pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  39. pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  40. pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
  41. pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
  42. pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
  43. pyqt_code_editor/themes.py +36 -0
  44. pyqt_code_editor/utils.py +96 -0
  45. pyqt_code_editor/watchdog.py +141 -0
  46. pyqt_code_editor/widgets/__init__.py +6 -0
  47. pyqt_code_editor/widgets/calltip_widget.py +44 -0
  48. pyqt_code_editor/widgets/completion_popup.py +89 -0
  49. pyqt_code_editor/widgets/dock.py +15 -0
  50. pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
  51. pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
  52. pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
  53. pyqt_code_editor/widgets/search_replace_frame.py +128 -0
  54. pyqt_code_editor/widgets/tab_splitter.py +22 -0
  55. pyqt_code_editor/widgets/tabbed_editor.py +351 -0
  56. pyqt_code_editor/worker/__init__.py +0 -0
  57. pyqt_code_editor/worker/languages/__init__.py +0 -0
  58. pyqt_code_editor/worker/languages/generic.py +15 -0
  59. pyqt_code_editor/worker/languages/python.py +17 -0
  60. pyqt_code_editor/worker/manager.py +113 -0
  61. pyqt_code_editor/worker/process.py +128 -0
  62. pyqt_code_editor/worker/providers/__init__.py +1 -0
  63. pyqt_code_editor/worker/providers/codestral.py +56 -0
  64. pyqt_code_editor/worker/providers/jedi.py +115 -0
  65. pyqt_code_editor/worker/providers/ruff.py +45 -0
  66. pyqt_code_editor/worker/providers/sigmund.py +29 -0
  67. pyqt_code_editor/worker/providers/symbol.py +78 -0
  68. pyqt_code_editor/worker/settings.py +1 -0
  69. pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
  70. pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
  71. pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
  72. pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
  73. pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
@@ -0,0 +1,5 @@
1
+ """Fully featured code-editor widgets for PyQt"""
2
+
3
+ from ._settings import settings
4
+
5
+ __version__ = '0.0.1'
@@ -0,0 +1,232 @@
1
+ import os
2
+ import json
3
+ from qtpy.QtCore import QObject, Signal, QSettings
4
+ import logging
5
+ logging.basicConfig(level=logging.INFO, force=True)
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class SettingProperty:
10
+ """
11
+ Descriptor for setting properties that emit signals when changed.
12
+
13
+ This class uses Python's descriptor protocol to allow settings to be accessed
14
+ like normal attributes (settings.font_size) while providing custom behavior
15
+ when they are accessed or modified.
16
+ """
17
+ def __init__(self, default_value, category: str = "General"):
18
+ self.name = None # Will be set when the class is created
19
+ self.default_value = default_value
20
+ self.category = category
21
+
22
+ def __set_name__(self, owner, name):
23
+ """Called when the descriptor is assigned to a class attribute"""
24
+ self.name = name
25
+
26
+ def __get__(self, instance, owner):
27
+ """
28
+ Called when the attribute is accessed (settings.font_size)
29
+
30
+ This is why settings.font_size returns an int instead of a SettingProperty.
31
+ When you access settings.font_size, Python calls this method instead of
32
+ returning the descriptor object itself.
33
+
34
+ Args:
35
+ instance: The Settings instance (or None if accessed from the class)
36
+ owner: The Settings class
37
+
38
+ Returns:
39
+ The actual setting value (not the descriptor)
40
+ """
41
+ if instance is None:
42
+ return self # When accessed from the class (Settings.font_size), return the descriptor
43
+ return instance._values.get(self.name, self.default_value)
44
+
45
+ def __set__(self, instance, value):
46
+ """Called when the attribute is set (settings.font_size = 18)"""
47
+ old_value = self.__get__(instance, type(instance))
48
+ if old_value != value:
49
+ instance._values[self.name] = value
50
+ logger.info(f"Setting changed: {self.name} = {value} (was {old_value})")
51
+ instance.setting_changed.emit(self.name, value)
52
+
53
+
54
+ class Settings(QObject):
55
+ """Qt-based settings class with change signals and persistence"""
56
+
57
+ # Signal emitted when any setting changes: (setting_name, new_value)
58
+ setting_changed = Signal(str, object)
59
+
60
+ # Singleton instance
61
+ _instance = None
62
+
63
+ def __new__(cls):
64
+ if cls._instance is None:
65
+ cls._instance = super(Settings, cls).__new__(cls)
66
+ cls._instance._initialized = False
67
+ return cls._instance
68
+
69
+ def __init__(self):
70
+ if self._initialized:
71
+ return
72
+
73
+ super().__init__()
74
+ self._values = {}
75
+ self._qsettings = QSettings("cogscinl", "Sigmund Analyst")
76
+ self._initialized = True
77
+ logger.info("Initializing Settings manager")
78
+ self._load_settings()
79
+
80
+ # Appearance
81
+ font_size = SettingProperty(16, "Appearance")
82
+ font_family = SettingProperty('Ubuntu Mono', "Appearance")
83
+ color_scheme = SettingProperty('monokai', "Appearance")
84
+ tab_width = SettingProperty(4, "Appearance")
85
+ word_wrap = SettingProperty(False, "Appearance")
86
+ character_ruler = SettingProperty(80, "Appearance") # None to disable
87
+ search_replace_background = SettingProperty("#fdff74", "Appearance")
88
+ search_replace_foreground = SettingProperty("#000000", "Appearance")
89
+
90
+ # Files and Folders
91
+ current_folder = SettingProperty(os.path.expanduser('~'), "Files")
92
+ default_filename = SettingProperty('untitled.txt', "Files")
93
+ default_encoding = SettingProperty('utf-8', "Files")
94
+
95
+ # Keyboard shortcuts
96
+ shortcut_move_line_up = SettingProperty('Alt+Up', "Shortcuts")
97
+ shortcut_move_line_down = SettingProperty('Alt+Down', "Shortcuts")
98
+ shortcut_duplicate_line = SettingProperty('Ctrl+D', "Shortcuts")
99
+ shortcut_comment = SettingProperty('Ctrl+/', "Shortcuts")
100
+ shortcut_split_horizontally = SettingProperty('Ctrl+Shift+H', "Shortcuts")
101
+ shortcut_split_vertically = SettingProperty('Ctrl+Shift+V', "Shortcuts")
102
+ shortcut_quick_open_file = SettingProperty('Ctrl+P', "Shortcuts")
103
+ shortcut_symbols = SettingProperty('Ctrl+R', "Shortcuts")
104
+ shortcut_previous_tab = SettingProperty('Ctrl+Shift+Tab', "Shortcuts")
105
+ shortcut_close_all_tabs = SettingProperty('Ctrl+Shift+W', "Shortcuts")
106
+ shortcut_close_other_tabs = SettingProperty('Ctrl+Alt+W', "Shortcuts")
107
+ shortcut_open_folder = SettingProperty('Ctrl+Shift+O', "Shortcuts")
108
+ shortcut_find_in_files = SettingProperty('Ctrl+Shift+F', "Shortcuts")
109
+ shortcut_execute_code = SettingProperty('F9', "Shortcuts")
110
+ shortcut_execute_file = SettingProperty('F5', "Shortcuts")
111
+ shortcut_toggle_jupyter_console = SettingProperty('Ctrl+Shift+J', "Shortcuts")
112
+ shortcut_toggle_sigmund = SettingProperty('Ctrl+Shift+I', "Shortcuts")
113
+ shortcut_toggle_workspace_explorer = SettingProperty('Ctrl+Shift+X', "Shortcuts")
114
+ shortcut_toggle_project_explorers = SettingProperty('Ctrl+\\', "Shortcuts")
115
+
116
+ # Project explorer
117
+ max_files = SettingProperty(1000, "ProjectExplorer")
118
+ ignored_folders = SettingProperty(['.git'], "ProjectExplorer")
119
+
120
+ # Completions
121
+ max_completions = SettingProperty(5, "Completion")
122
+ full_completion_delay = SettingProperty(250, "Completion")
123
+ hide_completion_delay = SettingProperty(500, "Completion")
124
+
125
+ # Code checking
126
+ check_debounce_delay = SettingProperty(500, "Checking")
127
+ check_interval_delay = SettingProperty(5000, "Checking")
128
+
129
+ # Sigmund provider
130
+ sigmund_max_context = SettingProperty(2000, "Sigmund")
131
+ sigmund_fim_endpoint = SettingProperty('http://localhost:5000/code_completion/fim', "Sigmund")
132
+ sigmund_token = SettingProperty(None, "Sigmund")
133
+ sigmund_timeout = SettingProperty(1, "Sigmund") # seconds
134
+
135
+ # Codestral provider
136
+ codestral_max_context = SettingProperty(2000, "Codestral")
137
+ codestral_min_context = SettingProperty(100, "Codestral")
138
+ codestral_model = SettingProperty('codestral-latest', "Codestral")
139
+ codestral_api_key = SettingProperty(os.environ.get('CODESTRAL_API_KEY', ''), "Codestral")
140
+ codestral_url = SettingProperty('https://codestral.mistral.ai', "Codestral")
141
+ codestral_timeout = SettingProperty(5000, "Codestral")
142
+
143
+ # Window geometry
144
+ window_geometry = SettingProperty('', "Window")
145
+ window_state = SettingProperty('', "Window")
146
+
147
+ def save(self):
148
+ """Save all settings to persistent storage"""
149
+ logger.info("Saving settings to persistent storage")
150
+ for setting_name, value in self._values.items():
151
+ self._qsettings.setValue(setting_name, value)
152
+ self._qsettings.sync()
153
+ logger.info(f"Saved {len(self._values)} settings")
154
+
155
+ def _load_settings(self):
156
+ """Load settings from persistent storage"""
157
+ logger.info("Loading settings from persistent storage")
158
+ setting_names = self.get_all_setting_names()
159
+ loaded_count = 0
160
+
161
+ for setting_name in setting_names:
162
+ if self._qsettings.contains(setting_name):
163
+ value = self._qsettings.value(setting_name)
164
+ # Convert the value back to the correct type based on default
165
+ default = getattr(type(self), setting_name).default_value
166
+
167
+ if isinstance(default, bool) and not isinstance(value, bool):
168
+ value = value.lower() == 'true'
169
+ elif isinstance(default, int) and not isinstance(value, int):
170
+ try:
171
+ value = int(value)
172
+ except (ValueError, TypeError):
173
+ logger.warning(f"Failed to convert setting '{setting_name}' value to int: {value}")
174
+ value = default
175
+ elif isinstance(default, float) and not isinstance(value, float):
176
+ try:
177
+ value = float(value)
178
+ except (ValueError, TypeError):
179
+ logger.warning(f"Failed to convert setting '{setting_name}' value to float: {value}")
180
+ value = default
181
+
182
+ self._values[setting_name] = value
183
+ loaded_count += 1
184
+ logger.info(f"Loaded setting: {setting_name} = {value}")
185
+
186
+ logger.info(f"Loaded {loaded_count} settings ({len(setting_names) - loaded_count} using defaults)")
187
+
188
+ def reset_to_defaults(self):
189
+ """Reset all settings to their default values"""
190
+ logger.info("Resetting all settings to defaults")
191
+ self._values.clear()
192
+ self.save()
193
+ for setting_name in self.get_all_setting_names():
194
+ # Access each property to trigger signal emission with default value
195
+ default_value = getattr(type(self), setting_name).default_value
196
+ logger.info(f"Reset setting: {setting_name} = {default_value} (default)")
197
+ self.setting_changed.emit(setting_name, default_value)
198
+
199
+ def get_all_setting_names(self) -> list[str]:
200
+ """Get a list of all setting names"""
201
+ names = [name for name, attr in vars(type(self)).items()
202
+ if isinstance(attr, SettingProperty)]
203
+ logger.info(f"Found {len(names)} settings in total")
204
+ return names
205
+
206
+ def get_settings_by_category(self) -> dict[str, list[str]]:
207
+ """Get a dictionary of settings grouped by category"""
208
+ result = {}
209
+ for name, attr in vars(type(self)).items():
210
+ if isinstance(attr, SettingProperty):
211
+ category = attr.category
212
+ if category not in result:
213
+ result[category] = []
214
+ result[category].append(name)
215
+
216
+ categories_count = len(result)
217
+ logger.info(f"Settings organized into {categories_count} categories")
218
+ return result
219
+
220
+ def __iter__(self):
221
+ """Iterate over all settings"""
222
+ for name in self.get_all_setting_names():
223
+ yield name, getattr(self, name)
224
+
225
+ def __str__(self):
226
+ """Returns all settings as a JSON array of objects."""
227
+ # todo
228
+ return json.dumps({name: value for name, value in self})
229
+
230
+
231
+ # Create the singleton instance
232
+ settings = Settings()
@@ -0,0 +1,438 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+ from qtpy.QtWidgets import QMainWindow, QShortcut, QMessageBox, \
5
+ QDockWidget, QToolBar, QAction, QApplication
6
+ from qtpy.QtCore import Qt, QByteArray, QSize
7
+ from qtpy.QtGui import QKeySequence
8
+ import qtawesome as qta
9
+ from .widgets import QuickOpenFileDialog
10
+ from .components.editor_panel import EditorPanel
11
+ from .components.project_explorer import ProjectExplorer
12
+ from .components.find_in_files import FindInFiles
13
+ from .components.jupyter_console import JupyterConsole
14
+ from .components.workspace_explorer import WorkspaceExplorer
15
+ from .components.sigmund import Sigmund
16
+ from .components.settings_panel import SettingsPanel
17
+ from . import settings, watchdog
18
+ from pyqt_code_editor.signal_router import signal_router
19
+
20
+ logging.basicConfig(level=logging.INFO, force=True)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class MainWindow(QMainWindow):
25
+ def __init__(self, root_path=None):
26
+ super().__init__()
27
+ logger.info("MainWindow initialized")
28
+ # If no path is provided, fall back to current dir
29
+ if not root_path:
30
+ root_path = settings.current_folder
31
+
32
+ self.setWindowTitle("Sigmund Analyst")
33
+ # The editor panel provides splittable editor tabs
34
+ self._editor_panel = EditorPanel()
35
+ self.setCentralWidget(self._editor_panel)
36
+ self._project_explorers = []
37
+ # Track if project explorers are hidden as a group
38
+ self._project_explorers_hidden = False
39
+ # Open initial project explorer
40
+ self._open_project_explorer(root_path)
41
+
42
+ # Set up the workspace explorer
43
+ self._workspace_explorer = WorkspaceExplorer(parent=self)
44
+ self.addDockWidget(Qt.RightDockWidgetArea, self._workspace_explorer)
45
+ self._setup_dock_widget(self._workspace_explorer, "Workspace Explorer")
46
+
47
+ # Set up the jupyter console
48
+ self._jupyter_console = JupyterConsole(parent=self)
49
+ self._jupyter_console.workspace_updated.connect(
50
+ self._workspace_explorer.update
51
+ )
52
+ self.addDockWidget(Qt.BottomDockWidgetArea, self._jupyter_console)
53
+ self._setup_dock_widget(self._jupyter_console, "Jupyter Console")
54
+
55
+ # Set up Sigmund
56
+ self._sigmund = Sigmund(parent=self, editor_panel=self._editor_panel)
57
+ self.addDockWidget(Qt.RightDockWidgetArea, self._sigmund)
58
+ self._setup_dock_widget(self._sigmund, "Sigmund")
59
+
60
+ # Set up settings panel
61
+ self._settings_panel = SettingsPanel(parent=self)
62
+ self.addDockWidget(Qt.RightDockWidgetArea, self._settings_panel)
63
+ self._setup_dock_widget(self._settings_panel, "Settings")
64
+
65
+ # Shortcuts
66
+ self._quick_open_shortcut = QShortcut(
67
+ QKeySequence(settings.shortcut_quick_open_file), self
68
+ )
69
+ self._quick_open_shortcut.activated.connect(self._show_quick_open)
70
+
71
+ self._open_folder_shortcut = QShortcut(
72
+ QKeySequence(settings.shortcut_open_folder), self
73
+ )
74
+ self._open_folder_shortcut.activated.connect(self._open_folder)
75
+
76
+ self._find_in_files_shortcut = QShortcut(
77
+ QKeySequence(settings.shortcut_find_in_files), self
78
+ )
79
+ self._find_in_files_shortcut.activated.connect(self._find_in_files)
80
+
81
+ # Add dock widget visibility toggle shortcuts
82
+ self._workspace_toggle_shortcut = QShortcut(
83
+ QKeySequence(settings.shortcut_toggle_workspace_explorer), self
84
+ )
85
+ self._workspace_toggle_shortcut.activated.connect(
86
+ lambda: self._toggle_dock_widget(self._workspace_explorer)
87
+ )
88
+
89
+ self._jupyter_toggle_shortcut = QShortcut(
90
+ QKeySequence(settings.shortcut_toggle_jupyter_console), self
91
+ )
92
+ self._jupyter_toggle_shortcut.activated.connect(
93
+ lambda: self._toggle_dock_widget(self._jupyter_console)
94
+ )
95
+
96
+ self._sigmund_toggle_shortcut = QShortcut(
97
+ QKeySequence(settings.shortcut_toggle_sigmund), self
98
+ )
99
+ self._sigmund_toggle_shortcut.activated.connect(
100
+ lambda: self._toggle_dock_widget(self._sigmund)
101
+ )
102
+
103
+ # Add project explorer group toggle shortcut
104
+ self._project_toggle_shortcut = QShortcut(
105
+ QKeySequence(settings.shortcut_toggle_project_explorers), self
106
+ )
107
+ self._project_toggle_shortcut.activated.connect(
108
+ self._toggle_project_explorers
109
+ )
110
+
111
+ # Connect to dynamically created signals
112
+ signal_router.connect_to_signal("execute_code", self._execute_code)
113
+ signal_router.connect_to_signal("execute_file", self._execute_file)
114
+
115
+ self._setup_toolbar()
116
+
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}")
135
+
136
+ def _setup_toolbar(self):
137
+ """Setup the main toolbar with Material Design icons."""
138
+ # Create toolbar
139
+ self.toolbar = QToolBar("Main Toolbar")
140
+ self.toolbar.setObjectName('main_toolbar')
141
+ self.toolbar.setIconSize(QSize(32, 32))
142
+ self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
143
+ self.addToolBar(self.toolbar)
144
+
145
+ # FILE OPERATIONS
146
+ new_file_action = QAction(qta.icon('mdi6.file-plus'), "New File", self)
147
+ new_file_action.setStatusTip("Create a new file")
148
+ new_file_action.triggered.connect(
149
+ lambda: self._editor_panel.open_file()
150
+ )
151
+ self.toolbar.addAction(new_file_action)
152
+
153
+ open_file_action = QAction(qta.icon('mdi6.folder-open'), "Open File", self)
154
+ open_file_action.setStatusTip("Open an existing file")
155
+ open_file_action.triggered.connect(
156
+ lambda: self._editor_panel.select_and_open_file()
157
+ )
158
+ self.toolbar.addAction(open_file_action)
159
+
160
+ open_folder_action = QAction(qta.icon('mdi6.folder-multiple'), "Open Folder", self)
161
+ open_folder_action.setStatusTip("Open a folder as project")
162
+ open_folder_action.triggered.connect(self._open_folder)
163
+ self.toolbar.addAction(open_folder_action)
164
+
165
+ self.toolbar.addSeparator()
166
+
167
+ # QUICK ACCESS
168
+ quick_file_action = QAction(qta.icon('mdi6.file-find'), "Quick Select File", self)
169
+ quick_file_action.setStatusTip("Quickly find and open a file")
170
+ quick_file_action.triggered.connect(self._show_quick_open)
171
+ self.toolbar.addAction(quick_file_action)
172
+
173
+ quick_symbol_action = QAction(qta.icon('mdi6.code-tags'), "Quick Select Symbol", self)
174
+ quick_symbol_action.setStatusTip("Quickly find a symbol in current file")
175
+ quick_symbol_action.triggered.connect(
176
+ lambda: self._trigger_editor_shortcut('symbol_shortcut')
177
+ )
178
+ self.toolbar.addAction(quick_symbol_action)
179
+
180
+ self.toolbar.addSeparator()
181
+
182
+ # EXECUTION CONTROLS
183
+ run_file_action = QAction(qta.icon('mdi6.play'), "Run File", self)
184
+ run_file_action.setStatusTip("Run the current file")
185
+ run_file_action.triggered.connect(
186
+ lambda: self._trigger_editor_shortcut('execute_file_shortcut')
187
+ )
188
+ self.toolbar.addAction(run_file_action)
189
+
190
+ run_selection_action = QAction(
191
+ qta.icon('mdi6.play-box-outline'),
192
+ "Run Cell or Selection",
193
+ self
194
+ )
195
+ run_selection_action.setStatusTip("Run the selected cell or lines")
196
+ run_selection_action.triggered.connect(
197
+ lambda: self._trigger_editor_shortcut('execute_code_shortcut')
198
+ )
199
+ self.toolbar.addAction(run_selection_action)
200
+
201
+ break_kernel_action = QAction(qta.icon('mdi6.stop'), "Break Kernel", self)
202
+ break_kernel_action.setStatusTip("Interrupt the kernel")
203
+ break_kernel_action.triggered.connect(
204
+ lambda: self._jupyter_console.interrupt_current_kernel()
205
+ )
206
+ self.toolbar.addAction(break_kernel_action)
207
+
208
+ restart_kernel_action = QAction(qta.icon('mdi6.restart'), "Restart Kernel", self)
209
+ restart_kernel_action.setStatusTip("Restart the kernel")
210
+ restart_kernel_action.triggered.connect(
211
+ lambda: self._jupyter_console.restart_current_kernel()
212
+ )
213
+ self.toolbar.addAction(restart_kernel_action)
214
+
215
+ self.toolbar.addSeparator()
216
+
217
+ # FIND IN FILES
218
+ find_in_files_action = QAction(qta.icon('mdi6.file-search'), "Find in Files", self)
219
+ find_in_files_action.setStatusTip("Search across multiple files")
220
+ find_in_files_action.triggered.connect(self._find_in_files)
221
+ self.toolbar.addAction(find_in_files_action)
222
+
223
+ self.toolbar.addSeparator()
224
+
225
+ # SPLIT VIEWS
226
+ split_vert_action = QAction(qta.icon('mdi6.arrow-split-horizontal'), "Split Vertically", self)
227
+ split_vert_action.setStatusTip("Split the editor vertically")
228
+ split_vert_action.triggered.connect(
229
+ lambda: self._editor_panel.split(Qt.Vertical)
230
+ )
231
+ self.toolbar.addAction(split_vert_action)
232
+
233
+ split_horz_action = QAction(qta.icon('mdi6.arrow-split-vertical'), "Split Horizontally", self)
234
+ split_horz_action.setStatusTip("Split the editor horizontally")
235
+ split_horz_action.triggered.connect(
236
+ lambda: self._editor_panel.split(Qt.Horizontal)
237
+ )
238
+ self.toolbar.addAction(split_horz_action)
239
+
240
+ self.toolbar.addSeparator()
241
+
242
+ # DOCK TOGGLES – now checkable
243
+ toggle_settings_panel_action = QAction(qta.icon('mdi6.cogs'), "Toggle Settings", self)
244
+ toggle_settings_panel_action.setCheckable(True)
245
+ toggle_settings_panel_action.triggered.connect(
246
+ lambda checked: self._toggle_dock_widget(self._settings_panel, show=checked)
247
+ )
248
+ self._settings_panel.visibilityChanged.connect(toggle_settings_panel_action.setChecked)
249
+ self.toolbar.addAction(toggle_settings_panel_action)
250
+
251
+ self.toolbar.addSeparator()
252
+
253
+ self._toggle_project_action = QAction(qta.icon('mdi6.folder-outline'), "Toggle Project Explorer", self)
254
+ self._toggle_project_action.setCheckable(True)
255
+ self._toggle_project_action.setChecked(True)
256
+ self._toggle_project_action.setStatusTip("Show/hide project explorers")
257
+ self._toggle_project_action.triggered.connect(self._toggle_project_explorers)
258
+ self.toolbar.addAction(self._toggle_project_action)
259
+
260
+ toggle_jupyter_action = QAction(qta.icon('mdi6.console'), "Toggle Jupyter Console", self)
261
+ toggle_jupyter_action.setCheckable(True)
262
+ toggle_jupyter_action.triggered.connect(
263
+ lambda checked: self._toggle_dock_widget(self._jupyter_console, show=checked)
264
+ )
265
+ self._jupyter_console.visibilityChanged.connect(toggle_jupyter_action.setChecked)
266
+ self.toolbar.addAction(toggle_jupyter_action)
267
+
268
+ toggle_workspace_action = QAction(qta.icon('mdi6.view-list'), "Toggle Workspace Explorer", self)
269
+ toggle_workspace_action.setCheckable(True)
270
+ toggle_workspace_action.triggered.connect(
271
+ lambda checked: self._toggle_dock_widget(self._workspace_explorer, show=checked)
272
+ )
273
+ self._workspace_explorer.visibilityChanged.connect(toggle_workspace_action.setChecked)
274
+ self.toolbar.addAction(toggle_workspace_action)
275
+
276
+ toggle_sigmund_action = QAction(qta.icon('mdi6.brain'), "Toggle Sigmund", self)
277
+ toggle_sigmund_action.setCheckable(True)
278
+ toggle_sigmund_action.triggered.connect(
279
+ lambda checked: self._toggle_dock_widget(self._sigmund, show=checked)
280
+ )
281
+ self._sigmund.visibilityChanged.connect(toggle_sigmund_action.setChecked)
282
+ self.toolbar.addAction(toggle_sigmund_action)
283
+
284
+ def _trigger_editor_shortcut(self, shortcut):
285
+ editor = self._editor_panel.active_editor()
286
+ if editor is not None and hasattr(editor, shortcut):
287
+ getattr(editor, shortcut).activated.emit()
288
+
289
+ def _setup_dock_widget(self, dock_widget, name):
290
+ """Configures dock widget to hide on close instead of actually closing."""
291
+ if isinstance(dock_widget, QDockWidget):
292
+ # Override close behavior
293
+ dock_widget.closeEvent = lambda event: self._handle_dock_close(event, dock_widget)
294
+ else:
295
+ # If the dock widget is a custom class with a close_requested signal
296
+ if hasattr(dock_widget, 'close_requested'):
297
+ dock_widget.close_requested.connect(
298
+ lambda: self._toggle_dock_widget(dock_widget, show=False)
299
+ )
300
+
301
+ def _handle_dock_close(self, event, dock_widget):
302
+ """Handles close events for dock widgets by hiding them instead."""
303
+ event.ignore() # Prevent the actual close
304
+ dock_widget.hide()
305
+
306
+ def _toggle_dock_widget(self, dock_widget, show=None):
307
+ """Toggle the visibility of a dock widget."""
308
+ if show is None:
309
+ # Toggle visibility
310
+ dock_widget.setVisible(not dock_widget.isVisible())
311
+ else:
312
+ # Set to specific state
313
+ dock_widget.setVisible(show)
314
+
315
+ def _toggle_project_explorers(self):
316
+ """Toggle visibility of all project explorers as a group."""
317
+ # Toggle the hidden state
318
+ self._project_explorers_hidden = not self._project_explorers_hidden
319
+ self._toggle_project_action.setChecked(not self._project_explorers_hidden)
320
+ # Apply the visibility to all project explorers
321
+ for explorer in self._project_explorers:
322
+ explorer.setVisible(not self._project_explorers_hidden)
323
+
324
+ def _normalize_line_breaks(self, text):
325
+ """Convert paragraph separators (U+2029) to standard newlines."""
326
+ return text.replace('\u2029', '\n')
327
+
328
+ def _execute_code(self, code):
329
+ editor = self._editor_panel.active_editor()
330
+ if editor is not None and editor.code_editor_file_path is not None:
331
+ self._jupyter_console.change_directory(
332
+ os.path.dirname(editor.code_editor_file_path)
333
+ )
334
+ self._jupyter_console.execute_code(code)
335
+
336
+ def _execute_file(self, path):
337
+ self._jupyter_console.execute_file(path)
338
+
339
+ def _find_in_files(self):
340
+ file_list = []
341
+ for project_explorer in self._project_explorers:
342
+ file_list.extend(project_explorer.list_files())
343
+ editor = self._editor_panel.active_editor()
344
+ if editor is not None:
345
+ selected_text = editor.textCursor().selectedText()
346
+ # Normalize line breaks
347
+ selected_text = self._normalize_line_breaks(selected_text)
348
+ lines = selected_text.splitlines()
349
+ if len(lines) == 1:
350
+ needle = lines[0].strip()
351
+ else:
352
+ needle = None
353
+ else:
354
+ needle = None
355
+ find_in_files = FindInFiles(file_list, parent=self, needle=needle)
356
+ find_in_files.open_file_requested.connect(self._open_found_file)
357
+ self.addDockWidget(Qt.RightDockWidgetArea, find_in_files)
358
+
359
+ def _open_found_file(self, path, line_number):
360
+ self._editor_panel.open_file(path, line_number)
361
+
362
+ def _open_project_explorer(self, path):
363
+ project_explorer = ProjectExplorer(self._editor_panel, path, parent=self)
364
+ project_explorer.closed.connect(self._close_project_explorer)
365
+ self.addDockWidget(Qt.LeftDockWidgetArea, project_explorer)
366
+ self._project_explorers.append(project_explorer)
367
+
368
+ # If project explorers were hidden as a group, make them all visible again
369
+ if self._project_explorers_hidden:
370
+ self._project_explorers_hidden = False
371
+ for explorer in self._project_explorers:
372
+ explorer.setVisible(True)
373
+
374
+ def _close_project_explorer(self, project_explorer):
375
+ self._project_explorers.remove(project_explorer)
376
+
377
+ def _open_folder(self):
378
+ project_explorer = ProjectExplorer.open_folder(
379
+ self._editor_panel, parent=self
380
+ )
381
+ if project_explorer is None:
382
+ return
383
+ project_explorer.closed.connect(self._close_project_explorer)
384
+ self.addDockWidget(Qt.LeftDockWidgetArea, project_explorer)
385
+ self._project_explorers.append(project_explorer)
386
+
387
+ # If project explorers were hidden as a group, make them all visible again
388
+ if self._project_explorers_hidden:
389
+ self._project_explorers_hidden = False
390
+ for explorer in self._project_explorers:
391
+ explorer.setVisible(True)
392
+
393
+ def closeEvent(self, event):
394
+ # Check if there are unsaved changes, and ask for confirmation if there
395
+ # are
396
+ if self._editor_panel.unsaved_changes():
397
+ message_box = QMessageBox(self)
398
+ message_box.setWindowTitle("Save changes?")
399
+ message_box.setText("Unsaved changes will be permanently lost.")
400
+ message_box.setStandardButtons(
401
+ QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
402
+ )
403
+ message_box.setDefaultButton(QMessageBox.Cancel)
404
+ answer = message_box.exec()
405
+ if answer == QMessageBox.Cancel:
406
+ event.ignore()
407
+ return
408
+ if answer == QMessageBox.Yes:
409
+ self._editor_panel.save_all_unsaved_changes()
410
+
411
+ # Persist window geometry/state
412
+ geom_str = self.saveGeometry().toBase64().data().decode('utf-8')
413
+ state_str = self.saveState().toBase64().data().decode('utf-8')
414
+ settings.window_geometry = geom_str
415
+ settings.window_state = state_str
416
+
417
+ settings.save()
418
+ watchdog.shutdown()
419
+ super().closeEvent(event)
420
+
421
+ def _show_quick_open(self):
422
+ """Show a dialog with all files in the project, filtered as user types."""
423
+ file_list = []
424
+ for project_explorer in self._project_explorers:
425
+ file_list.extend(project_explorer.list_files())
426
+ dlg = QuickOpenFileDialog(
427
+ parent=self,
428
+ file_list=file_list,
429
+ open_file_callback=self._editor_panel.open_file,
430
+ )
431
+ dlg.exec_()
432
+
433
+
434
+ def launch_app():
435
+ app = QApplication(sys.argv)
436
+ window = MainWindow()
437
+ window.show()
438
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ from .code_editors import create_editor