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

780 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

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] = [] # List of positions containing @unit. 

38 

39 #@+others 

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

41 def check_clone_move(self, p, parent): 

42 """ 

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

44 or any of parents ancestors. 

45 """ 

46 # Like as checkMoveWithParentWithWarning without warning. 

47 clonedVnodes = {} 

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

49 if ancestor.isCloned(): 

50 v = ancestor.v 

51 clonedVnodes[v] = v 

52 if not clonedVnodes: 

53 return True 

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

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

56 return False 

57 return True 

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

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

60 def convert_file(self, c): 

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

62 self.find_all_units(c) 

63 for p in c.all_positions(): 

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

65 path = m and m.group(1) 

66 if path: 

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

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

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

70 self.errors += 1 

71 else: 

72 self.root = p.copy() 

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

74 self.do_root(p) 

75 self.root = None 

76 # 

77 # Check the results. 

78 link_errors = c.checkOutline(check_links=True) 

79 self.errors += link_errors 

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

81 c.redraw() 

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

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

84 def dump(self, c): 

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

86 for p in c.all_positions(): 

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

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

89 def do_root(self, p): 

90 """ 

91 Make all necessary clones for section defintions. 

92 """ 

93 for p in p.self_and_subtree(): 

94 self.make_clones(p) 

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

96 def find_all_units(self, c): 

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

98 for p in c.all_positions(): 

99 if '@unit' in p.b: 

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

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

102 def find_section(self, root, section_name): 

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

104 

105 def munge(s): 

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

107 

108 for p in root.subtree(): 

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

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

111 return p 

112 

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

114 return None 

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

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

117 

118 def make_clones(self, p): 

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

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

121 m = self.section_pat.match(s) 

122 if m: 

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

124 section_p = self.make_clone(p, section_name) 

125 if not section_p: 

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

127 self.errors += 1 

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

129 def make_clone(self, p, section_name): 

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

131 

132 def clone_and_move(parent, section_p): 

133 clone = section_p.clone() 

134 if self.check_clone_move(clone, parent): 

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

136 clone.moveToLastChildOf(parent) 

137 else: 

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

139 clone.doDelete() 

140 self.errors += 1 

141 # 

142 # First, look in p's subtree. 

143 section_p = self.find_section(p, section_name) 

144 if section_p: 

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

146 # Already defined in a good place. 

147 return section_p 

148 # 

149 # Finally, look in the @unit tree. 

150 for unit_p in self.units: 

151 section_p = self.find_section(unit_p, section_name) 

152 if section_p: 

153 clone_and_move(p, section_p) 

154 return section_p 

155 return None 

156 #@-others 

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

158class EditFileCommandsClass(BaseEditCommandsClass): 

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

160 #@+others 

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

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

163 def convert_at_root(self, event=None): 

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

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

166 #@@wrap 

167 """ 

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

169 outline. 

170 

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

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

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

174 

175 This command attempts to do the following: 

176 

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

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

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

180 

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

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

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

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

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

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

187 """ 

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

189 c = event.get('c') 

190 if not c: 

191 return 

192 ConvertAtRoot().convert_file(c) 

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

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

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

196 def cleanAtCleanFiles(self, event): 

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

198 c = self.c 

199 undoType = 'clean-@clean-files' 

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

201 total = 0 

202 for p in c.all_unique_positions(): 

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

204 n = 0 

205 for p2 in p.subtree(): 

206 bunch2 = c.undoer.beforeChangeNodeContents(p2) 

207 if self.cleanAtCleanNode(p2, undoType): 

208 n += 1 

209 total += 1 

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

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

212 if total > 0: 

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

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

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

216 def cleanAtCleanNode(self, p, undoType): 

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

218 s = p.b.strip() 

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

220 return False 

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

222 s2 = ws + s + ws 

223 changed = s2 != p.b 

224 if changed: 

225 p.b = s2 

226 p.setDirty() 

227 return changed 

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

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

230 def cleanAtCleanTree(self, event): 

231 """ 

232 Adjust whitespace in the nearest @clean tree, 

233 searching c.p and its ancestors. 

234 """ 

235 c = self.c 

236 # Look for an @clean node. 

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

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

239 break 

240 else: 

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

242 return 

243 # pylint: disable=undefined-loop-variable 

