pyqt-code-editor 0.0.32__tar.gz → 0.0.34__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 (79) hide show
  1. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/PKG-INFO +1 -1
  2. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/__init__.py +1 -1
  3. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/editor_panel.py +2 -1
  4. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/project_explorer.py +180 -57
  5. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/sigmund.py +10 -2
  6. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/providers/ruff.py +9 -3
  7. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/COPYING +0 -0
  8. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyproject.toml +0 -0
  9. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/_settings.py +0 -0
  10. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/app.py +0 -0
  11. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/__init__.py +0 -0
  12. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  13. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  14. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
  15. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  16. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/languages/python.py +0 -0
  17. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  18. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/__init__.py +0 -0
  19. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/find_in_files.py +0 -0
  20. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/jupyter_console.py +0 -0
  21. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/settings_panel.py +0 -0
  22. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  23. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/environment_manager.py +0 -0
  24. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/__init__.py +0 -0
  25. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/auto_indent.py +0 -0
  26. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  27. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/base.py +0 -0
  28. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/check.py +0 -0
  29. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/comment.py +0 -0
  30. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/complete.py +0 -0
  31. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/execute.py +0 -0
  32. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/file_link.py +0 -0
  33. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/line_number.py +0 -0
  34. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
  35. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/search_replace.py +0 -0
  36. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  37. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  38. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/symbols.py +0 -0
  39. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/theme.py +0 -0
  40. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  41. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/mixins/zoom.py +0 -0
  42. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/sigmund-analyst.png +0 -0
  43. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/signal_router.py +0 -0
  44. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  45. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  46. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  47. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  48. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  49. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/themes.py +0 -0
  50. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/utils/__init__.py +0 -0
  51. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/utils/languages/__init__.py +0 -0
  52. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
  53. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
  54. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
  55. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +0 -0
  56. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/watchdog.py +0 -0
  57. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/__init__.py +0 -0
  58. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  59. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  60. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/dock.py +0 -0
  61. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  62. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  63. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  64. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  65. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  66. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  67. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/__init__.py +0 -0
  68. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  69. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/languages/generic.py +0 -0
  70. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/languages/python.py +0 -0
  71. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/manager.py +0 -0
  72. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/process.py +0 -0
  73. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  74. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  75. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/providers/jedi.py +0 -0
  76. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  77. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  78. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/pyqt_code_editor/worker/settings.py +0 -0
  79. {pyqt_code_editor-0.0.32 → pyqt_code_editor-0.0.34}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqt_code_editor
3
- Version: 0.0.32
3
+ Version: 0.0.34
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>
@@ -2,4 +2,4 @@
2
2
 
3
3
  from ._settings import settings
4
4
 
5
- __version__ = '0.0.32'
5
+ __version__ = '0.0.34'
@@ -98,7 +98,8 @@ class EditorPanel(QWidget):
98
98
  self._active_tab_widget = self._active_editor.parent().parent()
99
99
 
100
100
  def open_file(self, path=None, line_number=None):
101
- path = os.path.normpath(path)
101
+ if path is not None:
102
+ path = os.path.normpath(path)
102
103
  if path is not None:
103
104
  # Don't allow the same file to be opened multiple times
104
105
  for editor in self.central_splitter.editors():
@@ -11,12 +11,13 @@ from qtpy.QtWidgets import (
11
11
  QInputDialog,
12
12
  QWidget,
13
13
  QVBoxLayout,
14
- QCheckBox
14
+ QCheckBox,
15
+ QShortcut
15
16
  )
16
17
  from qtpy.QtCore import Qt, QDir, QModelIndex, QSortFilterProxyModel, QUrl, \
17
18
  Signal
18
19
  from qtpy.QtWidgets import QFileSystemModel
19
- from qtpy.QtGui import QDesktopServices
20
+ from qtpy.QtGui import QDesktopServices, QKeySequence
20
21
  from pathspec import PathSpec
21
22
  from pathspec.patterns.gitwildmatch import GitWildMatchPattern
