pyqt-code-editor 0.0.2__tar.gz → 0.0.3__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.
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/PKG-INFO +2 -1
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyproject.toml +1 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/__init__.py +1 -1
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/_settings.py +1 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/app.py +35 -23
- pyqt_code_editor-0.0.3/pyqt_code_editor/code_editors/languages/generic.py +9 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/editor_panel.py +5 -1
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/__init__.py +1 -0
- pyqt_code_editor-0.0.3/pyqt_code_editor/mixins/auto_indent.py +267 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/symbols.py +1 -1
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/python_utils.py +4 -4
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +10 -2
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -1
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/tabbed_editor.py +7 -2
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/process.py +3 -3
- pyqt_code_editor-0.0.2/pyqt_code_editor/code_editors/languages/generic.py +0 -11
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/COPYING +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/code_editors.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/languages/python.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/languages/text.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/find_in_files.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/jupyter_console.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/project_explorer.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/settings_panel.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/sigmund.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/workspace_explorer.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/auto_pair.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/base.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/check.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/comment.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/complete.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/execute.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/file_link.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/line_number.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/search_replace.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/shortcuts.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/theme.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/zoom.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/signal_router.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/themes.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/utils.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/watchdog.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/completion_popup.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/dock.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/languages/generic.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/languages/python.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/manager.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/__init__.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/codestral.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/jedi.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/ruff.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/symbol.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/settings.py +0 -0
- {pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/readme.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyqt_code_editor
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
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>
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: COPYING
|
|
10
10
|
Requires-Dist: chardet
|
|
11
|
+
Requires-Dist: detect-indent
|
|
11
12
|
Requires-Dist: jedi
|
|
12
13
|
Requires-Dist: mistralai
|
|
13
14
|
Requires-Dist: pathspec
|
|
@@ -82,6 +82,7 @@ class Settings(QObject):
|
|
|
82
82
|
font_family = SettingProperty('Ubuntu Mono', "Appearance")
|
|
83
83
|
color_scheme = SettingProperty('monokai', "Appearance")
|
|
84
84
|
tab_width = SettingProperty(4, "Appearance")
|
|
85
|
+
default_indent = SettingProperty('\t', "Appearance")
|
|
85
86
|
word_wrap = SettingProperty(False, "Appearance")
|
|
86
87
|
character_ruler = SettingProperty(80, "Appearance") # None to disable
|
|
87
88
|
search_replace_background = SettingProperty("#fdff74", "Appearance")
|
|
@@ -32,6 +32,7 @@ class MainWindow(QMainWindow):
|
|
|
32
32
|
self.setWindowTitle("Sigmund Analyst")
|
|
33
33
|
# The editor panel provides splittable editor tabs
|
|
34
34
|
self._editor_panel = EditorPanel()
|
|
35
|
+
self._editor_panel.open_folder_requested.connect(self._open_folder)
|
|
35
36
|
self.setCentralWidget(self._editor_panel)
|
|
36
37
|
self._project_explorers = []
|
|
37
38
|
# Track if project explorers are hidden as a group
|
|
@@ -110,28 +111,35 @@ class MainWindow(QMainWindow):
|
|
|
110
111
|
|
|
111
112
|
# Connect to dynamically created signals
|
|
112
113
|
signal_router.connect_to_signal("execute_code", self._execute_code)
|
|
113
|
-
signal_router.connect_to_signal("execute_file", self._execute_file)
|
|
114
|
+
signal_router.connect_to_signal("execute_file", self._execute_file)
|
|
114
115
|
|
|
115
116
|
self._setup_toolbar()
|
|
116
117
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
118
|
+
# Restoring the geometry often crashes with a segfault, so for now we
|
|
119
|
+
# just use this default
|
|
120
|
+
# just use this default
|
|
121
|
+
self._workspace_explorer.hide()
|
|
122
|
+
self._jupyter_console.hide()
|
|
123
|
+
self._sigmund.hide()
|
|
124
|
+
self._settings_panel.hide()
|
|
125
|
+
# # If we have no saved geometry/state, hide the docks by default
|
|
126
|
+
# if not settings.window_geometry or not settings.window_state:
|
|
127
|
+
# self._workspace_explorer.hide()
|
|
128
|
+
# self._jupyter_console.hide()
|
|
129
|
+
# self._settings_panel.hide()
|
|
130
|
+
# else:
|
|
131
|
+
# # Restore geometry and state
|
|
132
|
+
# try:
|
|
133
|
+
# geom = QByteArray.fromBase64(
|
|
134
|
+
# settings.window_geometry.encode('utf-8')
|
|
135
|
+
# )
|
|
136
|
+
# state = QByteArray.fromBase64(
|
|
137
|
+
# settings.window_state.encode('utf-8')
|
|
138
|
+
# )
|
|
139
|
+
# self.restoreGeometry(geom)
|
|
140
|
+
# self.restoreState(state)
|
|
141
|
+
# except Exception as e:
|
|
142
|
+
# logger.warning(f"Could not restore window geometry/state: {e}")
|
|
135
143
|
|
|
136
144
|
def _setup_toolbar(self):
|
|
137
145
|
"""Setup the main toolbar with Material Design icons."""
|
|
@@ -374,10 +382,14 @@ class MainWindow(QMainWindow):
|
|
|
374
382
|
def _close_project_explorer(self, project_explorer):
|
|
375
383
|
self._project_explorers.remove(project_explorer)
|
|
376
384
|
|
|
377
|
-
def _open_folder(self):
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
385
|
+
def _open_folder(self, path=None):
|
|
386
|
+
# The event handler may pass a bool argument
|
|
387
|
+
if path is None or isinstance(path, bool):
|
|
388
|
+
project_explorer = ProjectExplorer.open_folder(
|
|
389
|
+
self._editor_panel, parent=self)
|
|
390
|
+
else:
|
|
391
|
+
project_explorer = ProjectExplorer(self._editor_panel, path,
|
|
392
|
+
parent=self)
|
|
381
393
|
if project_explorer is None:
|
|
382
394
|
return
|
|
383
395
|
project_explorer.closed.connect(self._close_project_explorer)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
+
from pyqt_code_editor.mixins import Complete, Theme, Zoom, LineNumber, Comment, \
|
|
3
|
+
AutoIndent, AutoPair, SearchReplace, Base, Check, Shortcuts, FileLink, Symbols
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Editor(LineNumber, Zoom, Complete, AutoIndent, AutoPair, Comment,
|
|
7
|
+
SearchReplace, FileLink, Theme, Check, Shortcuts, Symbols, Base,
|
|
8
|
+
QPlainTextEdit):
|
|
9
|
+
pass
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/editor_panel.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from qtpy.QtWidgets import QApplication, QShortcut, QWidget, QHBoxLayout, QFileDialog
|
|
3
3
|
from qtpy.QtGui import QKeySequence
|
|
4
|
-
from qtpy.QtCore import Qt
|
|
4
|
+
from qtpy.QtCore import Qt, Signal
|
|
5
5
|
from ..widgets import TabbedEditor, TabSplitter
|
|
6
6
|
from .. import settings, utils
|
|
7
7
|
import logging
|
|
@@ -10,6 +10,9 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class EditorPanel(QWidget):
|
|
13
|
+
|
|
14
|
+
open_folder_requested = Signal(str)
|
|
15
|
+
|
|
13
16
|
def __init__(self, parent=None, initial_path=None):
|
|
14
17
|
super().__init__(parent)
|
|
15
18
|
logger.info("EditorPanel initialized")
|
|
@@ -55,6 +58,7 @@ class EditorPanel(QWidget):
|
|
|
55
58
|
# Connect the lastTabClosed signal so that we can remove empty panels
|
|
56
59
|
t.lastTabClosed.connect(self.handle_tabbed_editor_empty)
|
|
57
60
|
t.tabCloseRequested.connect(self._relabel_tabs)
|
|
61
|
+
t.open_folder_requested.connect(self.open_folder_requested)
|
|
58
62
|
return t
|
|
59
63
|
|
|
60
64
|
def unsaved_changes(self):
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from qtpy.QtCore import Qt
|
|
2
|
+
from .. import settings
|
|
3
|
+
import logging
|
|
4
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AutoIndent:
|
|
8
|
+
"""
|
|
9
|
+
A generic mixin class to provide auto-indentation features for a QPlainTextEdit.
|
|
10
|
+
Implements the following specs:
|
|
11
|
+
|
|
12
|
+
1. Indentation Style Detection
|
|
13
|
+
- detect_indentation_style() returns a string representing a single indentation
|
|
14
|
+
(e.g. '\t' or ' ').
|
|
15
|
+
- Currently, we return just '\t' by default (dummy implementation).
|
|
16
|
+
|
|
17
|
+
2. Multiline Indent/Dedent
|
|
18
|
+
- Tab in multi-line selection indents those lines by one chunk.
|
|
19
|
+
- Shift+Tab (or Backtab) dedents those lines.
|
|
20
|
+
|
|
21
|
+
3. Preserving Indentation on New Lines
|
|
22
|
+
- Pressing Enter creates a new line with the same leading indentation
|
|
23
|
+
(no language-specific logic).
|
|
24
|
+
|
|
25
|
+
4. Single-Line Insert or Remove Indentation
|
|
26
|
+
- Within a single line, Tab inserts one indentation chunk at the cursor location.
|
|
27
|
+
- Shift+Tab removes up to one indentation chunk from the start of the line if possible.
|
|
28
|
+
|
|
29
|
+
5. Handling Backspace and Delete in Leading Whitespace
|
|
30
|
+
- Backspace/Delete in leading whitespace removes one indentation chunk worth of characters
|
|
31
|
+
if they match the indentation style (e.g., one tab or up to len(indent_chunk) spaces).
|
|
32
|
+
|
|
33
|
+
6. Completely Generic (No Language-Specific Rules)
|
|
34
|
+
- No indentation changes based on braces, parentheses, or keywords.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def detect_indentation_style(self):
|
|
38
|
+
"""
|
|
39
|
+
Dummy function that always returns a single-tab string for now.
|
|
40
|
+
Later, this can analyze the document to decide if it should
|
|
41
|
+
use tabs or spaces (e.g. ' ' for four spaces).
|
|
42
|
+
"""
|
|
43
|
+
from detect_indent import detect_indent
|
|
44
|
+
|
|
45
|
+
indent = detect_indent(self.toPlainText())
|
|
46
|
+
if indent['type'] is None:
|
|
47
|
+
return settings.default_indent
|
|
48
|
+
return indent['indent']
|
|
49
|
+
|
|
50
|
+
def keyPressEvent(self, event):
|
|
51
|
+
key = event.key()
|
|
52
|
+
modifiers = event.modifiers()
|
|
53
|
+
logging.info("keyPressEvent: key=%s, modifiers=%s", key, modifiers)
|
|
54
|
+
|
|
55
|
+
# Check for Tab/Shift+Tab indentation
|
|
56
|
+
if key == Qt.Key_Tab and modifiers == Qt.NoModifier:
|
|
57
|
+
logging.info("Tab pressed -> indent code")
|
|
58
|
+
self.indent_code()
|
|
59
|
+
return
|
|
60
|
+
elif (key == Qt.Key_Tab and modifiers == Qt.ShiftModifier) or key == Qt.Key_Backtab:
|
|
61
|
+
logging.info("Shift+Tab pressed -> dedent code")
|
|
62
|
+
self.dedent_code()
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Handle backspace in leading indentation
|
|
66
|
+
if key == Qt.Key_Backspace and modifiers == Qt.NoModifier:
|
|
67
|
+
if self._handle_backspace():
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Handle delete in leading indentation
|
|
71
|
+
if key == Qt.Key_Delete and modifiers == Qt.NoModifier:
|
|
72
|
+
if self._handle_delete():
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Check for Enter/Return (preserve indentation)
|
|
76
|
+
if key in (Qt.Key_Enter, Qt.Key_Return):
|
|
77
|
+
logging.info("Enter/Return pressed -> preserve indentation on new line")
|
|
78
|
+
cursor = self.textCursor()
|
|
79
|
+
|
|
80
|
+
# Get leading whitespace of the current line
|
|
81
|
+
block_text = cursor.block().text()
|
|
82
|
+
leading_ws = len(block_text) - len(block_text.lstrip(' \t'))
|
|
83
|
+
preserve_ws = block_text[:leading_ws]
|
|
84
|
+
|
|
85
|
+
# Perform normal newline
|
|
86
|
+
super().keyPressEvent(event)
|
|
87
|
+
|
|
88
|
+
# Insert the same leading whitespace on the new line
|
|
89
|
+
cursor.insertText(preserve_ws)
|
|
90
|
+
self.setTextCursor(cursor)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Default behavior for any other keys
|
|
94
|
+
super().keyPressEvent(event)
|
|
95
|
+
|
|
96
|
+
def _get_indent_string(self):
|
|
97
|
+
"""
|
|
98
|
+
Returns the indentation chunk based on detect_indentation_style().
|
|
99
|
+
By default, this is a single tab ('\t') but could be ' ' for spaces, etc.
|
|
100
|
+
"""
|
|
101
|
+
return self.detect_indentation_style()
|
|
102
|
+
|
|
103
|
+
def _is_multiline_selection(self):
|
|
104
|
+
"""
|
|
105
|
+
Returns True if selection spans more than one line, else False.
|
|
106
|
+
"""
|
|
107
|
+
cursor = self.textCursor()
|
|
108
|
+
start = cursor.selectionStart()
|
|
109
|
+
end = cursor.selectionEnd()
|
|
110
|
+
start_block = self.document().findBlock(start).blockNumber()
|
|
111
|
+
end_block = self.document().findBlock(end).blockNumber()
|
|
112
|
+
return end_block > start_block
|
|
113
|
+
|
|
114
|
+
def indent_code(self):
|
|
115
|
+
"""
|
|
116
|
+
Indent either the selected lines (if multi-line selection)
|
|
117
|
+
or just the current cursor position (if single-line).
|
|
118
|
+
"""
|
|
119
|
+
logging.info("indent_code triggered")
|
|
120
|
+
cursor = self.textCursor()
|
|
121
|
+
cursor.beginEditBlock()
|
|
122
|
+
|
|
123
|
+
if self._is_multiline_selection():
|
|
124
|
+
logging.info("Multi-line selection -> indent each line")
|
|
125
|
+
self._indent_selection()
|
|
126
|
+
else:
|
|
127
|
+
logging.info("Single line -> insert indent at cursor")
|
|
128
|
+
indent_str = self._get_indent_string()
|
|
129
|
+
cursor.insertText(indent_str)
|
|
130
|
+
|
|
131
|
+
cursor.endEditBlock()
|
|
132
|
+
|
|
133
|
+
def dedent_code(self):
|
|
134
|
+
"""
|
|
135
|
+
Dedent either the selected lines (if multi-line selection)
|
|
136
|
+
or just the current line (if single-line).
|
|
137
|
+
"""
|
|
138
|
+
logging.info("dedent_code triggered")
|
|
139
|
+
cursor = self.textCursor()
|
|
140
|
+
cursor.beginEditBlock()
|
|
141
|
+
|
|
142
|
+
if self._is_multiline_selection():
|
|
143
|
+
logging.info("Multi-line selection -> dedent each line")
|
|
144
|
+
self._dedent_selection()
|
|
145
|
+
else:
|
|
146
|
+
logging.info("Single line -> remove up to one indent chunk from start")
|
|
147
|
+
line_start = cursor.block().position()
|
|
148
|
+
line_text = cursor.block().text()
|
|
149
|
+
leading_ws = len(line_text) - len(line_text.lstrip(' \t'))
|
|
150
|
+
|
|
151
|
+
chunk_size = self._compute_chunk_size()
|
|
152
|
+
remove_chars = min(chunk_size, leading_ws)
|
|
153
|
+
logging.info("Removing up to %d chars from leading whitespace", remove_chars)
|
|
154
|
+
|
|
155
|
+
cursor.setPosition(line_start)
|
|
156
|
+
for _ in range(remove_chars):
|
|
157
|
+
self._delete_forward_if_tab_or_space(cursor)
|
|
158
|
+
|
|
159
|
+
cursor.endEditBlock()
|
|
160
|
+
|
|
161
|
+
def _indent_selection(self):
|
|
162
|
+
"""
|
|
163
|
+
Increase indent for each selected line by one chunk.
|
|
164
|
+
"""
|
|
165
|
+
logging.info("indent_selection (multi-line)")
|
|
166
|
+
indent_str = self._get_indent_string()
|
|
167
|
+
|
|
168
|
+
cursor = self.textCursor()
|
|
169
|
+
start = cursor.selectionStart()
|
|
170
|
+
end = cursor.selectionEnd()
|
|
171
|
+
|
|
172
|
+
start_block = self.document().findBlock(start).blockNumber()
|
|
173
|
+
end_block = self.document().findBlock(end).blockNumber()
|
|
174
|
+
|
|
175
|
+
for block_num in range(start_block, end_block + 1):
|
|
176
|
+
block = self.document().findBlockByNumber(block_num)
|
|
177
|
+
tmp_cursor = self.textCursor()
|
|
178
|
+
tmp_cursor.setPosition(block.position())
|
|
179
|
+
tmp_cursor.insertText(indent_str)
|
|
180
|
+
|
|
181
|
+
def _dedent_selection(self):
|
|
182
|
+
"""
|
|
183
|
+
Decrease indent for each selected line by one chunk,
|
|
184
|
+
ensuring we don't go below zero indentation.
|
|
185
|
+
"""
|
|
186
|
+
logging.info("dedent_selection (multi-line)")
|
|
187
|
+
cursor = self.textCursor()
|
|
188
|
+
start = cursor.selectionStart()
|
|
189
|
+
end = cursor.selectionEnd()
|
|
190
|
+
|
|
191
|
+
start_block = self.document().findBlock(start).blockNumber()
|
|
192
|
+
end_block = self.document().findBlock(end).blockNumber()
|
|
193
|
+
|
|
194
|
+
for block_num in range(start_block, end_block + 1):
|
|
195
|
+
block = self.document().findBlockByNumber(block_num)
|
|
196
|
+
line_text = block.text()
|
|
197
|
+
leading_ws = len(line_text) - len(line_text.lstrip(' \t'))
|
|
198
|
+
|
|
199
|
+
chunk_size = self._compute_chunk_size()
|
|
200
|
+
remove_chars = min(chunk_size, leading_ws)
|
|
201
|
+
|
|
202
|
+
tmp_cursor = self.textCursor()
|
|
203
|
+
tmp_cursor.setPosition(block.position())
|
|
204
|
+
for _ in range(remove_chars):
|
|
205
|
+
self._delete_forward_if_tab_or_space(tmp_cursor)
|
|
206
|
+
|
|
207
|
+
def _handle_backspace(self):
|
|
208
|
+
"""
|
|
209
|
+
If the cursor is in leading indentation, remove one indentation chunk
|
|
210
|
+
(based on the length of detect_indentation_style()).
|
|
211
|
+
Returns True if handled, False otherwise.
|
|
212
|
+
"""
|
|
213
|
+
cursor = self.textCursor()
|
|
214
|
+
if not cursor.hasSelection():
|
|
215
|
+
block_text = cursor.block().text()
|
|
216
|
+
pos_in_block = cursor.positionInBlock()
|
|
217
|
+
leading_ws = len(block_text) - len(block_text.lstrip(' \t'))
|
|
218
|
+
|
|
219
|
+
# Only if we're in the leading whitespace region:
|
|
220
|
+
if 0 < pos_in_block <= leading_ws:
|
|
221
|
+
chunk_size = self._compute_chunk_size()
|
|
222
|
+
remove_count = min(chunk_size, pos_in_block)
|
|
223
|
+
logging.info("Backspace in leading whitespace -> removing %d chars", remove_count)
|
|
224
|
+
|
|
225
|
+
for _ in range(remove_count):
|
|
226
|
+
cursor.deletePreviousChar()
|
|
227
|
+
return True
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def _handle_delete(self):
|
|
231
|
+
"""
|
|
232
|
+
If the cursor is in leading indentation, remove one indentation chunk going forward.
|
|
233
|
+
Returns True if handled, False otherwise.
|
|
234
|
+
"""
|
|
235
|
+
cursor = self.textCursor()
|
|
236
|
+
if not cursor.hasSelection():
|
|
237
|
+
block_text = cursor.block().text()
|
|
238
|
+
pos_in_block = cursor.positionInBlock()
|
|
239
|
+
leading_ws = len(block_text) - len(block_text.lstrip(' \t'))
|
|
240
|
+
|
|
241
|
+
if pos_in_block < leading_ws:
|
|
242
|
+
chunk_size = self._compute_chunk_size()
|
|
243
|
+
remove_count = min(chunk_size, leading_ws - pos_in_block)
|
|
244
|
+
logging.info("Delete in leading whitespace -> removing %d chars", remove_count)
|
|
245
|
+
|
|
246
|
+
for _ in range(remove_count):
|
|
247
|
+
cursor.deleteChar()
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def _compute_chunk_size(self):
|
|
252
|
+
"""
|
|
253
|
+
Returns the length of the indentation chunk returned by detect_indentation_style().
|
|
254
|
+
For example, if detect_indentation_style() returns '\t', the size is 1;
|
|
255
|
+
if it returns ' ', the size is 4, etc.
|
|
256
|
+
"""
|
|
257
|
+
chunk = self.detect_indentation_style()
|
|
258
|
+
return len(chunk)
|
|
259
|
+
|
|
260
|
+
def _delete_forward_if_tab_or_space(self, cursor):
|
|
261
|
+
"""
|
|
262
|
+
Deletes one character if it is a space or a tab in leading whitespace.
|
|
263
|
+
(We remove characters one by one for partial or exact chunk removal.)
|
|
264
|
+
"""
|
|
265
|
+
ch = cursor.document().characterAt(cursor.position())
|
|
266
|
+
if ch in (' ', '\t'):
|
|
267
|
+
cursor.deleteChar()
|
|
@@ -31,7 +31,7 @@ class Symbols:
|
|
|
31
31
|
def handle_worker_result(self, action, result):
|
|
32
32
|
"""Check for completion or calltip results from the external worker."""
|
|
33
33
|
super().handle_worker_result(action, result)
|
|
34
|
-
if action != 'symbols':
|
|
34
|
+
if action != 'symbols' or not result.get('symbols', False):
|
|
35
35
|
return
|
|
36
36
|
QuickSymbolDialog(self, result['symbols'], self.jump_to_symbol).exec()
|
|
37
37
|
|
|
@@ -821,12 +821,12 @@ def extract_cells_from_code(code: str) -> list:
|
|
|
821
821
|
# Store all detected cells
|
|
822
822
|
cells = []
|
|
823
823
|
|
|
824
|
-
# Track the current position
|
|
825
|
-
current_pos = 0
|
|
826
|
-
|
|
827
824
|
# Define regex patterns for cell markers
|
|
828
825
|
marker_pattern = re.compile(r'^(# %%|#%%|# In\[\]:?).*?$', re.MULTILINE)
|
|
829
|
-
triple_quotes_pattern = re.compile(
|
|
826
|
+
triple_quotes_pattern = re.compile(
|
|
827
|
+
r'(?m)^(""".*?"""|\'\'\'.*?\'\'\')',
|
|
828
|
+
re.DOTALL
|
|
829
|
+
)
|
|
830
830
|
|
|
831
831
|
# Find all marker-based cells
|
|
832
832
|
marker_matches = list(marker_pattern.finditer(code))
|
|
@@ -5,11 +5,19 @@ logging.basicConfig(level=logging.INFO, force=True)
|
|
|
5
5
|
logger = logging.getLogger(__name__)
|
|
6
6
|
module_cache = {}
|
|
7
7
|
|
|
8
|
+
# Some lexers have weird names that are not recognized by get_lexer_by_name().
|
|
9
|
+
# Often this seems to be a matter of discarding a suffix after a space or /
|
|
10
|
+
# This may not be a foolproof solution.
|
|
11
|
+
LANGUAGE_SEPARATORS = ' ', '/'
|
|
8
12
|
|
|
9
13
|
def create_syntax_highlighter(language, *args, **kwargs):
|
|
10
|
-
|
|
14
|
+
for ch in LANGUAGE_SEPARATORS:
|
|
15
|
+
if ch in language:
|
|
16
|
+
logger.info(f'mapping {language} to {language[:language.find(ch)]}')
|
|
17
|
+
language = language[:language.find(ch)]
|
|
18
|
+
try:
|
|
11
19
|
lexer = get_lexer_by_name(language)
|
|
12
|
-
except:
|
|
20
|
+
except Exception:
|
|
13
21
|
lexer = get_lexer_by_name('markdown')
|
|
14
22
|
if language not in module_cache:
|
|
15
23
|
try:
|
|
@@ -110,6 +110,7 @@ class DraggableTabBar(QTabBar):
|
|
|
110
110
|
class TabbedEditor(QTabWidget):
|
|
111
111
|
"""A tab widget that can hold multiple CodeEditor instances."""
|
|
112
112
|
lastTabClosed = Signal(QTabWidget)
|
|
113
|
+
open_folder_requested = Signal(str)
|
|
113
114
|
|
|
114
115
|
def __init__(self, parent=None):
|
|
115
116
|
super().__init__(parent)
|
|
@@ -312,7 +313,12 @@ class TabbedEditor(QTabWidget):
|
|
|
312
313
|
if event.mimeData().hasUrls():
|
|
313
314
|
for url in event.mimeData().urls():
|
|
314
315
|
path = url.toLocalFile()
|
|
315
|
-
|
|
316
|
+
# If path is a folder, open a code editor, otherwise emit the
|
|
317
|
+
# open_folder signal
|
|
318
|
+
if os.path.isdir(path):
|
|
319
|
+
self.open_folder_requested.emit(path)
|
|
320
|
+
else:
|
|
321
|
+
self.add_code_editor(path)
|
|
316
322
|
|
|
317
323
|
if not event.mimeData().hasFormat("application/x-tabdata"):
|
|
318
324
|
event.ignore()
|
|
@@ -325,7 +331,6 @@ class TabbedEditor(QTabWidget):
|
|
|
325
331
|
dataByteArray = event.mimeData().data("application/x-tabdata")
|
|
326
332
|
stream = QDataStream(dataByteArray, QDataStream.ReadOnly)
|
|
327
333
|
tabText = stream.readQString()
|
|
328
|
-
hasIcon = stream.readBool()
|
|
329
334
|
# (We ignore the icon detail in this example or simply set a blank icon.)
|
|
330
335
|
# If you’re handing around pixmaps, you could store them more explicitly.
|
|
331
336
|
|
|
@@ -49,7 +49,7 @@ def main_worker_process_function(request_queue, result_queue):
|
|
|
49
49
|
try:
|
|
50
50
|
worker_functions = importlib.import_module(
|
|
51
51
|
f".languages.{language}", package=__package__)
|
|
52
|
-
except ImportError
|
|
52
|
+
except ImportError:
|
|
53
53
|
from .languages import generic as worker_functions
|
|
54
54
|
logger.info(f'failed to load worker functions for {language}, falling back to generic')
|
|
55
55
|
else:
|
|
@@ -84,11 +84,11 @@ def main_worker_process_function(request_queue, result_queue):
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
elif action == 'calltip':
|
|
87
|
+
cursor_pos = request.get('cursor_pos', 0)
|
|
87
88
|
if worker_functions.calltip is None:
|
|
88
89
|
signatures = None
|
|
89
90
|
else:
|
|
90
91
|
code = request.get('code', '')
|
|
91
|
-
cursor_pos = request.get('cursor_pos', 0)
|
|
92
92
|
path = request.get('path', None)
|
|
93
93
|
logger.info(f"Performing calltip: language='{language}', path={path}")
|
|
94
94
|
signatures = worker_functions.calltip(
|
|
@@ -105,7 +105,7 @@ def main_worker_process_function(request_queue, result_queue):
|
|
|
105
105
|
|
|
106
106
|
elif action == 'symbols':
|
|
107
107
|
if worker_functions.symbols is None:
|
|
108
|
-
|
|
108
|
+
symbols_results = []
|
|
109
109
|
else:
|
|
110
110
|
code = request.get('code', '')
|
|
111
111
|
symbols_results = worker_functions.symbols(code)
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from qtpy.QtWidgets import QPlainTextEdit
|
|
2
|
-
from pyqt_code_editor.mixins import Complete, \
|
|
3
|
-
Theme, Zoom, LineNumber, Comment, \
|
|
4
|
-
AutoPair, SearchReplace, Base, Check, Shortcuts, FileLink, Symbols
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Editor(LineNumber, Zoom, Complete, AutoPair,
|
|
8
|
-
Comment, SearchReplace, FileLink,
|
|
9
|
-
Theme, Check, Shortcuts, Symbols, Base,
|
|
10
|
-
QPlainTextEdit):
|
|
11
|
-
pass
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/code_editors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/languages/python.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/code_editors/languages/text.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/find_in_files.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/jupyter_console.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/project_explorer.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/settings_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/components/workspace_explorer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/python_auto_indent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/smart_backspace_delete.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/mixins/widgets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/syntax_highlighters/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/calltip_widget.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/completion_popup.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/quick_open_dialog.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/quick_open_file_dialog.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/widgets/search_replace_frame.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/languages/__init__.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/languages/generic.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/languages/python.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/__init__.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/codestral.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/sigmund.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.2 → pyqt_code_editor-0.0.3}/pyqt_code_editor/worker/providers/symbol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|