pyqt-code-editor 0.0.20__tar.gz → 0.0.22__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 (78) hide show
  1. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/PKG-INFO +1 -1
  2. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/__init__.py +1 -1
  3. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/_settings.py +2 -0
  4. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/app.py +6 -6
  5. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/languages/python.py +1 -1
  6. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/jupyter_console.py +18 -12
  7. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/project_explorer.py +10 -2
  8. pyqt_code_editor-0.0.22/pyqt_code_editor/environment_manager.py +29 -0
  9. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/comment.py +21 -2
  10. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/complete.py +10 -2
  11. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/file_link.py +51 -5
  12. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/languages/python.py +3 -2
  13. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/process.py +7 -4
  14. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/providers/jedi.py +14 -8
  15. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/COPYING +0 -0
  16. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyproject.toml +0 -0
  17. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/__init__.py +0 -0
  18. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  19. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  20. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
  21. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  22. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  23. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/__init__.py +0 -0
  24. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/editor_panel.py +0 -0
  25. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/find_in_files.py +0 -0
  26. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/settings_panel.py +0 -0
  27. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/sigmund.py +0 -0
  28. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  29. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/__init__.py +0 -0
  30. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/auto_indent.py +0 -0
  31. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  32. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/base.py +0 -0
  33. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/check.py +0 -0
  34. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/execute.py +0 -0
  35. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/line_number.py +0 -0
  36. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
  37. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/search_replace.py +0 -0
  38. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  39. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  40. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/symbols.py +0 -0
  41. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/theme.py +0 -0
  42. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  43. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/mixins/zoom.py +0 -0
  44. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/signal_router.py +0 -0
  45. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  46. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  47. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  48. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  49. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  50. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/themes.py +0 -0
  51. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/utils/__init__.py +0 -0
  52. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/utils/languages/__init__.py +0 -0
  53. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
  54. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
  55. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
  56. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +0 -0
  57. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/watchdog.py +0 -0
  58. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/__init__.py +0 -0
  59. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  60. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  61. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/dock.py +0 -0
  62. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  63. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  64. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  65. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  66. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  67. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  68. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/__init__.py +0 -0
  69. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  70. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/languages/generic.py +0 -0
  71. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/manager.py +0 -0
  72. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  73. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  74. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/providers/ruff.py +0 -0
  75. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  76. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  77. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/pyqt_code_editor/worker/settings.py +0 -0
  78. {pyqt_code_editor-0.0.20 → pyqt_code_editor-0.0.22}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqt_code_editor
3
- Version: 0.0.20
3
+ Version: 0.0.22
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.20'
5
+ __version__ = '0.0.22'
@@ -80,9 +80,11 @@ class Settings(QObject):
80
80
 
81
81
  # Files and Folders
82
82
  current_folder = SettingProperty(os.path.expanduser('~'), "Files")
83
+ project_folders = SettingProperty(os.path.expanduser('~'), "Files")
83
84
  default_filename = SettingProperty('untitled.txt', "Files")
84
85
  default_language = SettingProperty('python', "Files")
85
86
  default_encoding = SettingProperty('utf-8', "Files")
87
+ max_file_size = SettingProperty(5 * 1024 ** 2, "Files") # 5 MB)
86
88
 
87
89
  # Keyboard shortcuts
88
90
  shortcut_move_line_up = SettingProperty('Alt+Up', "Shortcuts")
@@ -26,9 +26,8 @@ class MainWindow(QMainWindow):
26
26
  super().__init__()
27
27
  logger.info("MainWindow initialized")
28
28
  # If no path is provided, fall back to current dir
29
- if not root_path:
30
- root_path = settings.current_folder
31
-
29
+ if root_path:
30
+ settings.project_folders = settings.current_folder = root_path
32
31
  self.setWindowTitle(f"Sigmund Analyst {__version__}")
33
32
  # The editor panel provides splittable editor tabs
34
33
  self._editor_panel = EditorPanel()
