pyqt-code-editor 0.0.27__tar.gz → 0.0.29__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 (81) hide show
  1. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/PKG-INFO +1 -1
  2. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/__init__.py +1 -1
  3. pyqt_code_editor-0.0.29/pyqt_code_editor/environment_manager.py +37 -0
  4. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/base.py +4 -2
  5. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/check.py +5 -5
  6. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/complete.py +4 -2
  7. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/languages/generic.py +2 -2
  8. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/languages/python.py +4 -3
  9. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/manager.py +8 -2
  10. pyqt_code_editor-0.0.29/pyqt_code_editor/worker/process.py +215 -0
  11. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/providers/codestral.py +6 -1
  12. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/providers/jedi.py +12 -5
  13. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/providers/ruff.py +9 -2
  14. pyqt_code_editor-0.0.27/pyqt_code_editor/environment_manager.py +0 -37
  15. pyqt_code_editor-0.0.27/pyqt_code_editor/worker/process.py +0 -130
  16. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/COPYING +0 -0
  17. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyproject.toml +0 -0
  18. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/_settings.py +0 -0
  19. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/app.py +0 -0
  20. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/__init__.py +0 -0
  21. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  22. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  23. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
  24. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  25. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/languages/python.py +0 -0
  26. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  27. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/__init__.py +0 -0
  28. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/editor_panel.py +0 -0
  29. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/find_in_files.py +0 -0
  30. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/jupyter_console.py +0 -0
  31. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/project_explorer.py +0 -0
  32. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/settings_panel.py +0 -0
  33. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/sigmund.py +0 -0
  34. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  35. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/__init__.py +0 -0
  36. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/auto_indent.py +0 -0
  37. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  38. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/comment.py +0 -0
  39. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/execute.py +0 -0
  40. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/file_link.py +0 -0
  41. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/line_number.py +0 -0
  42. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
  43. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/search_replace.py +0 -0
  44. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  45. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  46. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/symbols.py +0 -0
  47. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/theme.py +0 -0
  48. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  49. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/mixins/zoom.py +0 -0
  50. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/sigmund-analyst.png +0 -0
  51. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/signal_router.py +0 -0
  52. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  53. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  54. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  55. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  56. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  57. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/themes.py +0 -0
  58. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/utils/__init__.py +0 -0
  59. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/utils/languages/__init__.py +0 -0
  60. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
  61. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
  62. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
  63. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +0 -0
  64. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/watchdog.py +0 -0
  65. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/__init__.py +0 -0
  66. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  67. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  68. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/dock.py +0 -0
  69. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  70. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  71. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  72. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  73. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  74. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  75. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/__init__.py +0 -0
  76. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  77. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  78. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  79. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  80. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/pyqt_code_editor/worker/settings.py +0 -0
  81. {pyqt_code_editor-0.0.27 → pyqt_code_editor-0.0.29}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqt_code_editor
3
- Version: 0.0.27
3
+ Version: 0.0.29
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.27'
5
+ __version__ = '0.0.29'
@@ -0,0 +1,37 @@
1
+ import os
2
+ from qtpy.QtCore import Signal, QObject
3
+ import logging
4
+ logger = logging.getLogger(__name__)
5
+
6
+ class EnvironmentManager(QObject):
7
+ """Singleton class to share the current environment and language between all
8
+ components of the app.
9
+ """
10
+ environment_changed = Signal(str, str, str, str)
11
+
12
+ def __init__(self):
13
+ super().__init__()
14
+ self.name = None
15
+ self.path = None
16
+ self.language = None
17
+ self.prefix = None
18
+
19
+ @property
20
+ def current_environment(self):
21
+ return self.name, self.path, self.language
22
+
23
+ def set_environment(self, name, path, language, prefix=None):
24
+ if path != self.path:
25
+ # The path may not exist if the environment is simply python or
26
+ # python3, in which case we do not need to specify it at all.
27
+ if not os.path.exists(path):
28
+ path = None
29
+ self.name = name
30
+ self.path = path
31
+ self.language = language
32
+ self.prefix = prefix
33
+ self.environment_changed.emit(name, path, language, prefix)
34
+
35
+
36
+ # Singleton instance
37
+ environment_manager = EnvironmentManager()
@@ -125,9 +125,11 @@ class Base:
125
125
  # 6) We're done with this particular request
