pyqt-code-editor 0.0.14__tar.gz → 0.0.16__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.14 → pyqt_code_editor-0.0.16}/PKG-INFO +1 -1
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/__init__.py +1 -1
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/app.py +1 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/complete.py +33 -3
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/execute.py +1 -1
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/python_auto_indent.py +11 -3
- pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/__init__.py +3 -0
- pyqt_code_editor-0.0.14/pyqt_code_editor/python_utils.py → pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/_auto_indent.py +57 -190
- pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/_extract_cells.py +138 -0
- pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +185 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/completion_popup.py +0 -1
- pyqt_code_editor-0.0.16/pyqt_code_editor/worker/languages/__init__.py +0 -0
- pyqt_code_editor-0.0.16/pyqt_code_editor/worker/languages/python.py +24 -0
- pyqt_code_editor-0.0.14/pyqt_code_editor/worker/languages/python.py +0 -17
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/COPYING +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyproject.toml +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/_settings.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/code_editors.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/python.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/text.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/editor_panel.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/find_in_files.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/jupyter_console.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/project_explorer.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/settings_panel.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/sigmund.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/workspace_explorer.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/auto_indent.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/auto_pair.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/base.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/check.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/comment.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/file_link.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/line_number.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/search_replace.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/shortcuts.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/symbols.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/theme.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/zoom.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/signal_router.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/themes.py +0 -0
- /pyqt_code_editor-0.0.14/pyqt_code_editor/utils.py → /pyqt_code_editor-0.0.16/pyqt_code_editor/utils/__init__.py +0 -0
- {pyqt_code_editor-0.0.14/pyqt_code_editor/worker → pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages}/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/watchdog.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/dock.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
- {pyqt_code_editor-0.0.14/pyqt_code_editor/worker/languages → pyqt_code_editor-0.0.16/pyqt_code_editor/worker}/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/languages/generic.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/manager.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/process.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/__init__.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/codestral.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/jedi.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/ruff.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/symbol.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/settings.py +0 -0
- {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/readme.md +0 -0
|
@@ -339,6 +339,7 @@ class MainWindow(QMainWindow):
|
|
|
339
339
|
self._jupyter_console.change_directory(
|
|
340
340
|
os.path.dirname(editor.code_editor_file_path)
|
|
341
341
|
)
|
|
342
|
+
self._toggle_dock_widget(self._jupyter_console, show=True)
|
|
342
343
|
self._jupyter_console.execute_code(code)
|
|
343
344
|
|
|
344
345
|
def _execute_file(self, path):
|
|
@@ -354,10 +354,40 @@ class Complete:
|
|
|
354
354
|
left_overlap_size = i
|
|
355
355
|
break
|
|
356
356
|
# Captures special case of exactly 1 character of left overlap, which
|
|
357
|
-
# can reflect words with double letters,
|
|
358
|
-
#
|
|
357
|
+
# can reflect words with double letters, but it doesn't need to. To
|
|
358
|
+
# decide whether the overlap needs to be removed we check whether the
|
|
359
|
+
# stripped version exists in the text. If so, we use that, if not,
|
|
360
|
+
# we assume that the doubling is intended.
|
|
361
|
+
#
|
|
362
|
+
# def aabb(): pass
|
|
363
|
+
# a| completion is 'abb' overlap = 0
|
|
364
|
+
#
|
|
365
|
+
# def abb(): pass
|
|
366
|
+
# a| completion is 'abb' overlap = 1
|
|
359
367
|
if left_overlap_size == 1:
|
|
360
|
-
|
|
368
|
+
# Get the index of the last character in before_text that is
|
|
369
|
+
# alphanumeric or underscore
|
|
370
|
+
last_symbol_index = -1
|
|
371
|
+
while -last_symbol_index <= len(before_text) and \
|
|
372
|
+
(before_text[last_symbol_index].isalnum() or
|
|
373
|
+
before_text[last_symbol_index] == '_'):
|
|
374
|
+
last_symbol_index -= 1
|
|
375
|
+
last_symbol_index += 1
|
|
376
|
+
symbol_before = before_text[last_symbol_index:]
|
|
377
|
+
# Get the index of the first character in completion that is
|
|
378
|
+
# alphanumeric or underscore
|
|
379
|
+
last_symbol_index = 0
|
|
380
|
+
while last_symbol_index < len(completion) and \
|
|
381
|
+
(completion[last_symbol_index].isalnum() or
|
|
382
|
+
completion[last_symbol_index] == '_'):
|
|
383
|
+
last_symbol_index += 1
|
|
384
|
+
symbol_after = completion[:last_symbol_index]
|
|
385
|
+
symbol = symbol_before + symbol_after
|
|
386
|
+
if symbol in doc_text:
|
|
387
|
+
left_overlap_size = 0
|
|
388
|
+
logger.info(f'{symbol} exists in text, keeping overlap')
|
|
389
|
+
else:
|
|
390
|
+
logger.info(f'{symbol} does not exists in text, stripping overlap')
|
|
361
391
|
# Create a new completion without what's already typed
|
|
362
392
|
new_completion = completion[left_overlap_size:]
|
|
363
393
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from qtpy.QtCore import Signal, Qt, QTimer
|
|
2
2
|
from qtpy.QtGui import QTextCursor, QTextCharFormat, QColor, QKeySequence
|
|
3
3
|
from qtpy.QtWidgets import QShortcut, QTextEdit
|
|
4
|
-
from ..
|
|
4
|
+
from ..utils.languages.python import extract_cells_from_code
|
|
5
5
|
from .. import settings
|
|
6
6
|
import logging
|
|
7
7
|
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/python_auto_indent.py
RENAMED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from qtpy.QtCore import Qt
|
|
2
|
-
from
|
|
2
|
+
from qtpy.QtGui import QTextCursor
|
|
3
|
+
from .. import settings
|
|
4
|
+
from ..utils.languages import python as python_utils
|
|
3
5
|
import logging
|
|
4
6
|
logging.basicConfig(level=logging.INFO, force=True)
|
|
5
7
|
|
|
@@ -50,7 +52,14 @@ class PythonAutoIndent:
|
|
|
50
52
|
logging.info("Enter/Return pressed")
|
|
51
53
|
cursor = self.textCursor()
|
|
52
54
|
cursor.beginEditBlock()
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
# Use QTextCursor to get text up to cursor position
|
|
57
|
+
# This handles UTF-16 encoding correctly
|
|
58
|
+
temp_cursor = self.textCursor()
|
|
59
|
+
temp_cursor.movePosition(QTextCursor.Start)
|
|
60
|
+
temp_cursor.setPosition(cursor.position(), QTextCursor.KeepAnchor)
|
|
61
|
+
code = temp_cursor.selectedText().replace('\u2029', '\n')
|
|
62
|
+
|
|
54
63
|
new_indent = python_utils.python_auto_indent(code)
|
|
55
64
|
# Insert the computed indentation
|
|
56
65
|
super().keyPressEvent(event)
|
|
@@ -60,7 +69,6 @@ class PythonAutoIndent:
|
|
|
60
69
|
cursor.endEditBlock()
|
|
61
70
|
self.setTextCursor(cursor)
|
|
62
71
|
return
|
|
63
|
-
|
|
64
72
|
# Otherwise, default behavior
|
|
65
73
|
super().keyPressEvent(event)
|
|
66
74
|
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
from . import
|
|
1
|
+
from .... import settings
|
|
2
|
+
from ._mask_str_in_code import mask_str_in_code
|
|
3
3
|
import logging
|
|
4
4
|
logging.basicConfig(level=logging.INFO, force=True)
|
|
5
5
|
logger = logging.getLogger(__name__)
|
|
6
6
|
|
|
7
|
+
BLOCK_KEYWORDS = (
|
|
8
|
+
"def", "class", "if", "elif", "else", "while",
|
|
9
|
+
"for", "with", "try", "except", "finally"
|
|
10
|
+
)
|
|
11
|
+
OPEN_TO_CLOSE = {'(': ')', '[': ']', '{': '}'}
|
|
12
|
+
CLOSE_TO_OPEN = {')': '(', ']': '[', '}': '{'}
|
|
13
|
+
|
|
7
14
|
|
|
8
15
|
def get_leading_spaces(line: str) -> int:
|
|
9
16
|
"""
|
|
@@ -11,8 +18,20 @@ def get_leading_spaces(line: str) -> int:
|
|
|
11
18
|
"""
|
|
12
19
|
return len(line) - len(line.lstrip(' '))
|
|
13
20
|
|
|
21
|
+
|
|
22
|
+
def _is_block_opener(line: str) -> bool:
|
|
23
|
+
stripped = line.lstrip()
|
|
24
|
+
lower_stripped = stripped.lower()
|
|
25
|
+
for kw in BLOCK_KEYWORDS:
|
|
26
|
+
if lower_stripped.startswith(kw) and (
|
|
27
|
+
len(stripped) == len(kw)
|
|
28
|
+
or stripped[len(kw)] in (' ', '(', ':')
|
|
29
|
+
):
|
|
30
|
+
return True
|
|
31
|
+
return False
|
|
32
|
+
|
|
14
33
|
|
|
15
|
-
def parse_brackets(code: str):
|
|
34
|
+
def parse_brackets(code: str) -> tuple[list[tuple[str, int, int]], dict]:
|
|
16
35
|
"""
|
|
17
36
|
Parse all lines of code, ignoring quoted strings in a simple way,
|
|
18
37
|
and return:
|
|
@@ -20,30 +39,20 @@ def parse_brackets(code: str):
|
|
|
20
39
|
each as (bracket_char, line_index, col_index).
|
|
21
40
|
bracket_closings: A dict mapping close_line_idx -> (open_line_idx, open_line_indent, open_bracket).
|
|
22
41
|
"""
|
|
23
|
-
|
|
24
|
-
close_to_open = {')': '(', ']': '[', '}': '{'}
|
|
25
|
-
|
|
26
|
-
# A naive bracket parser that ignores strings by removing them first
|
|
27
|
-
# (this is simplistic; real logic may need to handle triple quotes, etc.)
|
|
28
|
-
def remove_strings(line: str) -> str:
|
|
29
|
-
return re.sub(r'(".*?"|\'.*?\')', '', line)
|
|
30
|
-
|
|
42
|
+
|
|
31
43
|
lines = code.split("\n")
|
|
32
44
|
bracket_stack = []
|
|
33
45
|
bracket_closings = {}
|
|
34
46
|
|
|
35
|
-
for i,
|
|
36
|
-
# remove quoted strings
|
|
37
|
-
stripped_line = remove_strings(raw_line)
|
|
38
|
-
|
|
47
|
+
for i, line in enumerate(lines):
|
|
39
48
|
j = 0
|
|
40
|
-
while j < len(
|
|
41
|
-
ch =
|
|
42
|
-
if ch in
|
|
49
|
+
while j < len(line):
|
|
50
|
+
ch = line[j]
|
|
51
|
+
if ch in OPEN_TO_CLOSE:
|
|
43
52
|
bracket_stack.append((ch, i, j))
|
|
44
|
-
elif ch in
|
|
53
|
+
elif ch in CLOSE_TO_OPEN:
|
|
45
54
|
# If there's something on the stack and it matches, pop it
|
|
46
|
-
if bracket_stack and bracket_stack[-1][0] ==
|
|
55
|
+
if bracket_stack and bracket_stack[-1][0] == CLOSE_TO_OPEN[ch]:
|
|
47
56
|
open_bracket_char, open_line_idx, open_col_idx = bracket_stack.pop()
|
|
48
57
|
# record the closing bracket
|
|
49
58
|
bracket_closings[i] = (open_line_idx, get_leading_spaces(lines[open_line_idx]), open_bracket_char)
|
|
@@ -57,16 +66,15 @@ def _get_unclosed_open_idx(code : str, open_char : str,
|
|
|
57
66
|
"""Returns the index of the last unclosed unopening character, such as an
|
|
58
67
|
opening parenthesis, or None if no unclosed opening character was found.
|
|
59
68
|
"""
|
|
60
|
-
# We only consider code from the end of the last code comment
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
last_quote = max(code.rfind("'"), code.rfind('"'))
|
|
69
|
+
# We only consider code from the end of the last code comment. This is a
|
|
70
|
+
# cheap way to avoid the auto indenter from being confused by parentheses
|
|
71
|
+
# in comments.
|
|
64
72
|
last_comment = code.rfind('#')
|
|
65
73
|
if last_comment >= 0:
|
|
66
74
|
last_comment_end = code[last_comment:].find('\n')
|
|
67
75
|
if last_comment_end >= 0:
|
|
68
76
|
last_comment += last_comment_end
|
|
69
|
-
search_start = max(0,
|
|
77
|
+
search_start = max(0, last_comment)
|
|
70
78
|
if search_start:
|
|
71
79
|
code = code[search_start:]
|
|
72
80
|
n_open_required = 1
|
|
@@ -112,7 +120,8 @@ def _line_col_from_idx(full_text, char_index):
|
|
|
112
120
|
return (line, column)
|
|
113
121
|
|
|
114
122
|
|
|
115
|
-
def _indent_inside_unclosed_call_def_or_class(code: str,
|
|
123
|
+
def _indent_inside_unclosed_call_def_or_class(code: str, lines: list,
|
|
124
|
+
char_idx : int) -> int:
|
|
116
125
|
"""
|
|
117
126
|
Return after opening parenthesis as part of function call should trigger indent
|
|
118
127
|
|
|
@@ -140,7 +149,14 @@ def _indent_inside_unclosed_call_def_or_class(code: str, char_idx : int) -> int:
|
|
|
140
149
|
```
|
|
141
150
|
function(arg1,>
|
|
142
151
|
|
|
|
143
|
-
```
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Return after opening parenthesis and one or more arguments as part of function call should trigger matching indent to first argument, also when the argument contains default values with strings.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
function("arg1", arg2='default', arg3=\"\"\"test\"\"\",>
|
|
158
|
+
|
|
|
159
|
+
```
|
|
144
160
|
|
|
145
161
|
Return after parenthesis and one or more arguments after function definition should trigger matching indent to first argument
|
|
146
162
|
|
|
@@ -223,10 +239,8 @@ def _indent_inside_unclosed_call_def_or_class(code: str, char_idx : int) -> int:
|
|
|
223
239
|
|
|
|
224
240
|
```
|
|
225
241
|
"""
|
|
226
|
-
lines = code.splitlines()
|
|
227
242
|
if not lines:
|
|
228
|
-
return 0
|
|
229
|
-
|
|
243
|
+
return 0
|
|
230
244
|
line_idx, col_idx = _line_col_from_idx(code, char_idx)
|
|
231
245
|
# Get the line where the last unclosed parenthesis is
|
|
232
246
|
open_line = lines[line_idx]
|
|
@@ -249,7 +263,7 @@ def _indent_inside_unclosed_call_def_or_class(code: str, char_idx : int) -> int:
|
|
|
249
263
|
return len(prefix) + after_paren_offset
|
|
250
264
|
|
|
251
265
|
|
|
252
|
-
def _indent_after_block_opener(code: str) -> int:
|
|
266
|
+
def _indent_after_block_opener(code: str, lines: list) -> int:
|
|
253
267
|
"""
|
|
254
268
|
Return after function definition should trigger indent
|
|
255
269
|
|
|
@@ -403,24 +417,7 @@ def _indent_after_block_opener(code: str) -> int:
|
|
|
403
417
|
if x == y:>
|
|
404
418
|
|
|
|
405
419
|
```
|
|
406
|
-
"""
|
|
407
|
-
BLOCK_KEYWORDS = (
|
|
408
|
-
"def", "class", "if", "elif", "else", "while",
|
|
409
|
-
"for", "with", "try", "except", "finally"
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
def is_block_opener(line: str) -> bool:
|
|
413
|
-
stripped = line.lstrip()
|
|
414
|
-
lower_stripped = stripped.lower()
|
|
415
|
-
for kw in BLOCK_KEYWORDS:
|
|
416
|
-
if lower_stripped.startswith(kw) and (
|
|
417
|
-
len(stripped) == len(kw)
|
|
418
|
-
or stripped[len(kw)] in (' ', '(', ':')
|
|
419
|
-
):
|
|
420
|
-
return True
|
|
421
|
-
return False
|
|
422
|
-
|
|
423
|
-
lines = code.splitlines()
|
|
420
|
+
"""
|
|
424
421
|
if not lines:
|
|
425
422
|
return 0
|
|
426
423
|
|
|
@@ -433,7 +430,7 @@ def _indent_after_block_opener(code: str) -> int:
|
|
|
433
430
|
# If it DOES end with a colon, we see if we can locate the block opener line
|
|
434
431
|
block_opener_idx = len(lines) - 1
|
|
435
432
|
for i in range(len(lines) - 1, -1, -1):
|
|
436
|
-
if
|
|
433
|
+
if _is_block_opener(lines[i]):
|
|
437
434
|
block_opener_idx = i
|
|
438
435
|
break
|
|
439
436
|
|
|
@@ -477,6 +474,13 @@ def _indent_inside_uncloded_list_tuple_set_or_dict(code: str, char_idx: int) ->
|
|
|
477
474
|
l = [item1,>
|
|
478
475
|
|
|
|
479
476
|
```
|
|
477
|
+
|
|
478
|
+
Return after opening of list with one or more elements should trigger matching indent to first element, also when the element is a string
|
|
479
|
+
|
|
480
|
+
```
|
|
481
|
+
l = ['item1',>
|
|
482
|
+
|
|
|
483
|
+
```
|
|
480
484
|
|
|
481
485
|
Return after opening of tuple with one or more elements should trigger matching indent to first element
|
|
482
486
|
|
|
@@ -733,6 +737,7 @@ def python_auto_indent(code: str) -> int:
|
|
|
733
737
|
|
|
|
734
738
|
```
|
|
735
739
|
"""
|
|
740
|
+
code = mask_str_in_code(code)
|
|
736
741
|
lines = code.splitlines()
|
|
737
742
|
# If code ends with a newline => cursor is on a fresh line
|
|
738
743
|
if code.endswith('\n'):
|
|
@@ -744,7 +749,7 @@ def python_auto_indent(code: str) -> int:
|
|
|
744
749
|
# 1. ends with colon => block opener
|
|
745
750
|
if last_line.endswith(":"):
|
|
746
751
|
logging.info('indent after block opener')
|
|
747
|
-
return _indent_after_block_opener(code)
|
|
752
|
+
return _indent_after_block_opener(code, lines)
|
|
748
753
|
|
|
749
754
|
# 2. unmatched '(' => function call/def or tuple
|
|
750
755
|
last_paren_idx_tuple = _get_unclosed_open_idx(code, '(', ')')
|
|
@@ -758,7 +763,7 @@ def python_auto_indent(code: str) -> int:
|
|
|
758
763
|
if preceding_char.isalnum() or preceding_char == "_":
|
|
759
764
|
logging.info("indent as unclosed call/def")
|
|
760
765
|
return _indent_inside_unclosed_call_def_or_class(
|
|
761
|
-
code, last_paren_idx_tuple)
|
|
766
|
+
code, lines, last_paren_idx_tuple)
|
|
762
767
|
else:
|
|
763
768
|
logging.info("indent as unclosed tuple")
|
|
764
769
|
return _indent_inside_uncloded_list_tuple_set_or_dict(
|
|
@@ -796,141 +801,3 @@ def python_auto_indent(code: str) -> int:
|
|
|
796
801
|
# 7. fallback => preserve current line indent
|
|
797
802
|
logging.info('indent fallback to current line')
|
|
798
803
|
return leading_spaces
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
def extract_cells_from_code(code: str) -> list:
|
|
802
|
-
"""Extracts cells from code. The return value is a
|
|
803
|
-
list of cells, where each cell is a dict like so:
|
|
804
|
-
|
|
805
|
-
{
|
|
806
|
-
'description': str | None,
|
|
807
|
-
'start_pos': int,
|
|
808
|
-
'end_pos': int,
|
|
809
|
-
'code': str
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
A description only applies to cells that have been defined with
|
|
813
|
-
triple-quoted strings. Otherwise it is None.
|
|
814
|
-
|
|
815
|
-
Cells can be defined by:
|
|
816
|
-
- # %%
|
|
817
|
-
- #%%
|
|
818
|
-
- # In[]:
|
|
819
|
-
- Triple-quoted strings
|
|
820
|
-
"""
|
|
821
|
-
# Store all detected cells
|
|
822
|
-
cells = []
|
|
823
|
-
|
|
824
|
-
# Define regex patterns for cell markers
|
|
825
|
-
marker_pattern = re.compile(r'^(# %%|#%%|# In\[\]:?).*?$', re.MULTILINE)
|
|
826
|
-
triple_quotes_pattern = re.compile(
|
|
827
|
-
r'(?m)^(""".*?"""|\'\'\'.*?\'\'\')',
|
|
828
|
-
re.DOTALL
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
# Find all marker-based cells
|
|
832
|
-
marker_matches = list(marker_pattern.finditer(code))
|
|
833
|
-
|
|
834
|
-
# Find all triple-quoted docstrings that are at the top level
|
|
835
|
-
# (we'll filter out non-top-level ones later)
|
|
836
|
-
docstring_matches = list(triple_quotes_pattern.finditer(code))
|
|
837
|
-
|
|
838
|
-
# Combine all match types and sort by position
|
|
839
|
-
all_markers = []
|
|
840
|
-
|
|
841
|
-
# Add marker matches
|
|
842
|
-
for match in marker_matches:
|
|
843
|
-
start_pos = match.start()
|
|
844
|
-
all_markers.append({
|
|
845
|
-
'type': 'marker',
|
|
846
|
-
'match': match,
|
|
847
|
-
'start_pos': start_pos
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
# Add docstring matches
|
|
851
|
-
for match in docstring_matches:
|
|
852
|
-
start_pos = match.start()
|
|
853
|
-
# Only include if not inside string/comment (simplified check)
|
|
854
|
-
# We'll do more thorough validation later
|
|
855
|
-
all_markers.append({
|
|
856
|
-
'type': 'docstring',
|
|
857
|
-
'match': match,
|
|
858
|
-
'start_pos': start_pos
|
|
859
|
-
})
|
|
860
|
-
|
|
861
|
-
# Sort all markers by position
|
|
862
|
-
all_markers.sort(key=lambda x: x['start_pos'])
|
|
863
|
-
|
|
864
|
-
# Process each marker to create cells
|
|
865
|
-
for i, marker in enumerate(all_markers):
|
|
866
|
-
match = marker['match']
|
|
867
|
-
start_pos = marker['start_pos']
|
|
868
|
-
|
|
869
|
-
# Determine where this cell ends (next marker or end of file)
|
|
870
|
-
if i < len(all_markers) - 1:
|
|
871
|
-
end_pos = all_markers[i + 1]['start_pos']
|
|
872
|
-
else:
|
|
873
|
-
end_pos = len(code)
|
|
874
|
-
|
|
875
|
-
# Check if this docstring is actually a top-level triple-quoted string
|
|
876
|
-
# and not part of a function definition, string literal, etc.
|
|
877
|
-
if marker['type'] == 'docstring':
|
|
878
|
-
# Skip if not a top-level docstring (simplified check)
|
|
879
|
-
# This is a simplification; a more robust solution would check code structure
|
|
880
|
-
line_start = code.rfind('\n', 0, start_pos) + 1
|
|
881
|
-
if line_start < start_pos:
|
|
882
|
-
indent = code[line_start:start_pos].strip()
|
|
883
|
-
if indent: # If there's text before the docstring on the same line, skip it
|
|
884
|
-
continue
|
|
885
|
-
|
|
886
|
-
# Extract description from the docstring
|
|
887
|
-
docstring_content = match.group(0)[3:-3].strip()
|
|
888
|
-
|
|
889
|
-
# Extract code content (from end of docstring to end of cell)
|
|
890
|
-
docstring_end = match.end()
|
|
891
|
-
cell_code = code[docstring_end:end_pos].strip()
|
|
892
|
-
|
|
893
|
-
cells.append({
|
|
894
|
-
'description': docstring_content,
|
|
895
|
-
'start_pos': start_pos,
|
|
896
|
-
'end_pos': end_pos,
|
|
897
|
-
'code': cell_code
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
else: # Marker-based cell
|
|
901
|
-
# Extract code content (from end of marker line to end of cell)
|
|
902
|
-
marker_line_end = code.find('\n', start_pos)
|
|
903
|
-
if marker_line_end == -1: # If no newline is found
|
|
904
|
-
marker_line_end = len(code)
|
|
905
|
-
|
|
906
|
-
cell_code = code[marker_line_end + 1:end_pos].strip()
|
|
907
|
-
|
|
908
|
-
cells.append({
|
|
909
|
-
'description': None,
|
|
910
|
-
'start_pos': start_pos,
|
|
911
|
-
'end_pos': end_pos,
|
|
912
|
-
'code': cell_code
|
|
913
|
-
})
|
|
914
|
-
|
|
915
|
-
# Handle the case where the file starts with code before any marker
|
|
916
|
-
if all_markers and all_markers[0]['start_pos'] > 0:
|
|
917
|
-
first_code = code[:all_markers[0]['start_pos']].strip()
|
|
918
|
-
if first_code:
|
|
919
|
-
cells.insert(0, {
|
|
920
|
-
'description': None,
|
|
921
|
-
'start_pos': 0,
|
|
922
|
-
'end_pos': all_markers[0]['start_pos'],
|
|
923
|
-
'code': first_code
|
|
924
|
-
})
|
|
925
|
-
|
|
926
|
-
# Handle the case where there are no markers at all
|
|
927
|
-
if not cells and code.strip():
|
|
928
|
-
cells.append({
|
|
929
|
-
'description': None,
|
|
930
|
-
'start_pos': 0,
|
|
931
|
-
'end_pos': len(code),
|
|
932
|
-
'code': code.strip()
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
return cells
|
|
936
|
-
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_cells_from_code(code: str) -> list:
|
|
5
|
+
"""Extracts cells from code. The return value is a
|
|
6
|
+
list of cells, where each cell is a dict like so:
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
'description': str | None,
|
|
10
|
+
'start_pos': int,
|
|
11
|
+
'end_pos': int,
|
|
12
|
+
'code': str
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
A description only applies to cells that have been defined with
|
|
16
|
+
triple-quoted strings. Otherwise it is None.
|
|
17
|
+
|
|
18
|
+
Cells can be defined by:
|
|
19
|
+
- # %%
|
|
20
|
+
- #%%
|
|
21
|
+
- # In[]:
|
|
22
|
+
- Triple-quoted strings
|
|
23
|
+
"""
|
|
24
|
+
# Store all detected cells
|
|
25
|
+
cells = []
|
|
26
|
+
|
|
27
|
+
# Define regex patterns for cell markers
|
|
28
|
+
marker_pattern = re.compile(r'^(# %%|#%%|# In\[\]:?).*?$', re.MULTILINE)
|
|
29
|
+
triple_quotes_pattern = re.compile(
|
|
30
|
+
r'(?m)^(""".*?"""|\'\'\'.*?\'\'\')',
|
|
31
|
+
re.DOTALL
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Find all marker-based cells
|
|
35
|
+
marker_matches = list(marker_pattern.finditer(code))
|
|
36
|
+
|
|
37
|
+
# Find all triple-quoted docstrings that are at the top level
|
|
38
|
+
# (we'll filter out non-top-level ones later)
|
|
39
|
+
docstring_matches = list(triple_quotes_pattern.finditer(code))
|
|
40
|
+
|
|
41
|
+
# Combine all match types and sort by position
|
|
42
|
+
all_markers = []
|
|
43
|
+
|
|
44
|
+
# Add marker matches
|
|
45
|
+
for match in marker_matches:
|
|
46
|
+
start_pos = match.start()
|
|
47
|
+
all_markers.append({
|
|
48
|
+
'type': 'marker',
|
|
49
|
+
'match': match,
|
|
50
|
+
'start_pos': start_pos
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# Add docstring matches
|
|
54
|
+
for match in docstring_matches:
|
|
55
|
+
start_pos = match.start()
|
|
56
|
+
# Only include if not inside string/comment (simplified check)
|
|
57
|
+
# We'll do more thorough validation later
|
|
58
|
+
all_markers.append({
|
|
59
|
+
'type': 'docstring',
|
|
60
|
+
'match': match,
|
|
61
|
+
'start_pos': start_pos
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Sort all markers by position
|
|
65
|
+
all_markers.sort(key=lambda x: x['start_pos'])
|
|
66
|
+
|
|
67
|
+
# Process each marker to create cells
|
|
68
|
+
for i, marker in enumerate(all_markers):
|
|
69
|
+
match = marker['match']
|
|
70
|
+
start_pos = marker['start_pos']
|
|
71
|
+
|
|
72
|
+
# Determine where this cell ends (next marker or end of file)
|
|
73
|
+
if i < len(all_markers) - 1:
|
|
74
|
+
end_pos = all_markers[i + 1]['start_pos']
|
|
75
|
+
else:
|
|
76
|
+
end_pos = len(code)
|
|
77
|
+
|
|
78
|
+
# Check if this docstring is actually a top-level triple-quoted string
|
|
79
|
+
# and not part of a function definition, string literal, etc.
|
|
80
|
+
if marker['type'] == 'docstring':
|
|
81
|
+
# Skip if not a top-level docstring (simplified check)
|
|
82
|
+
# This is a simplification; a more robust solution would check code structure
|
|
83
|
+
line_start = code.rfind('\n', 0, start_pos) + 1
|
|
84
|
+
if line_start < start_pos:
|
|
85
|
+
indent = code[line_start:start_pos].strip()
|
|
86
|
+
if indent: # If there's text before the docstring on the same line, skip it
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Extract description from the docstring
|
|
90
|
+
docstring_content = match.group(0)[3:-3].strip()
|
|
91
|
+
|
|
92
|
+
# Extract code content (from end of docstring to end of cell)
|
|
93
|
+
docstring_end = match.end()
|
|
94
|
+
cell_code = code[docstring_end:end_pos].strip()
|
|
95
|
+
|
|
96
|
+
cells.append({
|
|
97
|
+
'description': docstring_content,
|
|
98
|
+
'start_pos': start_pos,
|
|
99
|
+
'end_pos': end_pos,
|
|
100
|
+
'code': cell_code
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
else: # Marker-based cell
|
|
104
|
+
# Extract code content (from end of marker line to end of cell)
|
|
105
|
+
marker_line_end = code.find('\n', start_pos)
|
|
106
|
+
if marker_line_end == -1: # If no newline is found
|
|
107
|
+
marker_line_end = len(code)
|
|
108
|
+
|
|
109
|
+
cell_code = code[marker_line_end + 1:end_pos].strip()
|
|
110
|
+
|
|
111
|
+
cells.append({
|
|
112
|
+
'description': None,
|
|
113
|
+
'start_pos': start_pos,
|
|
114
|
+
'end_pos': end_pos,
|
|
115
|
+
'code': cell_code
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Handle the case where the file starts with code before any marker
|
|
119
|
+
if all_markers and all_markers[0]['start_pos'] > 0:
|
|
120
|
+
first_code = code[:all_markers[0]['start_pos']].strip()
|
|
121
|
+
if first_code:
|
|
122
|
+
cells.insert(0, {
|
|
123
|
+
'description': None,
|
|
124
|
+
'start_pos': 0,
|
|
125
|
+
'end_pos': all_markers[0]['start_pos'],
|
|
126
|
+
'code': first_code
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
# Handle the case where there are no markers at all
|
|
130
|
+
if not cells and code.strip():
|
|
131
|
+
cells.append({
|
|
132
|
+
'description': None,
|
|
133
|
+
'start_pos': 0,
|
|
134
|
+
'end_pos': len(code),
|
|
135
|
+
'code': code.strip()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return cells
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import tokenize
|
|
2
|
+
import io
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def mask_str_in_code(code: str, mask_char: str = 'X') -> str:
|
|
7
|
+
"""Takes Python code and masks all characters that are part of a string
|
|
8
|
+
by the mask_char.
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
---------
|
|
12
|
+
|
|
13
|
+
In:
|
|
14
|
+
|
|
15
|
+
print('test')
|
|
16
|
+
|
|
17
|
+
Out:
|
|
18
|
+
|
|
19
|
+
print('XXXX')
|
|
20
|
+
|
|
21
|
+
In:
|
|
22
|
+
|
|
23
|
+
print('A string with \'quotes\'')
|
|
24
|
+
|
|
25
|
+
Out:
|
|
26
|
+
|
|
27
|
+
print('XXXXXXXXXXXXXXXXXXXXXXXX')
|
|
28
|
+
|
|
29
|
+
In:
|
|
30
|
+
|
|
31
|
+
'''A triple quoted string
|
|
32
|
+
across multiple lines
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
Out:
|
|
36
|
+
|
|
37
|
+
'''XXXXXXXXXXXXXXXXXXXXXX
|
|
38
|
+
XXXXXXXXXXXXXXXXXXXXX
|
|
39
|
+
'''
|
|
40
|
+
"""
|
|
41
|
+
# First handle f-strings separately using regex
|
|
42
|
+
# This regex matches f-strings with their content
|
|
43
|
+
fstring_pattern = r'(f)(""".*?"""|\'\'\'.*?\'\'\'|".*?"|\'.*?\')'
|
|
44
|
+
|
|
45
|
+
def mask_fstring(match):
|
|
46
|
+
prefix = match.group(1)
|
|
47
|
+
full_string = match.group(2)
|
|
48
|
+
quote = ''
|
|
49
|
+
content = ''
|
|
50
|
+
|
|
51
|
+
if full_string.startswith(('"""', "'''")):
|
|
52
|
+
quote = full_string[:3]
|
|
53
|
+
content = full_string[3:-3]
|
|
54
|
+
else:
|
|
55
|
+
quote = full_string[0]
|
|
56
|
+
content = full_string[1:-1]
|
|
57
|
+
|
|
58
|
+
# Mask everything in f-string content
|
|
59
|
+
masked_content = mask_char * len(content)
|
|
60
|
+
return prefix + quote + masked_content + quote
|
|
61
|
+
|
|
62
|
+
# Apply f-string masking
|
|
63
|
+
code = re.sub(fstring_pattern, mask_fstring, code, flags=re.DOTALL)
|
|
64
|
+
|
|
65
|
+
# Now handle regular strings with tokenizer
|
|
66
|
+
code_bytes = code.encode('utf-8')
|
|
67
|
+
readline = io.BytesIO(code_bytes).readline
|
|
68
|
+
|
|
69
|
+
masked_parts = []
|
|
70
|
+
last_end = (1, 0)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
tokens = tokenize.tokenize(readline)
|
|
74
|
+
|
|
75
|
+
for token in tokens:
|
|
76
|
+
if token.type == tokenize.STRING:
|
|
77
|
+
# Get the string content including quotes
|
|
78
|
+
string_value = token.string
|
|
79
|
+
|
|
80
|
+
# Skip if it looks like an f-string (already handled)
|
|
81
|
+
if string_value.startswith(('f"', "f'", 'f"""', "f'''")):
|
|
82
|
+
# Add any code between last token and this one
|
|
83
|
+
start_line, start_col = token.start
|
|
84
|
+
end_line, end_col = last_end
|
|
85
|
+
|
|
86
|
+
if start_line > end_line or (start_line == end_line and start_col > end_col):
|
|
87
|
+
lines = code.split('\n')
|
|
88
|
+
if end_line == start_line:
|
|
89
|
+
masked_parts.append(lines[end_line - 1][end_col:start_col])
|
|
90
|
+
else:
|
|
91
|
+
# Add remainder of last line
|
|
92
|
+
masked_parts.append(lines[end_line - 1][end_col:])
|
|
93
|
+
masked_parts.append('\n')
|
|
94
|
+
# Add intermediate lines
|
|
95
|
+
for line_num in range(end_line, start_line - 1):
|
|
96
|
+
masked_parts.append(lines[line_num])
|
|
97
|
+
masked_parts.append('\n')
|
|
98
|
+
# Add beginning of current line
|
|
99
|
+
masked_parts.append(lines[start_line - 1][:start_col])
|
|
100
|
+
|
|
101
|
+
masked_parts.append(string_value)
|
|
102
|
+
last_end = token.end
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Determine quote style and content
|
|
106
|
+
prefix = ''
|
|
107
|
+
quote = ''
|
|
108
|
+
end_quote = ''
|
|
109
|
+
content = ''
|
|
110
|
+
|
|
111
|
+
# Check for string prefix (r, b, u, etc. - but not f)
|
|
112
|
+
i = 0
|
|
113
|
+
while i < len(string_value) and string_value[i].lower() in 'rbux':
|
|
114
|
+
prefix += string_value[i]
|
|
115
|
+
i += 1
|
|
116
|
+
|
|
117
|
+
remaining = string_value[i:]
|
|
118
|
+
|
|
119
|
+
if remaining.startswith(('"""', "'''")):
|
|
120
|
+
quote = prefix + remaining[:3]
|
|
121
|
+
end_quote = remaining[-3:]
|
|
122
|
+
content = remaining[3:-3]
|
|
123
|
+
elif remaining.startswith(('"', "'")):
|
|
124
|
+
quote = prefix + remaining[0]
|
|
125
|
+
end_quote = remaining[-1]
|
|
126
|
+
content = remaining[1:-1]
|
|
127
|
+
|
|
128
|
+
# For regular strings, mask content while preserving newlines and leading spaces
|
|
129
|
+
lines = content.split('\n')
|
|
130
|
+
masked_lines = []
|
|
131
|
+
|
|
132
|
+
for line in lines:
|
|
133
|
+
# Find leading whitespace
|
|
134
|
+
leading_space = ''
|
|
135
|
+
for char in line:
|
|
136
|
+
if char in (' ', '\t'):
|
|
137
|
+
leading_space += char
|
|
138
|
+
else:
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
# Mask the rest of the line
|
|
142
|
+
rest_of_line = line[len(leading_space):]
|
|
143
|
+
masked_rest = mask_char * len(rest_of_line)
|
|
144
|
+
masked_lines.append(leading_space + masked_rest)
|
|
145
|
+
|
|
146
|
+
masked_content = '\n'.join(masked_lines)
|
|
147
|
+
|
|
148
|
+
masked_string = quote + masked_content + end_quote
|
|
149
|
+
|
|
150
|
+
# Add any code between last token and this one
|
|
151
|
+
start_line, start_col = token.start
|
|
152
|
+
end_line, end_col = last_end
|
|
153
|
+
|
|
154
|
+
if start_line > end_line or (start_line == end_line and start_col > end_col):
|
|
155
|
+
lines = code.split('\n')
|
|
156
|
+
if end_line == start_line:
|
|
157
|
+
masked_parts.append(lines[end_line - 1][end_col:start_col])
|
|
158
|
+
else:
|
|
159
|
+
# Add remainder of last line
|
|
160
|
+
masked_parts.append(lines[end_line - 1][end_col:])
|
|
161
|
+
masked_parts.append('\n')
|
|
162
|
+
# Add intermediate lines
|
|
163
|
+
for line_num in range(end_line, start_line - 1):
|
|
164
|
+
masked_parts.append(lines[line_num])
|
|
165
|
+
masked_parts.append('\n')
|
|
166
|
+
# Add beginning of current line
|
|
167
|
+
masked_parts.append(lines[start_line - 1][:start_col])
|
|
168
|
+
|
|
169
|
+
masked_parts.append(masked_string)
|
|
170
|
+
last_end = token.end
|
|
171
|
+
|
|
172
|
+
except tokenize.TokenError:
|
|
173
|
+
# Handle incomplete code
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Add any remaining code
|
|
177
|
+
lines = code.split('\n')
|
|
178
|
+
end_line, end_col = last_end
|
|
179
|
+
if end_line <= len(lines):
|
|
180
|
+
masked_parts.append(lines[end_line - 1][end_col:])
|
|
181
|
+
if end_line < len(lines):
|
|
182
|
+
masked_parts.append('\n')
|
|
183
|
+
masked_parts.extend('\n'.join(lines[end_line:]))
|
|
184
|
+
|
|
185
|
+
return ''.join(masked_parts)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ..providers import jedi, codestral, ruff
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def complete(code, cursor_pos, path, multiline, full):
|
|
5
|
+
if full or multiline:
|
|
6
|
+
codestral_completions = codestral.codestral_complete(
|
|
7
|
+
code, cursor_pos, path=path, multiline=multiline)
|
|
8
|
+
else:
|
|
9
|
+
codestral_completions = []
|
|
10
|
+
if not multiline:
|
|
11
|
+
jedi_completions = jedi.jedi_complete(code, cursor_pos, path=path)
|
|
12
|
+
else:
|
|
13
|
+
jedi_completions = []
|
|
14
|
+
# If there is at least one jedi completion, it is always insert at first.
|
|
15
|
+
# This is to avoid the codestral completion from suddenly replacing the
|
|
16
|
+
# jedi completion that is shown already during the first (non-full) pass
|
|
17
|
+
if jedi_completions:
|
|
18
|
+
return [jedi_completions[0]] + codestral_completions + jedi_completions[1:]
|
|
19
|
+
return codestral_completions + jedi_completions
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
calltip = jedi.jedi_signatures
|
|
23
|
+
check = ruff.ruff_check
|
|
24
|
+
symbols = jedi.jedi_symbols
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from ..providers import jedi, codestral, ruff
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def complete(code, cursor_pos, path, multiline, full):
|
|
5
|
-
if full or multiline:
|
|
6
|
-
completions = codestral.codestral_complete(
|
|
7
|
-
code, cursor_pos, path=path, multiline=multiline)
|
|
8
|
-
else:
|
|
9
|
-
completions = []
|
|
10
|
-
if not multiline:
|
|
11
|
-
completions += jedi.jedi_complete(code, cursor_pos, path=path)
|
|
12
|
-
return completions
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
calltip = jedi.jedi_signatures
|
|
16
|
-
check = ruff.ruff_check
|
|
17
|
-
symbols = jedi.jedi_symbols
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/__init__.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/code_editors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/text.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/editor_panel.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/find_in_files.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/jupyter_console.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/project_explorer.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/settings_panel.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
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/search_replace.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/widgets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/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
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/calltip_widget.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_open_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_symbol_dialog.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/search_replace_frame.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/tab_splitter.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/tabbed_editor.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/languages/generic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/__init__.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/codestral.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/jedi.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/ruff.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/sigmund.py
RENAMED
|
File without changes
|
{pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/symbol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|