Coverage for C:\leo.repo\leo-editor\leo\core\leoBeautify.py : 12%

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