Coverage for C:\leo.repo\leo-editor\leo\commands\editFileCommands.py: 13%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

780 statements  

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20150514041209.1: * @file ../commands/editFileCommands.py 

4#@@first 

5"""Leo's file-editing commands.""" 

6#@+<< imports >> 

7#@+node:ekr.20170806094317.4: ** << imports >> (editFileCommands.py) 

8import difflib 

9import io 

10import os 

11import re 

12from typing import Any, List 

13from leo.core import leoGlobals as g 

14from leo.core import leoCommands 

15from leo.commands.baseCommands import BaseEditCommandsClass 

16#@-<< imports >> 

17 

18def cmd(name): 

19 """Command decorator for the EditFileCommandsClass class.""" 

20 return g.new_cmd_decorator(name, ['c', 'editFileCommands',]) 

21 

22#@+others 

23#@+node:ekr.20210307060752.1: ** class ConvertAtRoot 

24class ConvertAtRoot: 

25 """ 

26 A class to convert @root directives to @clean nodes: 

27 

28 - Change @root directive in body to @clean in the headline. 

29 - Make clones of section references defined outside of @clean nodes, 

30 moving them so they are children of the nodes that reference them. 

31 """ 

32 

33 errors = 0 

34 root = None # Root of @root tree. 

35 root_pat = re.compile(r'^@root\s+(.+)$', re.MULTILINE) 

36 section_pat = re.compile(r'\s*<\<.+>\>') 

37 units: List[Any] = [] 

38 # List of positions containing @unit. 

39 

40 #@+others 

41 #@+node:ekr.20210308044128.1: *3* atRoot.check_move 

42 def check_clone_move(self, p, parent): 

43 """ 

44 Return False if p or any of p's descendents is a clone of parent 

45 or any of parents ancestors. 

46 """ 

47 # Like as checkMoveWithParentWithWarning without warning. 

48 clonedVnodes = {} 

49 for ancestor in parent.self_and_parents(copy=False): 

50 if ancestor.isCloned(): 

51 v = ancestor.v 

52 clonedVnodes[v] = v 

53 if not clonedVnodes: 

54 return True 

55 for p in p.self_and_subtree(copy=False): 

56 if p.isCloned() and clonedVnodes.get(p.v): 

57 return False 

58 return True 

59 #@+node:ekr.20210307060752.2: *3* atRoot.convert_file 

60 @cmd('convert-at-root') 

61 def convert_file(self, c): 

62 """Convert @root to @clean in the the .leo file at the given path.""" 

63 self.find_all_units(c) 

64 for p in c.all_positions(): 

65 m = self.root_pat.search(p.b) 

66 path = m and m.group(1) 

67 if path: 

68 # Weird special case. Don't change section definition! 

69 if self.section_pat.match(p.h): 

70 print(f"\nCan not create @clean node: {p.h}\n") 

71 self.errors += 1 

72 else: 

73 self.root = p.copy() 

74 p.h = f"@clean {path}" 

75 self.do_root(p) 

76 self.root = None 

77 # 

78 # Check the results. 

79 link_errors = c.checkOutline(check_links=True) 

80 self.errors += link_errors 

81 print(f"{self.errors} error{g.plural(self.errors)} in {c.shortFileName()}") 

82 c.redraw() 

83 # if not self.errors: self.dump(c) 

84 #@+node:ekr.20210308045306.1: *3* atRoot.dump 

85 def dump(self, c): 

86 print(f"Dump of {c.shortFileName()}...") 

87 for p in c.all_positions(): 

88 print(' ' * 2 * p.level(), p.h) 

89 #@+node:ekr.20210307075117.1: *3* atRoot.do_root 

90 def do_root(self, p): 

91 """ 

92 Make all necessary clones for section defintions. 

93 """ 

94 for p in p.self_and_subtree(): 

95 self.make_clones(p) 

96 #@+node:ekr.20210307085034.1: *3* atRoot.find_all_units 

97 def find_all_units(self, c): 

98 """Scan for all @unit nodes.""" 

99 for p in c.all_positions(): 

100 if '@unit' in p.b: 

101 self.units.append(p.copy()) 

102 #@+node:ekr.20210307082125.1: *3* atRoot.find_section 

103 def find_section(self, root, section_name): 

104 """Find the section definition node in root's subtree for the given section.""" 

105 

106 def munge(s): 

107 return s.strip().replace(' ', '').lower() 

108 

109 for p in root.subtree(): 

110 if munge(p.h).startswith(munge(section_name)): 

111 # print(f" Found {section_name:30} in {root.h}::{root.gnx}") 

112 return p 

113 

114 # print(f" Not found {section_name:30} in {root.h}::{root.gnx}") 

115 return None 

116 #@+node:ekr.20210307075325.1: *3* atRoot.make_clones 

117 section_pat = re.compile(r'\s*<\<(.*)>\>') 

118 

119 def make_clones(self, p): 

120 """Make clones for all undefined sections in p.b.""" 

121 for s in g.splitLines(p.b): 

122 m = self.section_pat.match(s) 

123 if m: 

124 section_name = g.angleBrackets(m.group(1).strip()) 

125 section_p = self.make_clone(p, section_name) 

126 if not section_p: 

127 print(f"MISSING: {section_name:30} {p.h}") 

128 self.errors += 1 

129 #@+node:ekr.20210307080500.1: *3* atRoot.make_clone 

130 def make_clone(self, p, section_name): 

131 """Make c clone for section, if necessary.""" 

132 

133 def clone_and_move(parent, section_p): 

134 clone = section_p.clone() 

135 if self.check_clone_move(clone, parent): 

136 print(f" CLONE: {section_p.h:30} parent: {parent.h}") 

137 clone.moveToLastChildOf(parent) 

138 else: 

139 print(f"Can not clone: {section_p.h:30} parent: {parent.h}") 

140 clone.doDelete() 

141 self.errors += 1 

142 # 

143 # First, look in p's subtree. 

144 section_p = self.find_section(p, section_name) 

145 if section_p: 

146 # g.trace('FOUND', section_name) 

147 # Already defined in a good place. 

148 return section_p 

149 # 

150 # Finally, look in the @unit tree. 

151 for unit_p in self.units: 

152 section_p = self.find_section(unit_p, section_name) 

153 if section_p: 

154 clone_and_move(p, section_p) 

155 return section_p 

156 return None 

157 #@-others 

158#@+node:ekr.20170806094319.14: ** class EditFileCommandsClass 