@@ -38,8 +37,9 @@ class MainWindow(QMainWindow):
38
37
  # Track if project explorers are hidden as a group
39
38
  self._project_explorers_hidden = False
40
39
  # Open initial project explorer
41
- self._open_project_explorer(root_path)
42
-
40
+ for project_folder in str(settings.project_folders).split(':'):
41
+ if project_folder:
42
+ self._open_project_explorer(project_folder)
43
43
  # Set up the workspace explorer
44
44
  self._workspace_explorer = WorkspaceExplorer(parent=self)
45
45
  self.addDockWidget(Qt.RightDockWidgetArea, self._workspace_explorer)
@@ -442,7 +442,7 @@ class MainWindow(QMainWindow):
442
442
  open_file_callback=self._editor_panel.open_file,
443
443
  )
444
444
  dlg.exec_()
445
-
445
+
446
446
 
447
447
  def launch_app():
448
448
  app = QApplication(sys.argv)
@@ -6,7 +6,7 @@ from pyqt_code_editor.mixins import Complete, PythonAutoIndent, \
6
6
 
7
7
 
8
8
  class Editor(LineNumber, Zoom, SmartBackspaceDelete, PythonAutoPair, Complete,
9
- PythonAutoIndent, Comment, SearchReplace, FileLink, Execute,
9
+ Comment, PythonAutoIndent, SearchReplace, FileLink, Execute,
10
10
  Theme, Check, Shortcuts, Symbols, Base,
11
11
  QPlainTextEdit):
12
12
 
@@ -5,12 +5,14 @@ from qtconsole.rich_jupyter_widget import RichJupyterWidget
5
5
  from qtconsole.manager import QtKernelManager
6
6
  from jupyter_client.kernelspec import KernelSpecManager
7
7
  import qtawesome as qta
8
+ import sys
8
9
  import os
9
10
  import logging
10
11
  import uuid
11
12
  import json
12
13
  from concurrent.futures import Future
13
14
  from .. import settings
15
+ from ..environment_manager import environment_manager
14
16
  from ..themes import THEMES, OUTER_CONTENT_MARGINS, HORIZONTAL_SPACING
15
17
  from ..widgets import Dock
16
18
  from .. import watchdog
@@ -359,8 +361,8 @@ print(json.dumps({result_var}))
359
361
  class JupyterConsole(Dock):
360
362
  """Dockable widget containing tabbed Jupyter consoles"""
361
363
 
362
- execution_complete = Signal(str, object) # Signal for output interception
363
- workspace_updated = Signal(dict) # Signal for workspace updates
364
+ execution_complete = Signal(str, object)
365
+ workspace_updated = Signal(dict)
364
366
 
365
367
  def __init__(self, parent=None, default_kernel='python3'):
366
368
  super().__init__("Jupyter Console", parent)
@@ -370,7 +372,7 @@ class JupyterConsole(Dock):
370
372
  # ---------------------------------------------------------------------
371
373
  # Kernel cache
372
374
  # ---------------------------------------------------------------------
373
- self.available_kernels = [] # will be filled by refresh_kernel_menu()
375
+ self.available_kernels = {} # will be filled by refresh_kernel_menu()
374
376
 
375
377
  # ---------------------------------------------------------------------
376
378
  # UI setup
@@ -429,6 +431,14 @@ class JupyterConsole(Dock):
429
431
  console_tab = self.tab_widget.widget(index)
430
432
  if console_tab:
431
433
  console_tab.update_workspace()
434
+ spec = self.available_kernels.get(console_tab.kernel_name, None)
435
+ if spec:
436
+ python_executable = spec['spec']['argv'][0]
437
+ else:
438
+ python_executable = sys.executable
439
+ environment_manager.set_environment(console_tab.kernel_name,
440
+ python_executable,
441
+ 'python')
432
442
 
433
443
  # -------------------------------------------------------------------------
434
444
  # Kernel handling
@@ -439,12 +449,9 @@ class JupyterConsole(Dock):
439
449
 