126
126
  to_remove.append(pid)
127
127
 
128
- # Cleanup: remove completed or dead requests
128
+ # Cleanup: remove completed or dead requests. Also stop unused workers.
129
+ # This function periodically prunes worker processes if they are unused.
129
130
  for pid in to_remove:
130
- self._active_requests.pop(pid, None)
131
+ self._active_requests.pop(pid, None)
132
+ manager.stop_unused_workers()
131
133
 
132
134
  def set_modified(self, modified):
133
135
  logger.info(f'modified: {modified}')
@@ -1,7 +1,7 @@
1
- from qtpy.QtWidgets import QWidget, QPlainTextEdit
2
- from qtpy.QtCore import Qt, QRect, QPoint, QSize, QTimer
3
- from qtpy.QtGui import QPainter, QColor, QBrush
4
1
  import logging
2
+ from qtpy.QtCore import QTimer
3
+ from qtpy.QtGui import QColor
4
+ from ..environment_manager import environment_manager
5
5
  logger = logging.getLogger(__name__)
6
6
 
7
7
 
@@ -50,8 +50,8 @@ class Check:
50
50
  self.send_worker_request(
51
51
  action='check',
52
52
  code=code,
53
- language=getattr(self, "code_editor_language", "python")
54
- )
53
+ language=getattr(self, "code_editor_language", "python"),
54
+ prefix=environment_manager.prefix)
55
55
 
56
56
  def handle_worker_result(self, action: str, result):
57
57
  """
@@ -276,7 +276,8 @@ class Complete:
276
276
  multiline=multiline,
277
277
  path=self.code_editor_file_path,
278
278
  language=self.code_editor_language,
279
- env_path=environment_manager.path)
279
+ env_path=environment_manager.path,
280
+ prefix=environment_manager.prefix)
280
281
 
281
282
  def _cm_request_calltip(self):
282
283
  """Send a calltip request."""
@@ -288,7 +289,8 @@ class Complete:
288
289
  cursor_pos=cursor_pos,
289
290
  path=self.code_editor_file_path,
290
291
  language=self.code_editor_language,
291
- env_path=environment_manager.path)
292
+ env_path=environment_manager.path,
293
+ prefix=environment_manager.prefix)
292
294
 
293
295
  def _cm_hide_calltip(self):
294
296
  """Hide the calltip widget."""
@@ -1,9 +1,9 @@
1
1
  from ..providers import symbol, codestral
2
2
 
3
- def complete(code, cursor_pos, path, multiline, full, env_path):
3
+ def complete(code, cursor_pos, path, multiline, full, env_path, prefix):
4
4
  if full or multiline:
5
5
  completions = codestral.codestral_complete(
6
- code, cursor_pos, multiline=multiline)
6
+ code, cursor_pos, multiline=multiline, prefix=prefix)
7
7
  else:
8
8
  completions = []
9
9
  if not multiline:
@@ -1,15 +1,16 @@
1
1
  from ..providers import jedi, codestral, ruff
2
2
 
3
3
 
4
- def complete(code, cursor_pos, path, multiline, full, env_path):
4
+ def complete(code, cursor_pos, path, multiline, full, env_path, prefix):
5
5
  if full or multiline:
6
6
  codestral_completions = codestral.codestral_complete(
7
- code, cursor_pos, multiline=multiline)
7
+ code, cursor_pos, multiline=multiline, prefix=prefix)
8
8
  else:
9
9
  codestral_completions = []
10
10
  if not multiline:
11
11
  jedi_completions = jedi.jedi_complete(code, cursor_pos, path=path,
12
- env_path=env_path)
12
+ env_path=env_path,
13
+ prefix=prefix)
13
14
  else:
14
15
  jedi_completions = []
15
16
  # If there is at least one jedi completion, it is always insert at first.
@@ -1,9 +1,11 @@
1
1
  import logging
2
+ import time
2
3
  from multiprocessing import Process, Queue
3
4
  from .process import main_worker_process_function
4
5
  from .. import watchdog, settings
5
6
  logger = logging.getLogger(__name__)
6
-
7
+ STOP_UNUSED_INTERVAL = 10
8
+ _last_stop_unused_time = 0
7
9
  _workers = {} # pid -> {"process", "request_queue", "result_queue", "is_free"}
8
10
 
9
11
 
@@ -63,11 +65,15 @@ def check_worker_alive(pid: int) -> bool:
63
65
  w = _workers.get(pid)
64
66
  return w and w["process"].is_alive()
65
67
 
66
- def stop_unused_workers(max_free: int = 1):
68
+ def stop_unused_workers(max_free: int = 1, force: bool = False):
67
69
  """