244 # p is certainly defined here. 

245 bunch = c.undoer.beforeChangeTree(p) 

246 n = 0 

247 undoType = 'clean-@clean-tree' 

248 for p2 in p.subtree(): 

249 if self.cleanAtCleanNode(p2, undoType): 

250 n += 1 

251 if n > 0: 

252 c.setChanged() 

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

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

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

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

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

258 def compareAnyTwoFiles(self, event): 

259 """Compare two files.""" 

260 c = c1 = self.c 

261 w = c.frame.body.wrapper 

262 commanders = g.app.commanders() 

263 if g.app.diff: 

264 if len(commanders) == 2: 

265 c1, c2 = commanders 

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

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

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

269 g.es(fn1) 

270 g.es(fn2) 

271 else: 

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

273 return 

274 else: 

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

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

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

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

279 if not fileName: 

280 return 

281 # Read the file into the hidden commander. 

282 c2 = g.createHiddenCommander(fileName) 

283 if not c2: 

284 return 

285 # Compute the inserted, deleted and changed dicts. 

286 d1 = self.createFileDict(c1) 

287 d2 = self.createFileDict(c2) 

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

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

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

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

292 if not g.app.diff: 

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

294 c2.frame.destroySelf() 

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

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

297 def computeChangeDicts(self, d1, d2): 

298 """ 

299 Compute inserted, deleted, changed dictionaries. 

300 

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

302 """ 

303 inserted = {} 

304 for key in d2: 

305 if not d1.get(key): 

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

307 deleted = {} 

308 for key in d1: 

309 if not d2.get(key): 

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

311 changed = {} 

312 for key in d1: 

313 if d2.get(key): 

314 p1 = d1.get(key) 

315 p2 = d2.get(key) 

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

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

318 return inserted, deleted, changed 

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

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

321 """Create the comparison trees.""" 

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

323 assert c == c1 

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

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

326 u.beforeChangeGroup(c.p, undoType) 

327 undoData = u.beforeInsertNode(c.p) 

328 parent = c.p.insertAfter() 

329 parent.setHeadString(undoType) 

330 u.afterInsertNode(parent, undoType, undoData) 

331 # Use the wrapped file name if possible. 

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

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

334 for d, kind in ( 

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

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

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

338 ): 

339 self.createCompareClones(d, kind, parent) 

340 c.selectPosition(parent) 

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

342 c.redraw() 

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

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

345 if d: 

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

347 parent = parent.insertAsLastChild() 

348 parent.setHeadString(kind) 

349 for key in d: 

350 p = d.get(key) 

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

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

353 pass 

354 elif p.v.context == c: 

355 clone = p.clone() 

356 clone.moveToLastChildOf(parent) 

357 else: 

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

359 copy = p.copyTreeAfter() 

360 copy.moveToLastChildOf(parent) 

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

362 p2.v.context = c 

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

364 def createFileDict(self, c): 

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

366 d = {} 

367 for p in c.all_positions(): 

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

369 return d 

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

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

372 for d, kind in ( 

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

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

375 (changed, 'changed'), 

376 ): 

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

378 for key in d: 

379 p = d.get(key) 

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

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

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

383 

384 

385 class CompareTreesController: 

386 #@+others 

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

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

389 """Compare dicts d1 and d2.""" 

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

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

392 if h in d2: 

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

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

395 if aList: 

396 p = root.insertAsLastChild() 

397 p.h = h 

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

399 p1.clone().moveToLastChildOf(p) 

400 p2.clone().moveToLastChildOf(p) 

401 elif p1.b.strip(): 

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

403 p = root.insertAsLastChild() 

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

405 p1.clone().moveToLastChildOf(p) 

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

407 p2 = d2.get(h) 

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

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

410 p = root.insertAsLastChild() 

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

412 p2.clone().moveToLastChildOf(p) 

413 return root 

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

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

416 """Main line.""" 

417 self.c = c 

418 root = c.p.insertAfter() 

419 root.h = tag 

420 d1 = self.scan(p1) 

421 d2 = self.scan(p2) 

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

423 c.p.contract() 

424 root.expand() 

425 c.selectPosition(root) 

426 c.redraw() 

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

428 def scan(self, p1): 