22
23
  from .. import settings, themes
@@ -24,8 +25,7 @@ from .. import settings, themes
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
27
  class LazyQFileSystemModel(QFileSystemModel):
27
- """
28
- A QFileSystemModel that only fetches children for 'expanded' paths.
28
+ """A QFileSystemModel that only fetches children for 'expanded' paths.
29
29
  This prevents eagerly creating inotify watchers for large, collapsed directories.
30
30
  """
31
31
  def __init__(self, parent=None):
@@ -71,8 +71,7 @@ class LazyQFileSystemModel(QFileSystemModel):
71
71
  return super().fetchMore(index)
72
72
 
73
73
  class GitignoreFilterProxyModel(QSortFilterProxyModel):
74
- """
75
- A QSortFilterProxyModel that hides paths ignored by .gitignore (when enabled).
74
+ """A QSortFilterProxyModel that hides paths ignored by .gitignore (when enabled).
76
75
  Parses .gitignore with pathspec. Also forwards hasChildren/fetchMore to the source
77
76
  model so that folders may be expanded.
78
77
  """
@@ -83,8 +82,7 @@ class GitignoreFilterProxyModel(QSortFilterProxyModel):
83
82
  self.pathspec = None
84
83
 
85
84
  def set_root_folder(self, folder):
86
- """
87
- Load .gitignore (if present) from 'folder' and parse it into a PathSpec.
85
+ """Load .gitignore (if present) from 'folder' and parse it into a PathSpec.
88
86
  Keep the folder path for computing relative paths.