159class EditFileCommandsClass(BaseEditCommandsClass): 

160 """A class to load files into buffers and save buffers to files.""" 

161 #@+others 

162 #@+node:ekr.20210308051724.1: *3* efc.convert-at-root 

163 @cmd('convert-at-root') 

164 def convert_at_root(self, event=None): 

165 #@+<< convert-at-root docstring >> 

166 #@+node:ekr.20210309035627.1: *4* << convert-at-root docstring >> 

167 #@@wrap 

168 """ 

169 The convert-at-root command converts @root to @clean throughout the 

170 outline. 

171 

172 This command is not perfect. You will need to adjust the outline by hand if 

173 the command reports errors. I recommend using git diff to ensure that the 

174 resulting external files are roughly equivalent after running this command. 

175 

176 This command attempts to do the following: 

177 

178 - For each node with an @root <path> directive in the body, change the head to 

179 @clean <path>. The command does *not* change the headline if the node is 

180 a section definition node. In that case, the command reports an error. 

181 

182 - Clones and moves nodes as needed so that section definition nodes appear 

183 as descendants of nodes containing section references. To find section 

184 definition nodes, the command looks in all @unit trees. After finding the 

185 required definition node, the command makes a clone of the node and moves 

186 the clone so it is the last child of the node containing the section 

187 references. This move may fail. If so, the command reports an error. 

188 """ 

189 #@-<< convert-at-root docstring >> 

190 c = event.get('c') 

191 if not c: 

192 return 

193 ConvertAtRoot().convert_file(c) 

194 #@+node:ekr.20170806094319.11: *3* efc.clean-at-clean commands 

195 #@+node:ekr.20170806094319.5: *4* efc.cleanAtCleanFiles 

196 @cmd('clean-at-clean-files') 

197 def cleanAtCleanFiles(self, event): 

198 """Adjust whitespace in all @clean files.""" 

199 c = self.c 

200 undoType = 'clean-@clean-files' 

201 c.undoer.beforeChangeGroup(c.p, undoType, verboseUndoGroup=True) 

202 total = 0 

203 for p in c.all_unique_positions(): 

204 if g.match_word(p.h, 0, '@clean') and p.h.rstrip().endswith('.py'): 

205 n = 0 

206 for p2 in p.subtree(): 

207 bunch2 = c.undoer.beforeChangeNodeContents(p2) 

208 if self.cleanAtCleanNode(p2, undoType): 

209 n += 1 

210 total += 1 

211 c.undoer.afterChangeNodeContents(p2, undoType, bunch2) 

212 g.es_print(f"{n} node{g.plural(n)} {p.h}") 

213 if total > 0: 

214 c.undoer.afterChangeGroup(c.p, undoType) 

215 g.es_print(f"{total} total node{g.plural(total)}") 

216 #@+node:ekr.20170806094319.8: *4* efc.cleanAtCleanNode 

217 def cleanAtCleanNode(self, p, undoType): 

218 """Adjust whitespace in p, part of an @clean tree.""" 

219 s = p.b.strip() 

220 if not s or p.h.strip().startswith('<<'): 

221 return False 

222 ws = '\n\n' if g.match_word(s, 0, 'class') else '\n' 

223 s2 = ws + s + ws 

224 changed = s2 != p.b 

225 if changed: 

226 p.b = s2 

227 p.setDirty() 

228 return changed 

229 #@+node:ekr.20170806094319.10: *4* efc.cleanAtCleanTree 

230 @cmd('clean-at-clean-tree') 

231 def cleanAtCleanTree(self, event): 

232 """ 

233 Adjust whitespace in the nearest @clean tree, 

234 searching c.p and its ancestors. 

235 """ 

236 c = self.c 

237 # Look for an @clean node. 

238 for p in c.p.self_and_parents(copy=False): 

239 if g.match_word(p.h, 0, '@clean') and p.h.rstrip().endswith('.py'): 

240 break 

241 else: 

242 g.es_print('no an @clean node found', p.h, color='blue') 

243 return 

244 # pylint: disable=undefined-loop-variable 

245 # p is certainly defined here. 

246 bunch = c.undoer.beforeChangeTree(p) 

247 n = 0 

248 undoType = 'clean-@clean-tree' 

249 for p2 in p.subtree(): 

250 if self.cleanAtCleanNode(p2, undoType): 

251 n += 1 

252 if n > 0: 

253 c.setChanged() 

254 c.undoer.afterChangeTree(p, undoType, bunch) 

255 g.es_print(f"{n} node{g.plural(n)} cleaned") 

256 #@+node:ekr.20170806094317.6: *3* efc.compareAnyTwoFiles & helpers 

257 @cmd('file-compare-two-leo-files') 

258 @cmd('compare-two-leo-files') 

259 def compareAnyTwoFiles(self, event): 

260 """Compare two files.""" 

261 c = c1 = self.c 

262 w = c.frame.body.wrapper 

263 commanders = g.app.commanders() 

264 if g.app.diff: 

265 if len(commanders) == 2: 

266 c1, c2 = commanders 

267 fn1 = g.shortFileName(c1.wrappedFileName) or c1.shortFileName() 

268 fn2 = g.shortFileName(c2.wrappedFileName) or c2.shortFileName() 

269 g.es('--diff auto compare', color='red') 

270 g.es(fn1) 

271 g.es(fn2) 

272 else: 

273 g.es('expecting two .leo files') 

274 return 

275 else: 

276 # Prompt for the file to be compared with the present outline. 

277 filetypes = [("Leo files", "*.leo"), ("All files", "*"),] 

278 fileName = g.app.gui.runOpenFileDialog(c, 

279 title="Compare .leo Files", filetypes=filetypes, defaultextension='.leo') 

280 if not fileName: 

281 return 

282 # Read the file into the hidden commander. 

283 c2 = g.createHiddenCommander(fileName) 

284 if not c2: 

285 return 

286 # Compute the inserted, deleted and changed dicts. 

287 d1 = self.createFileDict(c1) 

288 d2 = self.createFileDict(c2) 

289 inserted, deleted, changed = self.computeChangeDicts(d1, d2) 

290 # Create clones of all inserted, deleted and changed dicts. 

291 self.createAllCompareClones(c1, c2, inserted, deleted, changed) 

292 # Fix bug 1231656: File-Compare-Leo-Files leaves other file open-count incremented. 

293 if not g.app.diff: 

294 g.app.forgetOpenFile(fn=c2.fileName()) 

