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
296 # The brackets indentation level.
297 self.p = None
298 # Set in indent.
299 self.parens = 0
300 # The parenthesis nesting level.
301 self.result = []
302 # The list of tokens that form the final result.
303 self.tab_width = 4
304 # The number of spaces in each unit of leading indentation.
305 #@+node:ekr.20191104195610.1: *3* cpp.pretty_print_tree
306 def pretty_print_tree(self, p):
308 c = self.c
309 if should_kill_beautify(p):
310 return
311 u, undoType = c.undoer, 'beautify-c'
312 u.beforeChangeGroup(c.p, undoType)
313 changed = False
314 for p in c.p.self_and_subtree():
315 if g.scanForAtLanguage(c, p) == "c":
316 bunch = u.beforeChangeNodeContents(p)
317 s = self.indent(p)
318 if p.b != s:
319 p.b = s
320 p.setDirty()
321 u.afterChangeNodeContents(p, undoType, bunch)
322 changed = True
323 if changed:
324 u.afterChangeGroup(c.p, undoType, reportFlag=False)
325 c.bodyWantsFocus()
326 #@+node:ekr.20110917174948.6911: *3* cpp.indent & helpers
327 def indent(self, p, toList=False, giveWarnings=True):
328 """Beautify a node with @language C in effect."""
329 if not should_beautify(p):
330 return [] if toList else '' # #2271
331 if not p.b:
332 return [] if toList else '' # #2271
333 self.p = p.copy()
334 aList = self.tokenize(p.b)
335 assert ''.join(aList) == p.b
336 aList = self.add_statement_braces(aList, giveWarnings=giveWarnings)
337 self.bracketLevel = 0
338 self.parens = 0
339 self.result = []
340 for s in aList:
341 self.put_token(s)
342 return self.result if toList else ''.join(self.result)
343 #@+node:ekr.20110918225821.6815: *4* add_statement_braces
344 def add_statement_braces(self, s, giveWarnings=False):
345 p = self.p
347 def oops(message, i, j):
348 # This can be called from c-to-python, in which case warnings should be suppressed.
349 if giveWarnings:
350 g.error('** changed ', p.h)
351 g.es_print(f'{message} after\n{repr("".join(s[i:j]))}')
353 i, n, result = 0, len(s), []
354 while i < n:
355 token = s[i]
356 progress = i
357 if token in ('if', 'for', 'while'):
358 j = self.skip_ws_and_comments(s, i + 1)
359 if self.match(s, j, '('):
360 j = self.skip_parens(s, j)
361 if self.match(s, j, ')'):
362 old_j = j + 1
363 j = self.skip_ws_and_comments(s, j + 1)
364 if self.match(s, j, ';'):
365 # Example: while (*++prefix);
366 result.extend(s[i:j])
367 elif self.match(s, j, '{'):
368 result.extend(s[i:j])
369 else:
370 oops("insert '{'", i, j)
371 # Back up, and don't go past a newline or comment.
372 j = self.skip_ws(s, old_j)
373 result.extend(s[i:j])
374 result.append(' ')
375 result.append('{')
376 result.append('\n')
377 i = j
378 j = self.skip_statement(s, i)
379 result.extend(s[i:j])
380 result.append('\n')
381 result.append('}')
382 oops("insert '}'", i, j)
383 else:
384 oops("missing ')'", i, j)
385 result.extend(s[i:j])
386 else:
387 oops("missing '('", i, j)
388 result.extend(s[i:j])
389 i = j
390 else:
391 result.append(token)
392 i += 1
393 assert progress < i
394 return result
395 #@+node:ekr.20110919184022.6903: *5* skip_ws
396 def skip_ws(self, s, i):
397 while i < len(s):
398 token = s[i]
399 if token.startswith(' ') or token.startswith('\t'):
400 i += 1
401 else:
402 break
403 return i
404 #@+node:ekr.20110918225821.6820: *5* skip_ws_and_comments
405 def skip_ws_and_comments(self, s, i):
406 while i < len(s):
407 token = s[i]
408 if token.isspace():
409 i += 1
410 elif token.startswith('//') or token.startswith('/*'):
411 i += 1
412 else:
413 break
414 return i
415 #@+node:ekr.20110918225821.6817: *5* skip_parens
416 def skip_parens(self, s, i):
417 """Skips from the opening ( to the matching ).
419 If no matching is found i is set to len(s)"""
420 assert self.match(s, i, '(')
421 level = 0
422 while i < len(s):
423 ch = s[i]
424 if ch == '(':
425 level += 1
426 i += 1
427 elif ch == ')':
428 level -= 1
429 if level <= 0:
430 return i
431 i += 1
432 else:
433 i += 1
434 return i
435 #@+node:ekr.20110918225821.6818: *5* skip_statement
436 def skip_statement(self, s, i):
437 """Skip to the next ';' or '}' token."""
438 while i < len(s):
439 if s[i] in ';}':
440 i += 1
441 break
442 else:
443 i += 1
444 return i
445 #@+node:ekr.20110917204542.6967: *4* put_token & helpers
446 def put_token(self, s):
447 """Append token s to self.result as is,
448 *except* for adjusting leading whitespace and comments.
450 '{' tokens bump self.brackets or self.ignored_brackets.
451 self.brackets determines leading whitespace.
452 """
453 if s == '{':
454 self.brackets += 1
455 elif s == '}':
456 self.brackets -= 1
457 self.remove_indent()
458 elif s == '(':
459 self.parens += 1
460 elif s == ')':
461 self.parens -= 1
462 elif s.startswith('\n'):
463 if self.parens <= 0:
464 s = f'\n{" "*self.brackets*self.tab_width}'
465 else:
466 pass # Use the existing indentation.
467 elif s.isspace():
468 if self.parens <= 0 and self.result and self.result[-1].startswith('\n'):
469 # Kill the whitespace.
470 s = ''
471 else:
472 pass # Keep the whitespace.
473 elif s.startswith('/*'):
474 s = self.reformat_block_comment(s)
475 else:
476 pass # put s as it is.
477 if s:
478 self.result.append(s)
479 #@+node:ekr.20110917204542.6968: *5* prev_token
480 def prev_token(self, s):
481 """Return the previous token, ignoring whitespace and comments."""
482 i = len(self.result) - 1
483 while i >= 0:
484 s2 = self.result[i]
485 if s == s2:
486 return True
487 if s.isspace() or s.startswith('//') or s.startswith('/*'):
488 i -= 1
489 else:
490 return False
491 return False
492 #@+node:ekr.20110918184425.6916: *5* reformat_block_comment
493 def reformat_block_comment(self, s):
494 return s
495 #@+node:ekr.20110917204542.6969: *5* remove_indent
496 def remove_indent(self):
497 """Remove one tab-width of blanks from the previous token."""
498 w = abs(self.tab_width)
499 if self.result:
500 s = self.result[-1]
501 if s.isspace():
502 self.result.pop()
503 s = s.replace('\t', ' ' * w)
504 if s.startswith('\n'):
505 s2 = s[1:]
506 self.result.append('\n' + s2[: -w])
507 else:
508 self.result.append(s[: -w])
509 #@+node:ekr.20110918225821.6819: *3* cpp.match
510 def match(self, s, i, pat):
511 return i < len(s) and s[i] == pat
512 #@+node:ekr.20110917174948.6930: *3* cpp.tokenize & helper
513 def tokenize(self, s):
514 """Tokenize comments, strings, identifiers, whitespace and operators."""
515 i, result = 0, []
516 while i < len(s):
517 # Loop invariant: at end: j > i and s[i:j] is the new token.
518 j = i
519 ch = s[i]
520 if ch in '@\n': # Make *sure* these are separate tokens.
521 j += 1
522 elif ch == '#': # Preprocessor directive.
523 j = g.skip_to_end_of_line(s, i)
524 elif ch in ' \t':
525 j = g.skip_ws(s, i)
526 elif ch.isalpha() or ch == '_':
527 j = g.skip_c_id(s, i)
528 elif g.match(s, i, '//'):
529 j = g.skip_line(s, i)
530 elif g.match(s, i, '/*'):
531 j = self.skip_block_comment(s, i)
532 elif ch in "'\"":
533 j = g.skip_string(s, i)
534 else:
535 j += 1
536 assert j > i
537 result.append(''.join(s[i:j]))
538 i = j # Advance.
539 return result
542 #@+at The following could be added to the 'else' clause::
543 # # Accumulate everything else.
544 # while (
545 # j < n and
546 # not s[j].isspace() and
547 # not s[j].isalpha() and
548 # not s[j] in '"\'_@' and
549 # # start of strings, identifiers, and single-character tokens.
550 # not g.match(s,j,'//') and
551 # not g.match(s,j,'/*') and
552 # not g.match(s,j,'-->')
553 # ):
554 # j += 1
555 #@+node:ekr.20110917193725.6974: *4* cpp.skip_block_comment
556 def skip_block_comment(self, s, i):
557 assert g.match(s, i, "/*")
558 j = s.find("*/", i)
559 if j == -1:
560 return len(s)
561 return j + 2
562 #@-others
563#@-others
564#@@language python
565#@@tabwidth -4
566#@-leo