Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#@+leo-ver=5-thin 

2#@+node:ekr.20180212072657.2: * @file leoCompare.py 

3"""Leo's base compare class.""" 

4import difflib 

5import filecmp 

6import os 

7from leo.core import leoGlobals as g 

8#@+others 

9#@+node:ekr.20031218072017.3633: ** class LeoCompare 

10class BaseLeoCompare: 

11 """The base class for Leo's compare code.""" 

12 #@+others 

13 #@+node:ekr.20031218072017.3634: *3* compare.__init__ 

14 # All these ivars are known to the LeoComparePanel class. 

15 

16 def __init__(self, 

17 # Keyword arguments are much convenient and more clear for scripts. 

18 commands=None, 

19 appendOutput=False, 

20 ignoreBlankLines=True, 

21 ignoreFirstLine1=False, 

22 ignoreFirstLine2=False, 

23 ignoreInteriorWhitespace=False, 

24 ignoreLeadingWhitespace=True, 

25 ignoreSentinelLines=False, 

26 limitCount=0, # Zero means don't stop. 

27 limitToExtension=".py", # For directory compares. 

28 makeWhitespaceVisible=True, 

29 printBothMatches=False, 

30 printMatches=False, 

31 printMismatches=True, 

32 printTrailingMismatches=False, 

33 outputFileName=None 

34 ): 

35 # It is more convenient for the LeoComparePanel to set these directly. 

36 self.c = commands 

37 self.appendOutput = appendOutput 

38 self.ignoreBlankLines = ignoreBlankLines 

39 self.ignoreFirstLine1 = ignoreFirstLine1 

40 self.ignoreFirstLine2 = ignoreFirstLine2 

41 self.ignoreInteriorWhitespace = ignoreInteriorWhitespace 

42 self.ignoreLeadingWhitespace = ignoreLeadingWhitespace 

43 self.ignoreSentinelLines = ignoreSentinelLines 

44 self.limitCount = limitCount 

45 self.limitToExtension = limitToExtension 

46 self.makeWhitespaceVisible = makeWhitespaceVisible 

47 self.printBothMatches = printBothMatches 

48 self.printMatches = printMatches 

49 self.printMismatches = printMismatches 

50 self.printTrailingMismatches = printTrailingMismatches 

51 # For communication between methods... 

52 self.outputFileName = outputFileName 

53 self.fileName1 = None 

54 self.fileName2 = None 

55 # Open files... 

56 self.outputFile = None 

57 #@+node:ekr.20031218072017.3635: *3* compare_directories (entry) 

58 # We ignore the filename portion of path1 and path2 if it exists. 

59 

60 def compare_directories(self, path1, path2): 

61 # Ignore everything except the directory name. 

62 dir1 = g.os_path_dirname(path1) 

63 dir2 = g.os_path_dirname(path2) 

64 dir1 = g.os_path_normpath(dir1) 

65 dir2 = g.os_path_normpath(dir2) 

66 if dir1 == dir2: 

67 return self.show("Please pick distinct directories.") 

68 try: 

69 list1 = os.listdir(dir1) 

70 except Exception: 

71 return self.show("invalid directory:" + dir1) 

72 try: 

73 list2 = os.listdir(dir2) 

74 except Exception: 

75 return self.show("invalid directory:" + dir2) 

76 if self.outputFileName: 

77 self.openOutputFile() 

78 ok = self.outputFileName is None or self.outputFile 

79 if not ok: 

80 return None 

81 # Create files and files2, the lists of files to be compared. 

82 files1 = [] 

83 files2 = [] 

84 for f in list1: 

85 junk, ext = g.os_path_splitext(f) 

86 if self.limitToExtension: 

87 if ext == self.limitToExtension: 

88 files1.append(f) 

89 else: 

90 files1.append(f) 

91 for f in list2: 

92 junk, ext = g.os_path_splitext(f) 

93 if self.limitToExtension: 

94 if ext == self.limitToExtension: 

95 files2.append(f) 

96 else: 

97 files2.append(f) 

98 # Compare the files and set the yes, no and missing lists. 