295 c2.frame.destroySelf() 

296 g.app.gui.set_focus(c, w) 

297 #@+node:ekr.20170806094317.9: *4* efc.computeChangeDicts 

298 def computeChangeDicts(self, d1, d2): 

299 """ 

300 Compute inserted, deleted, changed dictionaries. 

301 

302 New in Leo 4.11: show the nodes in the *invisible* file, d2, if possible. 

303 """ 

304 inserted = {} 

305 for key in d2: 

306 if not d1.get(key): 

307 inserted[key] = d2.get(key) 

308 deleted = {} 

309 for key in d1: 

310 if not d2.get(key): 

311 deleted[key] = d1.get(key) 

312 changed = {} 

313 for key in d1: 

314 if d2.get(key): 

315 p1 = d1.get(key) 

316 p2 = d2.get(key) 

317 if p1.h != p2.h or p1.b != p2.b: 

318 changed[key] = p2 # Show the node in the *other* file. 

319 return inserted, deleted, changed 

320 #@+node:ekr.20170806094317.11: *4* efc.createAllCompareClones & helper 

321 def createAllCompareClones(self, c1, c2, inserted, deleted, changed): 

322 """Create the comparison trees.""" 

323 c = self.c # Always use the visible commander 

324 assert c == c1 

325 # Create parent node at the start of the outline. 

326 u, undoType = c.undoer, 'Compare Two Files' 

327 u.beforeChangeGroup(c.p, undoType) 

328 undoData = u.beforeInsertNode(c.p) 

329 parent = c.p.insertAfter() 

330 parent.setHeadString(undoType) 

331 u.afterInsertNode(parent, undoType, undoData) 

332 # Use the wrapped file name if possible. 

333 fn1 = g.shortFileName(c1.wrappedFileName) or c1.shortFileName() 

334 fn2 = g.shortFileName(c2.wrappedFileName) or c2.shortFileName() 

335 for d, kind in ( 

336 (deleted, f"not in {fn2}"), 

337 (inserted, f"not in {fn1}"), 

338 (changed, f"changed: as in {fn2}"), 

339 ): 

340 self.createCompareClones(d, kind, parent) 

341 c.selectPosition(parent) 

342 u.afterChangeGroup(parent, undoType, reportFlag=True) 

343 c.redraw() 

344 #@+node:ekr.20170806094317.12: *5* efc.createCompareClones 

345 def createCompareClones(self, d, kind, parent): 

346 if d: 

347 c = self.c # Use the visible commander. 

348 parent = parent.insertAsLastChild() 

349 parent.setHeadString(kind) 

350 for key in d: 

351 p = d.get(key) 

352 if not kind.endswith('.leo') and p.isAnyAtFileNode(): 

353 # Don't make clones of @<file> nodes for wrapped files. 

354 pass 

355 elif p.v.context == c: 

356 clone = p.clone() 

357 clone.moveToLastChildOf(parent) 

358 else: 

359 # Fix bug 1160660: File-Compare-Leo-Files creates "other file" clones. 

360 copy = p.copyTreeAfter() 

361 copy.moveToLastChildOf(parent) 

362 for p2 in copy.self_and_subtree(copy=False): 

363 p2.v.context = c 

364 #@+node:ekr.20170806094317.17: *4* efc.createFileDict 

365 def createFileDict(self, c): 

366 """Create a dictionary of all relevant positions in commander c.""" 

367 d = {} 

368 for p in c.all_positions(): 

369 d[p.v.fileIndex] = p.copy() 

370 return d 

371 #@+node:ekr.20170806094317.19: *4* efc.dumpCompareNodes 

372 def dumpCompareNodes(self, fileName1, fileName2, inserted, deleted, changed): 

373 for d, kind in ( 

374 (inserted, f"inserted (only in {fileName1})"), 

375 (deleted, f"deleted (only in {fileName2})"), 

376 (changed, 'changed'), 

377 ): 

378 g.pr('\n', kind) 

379 for key in d: 

380 p = d.get(key) 

381 g.pr(f"{key:>32} {p.h}") 

382 #@+node:ekr.20170806094319.3: *3* efc.compareTrees 

383 def compareTrees(self, p1, p2, tag): 

384 

385 

386 class CompareTreesController: 

387 #@+others 

388 #@+node:ekr.20170806094318.18: *4* ct.compare 

389 def compare(self, d1, d2, p1, p2, root): 

390 """Compare dicts d1 and d2.""" 

391 for h in sorted(d1.keys()): 

392 p1, p2 = d1.get(h), d2.get(h) 

393 if h in d2: 

394 lines1, lines2 = g.splitLines(p1.b), g.splitLines(p2.b) 

395 aList = list(difflib.unified_diff(lines1, lines2, 'vr1', 'vr2')) 

396 if aList: 

397 p = root.insertAsLastChild() 

398 p.h = h 

399 p.b = ''.join(aList) 

400 p1.clone().moveToLastChildOf(p) 

401 p2.clone().moveToLastChildOf(p) 

402 elif p1.b.strip(): 

403 # Only in p1 tree, and not an organizer node. 

404 p = root.insertAsLastChild() 

405 p.h = h + f"({p1.h} only)" 

406 p1.clone().moveToLastChildOf(p) 

407 for h in sorted(d2.keys()): 

408 p2 = d2.get(h) 

409 if h not in d1 and p2.b.strip(): 

410 # Only in p2 tree, and not an organizer node. 

411 p = root.insertAsLastChild() 

412 p.h = h + f"({p2.h} only)" 

413 p2.clone().moveToLastChildOf(p) 

414 return root 

415 #@+node:ekr.20170806094318.19: *4* ct.run 

416 def run(self, c, p1, p2, tag): 

417 """Main line.""" 

418 self.c = c 

419 root = c.p.insertAfter() 

420 root.h = tag 

421 d1 = self.scan(p1) 

422 d2 = self.scan(p2) 

423 self.compare(d1, d2, p1, p2, root) 

424 c.p.contract() 

425 root.expand() 

426 c.selectPosition(root) 

427 c.redraw() 

428 #@+node:ekr.20170806094319.2: *4* ct.scan 

429 def scan(self, p1): 

430 """ 

431 Create a dict of the methods in p1. 

432 Keys are headlines, stripped of prefixes. 

433 Values are copies of positions. 

434 """ 

435 d = {} # 

436 for p in p1.self_and_subtree(copy=False): 

437 h = p.h.strip() 

