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

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

296 

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 

335 

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

341 

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

407 

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. 

438 

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 

529 

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