429 """ 

430 Create a dict of the methods in p1. 

431 Keys are headlines, stripped of prefixes. 

432 Values are copies of positions. 

433 """ 

434 d = {} # 

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

436 h = p.h.strip() 

437 i = h.find('.') 

438 if i > -1: 

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

440 if h in d: 

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

442 else: 

443 d[h] = p.copy() 

444 return d 

445 #@-others 

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

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

448 @cmd('file-delete') 

449 def deleteFile(self, event): 

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

451 k = self.c.k 

452 k.setLabelBlue('Delete File: ') 

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

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

455 

456 def deleteFile1(self, event): 

457 k = self.c.k 

458 k.keyboardQuit() 

459 k.clearState() 

460 try: 

461 os.remove(k.arg) 

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

463 except Exception: 

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

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

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

467 def diff(self, event=None): 

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

469 c = self.c 

470 fn = self.getReadableTextFile() 

471 if not fn: 

472 return 

473 fn2 = self.getReadableTextFile() 

474 if not fn2: 

475 return 

476 s1, e = g.readFileIntoString(fn) 

477 if s1 is None: 

478 return 

479 s2, e = g.readFileIntoString(fn2) 

480 if s2 is None: 

481 return 

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

483 aList = difflib.ndiff(lines1, lines2) 

484 p = c.p.insertAfter() 

485 p.h = 'diff' 

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

487 c.redraw() 

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

489 def getReadableTextFile(self): 

490 """Prompt for a text file.""" 

491 c = self.c 

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

493 title='Open Text File', 

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

495 defaultextension=".txt") 

496 return fn 

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

498 @cmd('git-diff') 

499 @cmd('gd') 

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

501 """Produce a Leonine git diff.""" 

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

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

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

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

506 def gitDiffPullRequest(self, event=None): 

507 """ 

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

509 """ 

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

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

512 @cmd('file-insert') 

513 def insertFile(self, event): 

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

515 w = self.editWidget(event) 

516 if not w: 

517 return 

518 fn = self.getReadableTextFile() 

519 if not fn: 

520 return 

521 s, e = g.readFileIntoString(fn) 

522 if s: 

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

524 i = w.getInsertPoint() 

525 w.insert(i, s) 

526 w.seeInsertPoint() 

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

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

529 @cmd('directory-make') 

530 def makeDirectory(self, event): 

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

532 k = self.c.k 

533 k.setLabelBlue('Make Directory: ') 

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

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

536 def makeDirectory1(self, event): 

537 k = self.c.k 

538 k.keyboardQuit() 

539 k.clearState() 

540 try: 

541 os.mkdir(k.arg) 

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

543 except Exception: 

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

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

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

547 def openOutlineByName(self, event): 

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

549 c, k = self.c, self.c.k 

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

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

552 if fileName and g.os_path_exists(fileName): 

553 g.openWithFileName(fileName, old_c=c) 

554 else: 

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

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

557 

558 def openOutlineByNameFinisher(self, fn): 

559 c = self.c 

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

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

562 try: 

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

564 except Exception: 

565 pass 

566 else: 

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

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

569 @cmd('directory-remove') 

570 def removeDirectory(self, event): 

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

572 k = self.c.k 

573 k.setLabelBlue('Remove Directory: ') 

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

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

576 

577 def removeDirectory1(self, event): 

578 k = self.c.k 

579 k.keyboardQuit() 

580 k.clearState() 

581 try: 

582 os.rmdir(k.arg) 

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

584 except Exception: 

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

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

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

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

589 def saveFile(self, event): 

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

591 c = self.c 

592 w = self.editWidget(event) 

593 if not w: 

594 return 

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

596 title='save-file', 

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

598 defaultextension=".txt") 

599 if fileName: 

600 try: 

601 s = w.getAllText() 

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

603 f.write(s) 

604 except IOError: 

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

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

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

608 def toggleAtAutoAtEdit(self, event): 

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

610 p = self.c.p 

611 if p.isAtEditNode(): 

612 self.toAtAuto(p) 

613 return 

614 for p in p.self_and_parents(): 

615 if p.isAtAutoNode(): 

616 self.toAtEdit(p) 

617 return 

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

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

620 def toAtAuto(self, p): 

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

622 c = self.c 

623 # Change the headline. 

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

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