438 i = h.find('.') 

439 if i > -1: 

440 h = h[i + 1 :].strip() 

441 if h in d: 

442 g.es_print('duplicate', p.h) 

443 else: 

444 d[h] = p.copy() 

445 return d 

446 #@-others 

447 CompareTreesController().run(self.c, p1, p2, tag) 

448 #@+node:ekr.20170806094318.1: *3* efc.deleteFile 

449 @cmd('file-delete') 

450 def deleteFile(self, event): 

451 """Prompt for the name of a file and delete it.""" 

452 k = self.c.k 

453 k.setLabelBlue('Delete File: ') 

454 k.extendLabel(os.getcwd() + os.sep) 

455 k.get1Arg(event, handler=self.deleteFile1) 

456 

457 def deleteFile1(self, event): 

458 k = self.c.k 

459 k.keyboardQuit() 

460 k.clearState() 

461 try: 

462 os.remove(k.arg) 

463 k.setStatusLabel(f"Deleted: {k.arg}") 

464 except Exception: 

465 k.setStatusLabel(f"Not Deleted: {k.arg}") 

466 #@+node:ekr.20170806094318.3: *3* efc.diff (file-diff-files) 

467 @cmd('file-diff-files') 

468 def diff(self, event=None): 

469 """Creates a node and puts the diff between 2 files into it.""" 

470 c = self.c 

471 fn = self.getReadableTextFile() 

472 if not fn: 

473 return 

474 fn2 = self.getReadableTextFile() 

475 if not fn2: 

476 return 

477 s1, e = g.readFileIntoString(fn) 

478 if s1 is None: 

479 return 

480 s2, e = g.readFileIntoString(fn2) 

481 if s2 is None: 

482 return 

483 lines1, lines2 = g.splitLines(s1), g.splitLines(s2) 

484 aList = difflib.ndiff(lines1, lines2) 

485 p = c.p.insertAfter() 

486 p.h = 'diff' 

487 p.b = ''.join(aList) 

488 c.redraw() 

489 #@+node:ekr.20170806094318.6: *3* efc.getReadableTextFile 

490 def getReadableTextFile(self): 

491 """Prompt for a text file.""" 

492 c = self.c 

493 fn = g.app.gui.runOpenFileDialog(c, 

494 title='Open Text File', 

495 filetypes=[("Text", "*.txt"), ("All files", "*")], 

496 defaultextension=".txt") 

497 return fn 

498 #@+node:ekr.20170819035801.90: *3* efc.gitDiff (gd & git-diff) 

499 @cmd('git-diff') 

500 @cmd('gd') 

501 def gitDiff(self, event=None): # 2020/07/18, for leoInteg. 

502 """Produce a Leonine git diff.""" 

503 GitDiffController(c=self.c).git_diff(rev1='HEAD') 

504 #@+node:ekr.20201215093414.1: *3* efc.gitDiffPR (git-diff-pr & git-diff-pull-request) 

505 @cmd('git-diff-pull-request') 

506 @cmd('git-diff-pr') 

507 def gitDiffPullRequest(self, event=None): 

508 """ 

509 Produce a Leonine diff of pull request in the current branch. 

510 """ 

511 GitDiffController(c=self.c).diff_pull_request() 

512 #@+node:ekr.20170806094318.7: *3* efc.insertFile 

513 @cmd('file-insert') 

514 def insertFile(self, event): 

515 """Prompt for the name of a file and put the selected text into it.""" 

516 w = self.editWidget(event) 

517 if not w: 

518 return 

519 fn = self.getReadableTextFile() 

520 if not fn: 

521 return 

522 s, e = g.readFileIntoString(fn) 

523 if s: 

524 self.beginCommand(w, undoType='insert-file') 

525 i = w.getInsertPoint() 

526 w.insert(i, s) 

527 w.seeInsertPoint() 

528 self.endCommand(changed=True, setLabel=True) 

529 #@+node:ekr.20170806094318.9: *3* efc.makeDirectory 

530 @cmd('directory-make') 

531 def makeDirectory(self, event): 

532 """Prompt for the name of a directory and create it.""" 

533 k = self.c.k 

534 k.setLabelBlue('Make Directory: ') 

535 k.extendLabel(os.getcwd() + os.sep) 

536 k.get1Arg(event, handler=self.makeDirectory1) 

537 def makeDirectory1(self, event): 

538 k = self.c.k 

539 k.keyboardQuit() 

540 k.clearState() 

541 try: 

542 os.mkdir(k.arg) 

543 k.setStatusLabel(f"Created: {k.arg}") 

544 except Exception: 

545 k.setStatusLabel(f"Not Created: {k.arg}") 

546 #@+node:ekr.20170806094318.12: *3* efc.openOutlineByName 

547 @cmd('file-open-by-name') 

548 def openOutlineByName(self, event): 

549 """file-open-by-name: Prompt for the name of a Leo outline and open it.""" 

550 c, k = self.c, self.c.k 

551 fileName = ''.join(k.givenArgs) 

552 # Bug fix: 2012/04/09: only call g.openWithFileName if the file exists. 

553 if fileName and g.os_path_exists(fileName): 

554 g.openWithFileName(fileName, old_c=c) 

555 else: 

556 k.setLabelBlue('Open Leo Outline: ') 

557 k.getFileName(event, callback=self.openOutlineByNameFinisher) 

558 

559 def openOutlineByNameFinisher(self, fn): 

560 c = self.c 

561 if fn and g.os_path_exists(fn) and not g.os_path_isdir(fn): 

562 c2 = g.openWithFileName(fn, old_c=c) 

563 try: 

564 g.app.gui.runAtIdle(c2.treeWantsFocusNow) 

565 except Exception: 

566 pass 

567 else: 

568 g.es(f"ignoring: {fn}") 

569 #@+node:ekr.20170806094318.14: *3* efc.removeDirectory 

570 @cmd('directory-remove') 

571 def removeDirectory(self, event): 

572 """Prompt for the name of a directory and delete it.""" 

573 k = self.c.k 

574 k.setLabelBlue('Remove Directory: ') 

575 k.extendLabel(os.getcwd() + os.sep) 

576 k.get1Arg(event, handler=self.removeDirectory1) 

577 

578 def removeDirectory1(self, event): 

579 k = self.c.k 

580 k.keyboardQuit() 

581 k.clearState() 

582 try: 

583 os.rmdir(k.arg) 

584 k.setStatusLabel(f"Removed: {k.arg}") 