68
70
  Stop free worker processes until there is at most 'max_free' free processes left.
69
71
  This keeps us from accumulating too many idle worker processes.
70
72
  """
73
+ global _last_stop_unused_time
74
+ if force or time.time() - _last_stop_unused_time < STOP_UNUSED_INTERVAL:
75
+ return
76
+ _last_stop_unused_time = time.time()
71
77
  # Gather a list of free worker PIDs
72
78
  free_pids = [pid for pid, w in _workers.items() if w["is_free"] and w["process"].is_alive()]
73
79
  logger.info(f"stop_unused_workers called: max_free={max_free}, found {len(free_pids)} free workers")
@@ -0,0 +1,215 @@
1
+ import logging
2
+ import importlib
3
+ from . import settings
4
+ import queue
5
+ logger = logging.getLogger(__name__)
6
+ worker_functions_cache = {}
7
+
8
+
9
+ def main_worker_process_function(request_queue, result_queue):
10
+ """
11
+ Main worker process for handling editor backend requests.
12
+
13
+ Runs in a separate process, continuously processing requests from a queue
14
+ and sending results back through another queue. Supports code completion,
15
+ calltips, symbol extraction, code checking, and settings management.
16
+
17
+ Parameters
18
+ ----------
19
+ request_queue : multiprocessing.Queue
20
+ Queue containing request dictionaries to process
21
+ result_queue : multiprocessing.Queue
22
+ Queue where results are sent back to the main process
23
+
24
+ Request Format
25
+ --------------
26
+ Each request must be a dictionary with the following structure:
27
+
28
+ Required fields:
29
+ action : str
30
+ The action to perform. One of: 'complete', 'calltip', 'symbols',
31
+ 'check', 'set_settings', 'quit'
32
+
33
+ Optional fields (depending on action):
34
+ language : str, default 'text'
35
+ Programming language for syntax-aware processing
36
+ code : str, default ''
37
+ Source code content to analyze
38
+ cursor_pos : int, default 0
39
+ Character position of cursor in the code
40
+ path : str, optional
41
+ File path for context-aware processing
42
+ multiline : bool, default False
43
+ Whether to enable multiline completion mode
44
+ full : bool, default False
45
+ Whether to return full completion details
46
+ env_path : str, optional
47
+ Path to Python environment for context
48
+ prefix : str, optional
49
+ Prefix string for filtering results
50
+ settings : dict, optional
51
+ Settings to update (only for 'set_settings' action)
52
+
53
+ Response Format
54
+ ---------------
55
+ Results are dictionaries placed on result_queue with action-specific content:
56
+
57
+ For 'complete' action:
58
+ {
59
+ 'action': 'complete',
60
+ 'completions': list or None,
61
+ 'cursor_pos': int,
62
+ 'multiline': bool,
63
+ 'full': bool
64
+ }
65
+
66
+ For 'calltip' action:
67
+ {
68
+ 'action': 'calltip',
69
+ 'signatures': list or None,
70
+ 'cursor_pos': int
71
+ }
72
+
73
+ For 'symbols' action:
74
+ {
75
+ 'action': 'symbols',
76
+ 'symbols': list
77
+ }
78
+
79
+ For 'check' action:
80
+ {
81
+ 'action': 'check',
82
+ 'messages': dict
83
+ }
84
+
85
+ Behavior
86
+ --------
87
+ - Runs indefinitely until receiving a 'quit' action
88
+ - Dynamically loads language-specific worker modules on first use
89
+ - Caches imported worker modules for efficiency
90
+ - Falls back to generic worker functions if language-specific module unavailable
91
+ - Skips invalid requests (None, non-dict, or missing 'action' field)
92
+ - Logs all major operations and errors
93
+ - For 'set_settings' action, updates the settings module attributes directly
94
+ - Worker modules must provide functions: complete, calltip, symbols, check
95
+ (functions can be None if not supported for that language)
96
+
97
+ Notes
98
+ -----
99
+ This function is designed to run in a separate process to avoid blocking
100
+ the main editor thread during potentially slow operations like code analysis.
101
+ Language-specific worker modules are expected to be in the 'languages'
102
+ subpackage.
103
+ """
104
+ logger.info("Started completion worker.")
105
+ while True:
106
+ try:
107
+ request = request_queue.get(True, 5)
108
+ except queue.Empty:
109
+ continue
110
+ if request is None:
111
+ logger.info("Received None request (possibly legacy or invalid). Skipping.")
112
+ continue
113
+ # Expect a dict with at least an 'action' field
114
+ if not isinstance(request, dict):
115
+ logger.info(f"Invalid request type: {type(request)}. Skipping.")
116
+ continue
117
+ # Most of the parameters apply to multiple actions, so we extract them
118
+ # here.
119
+ action = request.get('action', None)
120
+ language = request.get('language', 'text')
121
+ code = request.get('code', '')
122
+ cursor_pos = request.get('cursor_pos', 0)
123
+ path = request.get('path', None)
124
+ multiline = request.get('multiline', False)
125
+ full = request.get('full', False)
126
+ env_path = request.get('env_path', None)
127
+ prefix = request.get('prefix', None)
128
+ if action is None:
129
+ logger.info("Request is missing 'action' field. Skipping.")
130
+ continue
131
+
132
+ logger.info(f"Received request action='{action}'")
133
+ if action == 'set_settings':
134
+ for name, value in request.get('settings', {}).items():
135
+ setattr(settings, name, value)
136
+ continue
137
+ if action == 'quit':
138
+ logger.info("Received 'quit' action. Worker will shut down.")
139
+ break
140
+
141
+ # Load the worker functions depending on the language. We store the
142
+ # imported module in a cache for efficiency
143
+ if language not in worker_functions_cache:
144
+ try:
145
+ worker_functions = importlib.import_module(
146
+ f".languages.{language}", package=__package__)
147
+ except ImportError:
148
+ from .languages import generic as worker_functions
149
+ logger.info(f'failed to load worker functions for {language}, falling back to generic')
150
+ else:
151
+ logger.info(f'loaded worker functions for {language}')
152
+ worker_functions_cache[language] = worker_functions
153
+ else:
154
+ worker_functions = worker_functions_cache[language]
155
+
156
+ if action == 'complete':
157
+ # Action not supported for language
158
+ if worker_functions.complete is None:
159
+ completions = None
160
+ else:
161
+ logger.info(f"Performing code completion: language='{language}', multiline={multiline}, path={path}, env_path={env_path}")
162
+ completions = worker_functions.complete(
163
+ code, cursor_pos, path=path, multiline=multiline, full=full,
164
+ env_path=env_path, prefix=prefix)
165
+ if not completions:
166
+ logger.info("No completions")
167
+ else:
168
+ logger.info(f"Generated {len(completions)} completions")
169
+ result_queue.put({
170
+ 'action': 'complete',
171
+ 'completions': completions,
172
+ 'cursor_pos': cursor_pos,
173
+ 'multiline': multiline,
174
+ 'full': full
175
+ })
176
+
177
+ elif action == 'calltip':
178
+ if worker_functions.calltip is None:
179
+ signatures = None
180
+ else:
181
+ logger.info(f"Performing calltip: language='{language}', path={path}, env_path={env_path}")
182
+ signatures = worker_functions.calltip(
183
+ code, cursor_pos, path=path, env_path=env_path,
184
+ prefix=prefix)
185
+ if signatures is None:
186
+ logger.info("No calltip signatures. Sending result back.")
187
+ else:
188
+ logger.info(f"Retrieved {len(signatures)} calltip signatures.")
189
+ result_queue.put({
190
+ 'action': 'calltip',
191
+ 'signatures': signatures,
192
+ 'cursor_pos': cursor_pos
193
+ })
194
+
195
+ elif action == 'symbols':
196
+ if worker_functions.symbols is None:
197
+ symbols_results = []
198
+ else:
199
+ symbols_results = worker_functions.symbols(code)
200
+ result_queue.put({
201
+ 'action': 'symbols',
202
+ 'symbols': symbols_results
203
+ })
204
+
205
+ elif action == 'check':
206
+ if worker_functions.check is None:
207
+ check_results = {}
208
+ else:
209
+ check_results = worker_functions.check(code, prefix=prefix)
210
+ result_queue.put({
211
+ 'action': 'check',
212
+ 'messages': check_results
213
+ })
214
+
215
+ logger.info("Completion worker has shut down.")
@@ -6,7 +6,8 @@ client = None
6
6
 
7
7
 
8
8
  def codestral_complete(code: str, cursor_pos: int,
9
- multiline: bool = False) -> list[str]:
9
+ multiline: bool = False,
10
+ prefix: str | None = None) -> list[str]:
10
11
  global client
11
12
 
12
13
  if not settings.codestral_api_key:
@@ -22,6 +23,10 @@ def codestral_complete(code: str, cursor_pos: int,
22
23
  start = max(0, cursor_pos - settings.codestral_max_context)
23
24
  end = cursor_pos + settings.codestral_max_context
24
25
  prompt = code[start: cursor_pos]
26
+ if prefix:
27
+ if not prefix.endswith('\n'):
28
+ prefix += '\n'
29
+ prompt = prefix + prompt
25
30
  suffix = code[cursor_pos: end]
26
31
 
27
32
  request = dict(
@@ -21,11 +21,16 @@ def _signature_to_html(signature) -> str:
21
21
 
22
22
 
23
23
  def _prepare_jedi_script(code: str, cursor_position: int, path: str | None,
24
- env_path: str | None):
24
+ env_path: str | None, prefix: str | None = None):
25
25
  """