626 w = c.frame.body.wrapper 

627 ins = w.getInsertPoint() 

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

629 # Ignore *preceding* directive lines. 

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

631 row -= len(directives) 

632 row = max(0, row) 

633 # Reload the file, creating new nodes. 

634 c.selectPosition(p) 

635 c.refreshFromDisk() 

636 # Restore the line in the proper node. 

637 c.gotoCommands.find_file_line(row + 1) 

638 p.setDirty() 

639 c.setChanged() 

640 c.redraw() 

641 c.bodyWantsFocus() 

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

643 def toAtEdit(self, p): 

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

645 c = self.c 

646 w = c.frame.body.wrapper 

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

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

649 ins = w.getInsertPoint() 

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

651 # Ignore directive lines. 

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

653 row -= len(directives) 

654 row = max(0, row) 

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

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

657 if p2 == c.p: 

658 break 

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

660 row += len(lines) 

661 # Reload the file into a single node. 

662 c.selectPosition(p) 

663 c.refreshFromDisk() 

664 # Restore the line in the proper node. 

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

666 w.setInsertPoint(ins) 

667 p.setDirty() 

668 c.setChanged() 

669 c.redraw() 

670 c.bodyWantsFocus() 

671 #@-others 

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

673class GitDiffController: 

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

675 

676 def __init__(self, c): 

677 self.c = c 

678 self.file_node = None 

679 self.root = None 

680 #@+others 

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

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

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

684 """ 

685 Create an outline describing the git diffs for fn. 

686 """ 

687 # Common code. 

688 c = self.c 

689 # #1781, #2143 

690 directory = self.get_directory() 

691 if not directory: 

692 return 

693 s1 = self.get_file_from_rev(rev1, fn) 

694 s2 = self.get_file_from_rev(rev2, fn) 

695 lines1 = g.splitLines(s1) 

696 lines2 = g.splitLines(s2) 

697 diff_list = list(difflib.unified_diff( 

698 lines1, 

699 lines2, 

700 rev1 or 'uncommitted', 

701 rev2 or 'uncommitted', 

702 )) 

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

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

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

706 if not s1: 

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

708 return 

709 if not s2: 

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

711 return 

712 # Finish. 

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

714 c1 = c2 = None 

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

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

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

718 else: 

719 root = self.find_file(fn) 

720 if c.looksLikeDerivedFile(path): 

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

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

723 elif root: 

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

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

726 if c1 and c2: 

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