585 except Exception: 

586 k.setStatusLabel(f"Not Removed: {k.arg}") 

587 #@+node:ekr.20170806094318.15: *3* efc.saveFile (save-file-by-name) 

588 @cmd('file-save-by-name') 

589 @cmd('save-file-by-name') 

590 def saveFile(self, event): 

591 """Prompt for the name of a file and put the body text of the selected node into it..""" 

592 c = self.c 

593 w = self.editWidget(event) 

594 if not w: 

595 return 

596 fileName = g.app.gui.runSaveFileDialog(c, 

597 title='save-file', 

598 filetypes=[("Text", "*.txt"), ("All files", "*")], 

599 defaultextension=".txt") 

600 if fileName: 

601 try: 

602 s = w.getAllText() 

603 with open(fileName, 'w') as f: 

604 f.write(s) 

605 except IOError: 

606 g.es('can not create', fileName) 

607 #@+node:ekr.20170806094319.15: *3* efc.toggleAtAutoAtEdit & helpers 

608 @cmd('toggle-at-auto-at-edit') 

609 def toggleAtAutoAtEdit(self, event): 

610 """Toggle between @auto and @edit, preserving insert point, etc.""" 

611 p = self.c.p 

612 if p.isAtEditNode(): 

613 self.toAtAuto(p) 

614 return 

615 for p in p.self_and_parents(): 

616 if p.isAtAutoNode(): 

617 self.toAtEdit(p) 

618 return 

619 g.es_print('Not in an @auto or @edit tree.', color='blue') 

620 #@+node:ekr.20170806094319.17: *4* efc.toAtAuto 

621 def toAtAuto(self, p): 

622 """Convert p from @edit to @auto.""" 

623 c = self.c 

624 # Change the headline. 

625 p.h = '@auto' + p.h[5:] 

626 # Compute the position of the present line within the file. 

627 w = c.frame.body.wrapper 

628 ins = w.getInsertPoint() 

629 row, col = g.convertPythonIndexToRowCol(p.b, ins) 

630 # Ignore *preceding* directive lines. 

631 directives = [z for z in g.splitLines(c.p.b)[:row] if g.isDirective(z)] 

632 row -= len(directives) 

633 row = max(0, row) 

634 # Reload the file, creating new nodes. 

635 c.selectPosition(p) 

636 c.refreshFromDisk() 

637 # Restore the line in the proper node. 

638 c.gotoCommands.find_file_line(row + 1) 

639 p.setDirty() 

640 c.setChanged() 

641 c.redraw() 

642 c.bodyWantsFocus() 

643 #@+node:ekr.20170806094319.19: *4* efc.toAtEdit 

644 def toAtEdit(self, p): 

645 """Convert p from @auto to @edit.""" 

646 c = self.c 

647 w = c.frame.body.wrapper 

648 p.h = '@edit' + p.h[5:] 

649 # Compute the position of the present line within the *selected* node c.p 

650 ins = w.getInsertPoint() 

651 row, col = g.convertPythonIndexToRowCol(c.p.b, ins) 

652 # Ignore directive lines. 

653 directives = [z for z in g.splitLines(c.p.b)[:row] if g.isDirective(z)] 

654 row -= len(directives) 

655 row = max(0, row) 

656 # Count preceding lines from p to c.p, again ignoring directives. 

657 for p2 in p.self_and_subtree(copy=False): 

658 if p2 == c.p: 

659 break 

660 lines = [z for z in g.splitLines(p2.b) if not g.isDirective(z)] 

661 row += len(lines) 

662 # Reload the file into a single node. 

663 c.selectPosition(p) 

664 c.refreshFromDisk() 

665 # Restore the line in the proper node. 

666 ins = g.convertRowColToPythonIndex(p.b, row + 1, 0) 

667 w.setInsertPoint(ins) 

668 p.setDirty() 

669 c.setChanged() 

670 c.redraw() 

671 c.bodyWantsFocus() 

672 #@-others 

673#@+node:ekr.20170806094320.13: ** class GitDiffController 

674class GitDiffController: 

675 """A class to do git diffs.""" 

676 

677 def __init__(self, c): 

678 self.c = c 

679 self.file_node = None 

680 self.root = None 

681 #@+others 

682 #@+node:ekr.20180510095544.1: *3* gdc.Entries... 

683 #@+node:ekr.20170806094320.6: *4* gdc.diff_file 

684 def diff_file(self, fn, rev1='HEAD', rev2=''): 

685 """ 

686 Create an outline describing the git diffs for fn. 

687 """ 

688 # Common code. 

689 c = self.c 

690 # #1781, #2143 

691 directory = self.get_directory() 

692 if not directory: 

693 return 

694 s1 = self.get_file_from_rev(rev1, fn) 

695 s2 = self.get_file_from_rev(rev2, fn) 

696 lines1 = g.splitLines(s1) 

697 lines2 = g.splitLines(s2) 

698 diff_list = list(difflib.unified_diff( 

699 lines1, 

700 lines2, 

701 rev1 or 'uncommitted', 

702 rev2 or 'uncommitted', 

703 )) 

704 diff_list.insert(0, '@ignore\n@nosearch\n@language patch\n') 

705 self.file_node = self.create_file_node(diff_list, fn) 

706 # #1777: The file node will contain the entire added/deleted file. 

707 if not s1: 

708 self.file_node.h = f"Added: {self.file_node.h}" 

709 return 

710 if not s2: 

711 self.file_node.h = f"Deleted: {self.file_node.h}" 

712 return 

713 # Finish. 

714 path = g.os_path_finalize_join(directory, fn) # #1781: bug fix. 

715 c1 = c2 = None 

716 if fn.endswith('.leo'): 

717 c1 = self.make_leo_outline(fn, path, s1, rev1) 

718 c2 = self.make_leo_outline(fn, path, s2, rev2) 

719 else: 

720 root = self.find_file(fn) 

721 if c.looksLikeDerivedFile(path): 

722 c1 = self.make_at_file_outline(fn, s1, rev1) 

723 c2 = self.make_at_file_outline(fn, s2, rev2) 

724 elif root: 

725 c1 = self.make_at_clean_outline(fn, root, s1, rev1) 

726 c2 = self.make_at_clean_outline(fn, root, s2, rev2) 

727 if c1 and c2: 

728 self.make_diff_outlines(c1, c2, fn, rev1, rev2) 