440
450
  # Get available kernelspecs
441
451
  kernel_spec_manager = HomeAwareKernelSpecManager()
442
- specs = kernel_spec_manager.get_all_specs()
443
-
444
- # Cache available kernels
445
- self.available_kernels = list(specs.keys())
446
-
447
- for spec_name, spec in specs.items():
452
+ self.available_kernels = kernel_spec_manager.get_all_specs()
453
+ self.fallback_kernel = list(self.available_kernels.keys())[0]
454
+ for spec_name, spec in self.available_kernels.items():
448
455
  display_name = spec['spec']['display_name']
449
456
  action = QAction(display_name, self)
450
457
  action.setData(spec_name)
@@ -462,12 +469,11 @@ class JupyterConsole(Dock):
462
469
  """
463
470
  if kernel_name not in self.available_kernels:
464
471
  if self.available_kernels:
465
- fallback = self.available_kernels[0]
466
472
  logger.warning(
467
473
  "Requested kernel '%s' not found. "
468
- "Falling back to '%s'.", kernel_name, fallback
474
+ "Falling back to '%s'.", kernel_name, self.fallback_kernel
469
475
  )
470
- kernel_name = fallback
476
+ kernel_name = self.fallback_kernel
471
477
  else:
472
478
  logger.error("No available kernels found. Cannot create console tab.")
473
479
  return None
@@ -262,8 +262,12 @@ class ProjectExplorer(QDockWidget):
262
262
  if not selected_dir:
263
263
  return None
264
264
  settings.current_folder = selected_dir
265
+ # Add selected folder to project folders but make sure we don't
266
+ # duplicate
267
+ settings.project_folders = ':'.join(
268
+ set(str(settings.project_folders).split(':')) | {selected_dir})
265
269
  explorer = cls(editor_panel, root_path=selected_dir, parent=parent)
266
- return explorer
270
+ return explorer
267
271
 
268
272
  def _toggle_gitignore(self, enabled):
269
273
  """Toggles the .gitignore filter on or off and refreshes the model.
@@ -514,6 +518,10 @@ class ProjectExplorer(QDockWidget):
514
518
  self._clipboard_operation = None
515
519
  self._clipboard_source_path = None
516
520
 
517
- def closeEvent(self, event):
521
+ def closeEvent(self, event):
522
+ # Add selected folder to project folders but make sure we don't
523
+ # duplicate
524
+ settings.project_folders = ':'.join(
525
+ set(str(settings.project_folders).split(':')) - {self._display_root})
518
526
  super().closeEvent(event)
519
527
  self.closed.emit(self)
@@ -0,0 +1,29 @@
1
+ from qtpy.QtCore import Signal, QObject
2
+
3
+
4
+ class EnvironmentManager(QObject):
5
+ """Singleton class to share the current environment and language between all
6
+ components of the app.
7
+ """
8
+ environment_changed = Signal(str, str, str)
9
+
10
+ def __init__(self):
11
+ super().__init__()
12
+ self.name = None
13
+ self.path = None
14
+ self.language = None
15
+
16
+ @property
17
+ def current_environment(self):
18
+ return self.name, self.path, self.language
19
+
20
+ def set_environment(self, name, path, language):
21
+ if path != self.path:
22
+ self.name = name
23
+ self.path = path
24
+ self.language = language
25
+ self.environment_changed.emit(name, path, language)
26
+
27
+
28
+ # Singleton instance
29
+ environment_manager = EnvironmentManager()
@@ -17,12 +17,31 @@ class Comment:
17
17
  (default "# " for Python).