99 missing1, missing2, no, yes = [], [], [], [] 

100 for f1 in files1: 

101 head, f2 = g.os_path_split(f1) 

102 if f2 in files2: 

103 try: 

104 name1 = g.os_path_join(dir1, f1) 

105 name2 = g.os_path_join(dir2, f2) 

106 val = filecmp.cmp(name1, name2, 0) 

107 if val: 

108 yes.append(f1) 

109 else: 

110 no.append(f1) 

111 except Exception: 

112 self.show("exception in filecmp.cmp") 

113 g.es_exception() 

114 missing1.append(f1) 

115 else: 

116 missing1.append(f1) 

117 for f2 in files2: 

118 head, f1 = g.os_path_split(f2) 

119 if f1 not in files1: 

120 missing2.append(f1) 

121 # Print the results. 

122 for kind, files in ( 

123 ("----- matches --------", yes), 

124 ("----- mismatches -----", no), 

125 ("----- not found 1 ------", missing1), 

126 ("----- not found 2 ------", missing2), 

127 ): 

128 self.show(kind) 

129 for f in files: 

130 self.show(f) 

131 if self.outputFile: 

132 self.outputFile.close() 

133 self.outputFile = None 

134 return None # To keep pychecker happy. 

135 #@+node:ekr.20031218072017.3636: *3* compare_files (entry) 

136 def compare_files(self, name1, name2): 

137 if name1 == name2: 

138 self.show("File names are identical.\nPlease pick distinct files.") 

139 return 

140 self.compare_two_files(name1, name2) 

141 #@+node:ekr.20180211123531.1: *3* compare_list_of_files (entry for scripts) 

142 def compare_list_of_files(self, aList1): 

143 

144 aList = list(set(aList1)) 

145 while len(aList) > 1: 

146 path1 = aList[0] 

147 for path2 in aList[1:]: 

148 g.trace('COMPARE', path1, path2) 

149 self.compare_two_files(path1, path2) 

150 #@+node:ekr.20180211123741.1: *3* compare_two_files 

151 def compare_two_files(self, name1, name2): 

152 """A helper function.""" 

153 f1 = f2 = None 

154 try: 

155 f1 = self.doOpen(name1) 

156 f2 = self.doOpen(name2) 

157 if self.outputFileName: 

158 self.openOutputFile() 

159 ok = self.outputFileName is None or self.outputFile 

160 ok = 1 if ok and ok != 0 else 0 

161 if f1 and f2 and ok: 

162 # Don't compare if there is an error opening the output file. 

163 self.compare_open_files(f1, f2, name1, name2) 

164 except Exception: 

165 self.show("exception comparing files") 

166 g.es_exception() 

167 try: 

168 if f1: 

169 f1.close() 

170 if f2: 

171 f2.close() 

172 if self.outputFile: 

173 self.outputFile.close() 

174 self.outputFile = None 

175 except Exception: 

176 self.show("exception closing files") 

177 g.es_exception() 

178 #@+node:ekr.20031218072017.3637: *3* compare_lines 

179 def compare_lines(self, s1, s2): 

180 if self.ignoreLeadingWhitespace: 

181 s1 = s1.lstrip() 

182 s2 = s2.lstrip() 

183 if self.ignoreInteriorWhitespace: 

184 k1 = g.skip_ws(s1, 0) 

185 k2 = g.skip_ws(s2, 0) 

186 ws1 = s1[:k1] 

187 ws2 = s2[:k2] 

188 tail1 = s1[k1:] 

189 tail2 = s2[k2:] 

190 tail1 = tail1.replace(" ", "").replace("\t", "") 

191 tail2 = tail2.replace(" ", "").replace("\t", "") 

192 s1 = ws1 + tail1 

193 s2 = ws2 + tail2 

194 return s1 == s2 

195 #@+node:ekr.20031218072017.3638: *3* compare_open_files 

196 def compare_open_files(self, f1, f2, name1, name2): 

197 # self.show("compare_open_files") 

198 lines1 = 0 

199 lines2 = 0 

200 mismatches = 0 

201 printTrailing = True 

202 sentinelComment1 = sentinelComment2 = None 

203 if self.openOutputFile(): 

204 self.show("1: " + name1) 

205 self.show("2: " + name2) 

206 self.show("") 

207 s1 = s2 = None 

208 #@+<< handle opening lines >> 

209 #@+node:ekr.20031218072017.3639: *4* << handle opening lines >> 

210 if self.ignoreSentinelLines: 

211 s1 = g.readlineForceUnixNewline(f1) 

212 lines1 += 1 

213 s2 = g.readlineForceUnixNewline(f2) 

214 lines2 += 1 

215 # Note: isLeoHeader may return None. 

216 sentinelComment1 = self.isLeoHeader(s1) 

217 sentinelComment2 = self.isLeoHeader(s2) 

218 if not sentinelComment1: 

219 self.show("no @+leo line for " + name1) 

220 if not sentinelComment2: 

221 self.show("no @+leo line for " + name2) 

222 if self.ignoreFirstLine1: 

223 if s1 is None: 

224 g.readlineForceUnixNewline(f1) 

225 lines1 += 1 

226 s1 = None 

227 if self.ignoreFirstLine2: 

228 if s2 is None: 

229 g.readlineForceUnixNewline(f2) 

230 lines2 += 1 

231 s2 = None 

232 #@-<< handle opening lines >> 

233 while 1: 

234 if s1 is None: 

235 s1 = g.readlineForceUnixNewline(f1) 

236 lines1 += 1 

237 if s2 is None: 

238 s2 = g.readlineForceUnixNewline(f2) 

239 lines2 += 1 

240 #@+<< ignore blank lines and/or sentinels >> 

241 #@+node:ekr.20031218072017.3640: *4* << ignore blank lines and/or sentinels >> 

242 # Completely empty strings denotes end-of-file. 

243 if s1: 

244 if self.ignoreBlankLines and s1.isspace(): 

245 s1 = None 

246 continue 

247 if self.ignoreSentinelLines and sentinelComment1 and self.isSentinel( 

248 s1, sentinelComment1): 

249 s1 = None 

250 continue 

251 if s2: 

252 if self.ignoreBlankLines and s2.isspace(): 

253 s2 = None 

254 continue 

255 if self.ignoreSentinelLines and sentinelComment2 and self.isSentinel( 

256 s2, sentinelComment2): 

257 s2 = None 

258 continue 

259 #@-<< ignore blank lines and/or sentinels >> 

260 n1 = len(s1) 

261 n2 = len(s2) 

262 if n1 == 0 and n2 != 0: 

263 self.show("1.eof***:") 

264 if n2 == 0 and n1 != 0: 

265 self.show("2.eof***:") 

266 if n1 == 0 or n2 == 0: 

267 break 

268 match = self.compare_lines(s1, s2) 

269 if not match: 

270 mismatches += 1 

271 #@+<< print matches and/or mismatches >> 

272 #@+node:ekr.20031218072017.3641: *4* << print matches and/or mismatches >> 

273 if self.limitCount == 0 or mismatches <= self.limitCount: 

274 if match and self.printMatches: 

275 if self.printBothMatches: 

276 z1 = "1." + str(lines1) 

277 z2 = "2." + str(lines2) 

278 self.dump(z1.rjust(6) + ' :', s1) 

279 self.dump(z2.rjust(6) + ' :', s2) 

280 else: 

281 self.dump(str(lines1).rjust(6) + ' :', s1) 

282 if not match and self.printMismatches: 

283 z1 = "1." + str(lines1) 

284 z2 = "2." + str(lines2) 

285 self.dump(z1.rjust(6) + '*:', s1) 

286 self.dump(z2.rjust(6) + '*:', s2) 

287 #@-<< print matches and/or mismatches >> 

288 #@+<< warn if mismatch limit reached >> 

289 #@+node:ekr.20031218072017.3642: *4* << warn if mismatch limit reached >> 

290 if self.limitCount > 0 and mismatches >= self.limitCount: 

291 if printTrailing: 

292 self.show("") 

293 self.show("limit count reached") 

294 self.show("") 

295 printTrailing = False 

296 #@-<< warn if mismatch limit reached >> 

297 s1 = s2 = None # force a read of both lines. 

298 #@+<< handle reporting after at least one eof is seen >> 

299 #@+node:ekr.20031218072017.3643: *4* << handle reporting after at least one eof is seen >> 

300 if n1 > 0: 

301 lines1 += self.dumpToEndOfFile("1.", f1, s1, lines1, printTrailing) 

302 if n2 > 0: 

303 lines2 += self.dumpToEndOfFile("2.", f2, s2, lines2, printTrailing) 

304 self.show("") 

305 self.show("lines1:" + str(lines1)) 

306 self.show("lines2:" + str(lines2)) 

307 self.show("mismatches:" + str(mismatches)) 

308 #@-<< handle reporting after at least one eof is seen >> 

309 #@+node:ekr.20031218072017.3644: *3* compare.filecmp 

310 def filecmp(self, f1, f2): 

311 val = filecmp.cmp(f1, f2) 

312 if val: 

313 self.show("equal") 

314 else: 

315 self.show("*** not equal") 

316 return val 

317 #@+node:ekr.20031218072017.3645: *3* compare.utils... 

318 #@+node:ekr.20031218072017.3646: *4* compare.doOpen 

319 def doOpen(self, name): 

320 try: 

321 f = open(name, 'r') 

322 return f 

323 except Exception: 

324 self.show("can not open:" + '"' + name + '"') 

325 return None 

326 #@+node:ekr.20031218072017.3647: *4* compare.dump 

327 def dump(self, tag, s): 

328 compare = self 

329 out = tag 

330 for ch in s[:-1]: # don't print the newline 

331 if compare.makeWhitespaceVisible: 

332 if ch == '\t': 

333 out += "[" 

334 out += "t" 

335 out += "]" 

336 elif ch == ' ': 

337 out += "[" 

338 out += " " 

339 out += "]" 

340 else: 

341 out += ch 

342 else: 

343 out += ch 

344 self.show(out) 

345 #@+node:ekr.20031218072017.3648: *4* compare.dumpToEndOfFile 

346 def dumpToEndOfFile(self, tag, f, s, line, printTrailing): 

347 trailingLines = 0 

348 while 1: 

349 if not s: 

350 s = g.readlineForceUnixNewline(f) 

351 if not s: 

352 break 

353 trailingLines += 1 

354 if self.printTrailingMismatches and printTrailing: 

355 z = tag + str(line) 

356 tag2 = z.rjust(6) + "+:" 

357 self.dump(tag2, s) 

358 s = None 

359 self.show(tag + str(trailingLines) + " trailing lines") 

360 return trailingLines 

361 #@+node:ekr.20031218072017.3649: *4* compare.isLeoHeader & isSentinel 

362 #@+at These methods are based on AtFile.scanHeader(). They are simpler 

363 # because we only care about the starting sentinel comment: any line 

364 # starting with the starting sentinel comment is presumed to be a 

365 # sentinel line. 

366 #@@c 

367 

368 def isLeoHeader(self, s): 

369 tag = "@+leo" 

370 j = s.find(tag) 

371 if j > 0: 

372 i = g.skip_ws(s, 0) 

373 if i < j: 

374 return s[i:j] 

375 return None 

376 

377 def isSentinel(self, s, sentinelComment): 

378 i = g.skip_ws(s, 0) 

379 return g.match(s, i, sentinelComment) 

380 #@+node:ekr.20031218072017.1144: *4* compare.openOutputFile 

381 def openOutputFile(self): 

382 if self.outputFileName is None: 

383 return 

384 theDir, name = g.os_path_split(self.outputFileName) 

385 if not theDir: 

386 self.show("empty output directory") 

387 return 

388 if not name: 

389 self.show("empty output file name") 

390 return 

391 if not g.os_path_exists(theDir): 

392 self.show("output directory not found: " + theDir) 

393 else: 

394 try: 

395 if self.appendOutput: 

396 self.show("appending to " + self.outputFileName) 

397 self.outputFile = open(self.outputFileName, "ab") 

398 else: 

399 self.show("writing to " + self.outputFileName) 

400 self.outputFile = open(self.outputFileName, "wb") 

401 except Exception: 

402 self.outputFile = None 

403 self.show("exception opening output file") 

404 g.es_exception() 

405 #@+node:ekr.20031218072017.3650: *4* compare.show 

406 def show(self, s): 

407 # g.pr(s) 

408 if self.outputFile: 

409 # self.outputFile is opened in 'wb' mode. 

410 s = g.toEncodedString(s + '\n') 

411 self.outputFile.write(s) 

412 elif self.c: 

413 g.es(s) 

414 else: 

415 g.pr(s) 

416 g.pr('') 

417 #@+node:ekr.20031218072017.3651: *4* compare.showIvars 

418 def showIvars(self): 

419 self.show("fileName1:" + str(self.fileName1)) 

420 self.show("fileName2:" + str(self.fileName2)) 

421 self.show("outputFileName:" + str(self.outputFileName)) 

422 self.show("limitToExtension:" + str(self.limitToExtension)) 

423 self.show("") 

424 self.show("ignoreBlankLines:" + str(self.ignoreBlankLines)) 

425 self.show("ignoreFirstLine1:" + str(self.ignoreFirstLine1)) 

426 self.show("ignoreFirstLine2:" + str(self.ignoreFirstLine2)) 

427 self.show("ignoreInteriorWhitespace:" + str(self.ignoreInteriorWhitespace)) 

428 self.show("ignoreLeadingWhitespace:" + str(self.ignoreLeadingWhitespace)) 

429 self.show("ignoreSentinelLines:" + str(self.ignoreSentinelLines)) 

430 self.show("") 

431 self.show("limitCount:" + str(self.limitCount)) 

432 self.show("printMatches:" + str(self.printMatches)) 

433 self.show("printMismatches:" + str(self.printMismatches)) 

434 self.show("printTrailingMismatches:" + str(self.printTrailingMismatches)) 

435 #@-others 

436 

437class LeoCompare(BaseLeoCompare): 

438 """ 

439 A class containing Leo's compare code. 

440 

441 These are not very useful comparisons. 

442 """ 

443 pass 

444#@+node:ekr.20180211170333.1: ** class CompareLeoOutlines 

445class CompareLeoOutlines: 

446 """ 

447 A class to do outline-oriented diffs of two or more .leo files. 

448 Similar to GitDiffController, adapted for use by scripts. 

449 """ 

450 

451 def __init__(self, c): 

452 """Ctor for the LeoOutlineCompare class.""" 

453 self.c = c 

454 self.file_node = None 

455 self.root = None 

456 self.path1 = None 

457 self.path2 = None 

458 #@+others 

459 #@+node:ekr.20180211170333.2: *3* loc.diff_list_of_files (entry) 

460 def diff_list_of_files(self, aList, visible=True): 

461 """The main entry point for scripts.""" 

462 if len(aList) < 2: 

463 g.trace('Not enough files in', repr(aList)) 

464 return 

465 self.root = self.create_root(aList) 

466 self.visible = visible 

467 while len(aList) > 1: 

468 path1 = aList[0] 

469 aList = aList[1:] 

470 for path2 in aList: 

471 self.diff_two_files(path1, path2) 

472 self.finish() 

473 #@+node:ekr.20180211170333.3: *3* loc.diff_two_files 

474 def diff_two_files(self, fn1, fn2): 

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

476 self.path1, self.path2 = fn1, fn2 

477 s1 = self.get_file(fn1) 

478 s2 = self.get_file(fn2) 

479 lines1 = g.splitLines(s1) 

480 lines2 = g.splitLines(s2) 

481 diff_list = list(difflib.unified_diff(lines1, lines2, fn1, fn2)) 

482 diff_list.insert(0, '@language patch\n') 

483 self.file_node = self.create_file_node(diff_list, fn1, fn2) 

484 # These will be left open 

485 c1 = self.open_outline(fn1) 

486 c2 = self.open_outline(fn2) 

487 if c1 and c2: 

488 self.make_diff_outlines(c1, c2) 

489 self.file_node.b = ( 

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

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

492 #@+node:ekr.20180211170333.4: *3* loc.Utils 

493 #@+node:ekr.20180211170333.5: *4* loc.compute_dicts 

494 def compute_dicts(self, c1, c2): 

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

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

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

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

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

500 changed = {} 

501 for key in d1: 

502 if key in d2: 

503 v1 = d1.get(key) 

504 v2 = d2.get(key) 

505 assert v1 and v2 

506 assert v1.context != v2.context 

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

508 changed[key] = (v1, v2) 

509 return added, deleted, changed 

510 #@+node:ekr.20180211170333.6: *4* loc.create_compare_node 

511 def create_compare_node(self, c1, c2, d, kind): 

512 """Create nodes describing the changes.""" 

513 if not d: 

514 return 

515 parent = self.file_node.insertAsLastChild() 

516 parent.setHeadString(kind) 

517 for key in d: 

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

519 v1, v2 = d.get(key) 

520 # Organizer node: contains diff 

521 organizer = parent.insertAsLastChild() 

522 organizer.h = v2.h 

523 body = list(difflib.unified_diff( 

524 g.splitLines(v1.b), 

525 g.splitLines(v2.b), 

526 self.path1, 

527 self.path2, 

528 )) 

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

530 body.insert(0, '@language patch\n') 

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

532 else: 

533 body = ['Only headline has changed'] 

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

535 # Node 1: 

536 p1 = organizer.insertAsLastChild() 

537 p1.h = '1:' + v1.h 

538 p1.b = v1.b 

539 # Node 2: 

540 assert v1.fileIndex == v2.fileIndex 

541 p2 = organizer.insertAsLastChild() 

542 p2.h = '2:' + v2.h 

543 p2.b = v2.b 

544 else: 

545 v = d.get(key) 

546 p = parent.insertAsLastChild() 

547 p.h = v.h 

548 p.b = v.b 

549 #@+node:ekr.20180211170333.7: *4* loc.create_file_node 

550 def create_file_node(self, diff_list, fn1, fn2): 

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

552 p = self.root.insertAsLastChild() 

553 p.h = f"{g.shortFileName(fn1).strip()}, {g.shortFileName(fn2).strip()}" 

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

555 return p 

556 #@+node:ekr.20180211170333.8: *4* loc.create_root 

557 def create_root(self, aList): 

558 """Create the top-level organizer node describing all the diffs.""" 

559 c = self.c 

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

561 p.h = 'diff-leo-files' 

562 p.b = '\n'.join(aList) + '\n' 

563 return p 

564 #@+node:ekr.20180211170333.10: *4* loc.finish 

565 def finish(self): 

566 """Finish execution of this command.""" 

567 c = self.c 

568 if hasattr(g.app.gui, 'frameFactory'): 

569 tff = g.app.gui.frameFactory 

570 tff.setTabForCommander(c) 

571 c.selectPosition(self.root) 

572 self.root.expand() 

573 c.bodyWantsFocus() 

574 c.redraw() 

575 #@+node:ekr.20180211170333.11: *4* loc.get_file 

576 def get_file(self, path): 

577 """Return the contents of the file whose path is given.""" 

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

579 s = f.read() 

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

581 #@+node:ekr.20180211170333.13: *4* loc.make_diff_outlines 

582 def make_diff_outlines(self, c1, c2): 

583 """Create an outline-oriented diff from the outlines c1 and c2.""" 

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

585 table = ( 

586 (added, 'Added'), 

587 (deleted, 'Deleted'), 

588 (changed, 'Changed')) 

589 for d, kind in table: 

590 self.create_compare_node(c1, c2, d, kind) 

591 #@+node:ekr.20180211170333.14: *4* loc.open_outline 

592 def open_outline(self, fn): 

593 """ 

594 Find the commander for fn, creating a new outline tab if necessary. 

595 

596 Using open commanders works because we always read entire .leo files. 

597 """ 

598 for frame in g.app.windowList: 

599 if frame.c.fileName() == fn: 

600 return frame.c 

601 gui = None if self.visible else g.app.nullGui 

602 return g.openWithFileName(fn, gui=gui) 

603 #@-others 

604#@+node:ekr.20180214041049.1: ** Top-level commands and helpers 

605#@+node:ekr.20180213104556.1: *3* @g.command(diff-and-open-leo-files) 

606@g.command('diff-and-open-leo-files') 

607def diff_and_open_leo_files(event): 

608 """ 

609 Open a dialog prompting for two or more .leo files. 

610 

611 Opens all the files and creates a top-level node in c's outline showing 

612 the diffs of those files, two at a time. 

613 """ 

614 diff_leo_files_helper(event, 

615 title="Diff And Open Leo Files", 

616 visible=True, 

617 ) 

618#@+node:ekr.20180213040339.1: *3* @g.command(diff-leo-files) 

619@g.command('diff-leo-files') 

620def diff_leo_files(event): 

621 """ 

622 Open a dialog prompting for two or more .leo files. 

623 

624 Creates a top-level node showing the diffs of those files, two at a time. 

625 """ 

626 diff_leo_files_helper(event, 

627 title="Diff Leo Files", 

628 visible=False, 

629 ) 

630#@+node:ekr.20160331191740.1: *3* @g.command(diff-marked-nodes) 

631@g.command('diff-marked-nodes') 

632def diffMarkedNodes(event): 

633 """ 

634 When two or more nodes are marked, this command does the following: 

635 

636 - Creates a "diff marked node" as the last top-level node. The body of 

637 this node contains "diff n" nodes, one for each pair of compared 

638 nodes. 

639 

640 - Each diff n contains the diffs between the two diffed nodes, that is, 

641 difflib.Differ().compare(p1.b, p2.b). The children of the diff n are 

642 *clones* of the two compared nodes. 

643 """ 

644 c = event and event.get('c') 

645 if not c: 

646 return 

647 aList = [z for z in c.all_unique_positions() if z.isMarked()] 

648 n = 0 

649 if len(aList) >= 2: 

650 root = c.lastTopLevel().insertAfter() 

651 root.h = 'diff marked nodes' 

652 root.b = '\n'.join([z.h for z in aList]) + '\n' 

653 while len(aList) > 1: 

654 n += 1 

655 p1, p2 = aList[0], aList[1] 

656 aList = aList[1:] 

657 lines = difflib.Differ().compare( 

658 g.splitLines(p1.b.rstrip() + '\n'), 

659 g.splitLines(p2.b.rstrip() + '\n')) 

660 p = root.insertAsLastChild() 

661 # p.h = 'Compare: %s, %s' % (g.truncate(p1.h, 22), g.truncate(p2.h, 22)) 

662 p.h = f"diff {n}" 

663 p.b = f"1: {p1.h}\n2: {p2.h}\n{''.join(list(lines))}" 

664 for p3 in (p1, p2): 

665 clone = p3.clone() 

666 clone.moveToLastChildOf(p) 

667 root.expand() 

668 # c.unmarkAll() 

669 c.selectPosition(root) 

670 c.redraw() 

671 else: 

672 g.es_print('Please mark at least 2 nodes') 

673#@+node:ekr.20180213104627.1: *3* diff_leo_files_helper 

674def diff_leo_files_helper(event, title, visible): 

675 """Prompt for a list of .leo files to open.""" 

676 c = event and event.get('c') 

677 if not c: 

678 return 

679 types = [ 

680 ("Leo files", "*.leo"), 

681 ("All files", "*"), 

682 ] 

683 paths = g.app.gui.runOpenFileDialog(c, 

684 title=title, 

685 filetypes=types, 

686 defaultextension=".leo", 

687 multiple=True, 

688 ) 

689 c.bringToFront() 

690 # paths = [z for z in paths if g.os_path_exists(z)] 

691 if len(paths) > 1: 

692 CompareLeoOutlines(c).diff_list_of_files(paths, visible=visible) 

693 elif len(paths) == 1: 

694 g.es_print('Please pick two or more .leo files') 

695#@-others 

696#@@language python 

697#@@tabwidth -4 

698#@@pagewidth 70 

699#@-leo