26
26
  Prepare a Jedi Script object and calculate line_no/column_no from the
27
27
  given code and cursor_position. Returns (script, line_no, column_no).
28
28
  """
29
+ if prefix:
30
+ if not prefix.endswith('\n'):
31
+ prefix += '\n'
32
+ code = prefix + code
33
+ cursor_position += len(prefix)
29
34
  # Convert the flat cursor_position into line & column (1-based indexing for Jedi)
30
35
  line_no = code[:cursor_position].count('\n') + 1
31
36
  last_newline_idx = code.rfind('\n', 0, cursor_position)
@@ -43,7 +48,8 @@ def _prepare_jedi_script(code: str, cursor_position: int, path: str | None,
43
48
 
44
49
 
45
50
  def jedi_complete(code: str, cursor_position: int, path: str | None,
46
- env_path: str | None = None) -> list[str]:
51
+ env_path: str | None = None,
52
+ prefix: str | None = None) -> list[str]:
47
53
  """
48
54
  Perform Python-specific completion using Jedi. Returns a list of possible completions
49
55
  for the text at the given cursor position, or None if no completion is found.
@@ -61,7 +67,7 @@ def jedi_complete(code: str, cursor_position: int, path: str | None,
61
67
  return []
62
68
  # Go Jedi!
63
69
  script, line_no, column_no = _prepare_jedi_script(code, cursor_position,
64
- path, env_path)
70
+ path, env_path, prefix)
65
71
  completions = script.complete(line=line_no, column=column_no)
66
72
  if not completions:
67
73
  return []
@@ -75,7 +81,8 @@ def jedi_complete(code: str, cursor_position: int, path: str | None,
75
81
  def jedi_signatures(code: str, cursor_position: int, path: str | None,
76
82
  multiline: bool = False, max_width: int = 40,
77
83
  max_lines: int = 10,
78
- env_path: str | None = None):
84
+ env_path: str | None = None,
85
+ prefix: str | None = None) -> list[str]:
79
86
  """
80
87
  Retrieve function signatures (calltips) from Jedi given the current cursor position.
81
88
  Returns a list of strings describing each signature, or None if none.
@@ -90,7 +97,7 @@ def jedi_signatures(code: str, cursor_position: int, path: str | None,
90
97
 
91
98
  logger.info("Starting Jedi calltip request (multiline=%r).", multiline)
92
99
  script, line_no, column_no = _prepare_jedi_script(code, cursor_position,
93
- path, env_path)
100
+ path, env_path, prefix)
94
101
 
95
102
  signatures = script.get_signatures(line=line_no, column=column_no)
96
103
  if not signatures:
@@ -6,13 +6,20 @@ import logging
6
6
  logger = logging.getLogger(__name__)
7
7
 
8
8
 
9
- def ruff_check(code: str) -> list[dict]:
9
+ def ruff_check(code: str, prefix: str | None = None) -> list[dict]:
10
10
  """