729 self.file_node.b = ( 

730 f"{self.file_node.b.rstrip()}\n" 

731 f"@language {c2.target_language}\n") 

732 #@+node:ekr.20201208115447.1: *4* gdc.diff_pull_request 

733 def diff_pull_request(self): 

734 """ 

735 Create a Leonine version of the diffs that would be 

736 produced by a pull request between two branches. 

737 """ 

738 directory = self.get_directory() 

739 if not directory: 

740 return 

741 aList = g.execGitCommand("git rev-parse devel", directory) 

742 if aList: 

743 devel_rev = aList[0] 

744 devel_rev = devel_rev[:8] 

745 self.diff_two_revs( 

746 rev1=devel_rev, # Before: Latest devel commit. 

747 rev2='HEAD', # After: Lastest branch commit 

748 ) 

749 else: 

750 g.es_print('FAIL: git rev-parse devel') 

751 #@+node:ekr.20180506064102.10: *4* gdc.diff_two_branches 

752 def diff_two_branches(self, branch1, branch2, fn): 

753 """Create an outline describing the git diffs for fn.""" 

754 c = self.c 

755 if not self.get_directory(): 

756 return 

757 self.root = p = c.lastTopLevel().insertAfter() 

758 p.h = f"git-diff-branches {branch1} {branch2}" 

759 s1 = self.get_file_from_branch(branch1, fn) 

760 s2 = self.get_file_from_branch(branch2, fn) 

761 lines1 = g.splitLines(s1) 

762 lines2 = g.splitLines(s2) 

763 diff_list = list(difflib.unified_diff(lines1, lines2, branch1, branch2,)) 

764 diff_list.insert(0, '@ignore\n@nosearch\n@language patch\n') 

765 self.file_node = self.create_file_node(diff_list, fn) 

766 if c.looksLikeDerivedFile(fn): 

767 c1 = self.make_at_file_outline(fn, s1, branch1) 

768 c2 = self.make_at_file_outline(fn, s2, branch2) 

769 else: 

770 root = self.find_file(fn) 

771 if root: 

772 c1 = self.make_at_clean_outline(fn, root, s1, branch1) 

773 c2 = self.make_at_clean_outline(fn, root, s2, branch2) 

774 else: 

775 c1 = c2 = None 

776 if c1 and c2: 

777 self.make_diff_outlines(c1, c2, fn) 

778 self.file_node.b = f"{self.file_node.b.rstrip()}\n@language {c2.target_language}\n" 

779 self.finish() 

780 #@+node:ekr.20180507212821.1: *4* gdc.diff_two_revs 

781 def diff_two_revs(self, rev1='HEAD', rev2=''): 

782 """ 

783 Create an outline describing the git diffs for all files changed 

784 between rev1 and rev2. 

785 """ 

786 c = self.c 

787 if not self.get_directory(): 

788 return 

789 # Get list of changed files. 

790 files = self.get_files(rev1, rev2) 

791 n = len(files) 

792 message = f"diffing {n} file{g.plural(n)}" 

793 if n > 5: 

794 message += ". This may take awhile..." 

795 g.es_print(message) 

796 # Create the root node. 

797 self.root = c.lastTopLevel().insertAfter() 

798 self.root.h = f"git diff revs: {rev1} {rev2}" 

799 self.root.b = '@ignore\n@nosearch\n' 

800 # Create diffs of all files. 

801 for fn in files: 

802 self.diff_file(fn=fn, rev1=rev1, rev2=rev2) 

803 self.finish() 

804 #@+node:ekr.20170806094320.12: *4* gdc.git_diff & helper 

805 def git_diff(self, rev1='HEAD', rev2=''): 

806 """The main line of the git diff command.""" 

807 if not self.get_directory(): 

808 return 

809 # Diff the given revs. 

810 ok = self.diff_revs(rev1, rev2) 

811 if ok: 

812 return 

813 # Go back at most 5 revs... 

814 n1, n2 = 1, 0 

815 while n1 <= 5: 

816 ok = self.diff_revs( 

817 rev1=f"HEAD@{{{n1}}}", 

818 rev2=f"HEAD@{{{n2}}}") 

819 if ok: 

820 return 

821 n1, n2 = n1 + 1, n2 + 1 

822 if not ok: 

823 g.es_print('no changed readable files from HEAD@{1}..HEAD@{5}') 

824 #@+node:ekr.20170820082125.1: *5* gdc.diff_revs 

825 def diff_revs(self, rev1, rev2): 

826 """Diff all files given by rev1 and rev2.""" 

827 files = self.get_files(rev1, rev2) 

828 if files: 

829 self.root = self.create_root(rev1, rev2) 

830 for fn in files: 

831 self.diff_file(fn=fn, rev1=rev1, rev2=rev2) 

832 self.finish() 

833 return bool(files) 

834 #@+node:ekr.20180510095801.1: *3* gdc.Utils 

835 #@+node:ekr.20170806191942.2: *4* gdc.create_compare_node 

836 def create_compare_node(self, c1, c2, d, kind, rev1, rev2): 

837 """Create nodes describing the changes.""" 

838 if not d: 

839 return 

840 parent = self.file_node.insertAsLastChild() 

841 parent.setHeadString(kind) 

842 for key in d: 

843 if kind.lower() == 'changed': 

844 v1, v2 = d.get(key) 

845 # Organizer node: contains diff 

846 organizer = parent.insertAsLastChild() 

847 organizer.h = v2.h 

848 body = list(difflib.unified_diff( 

849 g.splitLines(v1.b), 

850 g.splitLines(v2.b), 

851 rev1 or 'uncommitted', 

852 rev2 or 'uncommitted', 

853 )) 

854 if ''.join(body).strip(): 

855 body.insert(0, '@ignore\n@nosearch\n@language patch\n') 

856 body.append(f"@language {c2.target_language}\n") 

857 else: 

858 body = ['Only headline has changed'] 

859 organizer.b = ''.join(body) 

860 # Node 2: Old node 

861 p2 = organizer.insertAsLastChild() 

862 p2.h = 'Old:' + v1.h 

863 p2.b = v1.b 

864 # Node 3: New node 

865 assert v1.fileIndex == v2.fileIndex 

866 p_in_c = self.find_gnx(self.c, v1.fileIndex) 

867 if p_in_c: # Make a clone, if possible. 

868 p3 = p_in_c.clone() 

869 p3.moveToLastChildOf(organizer) 

870 else: 

871 p3 = organizer.insertAsLastChild() 

872 p3.h = 'New:' + v2.h 

873 p3.b = v2.b 

874 elif kind.lower() == 'added': 

875 v = d.get(key) 

876 new_p = self.find_gnx(self.c, v.fileIndex) 

877 if new_p: # Make a clone, if possible. 

878 p = new_p.clone() 

879 p.moveToLastChildOf(parent) 

880 else: 

881 p = parent.insertAsLastChild() 

882 p.h = v.h 

883 p.b = v.b 

884 else: 

885 v = d.get(key) 

886 p = parent.insertAsLastChild() 

887 p.h = v.h 

888 p.b = v.b 

889 #@+node:ekr.20170806094321.1: *4* gdc.create_file_node 

890 def create_file_node(self, diff_list, fn): 

891 """Create an organizer node for the file.""" 

892 p = self.root.insertAsLastChild() 

893 p.h = fn.strip() 

894 p.b = ''.join(diff_list) 

895 return p 

896 #@+node:ekr.20170806094320.18: *4* gdc.create_root 

897 def create_root(self, rev1, rev2): 

898 """Create the top-level organizer node describing the git diff.""" 

899 c = self.c 

900 r1, r2 = rev1 or '', rev2 or '' 

901 p = c.lastTopLevel().insertAfter() 

902 p.h = f"git diff {r1} {r2}" 

903 p.b = '@ignore\n@nosearch\n' 

904 if r1 and r2: 

905 p.b += ( 

906 f"{r1}={self.get_revno(r1)}\n" 

907 f"{r2}={self.get_revno(r2)}") 

908 else: 

909 p.b += f"{r1}={self.get_revno(r1)}" 

910 return p 

911 #@+node:ekr.20170806094320.7: *4* gdc.find_file 

912 def find_file(self, fn): 

913 """Return the @<file> node matching fn.""" 

914 c = self.c 

915 fn = g.os_path_basename(fn) 

916 for p in c.all_unique_positions(): 

917 if p.isAnyAtFileNode(): 

918 fn2 = p.anyAtFileNodeName() 

919 if fn2.endswith(fn): 

920 return p 

921 return None 

922 #@+node:ekr.20170806094321.3: *4* gdc.find_git_working_directory 

923 def find_git_working_directory(self, directory): 

924 """Return the git working directory, starting at directory.""" 

925 while directory: 

926 if g.os_path_exists(g.os_path_finalize_join(directory, '.git')): 

927 return directory 

928 path2 = g.os_path_finalize_join(directory, '..') 

929 if path2 == directory: 

930 break 

931 directory = path2 

932 return None 

933 #@+node:ekr.20170819132219.1: *4* gdc.find_gnx 

934 def find_gnx(self, c, gnx): 

935 """Return a position in c having the given gnx.""" 

936 for p in c.all_unique_positions(): 

937 if p.v.fileIndex == gnx: 

938 return p 

939 return None 

940 #@+node:ekr.20170806094321.5: *4* gdc.finish 

941 def finish(self): 

942 """Finish execution of this command.""" 

943 c = self.c 

944 c.selectPosition(self.root) 

945 self.root.expand() 

946 c.redraw(self.root) 

947 c.treeWantsFocusNow() 

948 #@+node:ekr.20210819080657.1: *4* gdc.get_directory 

949 def get_directory(self): 

950 """ 

951 #2143. 

952 Resolve filename to the nearest directory containing a .git directory. 

953 """ 

954 c = self.c 

955 filename = c.fileName() 

956 if not filename: 

957 print('git-diff: outline has no name') 

958 return None 

959 directory = os.path.dirname(filename) 

960 if directory and not os.path.isdir(directory): 

961 directory = os.path.dirname(directory) 

962 if not directory: 

963 print(f"git-diff: outline has no directory. filename: {filename!r}") 

964 return None 

965 # Does path/../ref exist? 

966 base_directory = g.gitHeadPath(directory) 

967 if not base_directory: 

968 print(f"git-diff: no .git directory: {directory!r} filename: {filename!r}") 

969 return None 

970 # This should guarantee that the directory contains a .git directory. 

971 directory = g.os_path_finalize_join(base_directory, '..', '..') 

972 return directory 

973 #@+node:ekr.20180506064102.11: *4* gdc.get_file_from_branch 

974 def get_file_from_branch(self, branch, fn): 

975 """Get the file from the head of the given branch.""" 

976 # #2143 

977 directory = self.get_directory() 

978 if not directory: 

979 return '' 

980 command = f"git show {branch}:{fn}" 

981 lines = g.execGitCommand(command, directory) 

982 s = ''.join(lines) 

983 return g.toUnicode(s).replace('\r', '') 

984 #@+node:ekr.20170806094320.15: *4* gdc.get_file_from_rev 

985 def get_file_from_rev(self, rev, fn): 

986 """Get the file from the given rev, or the working directory if None.""" 

987 # #2143 

988 directory = self.get_directory() 

989 if not directory: 

990 return '' 

991 path = g.os_path_finalize_join(directory, fn) 

992 if not g.os_path_exists(path): 

993 g.trace(f"File not found: {path!r} fn: {fn!r}") 

994 return '' 

995 if rev: 

996 # Get the file using git. 

997 # Use the file name, not the path. 

998 command = f"git show {rev}:{fn}" 

999 lines = g.execGitCommand(command, directory) 

1000 return g.toUnicode(''.join(lines)).replace('\r', '') 

1001 try: 

1002 with open(path, 'rb') as f: 

1003 b = f.read() 

1004 return g.toUnicode(b).replace('\r', '') 

1005 except Exception: 

1006 g.es_print('Can not read', path) 

1007 g.es_exception() 

1008 return '' 

1009 #@+node:ekr.20170806094320.9: *4* gdc.get_files 

1010 def get_files(self, rev1, rev2): 

1011 """Return a list of changed files.""" 

1012 # #2143 

1013 directory = self.get_directory() 

1014 if not directory: 

1015 return [] 

1016 command = f"git diff --name-only {(rev1 or '')} {(rev2 or '')}" 

1017 # #1781: Allow diffs of .leo files. 

1018 return [ 

1019 z.strip() for z in g.execGitCommand(command, directory) 

1020 if not z.strip().endswith(('.db', '.zip')) 

1021 ] 

1022 #@+node:ekr.20170821052348.1: *4* gdc.get_revno 

1023 def get_revno(self, revspec, abbreviated=True): 

1024 """Return the abbreviated hash the given revision spec.""" 

1025 if not revspec: 

1026 return 'uncommitted' 

1027 # Return only the abbreviated hash for the revspec. 

1028 format_s = 'h' if abbreviated else 'H' 

1029 command = f"git show --format=%{format_s} --no-patch {revspec}" 

1030 directory = self.get_directory() 

1031 lines = g.execGitCommand(command, directory=directory) 

1032 return ''.join(lines).strip() 

1033 #@+node:ekr.20170820084258.1: *4* gdc.make_at_clean_outline 

1034 def make_at_clean_outline(self, fn, root, s, rev): 

1035 """ 

1036 Create a hidden temp outline from lines without sentinels. 

1037 root is the @<file> node for fn. 

1038 s is the contents of the (public) file, without sentinels. 

1039 """ 

1040 # A specialized version of at.readOneAtCleanNode. 

1041 hidden_c = leoCommands.Commands(fn, gui=g.app.nullGui) 

1042 at = hidden_c.atFileCommands 

1043 x = hidden_c.shadowController 

1044 hidden_c.frame.createFirstTreeNode() 

1045 hidden_root = hidden_c.rootPosition() 

1046 # copy root to hidden root, including gnxs. 

1047 root.copyTreeFromSelfTo(hidden_root, copyGnxs=True) 

1048 hidden_root.h = fn + ':' + rev if rev else fn 

1049 # Set at.encoding first. 

1050 at.initReadIvars(hidden_root, fn) 

1051 # Must be called before at.scanAllDirectives. 

1052 at.scanAllDirectives(hidden_root) 

1053 # Sets at.startSentinelComment/endSentinelComment. 

1054 new_public_lines = g.splitLines(s) 

1055 old_private_lines = at.write_at_clean_sentinels(hidden_root) 

1056 marker = x.markerFromFileLines(old_private_lines, fn) 

1057 old_public_lines, junk = x.separate_sentinels(old_private_lines, marker) 

1058 if old_public_lines: 

1059 # Fix #1136: The old lines might not exist. 

1060 new_private_lines = x.propagate_changed_lines( 

1061 new_public_lines, old_private_lines, marker, p=hidden_root) 

1062 at.fast_read_into_root( 

1063 c=hidden_c, 

1064 contents=''.join(new_private_lines), 

1065 gnx2vnode={}, 

1066 path=fn, 

1067 root=hidden_root, 

1068 ) 

1069 return hidden_c 

1070 #@+node:ekr.20170806094321.7: *4* gdc.make_at_file_outline 

1071 def make_at_file_outline(self, fn, s, rev): 

1072 """Create a hidden temp outline from lines.""" 

1073 # A specialized version of atFileCommands.read. 

1074 hidden_c = leoCommands.Commands(fn, gui=g.app.nullGui) 

1075 at = hidden_c.atFileCommands 

1076 hidden_c.frame.createFirstTreeNode() 

1077 root = hidden_c.rootPosition() 

1078 root.h = fn + ':' + rev if rev else fn 

1079 at.initReadIvars(root, fn) 

1080 if at.errors > 0: 

1081 g.trace('***** errors') 

1082 return None 

1083 at.fast_read_into_root( 

1084 c=hidden_c, 

1085 contents=s, 

1086 gnx2vnode={}, 

1087 path=fn, 

1088 root=root, 

1089 ) 

1090 return hidden_c 

1091 #@+node:ekr.20170806125535.1: *4* gdc.make_diff_outlines & helper 

1092 def make_diff_outlines(self, c1, c2, fn, rev1='', rev2=''): 

1093 """Create an outline-oriented diff from the *hidden* outlines c1 and c2.""" 

1094 added, deleted, changed = self.compute_dicts(c1, c2) 

1095 table = ( 

1096 (added, 'Added'), 

1097 (deleted, 'Deleted'), 

1098 (changed, 'Changed')) 

1099 for d, kind in table: 

1100 self.create_compare_node(c1, c2, d, kind, rev1, rev2) 

1101 #@+node:ekr.20170806191707.1: *5* gdc.compute_dicts 

1102 def compute_dicts(self, c1, c2): 

1103 """Compute inserted, deleted, changed dictionaries.""" 

1104 # Special case the root: only compare the body text. 

1105 root1, root2 = c1.rootPosition().v, c2.rootPosition().v 

1106 root1.h = root2.h 

1107 if 0: 

1108 g.trace('c1...') 

1109 for p in c1.all_positions(): 

1110 print(f"{len(p.b):4} {p.h}") 

1111 g.trace('c2...') 

1112 for p in c2.all_positions(): 

1113 print(f"{len(p.b):4} {p.h}") 

1114 d1 = {v.fileIndex: v for v in c1.all_unique_nodes()} 

1115 d2 = {v.fileIndex: v for v in c2.all_unique_nodes()} 

1116 added = {key: d2.get(key) for key in d2 if not d1.get(key)} 

1117 deleted = {key: d1.get(key) for key in d1 if not d2.get(key)} 

1118 # Remove the root from the added and deleted dicts. 

1119 if root2.fileIndex in added: 

1120 del added[root2.fileIndex] 

1121 if root1.fileIndex in deleted: 

1122 del deleted[root1.fileIndex] 

1123 changed = {} 

1124 for key in d1: 

1125 if key in d2: 

1126 v1 = d1.get(key) 

1127 v2 = d2.get(key) 

1128 assert v1 and v2 

1129 assert v1.context != v2.context 

1130 if v1.h != v2.h or v1.b != v2.b: 

1131 changed[key] = (v1, v2) 

1132 return added, deleted, changed 

1133 #@+node:ekr.20201215050832.1: *4* gdc.make_leo_outline 

1134 def make_leo_outline(self, fn, path, s, rev): 

1135 """Create a hidden temp outline for the .leo file in s.""" 

1136 hidden_c = leoCommands.Commands(fn, gui=g.app.nullGui) 

1137 hidden_c.frame.createFirstTreeNode() 

1138 root = hidden_c.rootPosition() 

1139 root.h = fn + ':' + rev if rev else fn 

1140 hidden_c.fileCommands.getLeoFile( 

1141 theFile=io.StringIO(initial_value=s), 

1142 fileName=path, 

1143 readAtFileNodesFlag=False, 

1144 silent=False, 

1145 checkOpenFiles=False, 

1146 ) 

1147 return hidden_c 

1148 #@-others 

1149#@-others 

1150#@@language python 

1151#@-leo