18
18
  """
19
19
  super().__init__(*args, **kwargs)
20
- # The text to prepend for a commented line
21
- self.code_editor_comment_string
22
20
  self._comment_shortcut = QShortcut(
23
21
  QKeySequence(settings.shortcut_comment), self)
24
22
  self._comment_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
25
23
  self._comment_shortcut.activated.connect(self._toggle_comment)
24
+
25
+ def keyPressEvent(self, event):
26
+ # When a return is pressed on a commented line, the comment should
27
+ # continue on the next line
28
+ if event.key() in (Qt.Key_Enter, Qt.Key_Return):
29
+ cursor = self.textCursor()
30
+ block = cursor.block()
31
+ line_text = block.text()
32
+ # Check if the current line is a comment
33
+ stripped = line_text.lstrip()
34
+ if stripped.startswith(self.code_editor_comment_string[0]):
35
+ # Extract the indentation from the current line
36
+ indentation = line_text[:len(line_text) - len(stripped)]
37
+ # Insert newline with same indentation plus comment marker
38
+ cursor.beginEditBlock()
39
+ cursor.insertText(
40
+ '\n' + indentation + self.code_editor_comment_string)
41
+ cursor.endEditBlock()
42
+ event.accept()
43
+ return
44
+ super().keyPressEvent(event)
26
45
 
27
46
  def _toggle_comment(self):
28
47
  """Toggle comment on the current selection or current line."""
@@ -1,9 +1,12 @@
1
+ import os
2
+ import sys
1
3
  import logging
2
4
  from qtpy.QtCore import QTimer, Qt
3
5
  from qtpy.QtGui import QTextCursor
4
6
  from qtpy.QtWidgets import QApplication
5
7
  from ..widgets.completion_popup import CompletionPopup
6
8
  from ..widgets.calltip_widget import CalltipWidget
9
+ from ..environment_manager import environment_manager
7
10
  from .. import settings
8
11
  logging.basicConfig(level=logging.INFO, force=True)
9
12
  logger = logging.getLogger(__name__)
@@ -273,7 +276,8 @@ class Complete:
273
276
  cursor_pos=cursor_pos, full=full,
274
277
  multiline=multiline,
275
278
  path=self.code_editor_file_path,
276
- language=self.code_editor_language)
279
+ language=self.code_editor_language,
280
+ env_path=environment_manager.path)
277
281
 
278
282
  def _cm_request_calltip(self):
279
283
  """Send a calltip request."""
@@ -284,7 +288,8 @@ class Complete:
284
288
  self.send_worker_request(action='calltip', code=code,
285
289
  cursor_pos=cursor_pos,
286
290
  path=self.code_editor_file_path,
287
- language=self.code_editor_language)
291
+ language=self.code_editor_language,
292
+ env_path=environment_manager.path)
288
293
 
289
294
  def _cm_hide_calltip(self):
290
295
  """Hide the calltip widget."""
@@ -533,3 +538,6 @@ class Complete:
533
538
  def update_theme(self):
534
539
  super().update_theme()
535
540
  self._cm_calltip_widget.apply_stylesheet()
541
+
542
+ def _current_environment(self):
543
+ return os.environ.get('SIGMUND_PYENV', sys.executable)
@@ -40,7 +40,12 @@ class FileLink:
40
40
  if not path.is_file():
41
41
  raise FileNotFoundError(f"No such file: {path}")
42
42
 
43
+ # Check file size
44
+ file_size = path.stat().st_size
45
+ is_large = file_size > settings.max_file_size
46
+
43
47
  raw_data = path.read_bytes()
48
+ encoding_unknown = False
44
49
 
45
50
  if encoding is None:
46
51
  # First see if everything is within ASCII range
@@ -51,14 +56,55 @@ class FileLink:
51
56
  except UnicodeDecodeError:
52
57
  # Not pure ASCII; let chardet pick
53
58
  detect_result = chardet.detect(raw_data)
54
- # If detection fails or returns None, default to utf-8
55
- used_encoding = detect_result["encoding"] or "utf-8"
59
+ # If detection fails or returns None, or confidence is low
60
+ if (detect_result is None or
61
+ detect_result.get("encoding") is None or
62
+ detect_result.get("confidence", 0) < 0.7):
63
+ used_encoding = "utf-8"
64
+ encoding_unknown = True
65
+ else:
66
+ used_encoding = detect_result["encoding"]
56
67
  else:
57
68
  used_encoding = encoding
69
+
70
+ # Show warning if file is large or encoding is uncertain
71
+ if is_large or encoding_unknown:
72
+ warnings = []
73
+ if is_large:
74
+ size_mb = file_size / (1024 * 1024)
75
+ warnings.append(f"This file is {size_mb:.1f} MB, which exceeds the recommended maximum of {settings.max_file_size / (1024 * 1024):.1f} MB.")
76
+ if encoding_unknown:
77
+ warnings.append("The file encoding could not be reliably detected. It may not display correctly.")
78
+
79
+ warning_message = "\n\n".join(warnings)
80
+ warning_message += "\n\nDo you want to continue opening this file?"
81
+
82
+ reply = QMessageBox.warning(
83
+ self,
84
+ "File Warning",
85
+ warning_message,
86
+ QMessageBox.Yes | QMessageBox.No,
87
+ QMessageBox.No
88
+ )
89
+
90
+ if reply != QMessageBox.Yes:
91
+ logger.info(f"User declined to open file: {path}")
92
+ return
93
+
58
94
  logger.info(f'opening file as {used_encoding}')
59
95
  # Try reading with the chosen encoding
60
- with path.open("r", encoding=used_encoding) as f:
61
- content = f.read()
96
+ try:
97
+ with path.open("r", encoding=used_encoding) as f:
98
+ content = f.read()
99
+ except UnicodeDecodeError as e:
100
+ # If decoding fails, offer to try as binary or cancel
101
+ reply = QMessageBox.critical(
102
+ self,
103
+ "Encoding Error",
104
+ f"Failed to decode file with {used_encoding} encoding.\n{str(e)}\n\nThis may not be a text file.",
105
+ QMessageBox.Cancel
106
+ )
107
+ return
62
108
 
63
109
  # Store the content in the editor
64
110
  self.setPlainText(content)
@@ -181,4 +227,4 @@ class FileLink:
181
227
  # Re-add the file to watcher so we keep listening for future changes
182
228
  if self._file_watcher is not None:
183
229
  self._file_watcher.addPath(changed_path)
184
- self._watch_file_changes = True
230
+ self._watch_file_changes = True
@@ -1,14 +1,15 @@
1
1
  from ..providers import jedi, codestral, ruff
2
2
 
3
3
 
4
- def complete(code, cursor_pos, path, multiline, full):
4
+ def complete(code, cursor_pos, path, multiline, full, env_path):
5
5
  if full or multiline:
6
6
  codestral_completions = codestral.codestral_complete(
7
7
  code, cursor_pos, path=path, multiline=multiline)
8
8
  else:
9
9
  codestral_completions = []
10
10
  if not multiline:
11
- jedi_completions = jedi.jedi_complete(code, cursor_pos, path=path)
11
+ jedi_completions = jedi.jedi_complete(code, cursor_pos, path=path,
12
+ env_path=env_path)
12
13
  else:
13
14
  jedi_completions = []
14
15
  # If there is at least one jedi completion, it is always insert at first.
@@ -68,9 +68,11 @@ def main_worker_process_function(request_queue, result_queue):
68
68
  path = request.get('path', None)
69
69
  multiline = request.get('multiline', False)
70
70
  full = request.get('full', False)
71
- logger.info(f"Performing code completion: language='{language}', multiline={multiline}, path={path}")
71
+ env_path = request.get('env_path', None)
72
+ logger.info(f"Performing code completion: language='{language}', multiline={multiline}, path={path}, env_path={env_path}")
72
73
  completions = worker_functions.complete(
73
- code, cursor_pos, path=path, multiline=multiline, full=full)
74
+ code, cursor_pos, path=path, multiline=multiline, full=full,
75
+ env_path=env_path)
74
76
  if not completions:
75
77
  logger.info("No completions")
76
78
  else:
@@ -90,9 +92,10 @@ def main_worker_process_function(request_queue, result_queue):
90
92
  else:
91
93
  code = request.get('code', '')
92
94
  path = request.get('path', None)
93
- logger.info(f"Performing calltip: language='{language}', path={path}")
95
+ env_path = request.get('env_path', None)
96
+ logger.info(f"Performing calltip: language='{language}', path={path}, env_path={env_path}")
94
97
  signatures = worker_functions.calltip(
95
- code, cursor_pos, path=path)
98
+ code, cursor_pos, path=path, env_path=env_path)
96
99
  if signatures is None:
97
100
  logger.info("No calltip signatures. Sending result back.")
98
101
  else:
@@ -20,7 +20,8 @@ def _signature_to_html(signature) -> str:
20
20
  return f"({sig_line}){return_hint}"
21
21
 
22
22
 
23
- def _prepare_jedi_script(code: str, cursor_position: int, path: str | None):
23
+ def _prepare_jedi_script(code: str, cursor_position: int, path: str | None,
24
+ env_path: str | None):
24
25
  """