728 self.file_node.b = ( 

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

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

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

732 def diff_pull_request(self): 

733 """ 

734 Create a Leonine version of the diffs that would be 

735 produced by a pull request between two branches. 

736 """ 

737 directory = self.get_directory() 

738 if not directory: 

739 return 

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

741 if aList: 

742 devel_rev = aList[0] 

743 devel_rev = devel_rev[:8] 

744 self.diff_two_revs( 

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

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

747 ) 

748 else: 

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

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

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

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

753 c = self.c 

754 if not self.get_directory(): 

755 return 

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

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

758 s1 = self.get_file_from_branch(branch1, fn) 

759 s2 = self.get_file_from_branch(branch2, fn) 

760 lines1 = g.splitLines(s1) 

761 lines2 = g.splitLines(s2) 

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

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

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

765 if c.looksLikeDerivedFile(fn): 

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

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

768 else: 

769 root = self.find_file(fn) 

770 if root: 

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

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

773 else: 

774 c1 = c2 = None 

775 if c1 and c2: 

776 self.make_diff_outlines(c1, c2, fn) 

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

778 self.finish() 

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

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

781 """ 

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

783 between rev1 and rev2. 

784 """ 

785 c = self.c 

786 if not self.get_directory(): 

787 return 

788 # Get list of changed files. 

789 files = self.get_files(rev1, rev2) 

790 n = len(files) 

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

792 if n > 5: 

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

794 g.es_print(message) 

795 # Create the root node. 

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

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

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

799 # Create diffs of all files. 

800 for fn in files: 

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

802 self.finish() 

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

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

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

806 if not self.get_directory(): 

807 return 

808 # Diff the given revs. 

809 ok = self.diff_revs(rev1, rev2) 

810 if ok: 

811 return 

812 # Go back at most 5 revs... 

813 n1, n2 = 1, 0 

814 while n1 <= 5: 

815 ok = self.diff_revs( 

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

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

818 if ok: 

819 return 

820 n1, n2 = n1 + 1, n2 + 1 

821 if not ok: 

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

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

824 def diff_revs(self, rev1, rev2): 

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

826 files = self.get_files(rev1, rev2) 

827 if files: 

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

829 for fn in files: 

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

831 self.finish() 

832 return bool(files) 

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

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

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

836 """Create nodes describing the changes.""" 

837 if not d: 

838 return 

839 parent = self.file_node.insertAsLastChild() 

840 parent.setHeadString(kind) 

841 for key in d: 

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

843 v1, v2 = d.get(key) 

844 # Organizer node: contains diff 

845 organizer = parent.insertAsLastChild() 

846 organizer.h = v2.h 

847 body = list(difflib.unified_diff( 

848 g.splitLines(v1.b), 

849 g.splitLines(v2.b), 

850 rev1 or 'uncommitted', 

851 rev2 or 'uncommitted', 

852 )) 

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

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

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

856 else: 

857 body = ['Only headline has changed'] 

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

859 # Node 2: Old node 

860 p2 = organizer.insertAsLastChild() 

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

862 p2.b = v1.b 

863 # Node 3: New node 

864 assert v1.fileIndex == v2.fileIndex 

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

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

867 p3 = p_in_c.clone() 

868 p3.moveToLastChildOf(organizer) 

869 else: 

870 p3 = organizer.insertAsLastChild() 

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

872 p3.b = v2.b 

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

874 v = d.get(key) 

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

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

877 p = new_p.clone() 

878 p.moveToLastChildOf(parent) 

879 else: 

880 p = parent.insertAsLastChild() 

881 p.h = v.h 

882 p.b = v.b 

883 else: 

884 v = d.get(key) 

885 p = parent.insertAsLastChild() 

886 p.h = v.h 

887 p.b = v.b 

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

889 def create_file_node(self, diff_list, fn): 

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

891 p = self.root.insertAsLastChild() 

892 p.h = fn.strip() 

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

894 return p 

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

896 def create_root(self, rev1, rev2): 

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

898 c = self.c 

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

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

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

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

903 if r1 and r2: 

904 p.b += ( 

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

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

907 else: 

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

909 return p 

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

911 def find_file(self, fn): 

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

913 c = self.c 

914 fn = g.os_path_basename(fn) 

915 for p in c.all_unique_positions(): 

916 if p.isAnyAtFileNode(): 

917 fn2 = p.anyAtFileNodeName() 

918 if fn2.endswith(fn): 

919 return p 

920 return None 

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

922 def find_git_working_directory(self, directory): 

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

924 while directory: 

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

926 return directory 

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

928 if path2 == directory: 

929 break 

930 directory = path2 

931 return None 

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

933 def find_gnx(self, c, gnx): 

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

935 for p in c.all_unique_positions(): 

936 if p.v.fileIndex == gnx: 

937 return p 

938 return None 

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

940 def finish(self): 

941 """Finish execution of this command.""" 

942 c = self.c 

943 c.selectPosition(self.root) 

944 self.root.expand() 

945 c.redraw(self.root) 

946 c.treeWantsFocusNow() 

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

948 def get_directory(self): 

949 """ 

950 #2143. 

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

952 """ 

953 c = self.c 

954 filename = c.fileName() 

955 if not filename: 

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

957 return None 

958 directory = os.path.dirname(filename) 

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

960 directory = os.path.dirname(directory) 

961 if not directory: 

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

963 return None 

964 # Does path/../ref exist? 

965 base_directory = g.gitHeadPath(directory) 

966 if not base_directory: 

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

968 return None 

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

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

971 return directory 

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

973 def get_file_from_branch(self, branch, fn): 

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

975 # #2143 

976 directory = self.get_directory() 

977 if not directory: 

978 return '' 

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

980 lines = g.execGitCommand(command, directory) 

981 s = ''.join(lines) 

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

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

984 def get_file_from_rev(self, rev, fn): 

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

986 # #2143 

987 directory = self.get_directory() 

988 if not directory: 

989 return '' 

990 path = g.os_path_finalize_join(directory, fn) 

991 if not g.os_path_exists(path): 

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

993 return '' 

994 if rev: 

995 # Get the file using git. 

996 # Use the file name, not the path. 

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

998 lines = g.execGitCommand(command, directory) 

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

1000 try: 

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

1002 b = f.read() 

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

1004 except Exception: 

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

1006 g.es_exception() 

1007 return '' 

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

1009 def get_files(self, rev1, rev2): 

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

1011 # #2143 

1012 directory = self.get_directory() 

1013 if not directory: 

1014 return [] 

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

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

1017 return [ 

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

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

1020 ] 

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

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

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

1024 if not revspec: 

1025 return 'uncommitted' 

1026 # Return only the abbreviated hash for the revspec. 

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

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

1029 directory = self.get_directory() 

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

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

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

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

1034 """ 

1035 Create a hidden temp outline from lines without sentinels. 

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

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

1038 """ 

1039 # A specialized version of at.readOneAtCleanNode. 

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

1041 at = hidden_c.atFileCommands 

1042 x = hidden_c.shadowController 

1043 hidden_c.frame.createFirstTreeNode() 

1044 hidden_root = hidden_c.rootPosition() 

1045 # copy root to hidden root, including gnxs. 

1046 root.copyTreeFromSelfTo(hidden_root, copyGnxs=True) 

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

1048 # Set at.encoding first. 

1049 # Must be called before at.scanAllDirectives. 

1050 at.initReadIvars(hidden_root, fn) 

1051 # Sets at.startSentinelComment/endSentinelComment. 

1052 at.scanAllDirectives(hidden_root) 

1053 new_public_lines = g.splitLines(s) 

1054 old_private_lines = at.write_at_clean_sentinels(hidden_root) 

1055 marker = x.markerFromFileLines(old_private_lines, fn) 

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

1057 if old_public_lines: 

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

1059 new_private_lines = x.propagate_changed_lines( 

1060 new_public_lines, old_private_lines, marker, p=hidden_root) 

1061 at.fast_read_into_root( 

1062 c=hidden_c, 

1063 contents=''.join(new_private_lines), 

1064 gnx2vnode={}, 

1065 path=fn, 

1066 root=hidden_root, 

1067 ) 

1068 return hidden_c 

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

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

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

1072 # A specialized version of atFileCommands.read. 

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

1074 at = hidden_c.atFileCommands 

1075 hidden_c.frame.createFirstTreeNode() 

1076 root = hidden_c.rootPosition() 

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

1078 at.initReadIvars(root, fn) 

1079 if at.errors > 0: 

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

1081 return None 

1082 at.fast_read_into_root( 

1083 c=hidden_c, 

1084 contents=s, 

1085 gnx2vnode={}, 

1086 path=fn, 

1087 root=root, 

1088 ) 

1089 return hidden_c 

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

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

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

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

1094 table = ( 

1095 (added, 'Added'), 

1096 (deleted, 'Deleted'), 

1097 (changed, 'Changed')) 

1098 for d, kind in table: 

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

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

1101 def compute_dicts(self, c1, c2): 

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

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

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

1105 root1.h = root2.h 

1106 if 0: 

1107 g.trace('c1...') 

1108 for p in c1.all_positions(): 

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

1110 g.trace('c2...') 

1111 for p in c2.all_positions(): 

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

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

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

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

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

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

1118 if root2.fileIndex in added: 

1119 del added[root2.fileIndex] 

1120 if root1.fileIndex in deleted: 

1121 del deleted[root1.fileIndex] 

1122 changed = {} 

1123 for key in d1: 

1124 if key in d2: 

1125 v1 = d1.get(key) 

1126 v2 = d2.get(key) 

1127 assert v1 and v2 

1128 assert v1.context != v2.context 

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

1130 changed[key] = (v1, v2) 

1131 return added, deleted, changed 

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

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

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

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

1136 hidden_c.frame.createFirstTreeNode() 

1137 root = hidden_c.rootPosition() 

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

1139 hidden_c.fileCommands.getLeoFile( 

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

1141 fileName=path, 

1142 readAtFileNodesFlag=False, 

1143 silent=False, 

1144 checkOpenFiles=False, 

1145 ) 

1146 return hidden_c 

1147 #@-others 

1148#@-others 

1149#@@language python 

1150#@-leo