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,116 @@
1
+ import inspect
2
+ from qtpy.QtCore import QObject, Signal
3
+ import logging
4
+
5
+ logging.basicConfig(level=logging.INFO, force=True)
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class SignalRouter(QObject):
10
+ """
11
+ A central signal router that dynamically discovers and forwards signals.
12
+ """
13
+ # Generic signal that forwards all signals with metadata
14
+ signal_triggered = Signal(str, object)
15
+
16
+ def __init__(self):
17
+ super().__init__()
18
+ self._registered_widgets = set()
19
+ self._signal_map = {} # Maps signal names to widgets
20
+ self._connections = {} # Maps widget IDs to connection info
21
+ logger.info("Signal router initialized")
22
+
23
+ def register_widget(self, widget):
24
+ """Register a widget and all its signals for routing"""
25
+ if widget in self._registered_widgets:
26
+ return
27
+
28
+ self._registered_widgets.add(widget)
29
+ widget_id = id(widget)
30
+ self._connections[widget_id] = []
31
+
32
+ # Connect to destroyed signal for auto cleanup
33
+ widget.destroyed.connect(lambda: self.unregister_widget(widget))
34
+
35
+ logger.info(f"Registering widget {widget.__class__.__name__} for signal routing")
36
+
37
+ # Discover all signals in the widget
38
+ for name, obj in inspect.getmembers(widget.__class__):
39
+ if isinstance(obj, Signal):
40
+ logger.info(f" Discovered signal: {name}")
41
+ self._signal_map[name] = self._signal_map.get(name, []) + [widget]
42
+
43
+ # Create a forwarder function that will be connected to the signal
44
+ def create_forwarder(signal_name=name):
45
+ forwarder = lambda *args: self.signal_triggered.emit(signal_name, args)
46
+ return forwarder
47
+
48
+ forwarder = create_forwarder()
49
+
50
+ # Connect the widget's signal to our forwarder
51
+ try:
52
+ signal = getattr(widget, name)
53
+ signal.connect(forwarder)
54
+
55
+ # Store connection information for cleanup
56
+ self._connections[widget_id].append({
57
+ 'signal': signal,
58
+ 'slot': forwarder,
59
+ 'name': name
60
+ })
61
+ except (RuntimeError, TypeError) as e:
62
+ logger.warning(f"Failed to connect to signal {name}: {e}")
63
+
64
+ def unregister_widget(self, widget):
65
+ """Unregister a widget and disconnect all its signals"""
66
+ widget_id = id(widget)
67
+
68
+ if widget_id in self._connections:
69
+ logger.info(f"Unregistering widget {widget.__class__.__name__}")
70
+
71
+ # Disconnect all signals from this widget
72
+ for connection in self._connections[widget_id]:
73
+ try:
74
+ connection['signal'].disconnect(connection['slot'])
75
+ logger.debug(f"Disconnected signal {connection['name']}")
76
+ except (RuntimeError, TypeError):
77
+ # Signal might already be disconnected or invalid
78
+ pass
79
+
80
+ # Clean up our data structures
81
+ del self._connections[widget_id]
82
+
83
+ # Update signal map to remove this widget
84
+ for signal_name, widgets in list(self._signal_map.items()):
85
+ self._signal_map[signal_name] = [w for w in widgets if id(w) != widget_id]
86
+ # Remove empty entries
87
+ if not self._signal_map[signal_name]:
88
+ del self._signal_map[signal_name]
89
+
90
+ # Try to remove from registered widgets
91
+ try:
92
+ self._registered_widgets.remove(widget)
93
+ except KeyError:
94
+ pass
95
+
96
+ def connect_to_signal(self, signal_name, slot):
97
+ """
98
+ Connect a slot to a specific named signal from any registered widget
99
+ """
100
+ def filtered_forwarder(name, args):
101
+ if name == signal_name:
102
+ # Use *args to unpack the arguments
103
+ if len(args) == 1:
104
+ slot(args[0])
105
+ else:
106
+ slot(*args)
107
+
108
+ self.signal_triggered.connect(filtered_forwarder)
109
+ logger.info(f"Connected external slot to '{signal_name}' signal")
110
+
111
+ def get_available_signals(self):
112
+ """Return a list of all available signals from registered widgets"""
113
+ return list(self._signal_map.keys())
114
+
115
+
116
+ signal_router = SignalRouter()
File without changes
@@ -0,0 +1,86 @@
1
+ from pygments.styles import get_style_by_name
2
+ from pygments.formatters.html import HtmlFormatter
3
+ from qtpy.QtGui import QSyntaxHighlighter, QTextCharFormat, QBrush, QColor, QFont
4
+ import logging
5
+ logging.basicConfig(level=logging.INFO, force=True)
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class SyntaxHighlighter(QSyntaxHighlighter):
10
+ """
11
+ A standalone syntax highlighter that uses Pygments to highlight code
12
+ in a QTextDocument. Merged from the original SyntaxHighlighter and
13
+ PygmentsSH, removing PyQode dependencies and extra logic.
14
+ """
15
+
16
+ def __init__(self, document, lexer, color_scheme=None):
17
+ super().__init__(document)
18
+ self._lexer = lexer
19
+ # If you have a custom ColorScheme, adapt here.
20
+ # If color_scheme is a string, interpret it as a Pygments style name:
21
+ self._color_scheme_name = color_scheme or "default"
22
+
23
+ # Prepare a style from Pygments
24
+ self._setup_style()
25
+ self._token_formats = {}
26
+ self._in_multiline_string = {}
27
+
28
+ def _setup_style(self):
29
+ """Initialize the Pygments style objects."""
30
+ try:
31
+ self._style = get_style_by_name(self._color_scheme_name)
32
+ except Exception:
33
+ logger.error(f"[PygmentsSyntaxHighlighter] style '{self._color_scheme_name}' not found")
34
+ self._style = get_style_by_name("default")
35
+ self._formatter = HtmlFormatter(style=self._style)
36
+
37
+ def highlightBlock(self, text):
38
+ """
39
+ Called automatically by QSyntaxHighlighter for each line in the document.
40
+ We tokenize the line with Pygments, then apply the relevant formats.
41
+ """
42
+ tokens = list(self._lexer.get_tokens(text))
43
+
44
+ index = 0
45
+ for token_type, token_text in tokens:
46
+ length = len(token_text)
47
+ fmt = self._get_format(token_type)
48
+ self.setFormat(index, length, fmt)
49
+ index += length
50
+
51
+
52
+ def _get_format(self, token_type):
53
+ """
54
+ Retrieve (or create) a QTextCharFormat for the given Pygments token type.
55
+ """
56
+ if token_type in self._token_formats:
57
+ return self._token_formats[token_type]
58
+
59
+ fmt = QTextCharFormat()
60
+ style_defs = self._style.style_for_token(token_type)
61
+
62
+ if style_defs['color']:
63
+ color_str = style_defs['color']
64
+ fmt.setForeground(self._make_brush(color_str))
65
+ if style_defs['bgcolor']:
66
+ bg_color_str = style_defs['bgcolor']
67
+ fmt.setBackground(self._make_brush(bg_color_str))
68
+ if style_defs['bold']:
69
+ fmt.setFontWeight(QFont.Bold)
70
+ if style_defs['italic']:
71
+ fmt.setFontItalic(True)
72
+ if style_defs['underline']:
73
+ fmt.setUnderlineStyle(QTextCharFormat.SingleUnderline)
74
+
75
+ self._token_formats[token_type] = fmt
76
+ return fmt
77
+
78
+ def _make_brush(self, color_str):
79
+ """Convert a hex string (e.g. 'fff' or 'ffffff') to a QBrush."""
80
+ color_str = color_str.lstrip('#')
81
+ if len(color_str) == 3:
82
+ color_str = ''.join(ch * 2 for ch in color_str)
83
+ red = int(color_str[0:2], 16)
84
+ green = int(color_str[2:4], 16)
85
+ blue = int(color_str[4:6], 16)
86
+ return QBrush(QColor(red, green, blue))
@@ -0,0 +1,62 @@
1
+ from pygments.token import Token
2
+ import logging
3
+ from .generic import SyntaxHighlighter as GenericSyntaxHighlighter
4
+ logging.basicConfig(level=logging.INFO, force=True)
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class SyntaxHighlighter(GenericSyntaxHighlighter):
9
+ """Extends the default highlighter with support for multiline strings."""
10
+
11
+ def highlightBlock(self, text):
12
+ """
13
+ 1) Determine if this line starts as 'in' or 'out' of a triple-quoted string
14
+ from the previous block's state.
15
+ 2) Scan for triple-quote transitions, toggling in/out state.
16
+ 3) If in triple-quote mode, highlight everything as a string, else use normal
17
+ Pygments highlighting for this line.
18
+ 4) Use self.setCurrentBlockState(...) to mark the final state for this line, so
19
+ that QSyntaxHighlighter can handle subsequent lines correctly.
20
+ """
21
+
22
+ # old_state is typically -1 for the very first block, otherwise what we last set
23
+ old_state = self.previousBlockState()
24
+ previously_in_string = (old_state == 1)
25
+ currently_in_string = previously_in_string
26
+
27
+ # Search line text for triple quotes
28
+ triple_quotes = ('"""', "'''")
29
+ idx = 0
30
+ while True:
31
+ next_quote_pos = -1
32
+ found_quote = None
33
+ for quote in triple_quotes:
34
+ pos = text.find(quote, idx)
35
+ if pos != -1 and (next_quote_pos == -1 or pos < next_quote_pos):
36
+ next_quote_pos = pos
37
+ found_quote = quote
38
+
39
+ if next_quote_pos == -1:
40
+ # No more triple quotes in this line
41
+ break
42
+ else:
43
+ # Toggle the multiline string state
44
+ currently_in_string = not currently_in_string
45
+ # Move index past the found triple quotes
46
+ idx = next_quote_pos + len(found_quote)
47
+
48
+ if currently_in_string:
49
+ # Highlight the entire line as string
50
+ string_format = self._get_format(Token.String)
51
+ self.setFormat(0, len(text), string_format)
52
+ self.setCurrentBlockState(1)
53
+ else:
54
+ # Use normal Pygments line-by-line highlighting
55
+ tokens = list(self._lexer.get_tokens(text))
56
+ index = 0
57
+ for token_type, token_text in tokens:
58
+ token_len = len(token_text)
59
+ fmt = self._get_format(token_type)
60
+ self.setFormat(index, token_len, fmt)
61
+ index += token_len
62
+ self.setCurrentBlockState(0)
@@ -0,0 +1,26 @@
1
+ import importlib
2
+ import logging
3
+ from pygments.lexers import get_lexer_by_name
4
+ logging.basicConfig(level=logging.INFO, force=True)
5
+ logger = logging.getLogger(__name__)
6
+ module_cache = {}
7
+
8
+
9
+ def create_syntax_highlighter(language, *args, **kwargs):
10
+ try:
11
+ lexer = get_lexer_by_name(language)
12
+ except:
13
+ lexer = get_lexer_by_name('markdown')
14
+ if language not in module_cache:
15
+ try:
16
+ module = importlib.import_module(
17
+ f".languages.{language}", package=__package__)
18
+ except ImportError:
19
+ from .languages import generic as module
20
+ logger.info(f'failed to load syntax highlighter module for {language}, falling back to generic')
21
+ else:
22
+ logger.info(f'loaded editor module for {language}')
23
+ module_cache[language] = module
24
+ else:
25
+ module = module_cache[language]
26
+ return module.SyntaxHighlighter(*args, lexer=lexer, **kwargs)
@@ -0,0 +1,36 @@
1
+ THEMES = {
2
+ 'light': {
3
+ 'background_color': '#FFFFFF',
4
+ 'text_color': '#000000',
5
+ 'selection_background': '#ADD6FF',
6
+ 'selection_color': '#000000',
7
+ },
8
+ 'dark': {
9
+ 'background_color': '#1E1E1E',
10
+ 'text_color': '#D4D4D4',
11
+ 'selection_background': '#264F78',
12
+ 'selection_color': '#FFFFFF',
13
+ },
14
+ 'monokai': {
15
+ 'background_color': '#272822',
16
+ 'text_color': '#F8F8F2',
17
+ 'selection_background': '#49483E',
18
+ 'selection_color': '#F8F8F2',
19
+ },
20
+ 'solarized_light': {
21
+ 'background_color': '#FDF6E3',
22
+ 'text_color': '#657B83',
23
+ 'selection_background': '#EEE8D5',
24
+ 'selection_color': '#586E75',
25
+ },
26
+ 'solarized_dark': {
27
+ 'background_color': '#002B36',
28
+ 'text_color': '#839496',
29
+ 'selection_background': '#073642',
30
+ 'selection_color': '#93A1A1',
31
+ }
32
+ }
33
+
34
+ OUTER_CONTENT_MARGINS = [6, 6, 6, 6]
35
+ HORIZONTAL_SPACING = 6
36
+ VERTICAL_SPACING = 12
@@ -0,0 +1,96 @@
1
+ import os
2
+ from pygments.lexers import guess_lexer_for_filename
3
+ from pygments.util import ClassNotFound
4
+
5
+
6
+ def guess_language_from_path(filepath):
7
+ """
8
+ Detect programming language for a given file using Pygments.
9
+ Returns a lowercase string, e.g. 'python', or falls back to 'text'.
10
+ """
11
+ if filepath is None:
12
+ return 'text'
13
+ try:
14
+ # Read entire file as text (note: could be slow for very large files)
15
+ with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
16
+ contents = f.read()
17
+
18
+ # Attempt to guess language
19
+ lexer = guess_lexer_for_filename(filepath, contents)
20
+ return lexer.name.lower()
21
+ except (FileNotFoundError, ClassNotFound, UnicodeDecodeError):
22
+ # If file doesn't exist or we can't guess the language,
23
+ # we fall back to 'text'
24
+ return 'text'
25
+
26
+
27
+ def shorten_paths(paths):
28
+ """
29
+ Given a list of file paths, return a list of shortened names that are unique,
30
+ but only as long as necessary. Files sharing the same base name will get
31
+ additional path components prepended from the right until they are unique.
32
+ """
33
+
34
+ # Split into lists of path components. We also note if the path is absolute.
35
+ splitted = []
36
+ for p in paths:
37
+ # On some systems, you might want to use os.path.normpath first:
38
+ # p = os.path.normpath(p)
39
+ is_abs = p.startswith(os.path.sep)
40
+ parts = p.strip(os.path.sep).split(os.path.sep)
41
+ splitted.append((is_abs, parts))
42
+
43
+ # Group by base name (last component)
44
+ from collections import defaultdict
45
+ base_map = defaultdict(list)
46
+ for i, (is_abs, parts) in enumerate(splitted):
47
+ base_map[parts[-1]].append(i) # group by last component
48
+
49
+ # For each group of files sharing the same base name, expand as necessary
50
+ expansions = [1] * len(splitted) # how many path parts (from the right) we are using
51
+
52
+ def make_short_name(is_abs, parts, count):
53
+ """
54
+ Produce the short name using the last `count` components.
55
+ If the original path was absolute and we use more than 1 component, we
56
+ add a leading slash. (You can tweak this logic as desired.)
57
+ """
58
+ selected = parts[-count:]
59
+ return "/".join(selected)
60
+
61
+
62
+ for base_name, indices in base_map.items():
63
+ if len(indices) == 1:
64
+ # No conflict, no need to expand
65
+ continue
66
+ # We have multiple files with the same base name
67
+ # so keep expanding path components until unique
68
+ done = False
69
+ while not done:
70
+ # Build short names for all members
71
+ used_names = {}
72
+ collisions = False
73
+ for idx in indices:
74
+ is_abs, parts = splitted[idx]
75
+ short = make_short_name(is_abs, parts, expansions[idx])
76
+ if short in used_names:
77
+ collisions = True # we have a collision
78
+ used_names[short] = idx
79
+ if collisions:
80
+ # Increase expansions for all in this group
81
+ for idx in indices:
82
+ expansions[idx] = min(expansions[idx] + 1, len(splitted[idx][1]))
83
+ # If expansions reach the entire path length, it can't expand further.
84
+ # Next loop check might still find collisions if two paths are identical.
85
+ else:
86
+ done = True
87
+
88
+ # Now build the final short names
89
+ results = []
90
+ for i, (is_abs, parts) in enumerate(splitted):
91
+ count = expansions[i]
92
+ short = make_short_name(is_abs, parts, count)
93
+ results.append(short)
94
+
95
+ return results
96
+
@@ -0,0 +1,141 @@
1
+ """This module implements a watchdog that ensures that subprocess are killed
2
+ when the main process is no longer alive. It does so by starting a separate
3
+ watchdog process that occasionally checks whether the main process is still
4
+ alive.
5
+
6
+ To handle unexpected crashes of the application, it periodically checks whether
7
+ the main process is still alive. When the main process has died, it first kills
8
+ all registerd subprocesses, and then shuts itself down.
9
+
10
+ To handle clean application exist, it also listens for an explicit shutdown
11
+ signal, in which case it does the same.
12
+
13
+ In all cases, the watchdog should make sure that no subprocesses, including
14
+ itself, stay alive after the main process has died.
15
+ """
16
+ import multiprocessing as mp
17
+ import os
18
+ import signal
19
+ import sys
20
+ import time
21
+ import logging
22
+ logging.basicConfig(level=logging.INFO, force=True)
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # Global variables
27
+ _main_pid = os.getpid()
28
+ logger.info(f'main process is {_main_pid}')
29
+ _watchdog_process = None
30
+ _conn = None
31
+ _registered_pids = set()
32
+ _CHECK_INTERVAL = 1.0 # seconds
33
+
34
+
35
+ def _watchdog_main(main_pid, child_conn):
36
+ """The main function of the watchdog process."""
37
+
38
+ registered_pids = set()
39
+
40
+ while True:
41
+ # Check if main process is still alive
42
+ if not _is_process_alive(main_pid):
43
+ logger.info("watchdog: Main process died, killing all subprocesses")
44
+ _kill_all_processes(registered_pids)
45
+ sys.exit(0)
46
+
47
+ # Check for messages from the main process
48
+ if child_conn.poll():
49
+ msg = child_conn.recv()
50
+ if msg["command"] == "shutdown":
51
+ logger.info("watchdog: Received shutdown command")
52
+ _kill_all_processes(registered_pids)
53
+ sys.exit(0)
54
+ elif msg["command"] == "register":
55
+ pid = msg["pid"]
56
+ if pid not in registered_pids:
57
+ registered_pids.add(pid)
58
+ logger.info(f"watchdog: Registered subprocess {pid}")
59
+
60
+ time.sleep(_CHECK_INTERVAL)
61
+
62
+
63
+ def _is_process_alive(pid):
64
+ """Check if a process with the given pid is alive."""
65
+ try:
66
+ # Sending signal 0 checks if process exists without affecting it
67
+ os.kill(pid, 0)
68
+ return True
69
+ except OSError:
70
+ return False
71
+
72
+
73
+ def _kill_all_processes(pids):
74
+ """Kill all processes with the given pids."""
75
+ for pid in pids:
76
+ try:
77
+ # Try SIGTERM first
78
+ os.kill(pid, signal.SIGTERM)
79
+ logger.info(f"watchdog: Sent SIGTERM to {pid}")
80
+ except OSError:
81
+ pass
82
+
83
+ # Give processes time to exit gracefully
84
+ time.sleep(0.5)
85
+
86
+ # Force kill any remaining processes
87
+ for pid in pids:
88
+ try:
89
+ if _is_process_alive(pid):
90
+ os.kill(pid, signal.SIGKILL)
91
+ logger.info(f"watchdog: Sent SIGKILL to {pid}")
92
+ except OSError:
93
+ pass
94
+
95
+
96
+ def _ensure_watchdog_running():
97
+ """Ensure that the watchdog process is running."""
98
+ global _watchdog_process, _conn
99
+
100
+ if _watchdog_process is None and _main_pid is not None:
101
+ parent_conn, child_conn = mp.Pipe()
102
+ _watchdog_process = mp.Process(
103
+ target=_watchdog_main,
104
+ args=(_main_pid, child_conn),
105
+ name="watchdog-process"
106
+ )
107
+ _watchdog_process.start()
108
+ _conn = parent_conn
109
+ logger.info(f"watchdog: Started watchdog process with PID {_watchdog_process.pid}")
110
+
111
+
112
+ def set_main_process(pid: int):
113
+ """Sets the main process pid. This does not yet launch the watchdog process."""
114
+ global _main_pid
115
+ _main_pid = pid
116
+
117
+
118
+ def register_subprocess(pid: int):
119
+ """If no watchdog process is running, this is launched now. The pid is
120
+ registered with the watchdog.
121
+ """
122
+ global _registered_pids
123
+
124
+ if _main_pid is None:
125
+ raise RuntimeError("Main process PID not set. Call set_main_process() first.")
126
+
127
+ _ensure_watchdog_running()
128
+ _registered_pids.add(pid)
129
+
130
+ # Send registration to watchdog process
131
+ if _conn is not None:
132
+ _conn.send({"command": "register", "pid": pid})
133
+
134
+
135
+ def shutdown():
136
+ """Explicitly tell the watchdog to kill all subprocesses."""
137
+ logger.info('shutdown')
138
+ if _conn is not None:
139
+ _conn.send({"command": "shutdown"})
140
+ # Give watchdog time to process the shutdown command
141
+ time.sleep(0.1)
@@ -0,0 +1,6 @@
1
+ from .quick_open_dialog import QuickOpenDialog
2
+ from .quick_open_file_dialog import QuickOpenFileDialog
3
+ from .quick_symbol_dialog import QuickSymbolDialog
4
+ from .tabbed_editor import TabbedEditor
5
+ from .tab_splitter import TabSplitter
6
+ from .dock import Dock
@@ -0,0 +1,44 @@
1
+ from qtpy.QtCore import Qt
2
+ from qtpy.QtWidgets import QFrame, QLabel, QVBoxLayout
3
+ from .. import settings
4
+
5
+
6
+ class CalltipWidget(QFrame):
7
+ """
8
+ A small persistent widget to show calltip text so it won't vanish
9
+ until explicitly hidden (unlike QToolTip).
10
+ """
11
+ def __init__(self, editor):
12
+ # We set window flags separately (cannot OR widget attributes).
13
+ super().__init__(editor)
14
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool)
15
+ # WA_ShowWithoutActivating means the widget won’t take focus
16
+ self.setAttribute(Qt.WA_ShowWithoutActivating)
17
+ # Make background translucent (use setAttribute):
18
+ self.setAttribute(Qt.WA_TranslucentBackground, True)
19
+ self.apply_stylesheet()
20
+ layout = QVBoxLayout(self)
21
+ layout.setContentsMargins(8, 4, 8, 4)
22
+ self._label = QLabel(self)
23
+ self._label.setTextFormat(Qt.RichText)
24
+ self._label.setWordWrap(True)
25
+ layout.addWidget(self._label)
26
+ self.setLayout(layout)
27
+
28
+ def setText(self, text):
29
+ self._label.setText(text)
30
+ self.adjustSize()
31
+
32
+ def apply_stylesheet(self):
33
+ editor = self.parent()
34
+ if editor.code_editor_colors is not None:
35
+ self.setStyleSheet(f'''QFrame {{
36
+ color: {editor.code_editor_colors['text']};
37
+ background-color: {editor.code_editor_colors['background']};
38
+ font: {settings.font_size}pt '{settings.font_family}';
39
+ border-color: {editor.code_editor_colors['border']};
40
+ border-width: 1px;
41
+ border-style: solid;
42
+ border-radius: 4px;
43
+ padding: 8px;
44
+ }}''')