25
26
  Prepare a Jedi Script object and calculate line_no/column_no from the
26
27
  given code and cursor_position. Returns (script, line_no, column_no).
@@ -32,15 +33,17 @@ def _prepare_jedi_script(code: str, cursor_position: int, path: str | None):
32
33
  column_no = cursor_position
33
34
  else:
34
35
  column_no = cursor_position - (last_newline_idx + 1)
35
-
36
36
  logger.info("Creating Jedi Script for path=%r at line=%d, column=%d",
37
37
  path, line_no, column_no)
38
-
39
- script = jedi.Script(code, path=path)
38
+ # We explicitly indicate that the environment is safe, because we know that
39
+ # they come from the app itself
40
+ env = jedi.create_environment(env_path, safe=False) if env_path else None
41
+ script = jedi.Script(code, path=path, environment=env)
40
42
  return script, line_no, column_no
41
43
 
42
44
 
43
- def jedi_complete(code: str, cursor_position: int, path: str | None) -> list[str]:
45
+ def jedi_complete(code: str, cursor_position: int, path: str | None,
46
+ env_path: str | None = None) -> list[str]:
44
47
  """
45
48
  Perform Python-specific completion using Jedi. Returns a list of possible completions
46
49
  for the text at the given cursor position, or None if no completion is found.
@@ -57,7 +60,8 @@ def jedi_complete(code: str, cursor_position: int, path: str | None) -> list[str
57
60
  if code_up_to_cursor.rfind('#') > code_up_to_cursor.rfind('\n'):
58
61
  return []
59
62
  # Go Jedi!
60
- script, line_no, column_no = _prepare_jedi_script(code, cursor_position, path)
63
+ script, line_no, column_no = _prepare_jedi_script(code, cursor_position,
64
+ path, env_path)
61
65
  completions = script.complete(line=line_no, column=column_no)
62
66
  if not completions:
63
67
  return []
@@ -70,7 +74,8 @@ def jedi_complete(code: str, cursor_position: int, path: str | None) -> list[str
70
74
 
71
75
  def jedi_signatures(code: str, cursor_position: int, path: str | None,
72
76
  multiline: bool = False, max_width: int = 40,
73
- max_lines: int = 10):
77
+ max_lines: int = 10,
78
+ env_path: str | None = None):
74
79
  """
75
80
  Retrieve function signatures (calltips) from Jedi given the current cursor position.
76
81
  Returns a list of strings describing each signature, or None if none.
@@ -84,7 +89,8 @@ def jedi_signatures(code: str, cursor_position: int, path: str | None,
84
89
  return None
85
90
 
86
91
  logger.info("Starting Jedi calltip request (multiline=%r).", multiline)
87
- script, line_no, column_no = _prepare_jedi_script(code, cursor_position, path)
92
+ script, line_no, column_no = _prepare_jedi_script(code, cursor_position,
93
+ path, env_path)
88
94
 
89
95
  signatures = script.get_signatures(line=line_no, column=column_no)
90
96
  if not signatures: