pyqt-code-editor 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyqt_code_editor/__init__.py +5 -0
- pyqt_code_editor/_settings.py +232 -0
- pyqt_code_editor/app.py +438 -0
- pyqt_code_editor/code_editors/__init__.py +1 -0
- pyqt_code_editor/code_editors/code_editors.py +33 -0
- pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- pyqt_code_editor/code_editors/languages/generic.py +11 -0
- pyqt_code_editor/code_editors/languages/markdown.py +11 -0
- pyqt_code_editor/code_editors/languages/python.py +13 -0
- pyqt_code_editor/code_editors/languages/text.py +9 -0
- pyqt_code_editor/components/__init__.py +0 -0
- pyqt_code_editor/components/editor_panel.py +399 -0
- pyqt_code_editor/components/find_in_files.py +199 -0
- pyqt_code_editor/components/jupyter_console.py +487 -0
- pyqt_code_editor/components/project_explorer.py +515 -0
- pyqt_code_editor/components/settings_panel.py +162 -0
- pyqt_code_editor/components/sigmund.py +75 -0
- pyqt_code_editor/components/workspace_explorer.py +162 -0
- pyqt_code_editor/mixins/__init__.py +15 -0
- pyqt_code_editor/mixins/auto_pair.py +201 -0
- pyqt_code_editor/mixins/base.py +142 -0
- pyqt_code_editor/mixins/check.py +94 -0
- pyqt_code_editor/mixins/comment.py +91 -0
- pyqt_code_editor/mixins/complete.py +482 -0
- pyqt_code_editor/mixins/execute.py +182 -0
- pyqt_code_editor/mixins/file_link.py +166 -0
- pyqt_code_editor/mixins/line_number.py +206 -0
- pyqt_code_editor/mixins/python_auto_indent.py +239 -0
- pyqt_code_editor/mixins/search_replace.py +396 -0
- pyqt_code_editor/mixins/shortcuts.py +228 -0
- pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
- pyqt_code_editor/mixins/symbols.py +45 -0
- pyqt_code_editor/mixins/theme.py +99 -0
- pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- pyqt_code_editor/mixins/zoom.py +51 -0
- pyqt_code_editor/python_utils.py +936 -0
- pyqt_code_editor/signal_router.py +116 -0
- pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
- pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
- pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
- pyqt_code_editor/themes.py +36 -0
- pyqt_code_editor/utils.py +96 -0
- pyqt_code_editor/watchdog.py +141 -0
- pyqt_code_editor/widgets/__init__.py +6 -0
- pyqt_code_editor/widgets/calltip_widget.py +44 -0
- pyqt_code_editor/widgets/completion_popup.py +89 -0
- pyqt_code_editor/widgets/dock.py +15 -0
- pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
- pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
- pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
- pyqt_code_editor/widgets/search_replace_frame.py +128 -0
- pyqt_code_editor/widgets/tab_splitter.py +22 -0
- pyqt_code_editor/widgets/tabbed_editor.py +351 -0
- pyqt_code_editor/worker/__init__.py +0 -0
- pyqt_code_editor/worker/languages/__init__.py +0 -0
- pyqt_code_editor/worker/languages/generic.py +15 -0
- pyqt_code_editor/worker/languages/python.py +17 -0
- pyqt_code_editor/worker/manager.py +113 -0
- pyqt_code_editor/worker/process.py +128 -0
- pyqt_code_editor/worker/providers/__init__.py +1 -0
- pyqt_code_editor/worker/providers/codestral.py +56 -0
- pyqt_code_editor/worker/providers/jedi.py +115 -0
- pyqt_code_editor/worker/providers/ruff.py +45 -0
- pyqt_code_editor/worker/providers/sigmund.py +29 -0
- pyqt_code_editor/worker/providers/symbol.py +78 -0
- pyqt_code_editor/worker/settings.py +1 -0
- pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
- pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
- pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
- pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
- pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from . import settings
|
|
3
|
+
import logging
|
|
4
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_leading_spaces(line: str) -> int:
|
|
9
|
+
"""
|
|
10
|
+
Return the number of leading spaces in line.
|
|
11
|
+
"""
|
|
12
|
+
return len(line) - len(line.lstrip(' '))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_brackets(code: str):
|
|
16
|
+
"""
|
|
17
|
+
Parse all lines of code, ignoring quoted strings in a simple way,
|
|
18
|
+
and return:
|
|
19
|
+
bracket_stack: A list of unclosed brackets in order of appearance,
|
|
20
|
+
each as (bracket_char, line_index, col_index).
|
|
21
|
+
bracket_closings: A dict mapping close_line_idx -> (open_line_idx, open_line_indent, open_bracket).
|
|
22
|
+
"""
|
|
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
|
+
|
|
31
|
+
lines = code.split("\n")
|
|
32
|
+
bracket_stack = []
|
|
33
|
+
bracket_closings = {}
|
|
34
|
+
|
|
35
|
+
for i, raw_line in enumerate(lines):
|
|
36
|
+
# remove quoted strings
|
|
37
|
+
stripped_line = remove_strings(raw_line)
|
|
38
|
+
|
|
39
|
+
j = 0
|
|
40
|
+
while j < len(stripped_line):
|
|
41
|
+
ch = stripped_line[j]
|
|
42
|
+
if ch in open_to_close:
|
|
43
|
+
bracket_stack.append((ch, i, j))
|
|
44
|
+
elif ch in close_to_open:
|
|
45
|
+
# 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]:
|
|
47
|
+
open_bracket_char, open_line_idx, open_col_idx = bracket_stack.pop()
|
|
48
|
+
# record the closing bracket
|
|
49
|
+
bracket_closings[i] = (open_line_idx, get_leading_spaces(lines[open_line_idx]), open_bracket_char)
|
|
50
|
+
j += 1
|
|
51
|
+
|
|
52
|
+
return bracket_stack, bracket_closings
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_unclosed_open_idx(code : str, open_char : str,
|
|
56
|
+
close_char : str) -> int | None:
|
|
57
|
+
"""Returns the index of the last unclosed unopening character, such as an
|
|
58
|
+
opening parenthesis, or None if no unclosed opening character was found.
|
|
59
|
+
"""
|
|
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('"'))
|
|
64
|
+
last_comment = code.rfind('#')
|
|
65
|
+
if last_comment >= 0:
|
|
66
|
+
last_comment_end = code[last_comment:].find('\n')
|
|
67
|
+
if last_comment_end >= 0:
|
|
68
|
+
last_comment += last_comment_end
|
|
69
|
+
search_start = max(0, last_quote, last_comment)
|
|
70
|
+
if search_start:
|
|
71
|
+
code = code[search_start:]
|
|
72
|
+
n_open_required = 1
|
|
73
|
+
while n_open_required:
|
|
74
|
+
close_idx = code.rfind(close_char)
|
|
75
|
+
open_idx = code.rfind(open_char)
|
|
76
|
+
if close_idx < 0 and open_idx < 0:
|
|
77
|
+
break
|
|
78
|
+
if open_idx > close_idx:
|
|
79
|
+
n_open_required -= 1
|
|
80
|
+
code = code[:open_idx]
|
|
81
|
+
else:
|
|
82
|
+
n_open_required += 1
|
|
83
|
+
code = code[:close_idx]
|
|
84
|
+
if open_idx < 0:
|
|
85
|
+
return 0
|
|
86
|
+
return open_idx + search_start
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _line_col_from_idx(full_text, char_index):
|
|
90
|
+
"""
|
|
91
|
+
Return the 0-based (line, column) pair in `full_text` for the given `char_index`.
|
|
92
|
+
If char_index is out of range, this function raises an IndexError.
|
|
93
|
+
"""
|
|
94
|
+
if not (0 <= char_index < len(full_text)):
|
|
95
|
+
raise IndexError("char_index out of range.")
|
|
96
|
+
|
|
97
|
+
line = 0
|
|
98
|
+
column = 0
|
|
99
|
+
idx = 0
|
|
100
|
+
|
|
101
|
+
for ch in full_text:
|
|
102
|
+
if idx == char_index:
|
|
103
|
+
return (line, column)
|
|
104
|
+
if ch == '\n':
|
|
105
|
+
line += 1
|
|
106
|
+
column = 0
|
|
107
|
+
else:
|
|
108
|
+
column += 1
|
|
109
|
+
idx += 1
|
|
110
|
+
|
|
111
|
+
# If char_index matches len(full_text)-1, return the final position:
|
|
112
|
+
return (line, column)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _indent_inside_unclosed_call_def_or_class(code: str, char_idx : int) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Return after opening parenthesis as part of function call should trigger indent
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
function(>
|
|
121
|
+
|
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Return after parenthesis after function definition should trigger indent
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
def test(>
|
|
128
|
+
|
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Return after parenthesis after class definition should trigger indent
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
class Test(>
|
|
135
|
+
|
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Return after opening parenthesis and one or more arguments as part of function call should trigger matching indent to first argument
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
function(arg1,>
|
|
142
|
+
|
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Return after parenthesis and one or more arguments after function definition should trigger matching indent to first argument
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
def test(arg1,>
|
|
149
|
+
|
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Return after parenthesis and one or more arguments after class definition should trigger matching indent to first argument
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
class Test(object,>
|
|
156
|
+
|
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Return after opening parenthesis and one or more arguments on the next line as part of function call should trigger matching indent to first argument
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
function(
|
|
163
|
+
arg1,>
|
|
164
|
+
|
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Return after parenthesis and one or more arguments on the next line after function definition should trigger matching indent to first argument
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
def test(
|
|
171
|
+
arg1,>
|
|
172
|
+
|
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Return after parenthesis and one or more arguments after class definition should trigger matching indent to first argument
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
class Test(
|
|
179
|
+
object,>
|
|
180
|
+
|
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Return after opening parenthesis should also work in nested situations
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
class Test:
|
|
187
|
+
def test(>
|
|
188
|
+
|
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Return after opening parenthesis with arguments should also work in nested situations
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
class Test:
|
|
195
|
+
def test(arg1,>
|
|
196
|
+
|
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Return after opening parenthesis with arguments should also work in nested situations
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
class Test:
|
|
203
|
+
def test(
|
|
204
|
+
arg1,>
|
|
205
|
+
|
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Return after opening parenthesis should also work with irrelevant parentheses
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
# 1) a list item
|
|
212
|
+
class Test:
|
|
213
|
+
def test(>
|
|
214
|
+
|
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Return after opening parenthesis should also work with irrelevant parentheses
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
# (1 a list item
|
|
221
|
+
class Test:
|
|
222
|
+
def test(>
|
|
223
|
+
|
|
|
224
|
+
```
|
|
225
|
+
"""
|
|
226
|
+
lines = code.splitlines()
|
|
227
|
+
if not lines:
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
line_idx, col_idx = _line_col_from_idx(code, char_idx)
|
|
231
|
+
# Get the line where the last unclosed parenthesis is
|
|
232
|
+
open_line = lines[line_idx]
|
|
233
|
+
# Collect text after '(' in that line
|
|
234
|
+
text_after_paren = open_line[col_idx + 1:].rstrip()
|
|
235
|
+
# If there's no text after '(' on the same line, just indent by tab
|
|
236
|
+
if not text_after_paren:
|
|
237
|
+
# Get indentation of open_line
|
|
238
|
+
open_line_indent = len(open_line) - len(open_line.lstrip())
|
|
239
|
+
return open_line_indent + settings.tab_width
|
|
240
|
+
# Otherwise, align with the first non-whitespace character after '('
|
|
241
|
+
prefix = open_line[: col_idx + 1]
|
|
242
|
+
after_paren_offset = 0
|
|
243
|
+
for char in open_line[col_idx + 1:]:
|
|
244
|
+
if char.isspace():
|
|
245
|
+
after_paren_offset += 1
|
|
246
|
+
else:
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
return len(prefix) + after_paren_offset
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _indent_after_block_opener(code: str) -> int:
|
|
253
|
+
"""
|
|
254
|
+
Return after function definition should trigger indent
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
def test():>
|
|
258
|
+
|
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Return after class definition should trigger indent
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
class Test:>
|
|
265
|
+
|
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Return after class definition with inheritance should trigger indent
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
class Test(object):>
|
|
272
|
+
|
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Return after colon after function definition should dedent to function level, regardless of the indentation of the current line
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
def test(arg1,
|
|
279
|
+
arg2):>
|
|
280
|
+
|
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Return after colon after class definition should dedent to class level, regardless of the indentation of the current line
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
class Test(object1,
|
|
287
|
+
object2):>
|
|
288
|
+
|
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Return after function definition should also work with irrelevant parentheses
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
# 1) a list item
|
|
295
|
+
class Test:
|
|
296
|
+
def test():>
|
|
297
|
+
|
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Return after function definition should also work with irrelevant parentheses
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
# (1 a list item
|
|
304
|
+
class Test:
|
|
305
|
+
def test():>
|
|
306
|
+
|
|
|
307
|
+
|
|
308
|
+
Return after colon after while should trigger indent:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
while True:>
|
|
312
|
+
|
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Return after colon after for should trigger indent:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
for i in range(10):>
|
|
319
|
+
|
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Return after if should trigger indent:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
if x == y:>
|
|
326
|
+
|
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Return after else should trigger indent:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
else:>
|
|
333
|
+
|
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Return after elif should trigger indent:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
elif x == y:>
|
|
340
|
+
|
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Return after colon after with should trigger indent:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
with context:>
|
|
347
|
+
|
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Return after colon after with ... as should trigger indent:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
with context as obj:>
|
|
354
|
+
|
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Return after try should trigger indent:
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
try:>
|
|
361
|
+
|
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Return after except should trigger indent:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
except:>
|
|
368
|
+
|
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Return after except ... some exception class should trigger indent:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
except Exception:>
|
|
375
|
+
|
|
|
376
|
+
```
|
|
377
|
+
Return after except ... as should trigger indent:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
except Exception as e:>
|
|
381
|
+
|
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Return after finally should trigger indent:
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
finally:>
|
|
388
|
+
|
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Indent after function definition should also work in nested situations:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
class Test:
|
|
395
|
+
def test():>
|
|
396
|
+
|
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Indent after if should also work in nested situations:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
def test():
|
|
403
|
+
if x == y:>
|
|
404
|
+
|
|
|
405
|
+
```
|
|
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()
|
|
424
|
+
if not lines:
|
|
425
|
+
return 0
|
|
426
|
+
|
|
427
|
+
last_line = lines[-1]
|
|
428
|
+
trimmed_line = last_line.rstrip()
|
|
429
|
+
if not trimmed_line.endswith(':'):
|
|
430
|
+
# just return current line's indentation
|
|
431
|
+
return get_leading_spaces(last_line)
|
|
432
|
+
|
|
433
|
+
# If it DOES end with a colon, we see if we can locate the block opener line
|
|
434
|
+
block_opener_idx = len(lines) - 1
|
|
435
|
+
for i in range(len(lines) - 1, -1, -1):
|
|
436
|
+
if is_block_opener(lines[i]):
|
|
437
|
+
block_opener_idx = i
|
|
438
|
+
break
|
|
439
|
+
|
|
440
|
+
block_opener_indent = get_leading_spaces(lines[block_opener_idx])
|
|
441
|
+
return block_opener_indent + settings.tab_width
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _indent_inside_uncloded_list_tuple_set_or_dict(code: str, char_idx: int) -> int:
|
|
445
|
+
"""
|
|
446
|
+
Return after opening of list should trigger matching indent to first element
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
l = [>
|
|
450
|
+
|
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Return after opening of tuple should trigger matching indent to first element
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
t = (>
|
|
457
|
+
|
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Return after opening of set should trigger matching indent to first element
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
s = {>
|
|
464
|
+
|
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Return after opening of dict should trigger matching indent to first element
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
d = {>
|
|
471
|
+
|
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Return after opening of list with one or more elements should trigger matching indent to first element
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
l = [item1,>
|
|
478
|
+
|
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Return after opening of tuple with one or more elements should trigger matching indent to first element
|
|
482
|
+
|
|
483
|
+
```
|
|
484
|
+
t = (item1,>
|
|
485
|
+
|
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Return after opening of set with one or more elements should trigger matching indent to first element
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
s = {item1,>
|
|
492
|
+
|
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Return after opening of dict with one or more elements should trigger matching indent to first element
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
d = {key1:val1,>
|
|
499
|
+
|
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Return after opening of list with one or more elements on the next line should trigger matching indent to first element
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
l = [
|
|
506
|
+
item1,>
|
|
507
|
+
|
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Return after opening of tuple with one or more elements on the next line should trigger matching indent to first element
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
t = (
|
|
514
|
+
item1,>
|
|
515
|
+
|
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Return after opening of set with one or more elements on the next line should trigger matching indent to first element
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
s = {
|
|
522
|
+
item1,>
|
|
523
|
+
|
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Return after opening of dict with one or more elements on the next line should trigger matching indent to first element
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
d = {
|
|
530
|
+
key1:val1,>
|
|
531
|
+
|
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Return after opening of list should also work in nested situations:
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
def test():
|
|
538
|
+
l = [>
|
|
539
|
+
|
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Return after opening of list with one or more elements should also work in nested situations:
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
def test():
|
|
546
|
+
l = [item1,>
|
|
547
|
+
|
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Return after opening of list with one or more elements on the next line should also work in nested situations:
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
def test():
|
|
554
|
+
l = [
|
|
555
|
+
item1,>
|
|
556
|
+
|
|
|
557
|
+
```
|
|
558
|
+
"""
|
|
559
|
+
lines = code.split('\n')
|
|
560
|
+
if not lines:
|
|
561
|
+
return 0
|
|
562
|
+
bracket_line_idx, bracket_col_idx = _line_col_from_idx(code, char_idx)
|
|
563
|
+
# Get the line where the last unclosed parenthesis is
|
|
564
|
+
bracket_line = lines[bracket_line_idx]
|
|
565
|
+
bracket_line_indent = get_leading_spaces(bracket_line)
|
|
566
|
+
|
|
567
|
+
# Check text after bracket on same line
|
|
568
|
+
post_bracket_text_idx = None
|
|
569
|
+
for i in range(bracket_col_idx + 1, len(bracket_line)):
|
|
570
|
+
if bracket_line[i] not in (' ', '\t'):
|
|
571
|
+
post_bracket_text_idx = i
|
|
572
|
+
break
|
|
573
|
+
|
|
574
|
+
if post_bracket_text_idx is not None:
|
|
575
|
+
# Align to the first non-whitespace character after bracket
|
|
576
|
+
return post_bracket_text_idx
|
|
577
|
+
else:
|
|
578
|
+
# No text after bracket on the same line
|
|
579
|
+
# find next non-empty line
|
|
580
|
+
next_non_empty_line_idx = None
|
|
581
|
+
for i in range(bracket_line_idx + 1, len(lines)):
|
|
582
|
+
if lines[i].strip():
|
|
583
|
+
next_non_empty_line_idx = i
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
if next_non_empty_line_idx is None:
|
|
587
|
+
# no subsequent non-empty lines
|
|
588
|
+
return bracket_col_idx + 1
|
|
589
|
+
else:
|
|
590
|
+
# align to the first non-whitespace character of the next line
|
|
591
|
+
next_line = lines[next_non_empty_line_idx]
|
|
592
|
+
for i, ch in enumerate(next_line):
|
|
593
|
+
if ch not in (' ', '\t'):
|
|
594
|
+
return i
|
|
595
|
+
return bracket_col_idx + 1
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _indent_after_list_tuple_set_or_dict(code: str, bracket_stack, bracket_closings) -> int:
|
|
599
|
+
"""
|
|
600
|
+
Return after closing of a single-line list should not change indentation.
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
l = [element1, element2]>
|
|
604
|
+
|
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Return after closing of a single-line tuple should not change indentation.
|
|
608
|
+
|
|
609
|
+
```
|
|
610
|
+
t = [element1, element2]>
|
|
611
|
+
|
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Return after closing of a single-line set should not change indentation.
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
s = {element1, element2}>
|
|
618
|
+
|
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Return after closing of a single-line list should not change indentation.
|
|
622
|
+
|
|
623
|
+
```
|
|
624
|
+
d = {key1: val1, key2: val2}>
|
|
625
|
+
|
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Return after closing of list should trigger dedent to scope level, regardless of the indentation of the current line
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
l = [element1,
|
|
632
|
+
element2]>
|
|
633
|
+
|
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Return after closing of tuple should trigger dedent to scope level, regardless of the indentation of the current line
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
t = (element1,
|
|
640
|
+
element2)>
|
|
641
|
+
|
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Return after closing of set should trigger dedent to scope level, regardless of the indentation of the current line
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
s = {element1,
|
|
648
|
+
element2}>
|
|
649
|
+
|
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Return after closing of dict should trigger dedent to scope level, regardless of the indentation of the current line
|
|
653
|
+
|
|
654
|
+
```
|
|
655
|
+
d = {key1: val1,
|
|
656
|
+
key2: val2}>
|
|
657
|
+
|
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Return after closing of dict should als work with trailing spaces
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
d = {key1: val1,
|
|
664
|
+
key2: val2} >
|
|
665
|
+
|
|
|
666
|
+
```
|
|
667
|
+
"""
|
|
668
|
+
lines = code.split('\n')
|
|
669
|
+
if not lines:
|
|
670
|
+
return 0
|
|
671
|
+
|
|
672
|
+
last_line_idx = len(lines) - 1
|
|
673
|
+
last_line = lines[last_line_idx]
|
|
674
|
+
last_line_indent = get_leading_spaces(last_line)
|
|
675
|
+
if last_line and last_line.rstrip()[-1] in (')', ']', '}'):
|
|
676
|
+
# We closed a bracket
|
|
677
|
+
if last_line_idx in bracket_closings:
|
|
678
|
+
open_line_idx, open_line_indent, open_bracket = bracket_closings[last_line_idx]
|
|
679
|
+
if open_line_idx == last_line_idx:
|
|
680
|
+
# single-line bracket => no change
|
|
681
|
+
return last_line_indent
|
|
682
|
+
else:
|
|
683
|
+
# multi-line => dedent to open bracket line's indent
|
|
684
|
+
return open_line_indent
|
|
685
|
+
else:
|
|
686
|
+
# bracket not found or mismatched => no change
|
|
687
|
+
return last_line_indent
|
|
688
|
+
else:
|
|
689
|
+
# no bracket close => no change
|
|
690
|
+
return last_line_indent
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def python_auto_indent(code: str) -> int:
|
|
694
|
+
"""
|
|
695
|
+
As fallback, should preserve indentation of previous line.
|
|
696
|
+
|
|
697
|
+
```
|
|
698
|
+
def test():
|
|
699
|
+
pass>
|
|
700
|
+
|
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
As fallback, should preserve indentation of previous line.
|
|
704
|
+
|
|
705
|
+
```
|
|
706
|
+
def test():
|
|
707
|
+
pass
|
|
708
|
+
>
|
|
709
|
+
|
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Should dedent after return
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
def test():
|
|
716
|
+
return>
|
|
717
|
+
|
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Should not be affected by irrelevant whitespace
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
# 1) list item
|
|
724
|
+
x = 1>
|
|
725
|
+
|
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Should not be affected by irrelevant whitespace
|
|
729
|
+
|
|
730
|
+
```
|
|
731
|
+
# (1 list item
|
|
732
|
+
x = 1>
|
|
733
|
+
|
|
|
734
|
+
```
|
|
735
|
+
"""
|
|
736
|
+
lines = code.splitlines()
|
|
737
|
+
# If code ends with a newline => cursor is on a fresh line
|
|
738
|
+
if code.endswith('\n'):
|
|
739
|
+
lines.append('')
|
|
740
|
+
if not lines:
|
|
741
|
+
return 0
|
|
742
|
+
last_line = lines[-1].rstrip()
|
|
743
|
+
|
|
744
|
+
# 1. ends with colon => block opener
|
|
745
|
+
if last_line.endswith(":"):
|
|
746
|
+
logging.info('indent after block opener')
|
|
747
|
+
return _indent_after_block_opener(code)
|
|
748
|
+
|
|
749
|
+
# 2. unmatched '(' => function call/def or tuple
|
|
750
|
+
last_paren_idx_tuple = _get_unclosed_open_idx(code, '(', ')')
|
|
751
|
+
if last_paren_idx_tuple:
|
|
752
|
+
preceding_idx = last_paren_idx_tuple - 1
|
|
753
|
+
while preceding_idx >= 0 and code[preceding_idx].isspace():
|
|
754
|
+
preceding_idx -= 1
|
|
755
|
+
if preceding_idx >= 0:
|
|
756
|
+
preceding_char = code[preceding_idx]
|
|
757
|
+
# If preceding char is alnum or _, treat as function call/def
|
|
758
|
+
if preceding_char.isalnum() or preceding_char == "_":
|
|
759
|
+
logging.info("indent as unclosed call/def")
|
|
760
|
+
return _indent_inside_unclosed_call_def_or_class(
|
|
761
|
+
code, last_paren_idx_tuple)
|
|
762
|
+
else:
|
|
763
|
+
logging.info("indent as unclosed tuple")
|
|
764
|
+
return _indent_inside_uncloded_list_tuple_set_or_dict(
|
|
765
|
+
code, last_paren_idx_tuple)
|
|
766
|
+
|
|
767
|
+
# 3. unmatched '[' or '{'
|
|
768
|
+
last_paren_idx_list = _get_unclosed_open_idx(code, '[', ']')
|
|
769
|
+
last_paren_idx_dict = _get_unclosed_open_idx(code, '{', '}')
|
|
770
|
+
char_idx = max(last_paren_idx_list, last_paren_idx_dict)
|
|
771
|
+
if char_idx:
|
|
772
|
+
logging.info("indent as unclosed list, dict, or set")
|
|
773
|
+
return _indent_inside_uncloded_list_tuple_set_or_dict(code, char_idx)
|
|
774
|
+
|
|
775
|
+
# 4. just closed a bracket => after bracket
|
|
776
|
+
bracket_stack, bracket_closings = parse_brackets(code)
|
|
777
|
+
if last_line.endswith(("]", "}", ")")):
|
|
778
|
+
logging.info('indent as after iterable')
|
|
779
|
+
return _indent_after_list_tuple_set_or_dict(
|
|
780
|
+
code, bracket_stack, bracket_closings)
|
|
781
|
+
|
|
782
|
+
# 5. Check if the current line is a dedent keyword
|
|
783
|
+
current_line_full = lines[-1]
|
|
784
|
+
current_line_strip = current_line_full.strip()
|
|
785
|
+
leading_spaces = get_leading_spaces(current_line_full)
|
|
786
|
+
if current_line_strip in ('return', 'break', 'continue'):
|
|
787
|
+
return max(0, leading_spaces - settings.tab_width)
|
|
788
|
+
|
|
789
|
+
# 6. Check if the current line starts with def or class. If so then we need
|
|
790
|
+
# to indent. This is a fallback mechanism to catch situations in which this
|
|
791
|
+
# was not properly caught by the above logic.
|
|
792
|
+
if current_line_strip.startswith('def') or current_line_strip .startswith('class'):
|
|
793
|
+
logging.info('fallback indent as def or class')
|
|
794
|
+
return leading_spaces + settings.tab_width
|
|
795
|
+
|
|
796
|
+
# 7. fallback => preserve current line indent
|
|
797
|
+
logging.info('indent fallback to current line')
|
|
798
|
+
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
|
+
# Track the current position
|
|
825
|
+
current_pos = 0
|
|
826
|
+
|
|
827
|
+
# Define regex patterns for cell markers
|
|
828
|
+
marker_pattern = re.compile(r'^(# %%|#%%|# In\[\]:?).*?$', re.MULTILINE)
|
|
829
|
+
triple_quotes_pattern = re.compile(r'(""".*?"""|\'\'\'.*?\'\'\')', re.DOTALL)
|
|
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
|
+
|