Coverage for C:\Repos\leo-editor\leo\core\leoBeautify.py: 12%
406 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1#@+leo-ver=5-thin
2#@+node:ekr.20150521115018.1: * @file leoBeautify.py
3"""Leo's beautification classes."""
5import sys
6import os
7import time
8# Third-party tools.
9try:
10 import black
11except Exception:
12 black = None # type:ignore
13# Leo imports.
14from leo.core import leoGlobals as g
15from leo.core import leoAst
16#@+others
17#@+node:ekr.20191104201534.1: ** Top-level functions (leoBeautify.py)
18#@+node:ekr.20150528131012.1: *3* Beautify:commands
19#@+node:ekr.20150528131012.3: *4* beautify-c
20@g.command('beautify-c')
21@g.command('pretty-print-c')
22def beautifyCCode(event):
23 """Beautify all C code in the selected tree."""
24 c = event.get('c')
25 if c:
26 CPrettyPrinter(c).pretty_print_tree(c.p)
27#@+node:ekr.20200107165628.1: *4* beautify-file-diff
28@g.command('diff-beautify-files')
29@g.command('beautify-files-diff')
30def orange_diff_files(event):
31 """
32 Show the diffs that would result from beautifying the external files at
33 c.p.
34 """
35 c = event.get('c')
36 if not c or not c.p:
37 return
38 t1 = time.process_time()
39 tag = 'beautify-files-diff'
40 g.es(f"{tag}...")
41 settings = orange_settings(c)
42 roots = g.findRootsWithPredicate(c, c.p)
43 for root in roots:
44 filename = g.fullPath(c, root)
45 if os.path.exists(filename):
46 print('')
47 print(f"{tag}: {g.shortFileName(filename)}")
48 changed = leoAst.Orange(settings=settings).beautify_file_diff(filename)
49 changed_s = 'changed' if changed else 'unchanged'
50 g.es(f"{changed_s:>9}: {g.shortFileName(filename)}")
51 else:
52 print('')
53 print(f"{tag}: file not found:{filename}")
54 g.es(f"file not found:\n{filename}")
55 t2 = time.process_time()
56 print('')
57 g.es_print(f"{tag}: {len(roots)} file{g.plural(len(roots))} in {t2 - t1:5.2f} sec.")
58#@+node:ekr.20200107165603.1: *4* beautify-files
59@g.command('beautify-files')
60def orange_files(event):
61 """beautify one or more files at c.p."""
62 c = event.get('c')
63 if not c or not c.p:
64 return
65 t1 = time.process_time()
66 tag = 'beautify-files'
67 g.es(f"{tag}...")
68 settings = orange_settings(c)
69 roots = g.findRootsWithPredicate(c, c.p)
70 n_changed = 0
71 for root in roots:
72 filename = g.fullPath(c, root)
73 if os.path.exists(filename):
74 changed = leoAst.Orange(settings=settings).beautify_file(filename)
75 if changed:
76 n_changed += 1
77 else:
78 g.es_print(f"file not found: {filename}")
79 t2 = time.process_time()
80 print('')
81 g.es_print(
82 f"total files: {len(roots)}, "
83 f"changed files: {n_changed}, "
84 f"in {t2 - t1:5.2f} sec.")
85#@+node:ekr.20200103055814.1: *4* blacken-files
86@g.command('blacken-files')
87def blacken_files(event):
88 """Run black on one or more files at c.p."""
89 tag = 'blacken-files'
90 if not black:
91 g.es_print(f"{tag} can not import black")
92 return
93 c = event.get('c')
94 if not c or not c.p:
95 return
96 python = sys.executable
97 for root in g.findRootsWithPredicate(c, c.p):
98 path = g.fullPath(c, root)
99 if path and os.path.exists(path):
100 g.es_print(f"{tag}: {path}")
101 g.execute_shell_commands(f'&"{python}" -m black --skip-string-normalization "{path}"')
102 else:
103 print(f"{tag}: file not found:{path}")
104 g.es(f"{tag}: file not found:\n{path}")
105#@+node:ekr.20200103060057.1: *4* blacken-files-diff
106@g.command('blacken-files-diff')
107def blacken_files_diff(event):
108 """
109 Show the diffs that would result from blacking the external files at
110 c.p.
111 """
112 tag = 'blacken-files-diff'
113 if not black:
114 g.es_print(f"{tag} can not import black")
115 return
116 c = event.get('c')
117 if not c or not c.p:
118 return
119 python = sys.executable
120 for root in g.findRootsWithPredicate(c, c.p):
121 path = g.fullPath(c, root)
122 if path and os.path.exists(path):
123 g.es_print(f"{tag}: {path}")
124 g.execute_shell_commands(f'&"{python}" -m black --skip-string-normalization --diff "{path}"')
125 else:
126 print(f"{tag}: file not found:{path}")
127 g.es(f"{tag}: file not found:\n{path}")
128#@+node:ekr.20191025072511.1: *4* fstringify-files
129@g.command('fstringify-files')
130def fstringify_files(event):
131 """fstringify one or more files at c.p."""
132 c = event.get('c')
133 if not c or not c.p:
134 return
135 t1 = time.process_time()
136 tag = 'fstringify-files'
137 g.es(f"{tag}...")
138 roots = g.findRootsWithPredicate(c, c.p)
139 n_changed = 0
140 for root in roots:
141 filename = g.fullPath(c, root)
142 if os.path.exists(filename):
143 print('')
144 print(g.shortFileName(filename))
145 changed = leoAst.Fstringify().fstringify_file(filename)
146 changed_s = 'changed' if changed else 'unchanged'
147 if changed:
148 n_changed += 1
149 g.es_print(f"{changed_s:>9}: {g.shortFileName(filename)}")
150 else:
151 print('')
152 print(f"File not found:{filename}")
153 g.es(f"File not found:\n{filename}")
154 t2 = time.process_time()
155 print('')
156 g.es_print(
157 f"total files: {len(roots)}, "
158 f"changed files: {n_changed}, "
159 f"in {t2 - t1:5.2f} sec.")
160#@+node:ekr.20200103055858.1: *4* fstringify-files-diff
161@g.command('diff-fstringify-files')
162@g.command('fstringify-files-diff')
163def fstringify_diff_files(event):
164 """
165 Show the diffs that would result from fstringifying the external files at
166 c.p.
167 """
168 c = event.get('c')
169 if not c or not c.p:
170 return
171 t1 = time.process_time()
172 tag = 'fstringify-files-diff'
173 g.es(f"{tag}...")
174 roots = g.findRootsWithPredicate(c, c.p)
175 for root in roots:
176 filename = g.fullPath(c, root)
177 if os.path.exists(filename):
178 print('')
179 print(g.shortFileName(filename))
180 changed = leoAst.Fstringify().fstringify_file_diff(filename)
181 changed_s = 'changed' if changed else 'unchanged'
182 g.es_print(f"{changed_s:>9}: {g.shortFileName(filename)}")
183 else:
184 print('')
185 print(f"File not found:{filename}")
186 g.es(f"File not found:\n{filename}")
187 t2 = time.process_time()
188 print('')
189 g.es_print(f"{len(roots)} file{g.plural(len(roots))} in {t2 - t1:5.2f} sec.")
190#@+node:ekr.20200112060001.1: *4* fstringify-files-silent
191@g.command('silent-fstringify-files')
192@g.command('fstringify-files-silent')
193def fstringify_files_silent(event):
194 """Silently fstringifying the external files at c.p."""
195 c = event.get('c')
196 if not c or not c.p:
197 return
198 t1 = time.process_time()
199 tag = 'silent-fstringify-files'
200 g.es(f"{tag}...")
201 n_changed = 0
202 roots = g.findRootsWithPredicate(c, c.p)
203 for root in roots:
204 filename = g.fullPath(c, root)
205 if os.path.exists(filename):
206 changed = leoAst.Fstringify().fstringify_file_silent(filename)
207 if changed:
208 n_changed += 1
209 else:
210 print('')
211 print(f"File not found:{filename}")
212 g.es(f"File not found:\n{filename}")
213 t2 = time.process_time()
214 print('')
215 n_tot = len(roots)
216 g.es_print(
217 f"{n_tot} total file{g.plural(len(roots))}, "
218 f"{n_changed} changed file{g.plural(n_changed)} "
219 f"in {t2 - t1:5.2f} sec.")
220#@+node:ekr.20200108045048.1: *4* orange_settings
221def orange_settings(c):
222 """Return a dictionary of settings for the leo.core.leoAst.Orange class."""
223 allow_joined_strings = c.config.getBool(
224 'beautify-allow-joined-strings', default=False)
225 n_max_join = c.config.getInt('beautify-max-join-line-length')
226 max_join_line_length = 88 if n_max_join is None else n_max_join
227 n_max_split = c.config.getInt('beautify-max-split-line-length')
228 max_split_line_length = 88 if n_max_split is None else n_max_split
229 # Join <= Split.
230 # pylint: disable=consider-using-min-builtin
231 if max_join_line_length > max_split_line_length:
232 max_join_line_length = max_split_line_length
233 return {
234 'allow_joined_strings': allow_joined_strings,
235 'max_join_line_length': max_join_line_length,
236 'max_split_line_length': max_split_line_length,
237 'tab_width': abs(c.tab_width),
238 }
239#@+node:ekr.20191028140926.1: *3* Beautify:test functions
240#@+node:ekr.20191029184103.1: *4* function: show
241def show(obj, tag, dump):
242 print(f"{tag}...\n")
243 if dump:
244 g.printObj(obj)
245 else:
246 print(obj)
247#@+node:ekr.20150602154951.1: *3* function: should_beautify
248def should_beautify(p):
249 """
250 Return True if @beautify is in effect for node p.
251 Ambiguous directives have no effect.
252 """
253 for p2 in p.self_and_parents(copy=False):
254 d = g.get_directives_dict(p2)
255 if 'killbeautify' in d:
256 return False
257 if 'beautify' in d and 'nobeautify' in d:
258 if p == p2:
259 # honor whichever comes first.
260 for line in g.splitLines(p2.b):
261 if line.startswith('@beautify'):
262 return True
263 if line.startswith('@nobeautify'):
264 return False
265 g.trace('can not happen', p2.h)
266 return False
267 # The ambiguous node has no effect.
268 # Look up the tree.
269 pass
270 elif 'beautify' in d:
271 return True
272 if 'nobeautify' in d:
273 # This message would quickly become annoying.
274 # g.warning(f"{p.h}: @nobeautify")
275 return False
276 # The default is to beautify.
277 return True
278#@+node:ekr.20150602204440.1: *3* function: should_kill_beautify
279def should_kill_beautify(p):
280 """Return True if p.b contains @killbeautify"""
281 return 'killbeautify' in g.get_directives_dict(p)
282#@+node:ekr.20110917174948.6903: ** class CPrettyPrinter
283class CPrettyPrinter:
284 #@+others
285 #@+node:ekr.20110917174948.6904: *3* cpp.__init__
286 def __init__(self, c):
287 """Ctor for CPrettyPrinter class."""
288 self.c = c
289 self.brackets = 0 # The brackets indentation level.
290 self.p = None # Set in indent.
291 self.parens = 0 # The parenthesis nesting level.
292 self.result = [] # The list of tokens that form the final result.
293 self.tab_width = 4 # The number of spaces in each unit of leading indentation.
294 #@+node:ekr.20191104195610.1: *3* cpp.pretty_print_tree
295 def pretty_print_tree(self, p):
297 c = self.c
298 if should_kill_beautify(p):
299 return
300 u, undoType = c.undoer, 'beautify-c'
301 u.beforeChangeGroup(c.p, undoType)
302 changed = False
303 for p in c.p.self_and_subtree():
304 if g.scanForAtLanguage(c, p) == "c":
305 bunch = u.beforeChangeNodeContents(p)
306 s = self.indent(p)
307 if p.b != s:
308 p.b = s
309 p.setDirty()
310 u.afterChangeNodeContents(p, undoType, bunch)
311 changed = True
312 if changed:
313 u.afterChangeGroup(c.p, undoType, reportFlag=False)
314 c.bodyWantsFocus()
315 #@+node:ekr.20110917174948.6911: *3* cpp.indent & helpers
316 def indent(self, p, toList=False, giveWarnings=True):
317 """Beautify a node with @language C in effect."""
318 if not should_beautify(p):
319 return [] if toList else '' # #2271
320 if not p.b:
321 return [] if toList else '' # #2271
322 self.p = p.copy()
323 aList = self.tokenize(p.b)
324 assert ''.join(aList) == p.b
325 aList = self.add_statement_braces(aList, giveWarnings=giveWarnings)
326 self.bracketLevel = 0
327 self.parens = 0
328 self.result = []
329 for s in aList:
330 self.put_token(s)
331 return self.result if toList else ''.join(self.result)
332 #@+node:ekr.20110918225821.6815: *4* add_statement_braces
333 def add_statement_braces(self, s, giveWarnings=False):
334 p = self.p
336 def oops(message, i, j):
337 # This can be called from c-to-python, in which case warnings should be suppressed.
338 if giveWarnings:
339 g.error('** changed ', p.h)
340 g.es_print(f'{message} after\n{repr("".join(s[i:j]))}')
342 i, n, result = 0, len(s), []
343 while i < n:
344 token = s[i]
345 progress = i
346 if token in ('if', 'for', 'while'):
347 j = self.skip_ws_and_comments(s, i + 1)
348 if self.match(s, j, '('):
349 j = self.skip_parens(s, j)
350 if self.match(s, j, ')'):
351 old_j = j + 1
352 j = self.skip_ws_and_comments(s, j + 1)
353 if self.match(s, j, ';'):
354 # Example: while (*++prefix);
355 result.extend(s[i:j])
356 elif self.match(s, j, '{'):
357 result.extend(s[i:j])
358 else:
359 oops("insert '{'", i, j)
360 # Back up, and don't go past a newline or comment.
361 j = self.skip_ws(s, old_j)
362 result.extend(s[i:j])
363 result.append(' ')
364 result.append('{')
365 result.append('\n')
366 i = j
367 j = self.skip_statement(s, i)
368 result.extend(s[i:j])
369 result.append('\n')
370 result.append('}')
371 oops("insert '}'", i, j)
372 else:
373 oops("missing ')'", i, j)
374 result.extend(s[i:j])
375 else:
376 oops("missing '('", i, j)
377 result.extend(s[i:j])
378 i = j
379 else:
380 result.append(token)
381 i += 1
382 assert progress < i
383 return result
384 #@+node:ekr.20110919184022.6903: *5* skip_ws
385 def skip_ws(self, s, i):
386 while i < len(s):
387 token = s[i]
388 if token.startswith(' ') or token.startswith('\t'):
389 i += 1
390 else:
391 break
392 return i
393 #@+node:ekr.20110918225821.6820: *5* skip_ws_and_comments
394 def skip_ws_and_comments(self, s, i):
395 while i < len(s):
396 token = s[i]
397 if token.isspace():
398 i += 1
399 elif token.startswith('//') or token.startswith('/*'):
400 i += 1
401 else:
402 break
403 return i
404 #@+node:ekr.20110918225821.6817: *5* skip_parens
405 def skip_parens(self, s, i):
406 """Skips from the opening ( to the matching ).
408 If no matching is found i is set to len(s)"""
409 assert self.match(s, i, '(')
410 level = 0
411 while i < len(s):
412 ch = s[i]
413 if ch == '(':
414 level += 1
415 i += 1
416 elif ch == ')':
417 level -= 1
418 if level <= 0:
419 return i
420 i += 1
421 else:
422 i += 1
423 return i
424 #@+node:ekr.20110918225821.6818: *5* skip_statement
425 def skip_statement(self, s, i):
426 """Skip to the next ';' or '}' token."""
427 while i < len(s):
428 if s[i] in ';}':
429 i += 1
430 break
431 else:
432 i += 1
433 return i
434 #@+node:ekr.20110917204542.6967: *4* put_token & helpers
435 def put_token(self, s):
436 """Append token s to self.result as is,
437 *except* for adjusting leading whitespace and comments.
439 '{' tokens bump self.brackets or self.ignored_brackets.
440 self.brackets determines leading whitespace.
441 """
442 if s == '{':
443 self.brackets += 1
444 elif s == '}':
445 self.brackets -= 1
446 self.remove_indent()
447 elif s == '(':
448 self.parens += 1
449 elif s == ')':
450 self.parens -= 1
451 elif s.startswith('\n'):
452 if self.parens <= 0:
453 s = f'\n{" "*self.brackets*self.tab_width}'
454 else:
455 pass # Use the existing indentation.
456 elif s.isspace():
457 if self.parens <= 0 and self.result and self.result[-1].startswith('\n'):
458 # Kill the whitespace.
459 s = ''
460 else:
461 pass # Keep the whitespace.
462 elif s.startswith('/*'):
463 s = self.reformat_block_comment(s)
464 else:
465 pass # put s as it is.
466 if s:
467 self.result.append(s)
468 #@+node:ekr.20110917204542.6968: *5* prev_token
469 def prev_token(self, s):
470 """Return the previous token, ignoring whitespace and comments."""
471 i = len(self.result) - 1
472 while i >= 0:
473 s2 = self.result[i]
474 if s == s2:
475 return True
476 if s.isspace() or s.startswith('//') or s.startswith('/*'):
477 i -= 1
478 else:
479 return False
480 return False
481 #@+node:ekr.20110918184425.6916: *5* reformat_block_comment
482 def reformat_block_comment(self, s):
483 return s
484 #@+node:ekr.20110917204542.6969: *5* remove_indent
485 def remove_indent(self):
486 """Remove one tab-width of blanks from the previous token."""
487 w = abs(self.tab_width)
488 if self.result:
489 s = self.result[-1]
490 if s.isspace():
491 self.result.pop()
492 s = s.replace('\t', ' ' * w)
493 if s.startswith('\n'):
494 s2 = s[1:]
495 self.result.append('\n' + s2[: -w])
496 else:
497 self.result.append(s[: -w])
498 #@+node:ekr.20110918225821.6819: *3* cpp.match
499 def match(self, s, i, pat):
500 return i < len(s) and s[i] == pat
501 #@+node:ekr.20110917174948.6930: *3* cpp.tokenize & helper
502 def tokenize(self, s):
503 """Tokenize comments, strings, identifiers, whitespace and operators."""
504 i, result = 0, []
505 while i < len(s):
506 # Loop invariant: at end: j > i and s[i:j] is the new token.
507 j = i
508 ch = s[i]
509 if ch in '@\n': # Make *sure* these are separate tokens.
510 j += 1
511 elif ch == '#': # Preprocessor directive.
512 j = g.skip_to_end_of_line(s, i)
513 elif ch in ' \t':
514 j = g.skip_ws(s, i)
515 elif ch.isalpha() or ch == '_':
516 j = g.skip_c_id(s, i)
517 elif g.match(s, i, '//'):
518 j = g.skip_line(s, i)
519 elif g.match(s, i, '/*'):
520 j = self.skip_block_comment(s, i)
521 elif ch in "'\"":
522 j = g.skip_string(s, i)
523 else:
524 j += 1
525 assert j > i
526 result.append(''.join(s[i:j]))
527 i = j # Advance.
528 return result
530 # The following could be added to the 'else' clause::
531 # Accumulate everything else.
532 # while (
533 # j < n and
534 # not s[j].isspace() and
535 # not s[j].isalpha() and
536 # # start of strings, identifiers, and single-character tokens.
537 # not s[j] in '"\'_@' and
538 # not g.match(s,j,'//') and
539 # not g.match(s,j,'/*') and
540 # not g.match(s,j,'-->')
541 # ):
542 # j += 1
543 #@+node:ekr.20110917193725.6974: *4* cpp.skip_block_comment
544 def skip_block_comment(self, s, i):
545 assert g.match(s, i, "/*")
546 j = s.find("*/", i)
547 if j == -1:
548 return len(s)
549 return j + 2
550 #@-others
551#@-others
552#@@language python
553#@@tabwidth -4
554#@-leo