11
11
  Lints Python source code using Ruff.
12
12
 
13
13
  Args:
14
14
  code (str): The Python source code to lint.
15
15
  """
16
+ if prefix:
17
+ if not prefix.endswith('\n'):
18
+ prefix += '\n'
19
+ code = prefix + code
20
+ prefix_line_offset = prefix.count('\n')
21
+ else:
22
+ prefix_line_offset = 0
16
23
  with tempfile.NamedTemporaryFile(delete=False, suffix=".py", mode="w") as tmp_file:
17
24
  tmp_file.write(code)
18
25
  tmp_file_path = tmp_file.name
@@ -35,7 +42,7 @@ def ruff_check(code: str) -> list[dict]:
35
42
  return {}
36
43
  formatted_result = {}
37
44
  for message in result:
38
- row = message['location']['row']
45
+ row = message['location']['row'] - prefix_line_offset
39
46
  if row not in formatted_result:
40
47
  formatted_result[row] = []
41
48
  formatted_result[row].append({
@@ -1,37 +0,0 @@
1
- import os
2
- from qtpy.QtCore import Signal, QObject
3
- import logging
4
- logger = logging.getLogger(__name__)
5
-
6
- class EnvironmentManager(QObject):
7
- """Singleton class to share the current environment and language between all
8
- components of the app.
9
- """
10
- environment_changed = Signal(str, str, str)
11
-
12
- def __init__(self):
13
- super().__init__()
14
- self.name = None
15
- self.path = None
16
- self.language = None
17
-
18
- @property
19
- def current_environment(self):
20
- return self.name, self.path, self.language
21
-
22
- def set_environment(self, name, path, language):
23
- path = os.path.abspath(path)
24
- if path == self.path:
25
- return
26
- if not os.path.exists(path):
27
- logger.warning(f'no such environment: {path}')
28
- return
29
- logger.info(f'environment changed: {path}')
30
- self.name = name
31
- self.path = path
32
- self.language = language
33
- self.environment_changed.emit(name, path, language)
34
-
35
-
36
- # Singleton instance
37
- environment_manager = EnvironmentManager()
@@ -1,130 +0,0 @@
1
- import logging
2
- import importlib
3
- from . import settings
4
- logger = logging.getLogger(__name__)
5
- worker_functions_cache = {}
6
-
7
-
8
- def main_worker_process_function(request_queue, result_queue):
9
- """
10
- Runs in a separate process, handling requests in dict form.
11
- Supported actions include:
12
- - 'complete': triggers code completion
13
- - 'calltip': fetches calltip/signature info
14
- - 'check': fetches code check/ linting info
15
- - 'setting': updates settings in the 'settings' module
16
- - 'quit': shuts down the worker
17
- """
18
- logger.info("Started completion worker.")
19
- while True:
20
- request = request_queue.get()
21
- if request is None:
22
- logger.info("Received None request (possibly legacy or invalid). Skipping.")
23
- continue
24
-
25
- # Expect a dict with at least an 'action' field
26
- if not isinstance(request, dict):
27
- logger.info(f"Invalid request type: {type(request)}. Skipping.")
28
- continue
29
-
30
- action = request.get('action', None)
31
- if action is None:
32
- logger.info("Request is missing 'action' field. Skipping.")
33
- continue
34
-
35
- logger.info(f"Received request action='{action}'")
36
- if action == 'set_settings':
37
- for name, value in request.get('settings', {}).items():
38
- setattr(settings, name, value)
39
- continue
40
- if action == 'quit':
41
- logger.info("Received 'quit' action. Worker will shut down.")
42
- break
43
-
44
- # Load the worker functions depending on the language. We store the
45
- # imported module in a cache for efficiency
46
- language = request.get('language', 'text')
47
- if language not in worker_functions_cache:
48
- try:
49
- worker_functions = importlib.import_module(
50
- f".languages.{language}", package=__package__)
51
- except ImportError:
52
- from .languages import generic as worker_functions
53
- logger.info(f'failed to load worker functions for {language}, falling back to generic')
54
- else:
55
- logger.info(f'loaded worker functions for {language}')
56
- worker_functions_cache[language] = worker_functions
57
- else:
58
- worker_functions = worker_functions_cache[language]
59
-
60
- if action == 'complete':
61
- # Action not supported for language
62
- if worker_functions.complete is None:
63
- completions = None
64
- else:
65
- code = request.get('code', '')
66
- cursor_pos = request.get('cursor_pos', 0)
67
- path = request.get('path', None)
68
- multiline = request.get('multiline', False)
69
- full = request.get('full', False)
70
- env_path = request.get('env_path', None)
71
- logger.info(f"Performing code completion: language='{language}', multiline={multiline}, path={path}, env_path={env_path}")
72
- completions = worker_functions.complete(
73
- code, cursor_pos, path=path, multiline=multiline, full=full,
74
- env_path=env_path)
75
- if not completions:
76
- logger.info("No completions")
77
- else:
78
- logger.info(f"Generated {len(completions)} completions")
79
- result_queue.put({
80
- 'action': 'complete',
81
- 'completions': completions,
82
- 'cursor_pos': cursor_pos,
83
- 'multiline': multiline,
84
- 'full': full
85
- })
86
-
87
- elif action == 'calltip':
88
- cursor_pos = request.get('cursor_pos', 0)
89
- if worker_functions.calltip is None:
90
- signatures = None
91
- else:
92
- code = request.get('code', '')
93
- path = request.get('path', None)
94
- env_path = request.get('env_path', None)
95
- logger.info(f"Performing calltip: language='{language}', path={path}, env_path={env_path}")
96
- signatures = worker_functions.calltip(
97
- code, cursor_pos, path=path, env_path=env_path)
98
- if signatures is None:
99
- logger.info("No calltip signatures. Sending result back.")
100
- else:
101
- logger.info(f"Retrieved {len(signatures)} calltip signatures.")
102
- result_queue.put({
103
- 'action': 'calltip',
104
- 'signatures': signatures,
105
- 'cursor_pos': cursor_pos
106
- })
107
-
108
- elif action == 'symbols':
109
- if worker_functions.symbols is None:
110
- symbols_results = []
111
- else:
112
- code = request.get('code', '')
113
- symbols_results = worker_functions.symbols(code)
114
- result_queue.put({
115
- 'action': 'symbols',
116
- 'symbols': symbols_results
117
- })
118
-
119
- elif action == 'check':
120
- if worker_functions.check is None:
121
- check_results = {}
122
- else:
123
- code = request.get('code', '')
124
- check_results = worker_functions.check(code)
125
- result_queue.put({
126
- 'action': 'check',
127
- 'messages': check_results
128
- })
129
-
130
- logger.info("Completion worker has shut down.")