89
87
  """
90
88
  self.root_folder = folder
@@ -115,14 +113,14 @@ class GitignoreFilterProxyModel(QSortFilterProxyModel):
115
113
  abs_path = source_model.filePath(index)
116
114
  # Make sure our designated root folder is never hidden
117
115
  if abs_path == self.root_folder:
118
- return True
116
+ return True
119
117
  if not self.root_folder:
120
118
  return True
121
- # Always keep directories. We need to be able to look inside them
119
+ # Always keep directories. We need to be able to look inside them
122
120
  # because their children might be re-included by negated patterns in
123
121
  # .gitignore.
124
122
  if source_model.isDir(index):
125
- return True
123
+ return True
126
124
  # Ignore explicitly ignored folders
127
125
  path_parts = Path(abs_path).parts
128
126
  if any(ignored in path_parts for ignored in settings.ignored_folders):
@@ -130,7 +128,7 @@ class GitignoreFilterProxyModel(QSortFilterProxyModel):
130
128
  # Compute relative path from the repository root
131
129
  rel_path = os.path.relpath(abs_path, self.root_folder)
132
130
  # If matched by pathspec => it is ignored => filter out
133
- return not self.pathspec.match_file(rel_path)
131
+ return not self.pathspec.match_file(rel_path)
134
132
 
135
133
  # ----------------------
136
134
  # Overridden methods to ensure directories can still expand
@@ -152,7 +150,7 @@ class GitignoreFilterProxyModel(QSortFilterProxyModel):
152
150
  self.sourceModel().fetchMore(source_index)
153
151
 
154
152
  class ProjectExplorer(QDockWidget):
155
-
153
+
156
154
  closed = Signal(object)
157
155
 
158
156
  def __init__(self, editor_panel, root_path=None, parent=None):
@@ -188,7 +186,7 @@ class ProjectExplorer(QDockWidget):
188
186
 
189
187
  # Optional: Hide columns other than the file name
190
188
  self._set_single_column_view(True)
191
-
189
+
192
190
  # Only if .gitignore exists in the root, create the checkbox
193
191
  gitignore_path = os.path.join(self._display_root, '.gitignore')
194
192
  if os.path.isfile(gitignore_path):
@@ -207,23 +205,92 @@ class ProjectExplorer(QDockWidget):
207
205
  self._tree_view.customContextMenuRequested.connect(self._show_context_menu)
208
206
  self._tree_view.doubleClicked.connect(self._on_double_click)
209
207
 
210
- # Set our container widget as the dock widget’s main widget
208
+ # Set our container widget as the dock widget's main widget
211
209
  self.setWidget(container_widget)
212
-
210
+
211
+ # Set up global keyboard shortcuts
212
+ self._setup_shortcuts()
213
+
214
+ def _setup_shortcuts(self):
215
+ """Set up global keyboard shortcuts for common actions."""
216
+ QShortcut(QKeySequence("F2"), self, activated=lambda: self._handle_rename_shortcut())
217
+ QShortcut(QKeySequence.Delete, self, activated=lambda: self._handle_delete_shortcut())
218
+ QShortcut(QKeySequence.Cut, self, activated=lambda: self._handle_cut_shortcut())
219
+ QShortcut(QKeySequence.Copy, self, activated=lambda: self._handle_copy_shortcut())
220
+ QShortcut(QKeySequence.Paste, self, activated=lambda: self._handle_paste_shortcut())
221
+
222
+ def _handle_rename_shortcut(self):
223
+ """Handle F2 shortcut for rename."""
224
+ proxy_index = self._tree_view.selectionModel().currentIndex()
225
+ if proxy_index.isValid():
226
+ source_index = self._filter_proxy.mapToSource(proxy_index)
227
+ if source_index.isValid():
228
+ path = self._model.filePath(source_index)
229
+ self._rename_file_or_folder(path)
230
+
231
+ def _handle_delete_shortcut(self):
232
+ """Handle Delete shortcut for delete."""
233
+ proxy_index = self._tree_view.selectionModel().currentIndex()
234
+ if proxy_index.isValid():
235
+ source_index = self._filter_proxy.mapToSource(proxy_index)
236
+ if source_index.isValid():
237
+ path = self._model.filePath(source_index)
238
+ self._delete_file_or_folder(path)
239
+
240
+ def _handle_cut_shortcut(self):
241
+ """Handle Ctrl+X shortcut for cut."""
242
+ proxy_index = self._tree_view.selectionModel().currentIndex()
243
+ if proxy_index.isValid():
244
+ source_index = self._filter_proxy.mapToSource(proxy_index)
245
+ if source_index.isValid():
246
+ path = self._model.filePath(source_index)
247
+ self._clipboard_operation = 'cut'
248
+ self._clipboard_source_path = path
249
+
250
+ def _handle_copy_shortcut(self):
251
+ """Handle Ctrl+C shortcut for copy."""
252
+ proxy_index = self._tree_view.selectionModel().currentIndex()
253
+ if proxy_index.isValid():
254
+ source_index = self._filter_proxy.mapToSource(proxy_index)
255
+ if source_index.isValid():
256
+ path = self._model.filePath(source_index)
257
+ self._clipboard_operation = 'copy'
258
+ self._clipboard_source_path = path
259
+
260
+ def _handle_paste_shortcut(self):
261
+ """Handle Ctrl+V shortcut for paste."""
262
+ if not self._clipboard_source_path:
263
+ return
264
+
265
+ if not self._tree_view.selectionModel().hasSelection():
266
+ # Paste into root if nothing is selected
267
+ root_path = self._model.rootPath()
268
+ if os.path.isdir(root_path):
269
+ self._paste_file_or_folder(root_path)
270
+ else:
271
+ proxy_index = self._tree_view.selectionModel().currentIndex()
272
+ source_index = self._filter_proxy.mapToSource(proxy_index)
273
+ if source_index.isValid():
274
+ path = self._model.filePath(source_index)
275
+ if os.path.isdir(path):
276
+ self._paste_file_or_folder(path)
277
+ else:
278
+ # If a file is selected, use its directory
279
+ self._paste_file_or_folder(os.path.dirname(path))
280
+
213
281
  def list_files(self) -> list[str]:
214
- """
215
- Returns a list of all non-ignored files under the display root,
282
+ """Returns a list of all non-ignored files under the display root,
216
283
  applying the same .gitignore-based logic.
217
284
  If there are more than max_files files, returns empty list.
218
285
  """
219
286
  if not self._display_root:
220
287
  return []
221
-
288
+
222
289
  results = []
223
290
  gitignore_enabled = self._filter_proxy.gitignore_enabled
224
291
  pathspec = self._filter_proxy.pathspec
225
292
  # Recursively walk the filesystem from _display_root
226
- for dirpath, dirnames, filenames in os.walk(self._display_root):
293
+ for dirpath, dirnames, filenames in os.walk(self._display_root):
227
294
  path_parts = Path(dirpath).parts
228
295
  if any(ignored in path_parts for ignored in settings.ignored_folders):
229
296
  continue
@@ -233,7 +300,7 @@ class ProjectExplorer(QDockWidget):
233
300
  # Avoid filtering _display_root, though it shouldn't typically be a file
234
301
  if abs_file == self._display_root:
235
302
  continue
236
-
303
+
237
304
  if gitignore_enabled and pathspec:
238
305
  rel_file = os.path.relpath(abs_file, self._display_root)
239
306
  # If pathspec matches => "ignored"
@@ -243,32 +310,31 @@ class ProjectExplorer(QDockWidget):
243
310
  if len(results) > settings.max_files:
244
311
  logger.warning("Too many files in project")
245
312
  return []
246
-
247
- return results
248
-
313
+
314
+ return results
315
+
249
316
  @classmethod
250
317
  def open_folder(cls, editor_panel, parent=None) -> object | None:
251
- """
252
- Shows a folder picker dialog to open a folder.
318
+ """Shows a folder picker dialog to open a folder.
253
319
  If a folder is selected, a ProjectExplorer instance is created and returned.
254
320
  Otherwise None is returned.
255
321
  """
256
322
  options = QFileDialog.Options(QFileDialog.ShowDirsOnly)
257
323
  if os.environ.get("DONT_USE_NATIVE_FILE_DIALOG", False):
258
324
  options |= QFileDialog.Option.DontUseNativeDialog
259
- logger.info('not using native file dialog')
325
+ logger.info('not using native file dialog')
260
326
  selected_dir = QFileDialog.getExistingDirectory(parent,
261
327
  "Open Project Folder", "", options=options)
262
328
  if not selected_dir:
263
329
  return None
264
330
  settings.current_folder = selected_dir
265
- # Add selected folder to project folders but make sure we don't
331
+ # Add selected folder to project folders but make sure we don't
266
332
  # duplicate
267
333
  settings.project_folders = '::'.join(
268
334
  set(str(settings.project_folders).split('::')) | {selected_dir})
269
335
  explorer = cls(editor_panel, root_path=selected_dir, parent=parent)
270
336
  return explorer
271
-
337
+
272
338
  def _toggle_gitignore(self, enabled):
273
339
  """Toggles the .gitignore filter on or off and refreshes the model.
274
340
  """
@@ -282,11 +348,10 @@ class ProjectExplorer(QDockWidget):
282
348
  proxy_root_index = self._filter_proxy.mapFromSource(root_idx)
283
349
  self._tree_view.setRootIndex(proxy_root_index)
284
350
  # Expand the root so it behaves like an expanded folder
285
- self._tree_view.expand(proxy_root_index)
351
+ self._tree_view.expand(proxy_root_index)
286
352
 
287
353
  def _on_expanded_proxy(self, proxy_index):
288
- """
289
- Convert the proxy index to the source model index and notify LazyQFileSystemModel.
354
+ """Convert the proxy index to the source model index and notify LazyQFileSystemModel.
290
355
  """
291
356
  source_index = self._filter_proxy.mapToSource(proxy_index)
292
357
  if source_index.isValid():
@@ -294,8 +359,7 @@ class ProjectExplorer(QDockWidget):
294
359
  self._model.notify_path_expanded(path)
295
360
 
296
361
  def _on_collapsed_proxy(self, proxy_index):
297
- """
298
- Convert the proxy index to the source model index and notify LazyQFileSystemModel.
362
+ """Convert the proxy index to the source model index and notify LazyQFileSystemModel.
299
363
  """
300
364
  source_index = self._filter_proxy.mapToSource(proxy_index)
301
365
  if source_index.isValid():
@@ -329,15 +393,15 @@ class ProjectExplorer(QDockWidget):
329
393
  """Build and show a context menu on right-click."""
330
394
  proxy_index = self._tree_view.indexAt(pos)
331
395
  source_index = self._filter_proxy.mapToSource(proxy_index)
332
-
396
+
333
397
  menu = QMenu(self)
334
-
398
+
335
399
  if not source_index.isValid():
336
- # Clicked on empty space, same as before
400
+ # Clicked on empty space
337
401
  new_file_action = menu.addAction("New File…")
338
402
  new_folder_action = menu.addAction("New Folder…")
339
403
  chosen_action = menu.exec_(self._tree_view.mapToGlobal(pos))
340
-
404
+
341
405
  if chosen_action == new_file_action:
342
406
  root_path = self._model.rootPath()
343
407
  if os.path.isdir(root_path):
@@ -346,24 +410,29 @@ class ProjectExplorer(QDockWidget):
346
410
  root_path = self._model.rootPath()
347
411
  if os.path.isdir(root_path):
348
412
  self._create_new_folder(root_path)
349
-
413
+
350
414
  else:
351
415
  path = self._model.filePath(source_index)
352
416
  is_file = os.path.isfile(path)
353
-
417
+
354
418
  if is_file:
355
419
  # Right-clicked on a file
356
420
  open_action = menu.addAction("Open")
357
421
  open_sys_action = menu.addAction("Open containing folder")
358
422
  rename_action = menu.addAction("Rename…")
423
+ rename_action.setShortcut(QKeySequence("F2"))
359
424
  delete_action = menu.addAction("Delete")
360
-
425
+ delete_action.setShortcut(QKeySequence.Delete)
426
+
361
427
  menu.addSeparator()
362
428
  cut_action = menu.addAction("Cut")
429
+ cut_action.setShortcut(QKeySequence.Cut)
363
430
  copy_action = menu.addAction("Copy")
431
+ copy_action.setShortcut(QKeySequence.Copy)
364
432
  paste_action = menu.addAction("Paste")
433
+ paste_action.setShortcut(QKeySequence.Paste)
365
434
  paste_action.setEnabled(self._clipboard_source_path is not None)
366
-
435
+
367
436
  chosen_action = menu.exec_(self._tree_view.mapToGlobal(pos))
368
437
  if chosen_action == open_action:
369
438
  self._editor_panel.open_file(path)
@@ -386,22 +455,30 @@ class ProjectExplorer(QDockWidget):
386
455
  self._clipboard_source_path = path
387
456
  elif chosen_action == paste_action:
388
457
  self._paste_file_or_folder(path)
389
-
458
+
390
459
  else:
391
460
  # Right-clicked on a folder
392
461
  open_action = menu.addAction("Open")
462
+ open_action.setShortcut(QKeySequence.Open)
393
463
  open_sys_action = menu.addAction("Open folder")
394
464
  new_file_action = menu.addAction("New File…")
465
+ new_file_action.setShortcut(QKeySequence.New)
395
466
  new_folder_action = menu.addAction("New Folder…")
467
+ new_folder_action.setShortcut(QKeySequence("Ctrl+Shift+N"))
396
468
  rename_action = menu.addAction("Rename…")
469
+ rename_action.setShortcut(QKeySequence("F2"))
397
470
  delete_action = menu.addAction("Delete")
398
-
471
+ delete_action.setShortcut(QKeySequence.Delete)
472
+
399
473
  menu.addSeparator()
400
474
  cut_action = menu.addAction("Cut")
475
+ cut_action.setShortcut(QKeySequence.Cut)
401
476
  copy_action = menu.addAction("Copy")
477
+ copy_action.setShortcut(QKeySequence.Copy)
402
478
  paste_action = menu.addAction("Paste")
479
+ paste_action.setShortcut(QKeySequence.Paste)
403
480
  paste_action.setEnabled(self._clipboard_source_path is not None)
404
-
481
+
405
482
  chosen_action = menu.exec_(self._tree_view.mapToGlobal(pos))
406
483
  if chosen_action == open_action:
407
484
  # Expand in the tree if not already expanded
@@ -434,13 +511,21 @@ class ProjectExplorer(QDockWidget):
434
511
  """Create a new file in the specified folder."""
435
512
  if not os.path.isdir(folder):
436
513
  return
437
- file_name, ok = QFileDialog.getSaveFileName(self, "New File", folder)
514
+
515
+ file_name, ok = QInputDialog.getText(self, "New File", "File name:")
438
516
  if not ok or not file_name:
439
517
  return
518
+
519
+ # Add default extension if none provided
520
+ if '.' not in file_name:
521
+ file_name += '.txt'
522
+
523
+ file_path = os.path.join(folder, file_name)
524
+
440
525
  try:
441
- with open(file_name, 'w', encoding='utf8') as f:
526
+ with open(file_path, 'w', encoding='utf8') as f:
442
527
  f.write("")
443
- logger.info(f"Created file: {file_name}")
528
+ logger.info(f"Created file: {file_path}")
444
529
  except Exception as e:
445
530
  QMessageBox.warning(self, "Error", f"Failed to create file:\n{str(e)}")
446
531
 
@@ -461,14 +546,30 @@ class ProjectExplorer(QDockWidget):
461
546
 
462
547
  def _rename_file_or_folder(self, path):
463
548
  """Rename a file or folder via an input dialog, then rename in filesystem."""
464
- base_dir = os.path.dirname(path)
465
549
  old_name = os.path.basename(path)
466
- new_name, ok = QFileDialog.getSaveFileName(self, "Rename", os.path.join(base_dir, old_name))
467
- if not ok or not new_name:
550
+ parent_dir = os.path.dirname(path)
551
+
552
+ # Show dialog with current name pre-filled
553
+ new_name, ok = QInputDialog.getText(
554
+ self,
555
+ "Rename",
556
+ f"Rename '{old_name}' to:",
557
+ text=old_name
558
+ )
559
+
560
+ if not ok or not new_name or new_name == old_name:
561
+ return
562
+
563
+ new_path = os.path.join(parent_dir, new_name)
564
+
565
+ # Check if target already exists
566
+ if os.path.exists(new_path):
567
+ QMessageBox.warning(self, "Error", f"'{new_name}' already exists.")
468
568
  return
569
+
469
570
  try:
470
- os.rename(path, new_name)
471
- logger.info(f"Renamed {path} to {new_name}")
571
+ os.rename(path, new_path)
572
+ logger.info(f"Renamed '{old_name}' to '{new_name}'")
472
573
  except Exception as e:
473
574
  QMessageBox.warning(self, "Error", f"Failed to rename:\n{str(e)}")
474
575
 
@@ -503,6 +604,28 @@ class ProjectExplorer(QDockWidget):
503
604
 
504
605
  src = self._clipboard_source_path
505
606
  dst = os.path.join(target_path, os.path.basename(src))
607
+
608
+ # Handle the case where source and destination are the same (copy operation only)
609
+ if self._clipboard_operation == 'copy' and os.path.abspath(src) == os.path.abspath(dst):
610
+ base_name = os.path.basename(src)
611
+ if os.path.isfile(src) and '.' in base_name:
612
+ # For files with extensions
613
+ name, ext = os.path.splitext(base_name)
614
+ dst = os.path.join(target_path, f"{name} (Copy){ext}")
615
+ else:
616
+ # For folders or files without extensions
617
+ dst = os.path.join(target_path, f"{base_name} (Copy)")
618
+
619
+ # If that name also exists, add numbers
620
+ counter = 2
621
+ while os.path.exists(dst):
622
+ if os.path.isfile(src) and '.' in base_name:
623
+ name, ext = os.path.splitext(base_name)
624
+ dst = os.path.join(target_path, f"{name} (Copy {counter}){ext}")
625
+ else:
626
+ dst = os.path.join(target_path, f"{base_name} (Copy {counter})")
627
+ counter += 1
628
+
506
629
  try:
507
630
  if self._clipboard_operation == 'copy':
508
631
  if os.path.isdir(src):
@@ -517,11 +640,11 @@ class ProjectExplorer(QDockWidget):
517
640
 
518
641
  self._clipboard_operation = None
519
642
  self._clipboard_source_path = None
520
-
521
- def closeEvent(self, event):
522
- # Add selected folder to project folders but make sure we don't
643
+
644
+ def closeEvent(self, event):
645
+ # Add selected folder to project folders but make sure we don't
523
646
  # duplicate
524
647
  settings.project_folders = '::'.join(
525
648
  set(str(settings.project_folders).split('::')) - {self._display_root})
526
649
  super().closeEvent(event)
527
- self.closed.emit(self)
650
+ self.closed.emit(self)
@@ -59,12 +59,20 @@ class EditorWorkspace:
59
59
  def get(self):
60
60
  text_cursor = self._editor.textCursor()
61
61
  if text_cursor.hasSelection():
62
+ # Move to the start of the first selected block
63
+ start = text_cursor.selectionStart()
64
+ end = text_cursor.selectionEnd()
65
+ text_cursor.setPosition(start)
66
+ text_cursor.movePosition(text_cursor.StartOfBlock)
67
+ text_cursor.setPosition(end, text_cursor.KeepAnchor)
68
+ text_cursor.movePosition(text_cursor.EndOfBlock, text_cursor.KeepAnchor)
69
+ self._editor.setTextCursor(text_cursor)
62
70
  content = self._normalize_line_breaks(text_cursor.selectedText())
63
71
  else:
64
72
  content = self._editor.toPlainText()
65
73
  self._indentation = self._get_indentation(content)
66
- logger.info(f'content was indented by "{self._indentation}"')
67
- return content, self._editor.code_editor_language
74
+ logger.info(f'content was indented by "{self._indentation}"')
75
+ return textwrap.dedent(content), self._editor.code_editor_language
68
76
 
69
77
  def set(self, content, language):
70
78
  text_cursor = self._editor.textCursor()
@@ -2,6 +2,7 @@ import subprocess
2
2
  import tempfile
3
3
  import json
4
4
  import os
5
+ import sys
5
6
  import logging
6
7
  logger = logging.getLogger(__name__)
7
8
 
@@ -9,7 +10,7 @@ logger = logging.getLogger(__name__)
9
10
  def ruff_check(code: str, prefix: str | None = None) -> list[dict]:
10
11
  """
11
12
  Lints Python source code using Ruff.
12
-
13
+
13
14
  Args:
14
15
  code (str): The Python source code to lint.
15
16
  """
@@ -25,9 +26,14 @@ def ruff_check(code: str, prefix: str | None = None) -> list[dict]:
25
26
  tmp_file_path = tmp_file.name
26
27
  print(tmp_file.name)
27
28
  cmd = ["ruff", "check", tmp_file_path, "--output-format", "json"]
29
+
30
+ # Set creation flags for Windows to prevent console window from appearing
31
+ creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
32
+
28
33
  try:
29
34
  process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
30
- stderr=subprocess.PIPE, text=True)
35
+ stderr=subprocess.PIPE, text=True,
36
+ creationflags=creation_flags)
31
37
  stdout, stderr = process.communicate()
32
38
  except Exception as e:
33
39
  logger.error(f'failed to invoke ruff: {e}')
@@ -49,4 +55,4 @@ def ruff_check(code: str, prefix: str | None = None) -> list[dict]:
49
55
  'code': message['code'],
50
56
  'message': message['message'],
51
57
  })
52
- return formatted_result
58
+ return formatted_result