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.
Files changed (78) hide show
  1. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/PKG-INFO +1 -1
  2. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/__init__.py +1 -1
  3. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/app.py +1 -0
  4. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/complete.py +33 -3
  5. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/execute.py +1 -1
  6. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/python_auto_indent.py +11 -3
  7. pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/__init__.py +3 -0
  8. 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
  9. pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/_extract_cells.py +138 -0
  10. pyqt_code_editor-0.0.16/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +185 -0
  11. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/completion_popup.py +0 -1
  12. pyqt_code_editor-0.0.16/pyqt_code_editor/worker/languages/__init__.py +0 -0
  13. pyqt_code_editor-0.0.16/pyqt_code_editor/worker/languages/python.py +24 -0
  14. pyqt_code_editor-0.0.14/pyqt_code_editor/worker/languages/python.py +0 -17
  15. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/COPYING +0 -0
  16. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyproject.toml +0 -0
  17. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/_settings.py +0 -0
  18. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/__init__.py +0 -0
  19. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  20. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  21. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
  22. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  23. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/python.py +0 -0
  24. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  25. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/__init__.py +0 -0
  26. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/editor_panel.py +0 -0
  27. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/find_in_files.py +0 -0
  28. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/jupyter_console.py +0 -0
  29. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/project_explorer.py +0 -0
  30. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/settings_panel.py +0 -0
  31. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/sigmund.py +0 -0
  32. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  33. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/__init__.py +0 -0
  34. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/auto_indent.py +0 -0
  35. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  36. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/base.py +0 -0
  37. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/check.py +0 -0
  38. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/comment.py +0 -0
  39. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/file_link.py +0 -0
  40. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/line_number.py +0 -0
  41. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/search_replace.py +0 -0
  42. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  43. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  44. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/symbols.py +0 -0
  45. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/theme.py +0 -0
  46. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  47. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/mixins/zoom.py +0 -0
  48. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/signal_router.py +0 -0
  49. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  50. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  51. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  52. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  53. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  54. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/themes.py +0 -0
  55. /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
  56. {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
  57. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/watchdog.py +0 -0
  58. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/__init__.py +0 -0
  59. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  60. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/dock.py +0 -0
  61. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  62. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  63. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  64. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  65. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  66. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  67. {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
  68. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/languages/generic.py +0 -0
  69. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/manager.py +0 -0
  70. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/process.py +0 -0
  71. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  72. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  73. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/jedi.py +0 -0
  74. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/ruff.py +0 -0
  75. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  76. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  77. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/pyqt_code_editor/worker/settings.py +0 -0
  78. {pyqt_code_editor-0.0.14 → pyqt_code_editor-0.0.16}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqt_code_editor
3
- Version: 0.0.14
3
+ Version: 0.0.16
4
4
  Summary: Fully featured code-editor widgets for PyQt
5
5
  Keywords: code editor,pyqt,ide
6
6
  Author-email: Sebastiaan Mathôt <s.mathot@cogsci.nl>
@@ -2,4 +2,4 @@
2
2
 
3
3
  from ._settings import settings
4
4
 
5
- __version__ = '0.0.14'
5
+ __version__ = '0.0.16'
@@ -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, such as Hel + lo = Hello,
358
- # and not Helo.
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
- left_overlap_size = 0
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 .. python_utils import extract_cells_from_code
4
+ from ..utils.languages.python import extract_cells_from_code
5
5
  from .. import settings
6
6
  import logging
7
7
 
@@ -1,5 +1,7 @@
1
1
  from qtpy.QtCore import Qt
2
- from .. import python_utils, settings
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
- code = self.toPlainText()[:cursor.position()]
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
 
@@ -0,0 +1,3 @@
1
+ from ._auto_indent import python_auto_indent
2
+ from ._extract_cells import extract_cells_from_code
3
+ from ._mask_str_in_code import mask_str_in_code
@@ -1,9 +1,16 @@
1
- import re
2
- from . import settings
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
- open_to_close = {'(': ')', '[': ']', '{': '}'}
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, raw_line in enumerate(lines):
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(stripped_line):
41
- ch = stripped_line[j]
42
- if ch in open_to_close:
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 close_to_open:
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] == close_to_open[ch]:
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 or the last
61
- # quote, whatever comes last. This is a cheap way to avoid the auto indenter
62
- # from being confused by parentheses in strings or comments.
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, last_quote, last_comment)
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, char_idx : int) -> int:
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 is_block_opener(lines[i]):
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)
@@ -1,5 +1,4 @@
1
1
  from qtpy.QtCore import Qt
2
- from qtpy.QtGui import QTextCursor
3
2
  from qtpy.QtWidgets import QListWidget, QListWidgetItem
4
3
  from .. import settings
5
4
 
@@ -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