Hide keyboard shortcuts

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.""" 

4 

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): 

307 

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 

346 

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]))}') 

352 

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 ). 

418 

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. 

449 

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 

540 

541 

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