Coverage for C:\Repos\leo-editor\leo\core\leoFileCommands.py: 49%

1428 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.20031218072017.3018: * @file leoFileCommands.py 

4#@@first 

5"""Classes relating to reading and writing .leo files.""" 

6#@+<< imports >> 

7#@+node:ekr.20050405141130: ** << imports >> (leoFileCommands) 

8import binascii 

9from collections import defaultdict 

10from contextlib import contextmanager 

11import difflib 

12import hashlib 

13import io 

14import json 

15import os 

16import pickle 

17import shutil 

18import sqlite3 

19import tempfile 

20import time 

21from typing import Dict 

22import zipfile 

23import xml.etree.ElementTree as ElementTree 

24import xml.sax 

25import xml.sax.saxutils 

26from leo.core import leoGlobals as g 

27from leo.core import leoNodes 

28#@-<< imports >> 

29PRIVAREA = '---begin-private-area---' 

30#@+others 

31#@+node:ekr.20150509194827.1: ** cmd (decorator) 

32def cmd(name): 

33 """Command decorator for the FileCommands class.""" 

34 return g.new_cmd_decorator(name, ['c', 'fileCommands',]) 

35#@+node:ekr.20210316035506.1: ** commands (leoFileCommands.py) 

36#@+node:ekr.20180708114847.1: *3* dump-clone-parents 

37@g.command('dump-clone-parents') 

38def dump_clone_parents(event): 

39 """Print the parent vnodes of all cloned vnodes.""" 

40 c = event.get('c') 

41 if not c: 

42 return 

43 print('dump-clone-parents...') 

44 d = c.fileCommands.gnxDict 

45 for gnx in d: 

46 v = d.get(gnx) 

47 if len(v.parents) > 1: 

48 print(v.h) 

49 g.printObj(v.parents) 

50#@+node:ekr.20210309114903.1: *3* dump-gnx-dict 

51@g.command('dump-gnx-dict') 

52def dump_gnx_dict(event): 

53 """Dump c.fileCommands.gnxDict.""" 

54 c = event.get('c') 

55 if not c: 

56 return 

57 d = c.fileCommands.gnxDict 

58 g.printObj(d, tag='gnxDict') 

59#@+node:ekr.20060918164811: ** class BadLeoFile 

60class BadLeoFile(Exception): 

61 

62 def __init__(self, message): 

63 self.message = message 

64 super().__init__(message) 

65 

66 def __str__(self): 

67 return "Bad Leo File:" + self.message 

68#@+node:ekr.20180602062323.1: ** class FastRead 

69class FastRead: 

70 

71 nativeVnodeAttributes = ( 

72 'a', 

73 'descendentTnodeUnknownAttributes', 

74 'descendentVnodeUnknownAttributes', 

75 'expanded', 'marks', 't', 

76 # 'tnodeList', # Removed in Leo 4.7. 

77 ) 

78 

79 def __init__(self, c, gnx2vnode): 

80 self.c = c 

81 self.gnx2vnode = gnx2vnode 

82 

83 #@+others 

84 #@+node:ekr.20180604110143.1: *3* fast.readFile 

85 def readFile(self, theFile, path): 

86 """Read the file, change splitter ratiors, and return its hidden vnode.""" 

87 s = theFile.read() 

88 v, g_element = self.readWithElementTree(path, s) 

89 if not v: # #1510. 

90 return None 

91 # #1047: only this method changes splitter sizes. 

92 self.scanGlobals(g_element) 

93 # 

94 # #1111: ensure that all outlines have at least one node. 

95 if not v.children: 

96 new_vnode = leoNodes.VNode(context=self.c) 

97 new_vnode.h = 'newHeadline' 

98 v.children = [new_vnode] 

99 return v 

100 

101 #@+node:ekr.20210316035646.1: *3* fast.readFileFromClipboard 

102 def readFileFromClipboard(self, s): 

103 """ 

104 Recreate a file from a string s, and return its hidden vnode. 

105 

106 Unlike readFile above, this does not affect splitter sizes. 

107 """ 

108 v, g_element = self.readWithElementTree(path=None, s=s) 

109 if not v: # #1510. 

110 return None 

111 # 

112 # #1111: ensure that all outlines have at least one node. 

113 if not v.children: 

114 new_vnode = leoNodes.VNode(context=self.c) 

115 new_vnode.h = 'newHeadline' 

116 v.children = [new_vnode] 

117 return v 

118 #@+node:ekr.20180602062323.7: *3* fast.readWithElementTree & helpers 

119 # #1510: https://en.wikipedia.org/wiki/Valid_characters_in_XML. 

120 translate_dict = {z: None for z in range(20) if chr(z) not in '\t\r\n'} 

121 

122 def readWithElementTree(self, path, s): 

123 

124 contents = g.toUnicode(s) 

125 table = contents.maketrans(self.translate_dict) # type:ignore #1510. 

126 contents = contents.translate(table) # #1036, #1046. 

127 try: 

128 xroot = ElementTree.fromstring(contents) 

129 except Exception as e: 

130 # #970: Report failure here. 

131 if path: 

132 message = f"bad .leo file: {g.shortFileName(path)}" 

133 else: 

134 message = 'The clipboard is not a vaild .leo file' 

135 g.es_print('\n' + message, color='red') 

136 g.es_print(g.toUnicode(e)) 

137 print('') 

138 return None, None # #1510: Return a tuple. 

139 g_element = xroot.find('globals') 

140 v_elements = xroot.find('vnodes') 

141 t_elements = xroot.find('tnodes') 

142 gnx2body, gnx2ua = self.scanTnodes(t_elements) 

143 hidden_v = self.scanVnodes(gnx2body, self.gnx2vnode, gnx2ua, v_elements) 

144 self.handleBits() 

145 return hidden_v, g_element 

146 #@+node:ekr.20180624125321.1: *4* fast.handleBits (reads c.db) 

147 def handleBits(self): 

148 """Restore the expanded and marked bits from c.db.""" 

149 c, fc = self.c, self.c.fileCommands 

150 expanded = c.db.get('expanded') 

151 marked = c.db.get('marked') 

152 expanded = expanded.split(',') if expanded else [] 

153 marked = marked.split(',') if marked else [] 

154 fc.descendentExpandedList = expanded 

155 fc.descendentMarksList = marked 

156 #@+node:ekr.20180606041211.1: *4* fast.resolveUa & helper 

157 def resolveUa(self, attr, val, kind=None): # Kind is for unit testing. 

158 """Parse an unknown attribute in a <v> or <t> element.""" 

159 try: 

160 val = g.toEncodedString(val) 

161 except Exception: 

162 g.es_print('unexpected exception converting hexlified string to string') 

163 g.es_exception() 

164 return None 

165 # Leave string attributes starting with 'str_' alone. 

166 if attr.startswith('str_'): 

167 if isinstance(val, (str, bytes)): 

168 return g.toUnicode(val) 

169 try: 

170 # Throws a TypeError if val is not a hex string. 

171 binString = binascii.unhexlify(val) 

172 except Exception: 

173 # Assume that Leo 4.1 or above wrote the attribute. 

174 if g.unitTesting: 

175 assert kind == 'raw', f"unit test failed: kind={kind}" 

176 else: 

177 g.trace(f"can not unhexlify {attr}={val}") 

178 return val 

179 try: 

180 # No change needed to support protocols. 

181 val2 = pickle.loads(binString) 

182 return val2 

183 except Exception: 

184 try: 

185 val2 = pickle.loads(binString, encoding='bytes') 

186 val2 = self.bytesToUnicode(val2) 

187 return val2 

188 except Exception: 

189 g.trace(f"can not unpickle {attr}={val}") 

190 return val 

191 #@+node:ekr.20180606044154.1: *5* fast.bytesToUnicode 

192 def bytesToUnicode(self, ob): 

193 """ 

194 Recursively convert bytes objects in strings / lists / dicts to str 

195 objects, thanks to TNT 

196 http://stackoverflow.com/questions/22840092 

197 Needed for reading Python 2.7 pickles in Python 3.4. 

198 """ 

199 # This is simpler than using isinstance. 

200 # pylint: disable=unidiomatic-typecheck 

201 t = type(ob) 

202 if t in (list, tuple): 

203 l = [str(i, 'utf-8') if type(i) is bytes else i for i in ob] 

204 l = [self.bytesToUnicode(i) 

205 if type(i) in (list, tuple, dict) else i 

206 for i in l] 

207 ro = tuple(l) if t is tuple else l 

208 elif t is dict: 

209 byte_keys = [i for i in ob if type(i) is bytes] 

210 for bk in byte_keys: 

211 v = ob[bk] 

212 del ob[bk] 

213 ob[str(bk, 'utf-8')] = v 

214 for k in ob: 

215 if type(ob[k]) is bytes: 

216 ob[k] = str(ob[k], 'utf-8') 

217 elif type(ob[k]) in (list, tuple, dict): 

218 ob[k] = self.bytesToUnicode(ob[k]) 

219 ro = ob 

220 elif t is bytes: # TNB added this clause 

221 ro = str(ob, 'utf-8') 

222 else: 

223 ro = ob 

224 return ro 

225 #@+node:ekr.20180605062300.1: *4* fast.scanGlobals & helper 

226 def scanGlobals(self, g_element): 

227 """Get global data from the cache, with reasonable defaults.""" 

228 c = self.c 

229 d = self.getGlobalData() 

230 windowSize = g.app.loadManager.options.get('windowSize') 

231 windowSpot = g.app.loadManager.options.get('windowSpot') 

232 if windowSize is not None: 

233 h, w = windowSize # checked in LM.scanOption. 

234 else: 

235 w, h = d.get('width'), d.get('height') 

236 if windowSpot is None: 

237 x, y = d.get('left'), d.get('top') 

238 else: 

239 y, x = windowSpot # #1263: (top, left) 

240 if 'size' in g.app.debug: 

241 g.trace(w, h, x, y, c.shortFileName()) 

242 # c.frame may be a NullFrame. 

243 c.frame.setTopGeometry(w, h, x, y) 

244 r1, r2 = d.get('r1'), d.get('r2') 

245 c.frame.resizePanesToRatio(r1, r2) 

246 frameFactory = getattr(g.app.gui, 'frameFactory', None) 

247 if not frameFactory: 

248 return 

249 assert frameFactory is not None 

250 mf = frameFactory.masterFrame 

251 if g.app.start_minimized: 

252 mf.showMinimized() 

253 elif g.app.start_maximized: 

254 # #1189: fast.scanGlobals calls showMaximized later. 

255 mf.showMaximized() 

256 elif g.app.start_fullscreen: 

257 mf.showFullScreen() 

258 else: 

259 mf.show() 

260 #@+node:ekr.20180708060437.1: *5* fast.getGlobalData 

261 def getGlobalData(self): 

262 """Return a dict containing all global data.""" 

263 c = self.c 

264 try: 

265 window_pos = c.db.get('window_position') 

266 r1 = float(c.db.get('body_outline_ratio', '0.5')) 

267 r2 = float(c.db.get('body_secondary_ratio', '0.5')) 

268 top, left, height, width = window_pos 

269 return { 

270 'top': int(top), 

271 'left': int(left), 

272 'height': int(height), 

273 'width': int(width), 

274 'r1': r1, 

275 'r2': r2, 

276 } 

277 except Exception: 

278 pass 

279 # Use reasonable defaults. 

280 return { 

281 'top': 50, 'left': 50, 

282 'height': 500, 'width': 800, 

283 'r1': 0.5, 'r2': 0.5, 

284 } 

285 #@+node:ekr.20180602062323.8: *4* fast.scanTnodes 

286 def scanTnodes(self, t_elements): 

287 

288 gnx2body: Dict[str, str] = {} 

289 gnx2ua: Dict[str, dict] = defaultdict(dict) 

290 for e in t_elements: 

291 # First, find the gnx. 

292 gnx = e.attrib['tx'] 

293 gnx2body[gnx] = e.text or '' 

294 # Next, scan for uA's for this gnx. 

295 for key, val in e.attrib.items(): 

296 if key != 'tx': 

297 gnx2ua[gnx][key] = self.resolveUa(key, val) 

298 return gnx2body, gnx2ua 

299 #@+node:ekr.20180602062323.9: *4* fast.scanVnodes & helper 

300 def scanVnodes(self, gnx2body, gnx2vnode, gnx2ua, v_elements): 

301 

302 c, fc = self.c, self.c.fileCommands 

303 #@+<< define v_element_visitor >> 

304 #@+node:ekr.20180605102822.1: *5* << define v_element_visitor >> 

305 def v_element_visitor(parent_e, parent_v): 

306 """Visit the given element, creating or updating the parent vnode.""" 

307 for e in parent_e: 

308 assert e.tag in ('v', 'vh'), e.tag 

309 if e.tag == 'vh': 

310 parent_v._headString = g.toUnicode(e.text or '') 

311 continue 

312 # #1581: Attempt to handle old Leo outlines. 

313 try: 

314 gnx = e.attrib['t'] 

315 v = gnx2vnode.get(gnx) 

316 except KeyError: 

317 # g.trace('no "t" attrib') 

318 gnx = None 

319 v = None 

320 if v: 

321 # A clone 

322 parent_v.children.append(v) 

323 v.parents.append(parent_v) 

324 # The body overrides any previous body text. 

325 body = g.toUnicode(gnx2body.get(gnx) or '') 

326 assert isinstance(body, str), body.__class__.__name__ 

327 v._bodyString = body 

328 else: 

329 #@+<< Make a new vnode, linked to the parent >> 

330 #@+node:ekr.20180605075042.1: *6* << Make a new vnode, linked to the parent >> 

331 v = leoNodes.VNode(context=c, gnx=gnx) 

332 gnx2vnode[gnx] = v 

333 parent_v.children.append(v) 

334 v.parents.append(parent_v) 

335 body = g.toUnicode(gnx2body.get(gnx) or '') 

336 assert isinstance(body, str), body.__class__.__name__ 

337 v._bodyString = body 

338 v._headString = 'PLACE HOLDER' 

339 #@-<< Make a new vnode, linked to the parent >> 

340 #@+<< handle all other v attributes >> 

341 #@+node:ekr.20180605075113.1: *6* << handle all other v attributes >> 

342 # FastRead.nativeVnodeAttributes defines the native attributes of <v> elements. 

343 d = e.attrib 

344 s = d.get('descendentTnodeUnknownAttributes') 

345 if s: 

346 aDict = fc.getDescendentUnknownAttributes(s, v=v) 

347 if aDict: 

348 fc.descendentTnodeUaDictList.append(aDict) 

349 s = d.get('descendentVnodeUnknownAttributes') 

350 if s: 

351 aDict = fc.getDescendentUnknownAttributes(s, v=v) 

352 if aDict: 

353 fc.descendentVnodeUaDictList.append((v, aDict),) 

354 # 

355 # Handle vnode uA's 

356 uaDict = gnx2ua[gnx] # A defaultdict(dict) 

357 for key, val in d.items(): 

358 if key not in self.nativeVnodeAttributes: 

359 uaDict[key] = self.resolveUa(key, val) 

360 if uaDict: 

361 v.unknownAttributes = uaDict 

362 #@-<< handle all other v attributes >> 

363 # Handle all inner elements. 

364 v_element_visitor(e, v) 

365 

366 #@-<< define v_element_visitor >> 

367 # 

368 # Create the hidden root vnode. 

369 

370 gnx = 'hidden-root-vnode-gnx' 

371 hidden_v = leoNodes.VNode(context=c, gnx=gnx) 

372 hidden_v._headString = '<hidden root vnode>' 

373 gnx2vnode[gnx] = hidden_v 

374 # 

375 # Traverse the tree of v elements. 

376 v_element_visitor(v_elements, hidden_v) 

377 return hidden_v 

378 #@-others 

379#@+node:ekr.20160514120347.1: ** class FileCommands 

380class FileCommands: 

381 """A class creating the FileCommands subcommander.""" 

382 #@+others 

383 #@+node:ekr.20090218115025.4: *3* fc.Birth 

384 #@+node:ekr.20031218072017.3019: *4* fc.ctor 

385 def __init__(self, c): 

386 """Ctor for FileCommands class.""" 

387 self.c = c 

388 self.frame = c.frame 

389 self.nativeTnodeAttributes = ('tx',) 

390 self.nativeVnodeAttributes = ( 

391 'a', 

392 'descendentTnodeUnknownAttributes', 

393 'descendentVnodeUnknownAttributes', # New in Leo 4.5. 

394 'expanded', 'marks', 't', 

395 # 'tnodeList', # Removed in Leo 4.7. 

396 ) 

397 self.initIvars() 

398 #@+node:ekr.20090218115025.5: *4* fc.initIvars 

399 def initIvars(self): 

400 """Init ivars of the FileCommands class.""" 

401 # General... 

402 c = self.c 

403 self.mFileName = "" 

404 self.fileDate = -1 

405 self.leo_file_encoding = c.config.new_leo_file_encoding 

406 # For reading... 

407 self.checking = False # True: checking only: do *not* alter the outline. 

408 self.descendentExpandedList = [] 

409 self.descendentMarksList = [] 

410 self.forbiddenTnodes = [] 

411 self.descendentTnodeUaDictList = [] 

412 self.descendentVnodeUaDictList = [] 

413 self.ratio = 0.5 

414 self.currentVnode = None 

415 # For writing... 

416 self.read_only = False 

417 self.rootPosition = None 

418 self.outputFile = None 

419 self.openDirectory = None 

420 self.usingClipboard = False 

421 self.currentPosition = None 

422 # New in 3.12... 

423 self.copiedTree = None 

424 # fc.gnxDict is never re-inited. 

425 self.gnxDict = {} # Keys are gnx strings. Values are vnodes. 

426 self.vnodesDict = {} # keys are gnx strings; values are ignored 

427 #@+node:ekr.20210316042224.1: *3* fc: Commands 

428 #@+node:ekr.20031218072017.2012: *4* fc.writeAtFileNodes 

429 @cmd('write-at-file-nodes') 

430 def writeAtFileNodes(self, event=None): 

431 """Write all @file nodes in the selected outline.""" 

432 c = self.c 

433 c.endEditing() 

434 c.init_error_dialogs() 

435 c.atFileCommands.writeAll(all=True) 

436 c.raise_error_dialogs(kind='write') 

437 #@+node:ekr.20031218072017.3050: *4* fc.write-outline-only 

438 @cmd('write-outline-only') 

439 def writeOutlineOnly(self, event=None): 

440 """Write the entire outline without writing any derived files.""" 

441 c = self.c 

442 c.endEditing() 

443 self.writeOutline(fileName=self.mFileName) 

444 

445 #@+node:ekr.20031218072017.1666: *4* fc.writeDirtyAtFileNodes 

446 @cmd('write-dirty-at-file-nodes') 

447 def writeDirtyAtFileNodes(self, event=None): 

448 """Write all changed @file Nodes.""" 

449 c = self.c 

450 c.endEditing() 

451 c.init_error_dialogs() 

452 c.atFileCommands.writeAll(dirty=True) 

453 c.raise_error_dialogs(kind='write') 

454 #@+node:ekr.20031218072017.2013: *4* fc.writeMissingAtFileNodes 

455 @cmd('write-missing-at-file-nodes') 

456 def writeMissingAtFileNodes(self, event=None): 

457 """Write all @file nodes for which the corresponding external file does not exist.""" 

458 c = self.c 

459 c.endEditing() 

460 c.atFileCommands.writeMissing(c.p) 

461 #@+node:ekr.20210316034350.1: *3* fc: File Utils 

462 #@+node:ekr.20031218072017.3047: *4* fc.createBackupFile 

463 def createBackupFile(self, fileName): 

464 """ 

465 Create a closed backup file and copy the file to it, 

466 but only if the original file exists. 

467 """ 

468 if g.os_path_exists(fileName): 

469 fd, backupName = tempfile.mkstemp(text=False) 

470 f = open(fileName, 'rb') # rb is essential. 

471 s = f.read() 

472 f.close() 

473 try: 

474 try: 

475 os.write(fd, s) 

476 finally: 

477 os.close(fd) 

478 ok = True 

479 except Exception: 

480 g.error('exception creating backup file') 

481 g.es_exception() 

482 ok, backupName = False, None 

483 if not ok and self.read_only: 

484 g.error("read only") 

485 else: 

486 ok, backupName = True, None 

487 return ok, backupName 

488 #@+node:ekr.20050404190914.2: *4* fc.deleteBackupFile 

489 def deleteBackupFile(self, fileName): 

490 try: 

491 os.remove(fileName) 

492 except Exception: 

493 if self.read_only: 

494 g.error("read only") 

495 g.error("exception deleting backup file:", fileName) 

496 g.es_exception(full=False) 

497 #@+node:ekr.20100119145629.6108: *4* fc.handleWriteLeoFileException 

498 def handleWriteLeoFileException(self, fileName, backupName, f): 

499 """Report an exception. f is an open file, or None.""" 

500 # c = self.c 

501 g.es("exception writing:", fileName) 

502 g.es_exception(full=True) 

503 if f: 

504 f.close() 

505 # Delete fileName. 

506 if fileName and g.os_path_exists(fileName): 

507 self.deleteBackupFile(fileName) 

508 # Rename backupName to fileName. 

509 if backupName and g.os_path_exists(backupName): 

510 g.es("restoring", fileName, "from", backupName) 

511 # No need to create directories when restoring. 

512 src, dst = backupName, fileName 

513 try: 

514 shutil.move(src, dst) 

515 except Exception: 

516 g.error('exception renaming', src, 'to', dst) 

517 g.es_exception(full=False) 

518 else: 

519 g.error('backup file does not exist!', repr(backupName)) 

520 #@+node:ekr.20040324080359.1: *4* fc.isReadOnly 

521 def isReadOnly(self, fileName): 

522 # self.read_only is not valid for Save As and Save To commands. 

523 if g.os_path_exists(fileName): 

524 try: 

525 if not os.access(fileName, os.W_OK): 

526 g.error("can not write: read only:", fileName) 

527 return True 

528 except Exception: 

529 pass # os.access() may not exist on all platforms. 

530 return False 

531 #@+node:ekr.20210315031535.1: *4* fc.openOutlineForWriting 

532 def openOutlineForWriting(self, fileName): 

533 """Open a .leo file for writing. Return the open file, or None.""" 

534 try: 

535 f = open(fileName, 'wb') # Always use binary mode. 

536 except Exception: 

537 g.es(f"can not open {fileName}") 

538 g.es_exception() 

539 f = None 

540 return f 

541 #@+node:ekr.20031218072017.3045: *4* fc.setDefaultDirectoryForNewFiles 

542 def setDefaultDirectoryForNewFiles(self, fileName): 

543 """Set c.openDirectory for new files for the benefit of leoAtFile.scanAllDirectives.""" 

544 c = self.c 

545 if not c.openDirectory: 

546 theDir = g.os_path_dirname(fileName) 

547 if theDir and g.os_path_isabs(theDir) and g.os_path_exists(theDir): 

548 c.openDirectory = c.frame.openDirectory = theDir 

549 #@+node:ekr.20031218072017.1554: *4* fc.warnOnReadOnlyFiles 

550 def warnOnReadOnlyFiles(self, fileName): 

551 # os.access may not exist on all platforms. 

552 try: 

553 self.read_only = not os.access(fileName, os.W_OK) 

554 except AttributeError: 

555 self.read_only = False 

556 except UnicodeError: 

557 self.read_only = False 

558 if self.read_only and not g.unitTesting: 

559 g.error("read only:", fileName) 

560 #@+node:ekr.20031218072017.3020: *3* fc: Reading 

561 #@+node:ekr.20031218072017.1559: *4* fc: Paste 

562 #@+node:ekr.20080410115129.1: *5* fc.checkPaste 

563 def checkPaste(self, parent, p): 

564 """Return True if p may be pasted as a child of parent.""" 

565 if not parent: 

566 return True 

567 parents = list(parent.self_and_parents()) 

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

569 for z in parents: 

570 if p.v == z.v: 

571 g.warning('Invalid paste: nodes may not descend from themselves') 

572 return False 

573 return True 

574 #@+node:ekr.20180709205603.1: *5* fc.getLeoOutlineFromClipBoard 

575 def getLeoOutlineFromClipboard(self, s): 

576 """Read a Leo outline from string s in clipboard format.""" 

577 c = self.c 

578 current = c.p 

579 if not current: 

580 g.trace('no c.p') 

581 return None 

582 self.initReadIvars() 

583 # Save the hidden root's children. 

584 old_children = c.hiddenRootNode.children 

585 # Save and clear gnxDict. 

586 oldGnxDict = self.gnxDict 

587 self.gnxDict = {} 

588 # This encoding must match the encoding used in outline_to_clipboard_string. 

589 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True) 

590 hidden_v = FastRead(c, self.gnxDict).readFileFromClipboard(s) 

591 v = hidden_v.children[0] 

592 v.parents = [] 

593 # Restore the hidden root's children 

594 c.hiddenRootNode.children = old_children 

595 if not v: 

596 return g.es("the clipboard is not valid ", color="blue") 

597 # Create the position. 

598 p = leoNodes.Position(v) 

599 # Do *not* adjust links when linking v. 

600 if current.hasChildren() and current.isExpanded(): 

601 p._linkCopiedAsNthChild(current, 0) 

602 else: 

603 p._linkCopiedAfter(current) 

604 assert not p.isCloned(), g.objToString(p.v.parents) 

605 self.gnxDict = oldGnxDict 

606 self.reassignAllIndices(p) 

607 c.selectPosition(p) 

608 self.initReadIvars() 

609 return p 

610 

611 getLeoOutline = getLeoOutlineFromClipboard # for compatibility 

612 #@+node:ekr.20180709205640.1: *5* fc.getLeoOutlineFromClipBoardRetainingClones 

613 def getLeoOutlineFromClipboardRetainingClones(self, s): 

614 """Read a Leo outline from string s in clipboard format.""" 

615 c = self.c 

616 current = c.p 

617 if not current: 

618 return g.trace('no c.p') 

619 self.initReadIvars() 

620 # Save the hidden root's children. 

621 old_children = c.hiddenRootNode.children 

622 # All pasted nodes should already have unique gnx's. 

623 ni = g.app.nodeIndices 

624 for v in c.all_unique_nodes(): 

625 ni.check_gnx(c, v.fileIndex, v) 

626 # This encoding must match the encoding used in outline_to_clipboard_string. 

627 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True) 

628 hidden_v = FastRead(c, self.gnxDict).readFileFromClipboard(s) 

629 v = hidden_v.children[0] 

630 v.parents.remove(hidden_v) 

631 # Restore the hidden root's children 

632 c.hiddenRootNode.children = old_children 

633 if not v: 

634 return g.es("the clipboard is not valid ", color="blue") 

635 # Create the position. 

636 p = leoNodes.Position(v) 

637 # Do *not* adjust links when linking v. 

638 if current.hasChildren() and current.isExpanded(): 

639 if not self.checkPaste(current, p): 

640 return None 

641 p._linkCopiedAsNthChild(current, 0) 

642 else: 

643 if not self.checkPaste(current.parent(), p): 

644 return None 

645 p._linkCopiedAfter(current) 

646 # Fix #862: paste-retaining-clones can corrupt the outline. 

647 self.linkChildrenToParents(p) 

648 c.selectPosition(p) 

649 self.initReadIvars() 

650 return p 

651 #@+node:ekr.20180424123010.1: *5* fc.linkChildrenToParents 

652 def linkChildrenToParents(self, p): 

653 """ 

654 Populate the parent links in all children of p. 

655 """ 

656 for child in p.children(): 

657 if not child.v.parents: 

658 child.v.parents.append(p.v) 

659 self.linkChildrenToParents(child) 

660 #@+node:ekr.20180425034856.1: *5* fc.reassignAllIndices 

661 def reassignAllIndices(self, p): 

662 """Reassign all indices in p's subtree.""" 

663 ni = g.app.nodeIndices 

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

665 v = p2.v 

666 index = ni.getNewIndex(v) 

667 if 'gnx' in g.app.debug: 

668 g.trace('**reassigning**', index, v) 

669 #@+node:ekr.20060919104836: *4* fc: Read Top-level 

670 #@+node:ekr.20031218072017.1553: *5* fc.getLeoFile (read switch) 

671 def getLeoFile(self, 

672 theFile, 

673 fileName, 

674 readAtFileNodesFlag=True, 

675 silent=False, 

676 checkOpenFiles=True, 

677 ): 

678 """ 

679 Read a .leo file. 

680 The caller should follow this with a call to c.redraw(). 

681 """ 

682 fc, c = self, self.c 

683 t1 = time.time() 

684 c.clearChanged() # May be set when reading @file nodes. 

685 fc.warnOnReadOnlyFiles(fileName) 

686 fc.checking = False 

687 fc.mFileName = c.mFileName 

688 fc.initReadIvars() 

689 recoveryNode = None 

690 try: 

691 c.loading = True # disable c.changed 

692 if not silent and checkOpenFiles: 

693 # Don't check for open file when reverting. 

694 g.app.checkForOpenFile(c, fileName) 

695 # Read the .leo file and create the outline. 

696 if fileName.endswith('.db'): 

697 v = fc.retrieveVnodesFromDb(theFile) or fc.initNewDb(theFile) 

698 elif fileName.endswith('.leojs'): 

699 v = fc.read_leojs(theFile, fileName) 

700 readAtFileNodesFlag = False # Suppress post-processing. 

701 else: 

702 v = FastRead(c, self.gnxDict).readFile(theFile, fileName) 

703 if v: 

704 c.hiddenRootNode = v 

705 if v: 

706 c.setFileTimeStamp(fileName) 

707 if readAtFileNodesFlag: 

708 recoveryNode = fc.readExternalFiles() 

709 finally: 

710 # lastTopLevel is a better fallback, imo. 

711 p = recoveryNode or c.p or c.lastTopLevel() 

712 c.selectPosition(p) 

713 # Delay the second redraw until idle time. 

714 # This causes a slight flash, but corrects a hangnail. 

715 c.redraw_later() 

716 c.checkOutline() # Must be called *after* ni.end_holding. 

717 c.loading = False # reenable c.changed 

718 if not isinstance(theFile, sqlite3.Connection): 

719 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1208942 

720 # Leo holding directory/file handles after file close? 

721 theFile.close() 

722 if c.changed: 

723 fc.propagateDirtyNodes() 

724 fc.initReadIvars() 

725 t2 = time.time() 

726 g.es(f"read outline in {t2 - t1:2.2f} seconds") 

727 return v, c.frame.ratio 

728 #@+node:ekr.20031218072017.2297: *5* fc.openLeoFile 

729 def openLeoFile(self, theFile, fileName, readAtFileNodesFlag=True, silent=False): 

730 """ 

731 Open a Leo file. 

732 

733 readAtFileNodesFlag: False when reading settings files. 

734 silent: True when creating hidden commanders. 

735 """ 

736 c, frame = self.c, self.c.frame 

737 # Set c.openDirectory 

738 theDir = g.os_path_dirname(fileName) 

739 if theDir: 

740 c.openDirectory = c.frame.openDirectory = theDir 

741 # Get the file. 

742 self.gnxDict = {} # #1437 

743 ok, ratio = self.getLeoFile( 

744 theFile, fileName, 

745 readAtFileNodesFlag=readAtFileNodesFlag, 

746 silent=silent, 

747 ) 

748 if ok: 

749 frame.resizePanesToRatio(ratio, frame.secondary_ratio) 

750 return ok 

751 #@+node:ekr.20120212220616.10537: *5* fc.readExternalFiles & helper 

752 def readExternalFiles(self): 

753 """ 

754 Read all external files in the outline. 

755 

756 A helper for fc.getLeoFile. 

757 """ 

758 c, fc = self.c, self 

759 c.atFileCommands.readAll(c.rootPosition()) 

760 recoveryNode = fc.handleNodeConflicts() 

761 # 

762 # Do this after reading external files. 

763 # The descendent nodes won't exist unless we have read 

764 # the @thin nodes! 

765 fc.restoreDescendentAttributes() 

766 fc.setPositionsFromVnodes() 

767 return recoveryNode 

768 #@+node:ekr.20100205060712.8314: *6* fc.handleNodeConflicts 

769 def handleNodeConflicts(self): 

770 """Create a 'Recovered Nodes' node for each entry in c.nodeConflictList.""" 

771 c = self.c 

772 if not c.nodeConflictList: 

773 return None 

774 if not c.make_node_conflicts_node: 

775 s = f"suppressed {len(c.nodeConflictList)} node conflicts" 

776 g.es(s, color='red') 

777 g.pr('\n' + s + '\n') 

778 return None 

779 # Create the 'Recovered Nodes' node. 

780 last = c.lastTopLevel() 

781 root = last.insertAfter() 

782 root.setHeadString('Recovered Nodes') 

783 root.expand() 

784 # For each conflict, create one child and two grandchildren. 

785 for bunch in c.nodeConflictList: 

786 tag = bunch.get('tag') or '' 

787 gnx = bunch.get('gnx') or '' 

788 fn = bunch.get('fileName') or '' 

789 b1, h1 = bunch.get('b_old'), bunch.get('h_old') 

790 b2, h2 = bunch.get('b_new'), bunch.get('h_new') 

791 root_v = bunch.get('root_v') or '' 

792 child = root.insertAsLastChild() 

793 h = f'Recovered node "{h1}" from {g.shortFileName(fn)}' 

794 child.setHeadString(h) 

795 if b1 == b2: 

796 lines = [ 

797 'Headline changed...', 

798 f"{tag} gnx: {gnx} root: {(root_v and root.v)!r}", 

799 f"old headline: {h1}", 

800 f"new headline: {h2}", 

801 ] 

802 child.setBodyString('\n'.join(lines)) 

803 else: 

804 line1 = f"{tag} gnx: {gnx} root: {root_v and root.v!r}\nDiff...\n" 

805 # 2017/06/19: reverse comparison order. 

806 d = difflib.Differ().compare(g.splitLines(b1), g.splitLines(b2)) 

807 diffLines = [z for z in d] 

808 lines = [line1] 

809 lines.extend(diffLines) 

810 # There is less need to show trailing newlines because 

811 # we don't report changes involving only trailing newlines. 

812 child.setBodyString(''.join(lines)) 

813 n1 = child.insertAsNthChild(0) 

814 n2 = child.insertAsNthChild(1) 

815 n1.setHeadString('old:' + h1) 

816 n1.setBodyString(b1) 

817 n2.setHeadString('new:' + h2) 

818 n2.setBodyString(b2) 

819 return root 

820 #@+node:ekr.20031218072017.3030: *5* fc.readOutlineOnly 

821 def readOutlineOnly(self, theFile, fileName): 

822 c = self.c 

823 # Set c.openDirectory 

824 theDir = g.os_path_dirname(fileName) 

825 if theDir: 

826 c.openDirectory = c.frame.openDirectory = theDir 

827 ok, ratio = self.getLeoFile(theFile, fileName, readAtFileNodesFlag=False) 

828 c.redraw() 

829 c.frame.deiconify() 

830 junk, junk, secondary_ratio = self.frame.initialRatios() 

831 c.frame.resizePanesToRatio(ratio, secondary_ratio) 

832 return ok 

833 #@+node:vitalije.20170630152841.1: *5* fc.retrieveVnodesFromDb & helpers 

834 def retrieveVnodesFromDb(self, conn): 

835 """ 

836 Recreates tree from the data contained in table vnodes. 

837 

838 This method follows behavior of readSaxFile. 

839 """ 

840 

841 c, fc = self.c, self 

842 sql = '''select gnx, head, 

843 body, 

844 children, 

845 parents, 

846 iconVal, 

847 statusBits, 

848 ua from vnodes''' 

849 vnodes = [] 

850 try: 

851 for row in conn.execute(sql): 

852 (gnx, h, b, children, parents, iconVal, statusBits, ua) = row 

853 try: 

854 ua = pickle.loads(g.toEncodedString(ua)) 

855 except ValueError: 

856 ua = None 

857 v = leoNodes.VNode(context=c, gnx=gnx) 

858 v._headString = h 

859 v._bodyString = b 

860 v.children = children.split() 

861 v.parents = parents.split() 

862 v.iconVal = iconVal 

863 v.statusBits = statusBits 

864 v.u = ua 

865 vnodes.append(v) 

866 except sqlite3.Error as er: 

867 if er.args[0].find('no such table') < 0: 

868 # there was an error raised but it is not the one we expect 

869 g.internalError(er) 

870 # there is no vnodes table 

871 return None 

872 

873 rootChildren = [x for x in vnodes if 'hidden-root-vnode-gnx' in x.parents] 

874 if not rootChildren: 

875 g.trace('there should be at least one top level node!') 

876 return None 

877 

878 findNode = lambda x: fc.gnxDict.get(x, c.hiddenRootNode) 

879 

880 # let us replace every gnx with the corresponding vnode 

881 for v in vnodes: 

882 v.children = [findNode(x) for x in v.children] 

883 v.parents = [findNode(x) for x in v.parents] 

884 c.hiddenRootNode.children = rootChildren 

885 (w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn) 

886 c.frame.setTopGeometry(w, h, x, y) 

887 c.frame.resizePanesToRatio(r1, r2) 

888 p = fc.decodePosition(encp) 

889 c.setCurrentPosition(p) 

890 return rootChildren[0] 

891 #@+node:vitalije.20170815162307.1: *6* fc.initNewDb 

892 def initNewDb(self, conn): 

893 """ Initializes tables and returns None""" 

894 c, fc = self.c, self 

895 v = leoNodes.VNode(context=c) 

896 c.hiddenRootNode.children = [v] 

897 (w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn) 

898 c.frame.setTopGeometry(w, h, x, y) 

899 c.frame.resizePanesToRatio(r1, r2) 

900 c.sqlite_connection = conn 

901 fc.exportToSqlite(c.mFileName) 

902 return v 

903 #@+node:vitalije.20170630200802.1: *6* fc.getWindowGeometryFromDb 

904 def getWindowGeometryFromDb(self, conn): 

905 geom = (600, 400, 50, 50, 0.5, 0.5, '') 

906 keys = ('width', 'height', 'left', 'top', 

907 'ratio', 'secondary_ratio', 

908 'current_position') 

909 try: 

910 d = dict( 

911 conn.execute( 

912 '''select * from extra_infos 

913 where name in (?, ?, ?, ?, ?, ?, ?)''', 

914 keys, 

915 ).fetchall(), 

916 ) 

917 # mypy complained that geom must be a tuple, not a generator. 

918 geom = tuple(d.get(*x) for x in zip(keys, geom)) # type:ignore 

919 except sqlite3.OperationalError: 

920 pass 

921 return geom 

922 #@+node:vitalije.20170831154734.1: *5* fc.setReferenceFile 

923 def setReferenceFile(self, fileName): 

924 c = self.c 

925 for v in c.hiddenRootNode.children: 

926 if v.h == PRIVAREA: 

927 v.b = fileName 

928 break 

929 else: 

930 v = c.rootPosition().insertBefore().v 

931 v.h = PRIVAREA 

932 v.b = fileName 

933 c.redraw() 

934 g.es('set reference file:', g.shortFileName(fileName)) 

935 #@+node:vitalije.20170831144643.1: *5* fc.updateFromRefFile 

936 def updateFromRefFile(self): 

937 """Updates public part of outline from the specified file.""" 

938 c, fc = self.c, self 

939 #@+others 

940 #@+node:vitalije.20170831144827.2: *6* function: get_ref_filename 

941 def get_ref_filename(): 

942 for v in priv_vnodes(): 

943 return g.splitLines(v.b)[0].strip() 

944 #@+node:vitalije.20170831144827.4: *6* function: pub_vnodes 

945 def pub_vnodes(): 

946 for v in c.hiddenRootNode.children: 

947 if v.h == PRIVAREA: 

948 break 

949 yield v 

950 #@+node:vitalije.20170831144827.5: *6* function: priv_vnodes 

951 def priv_vnodes(): 

952 pub = True 

953 for v in c.hiddenRootNode.children: 

954 if v.h == PRIVAREA: 

955 pub = False 

956 if pub: 

957 continue 

958 yield v 

959 #@+node:vitalije.20170831144827.6: *6* function: pub_gnxes 

960 def sub_gnxes(children): 

961 for v in children: 

962 yield v.gnx 

963 for gnx in sub_gnxes(v.children): 

964 yield gnx 

965 

966 def pub_gnxes(): 

967 return sub_gnxes(pub_vnodes()) 

968 

969 def priv_gnxes(): 

970 return sub_gnxes(priv_vnodes()) 

971 #@+node:vitalije.20170831144827.7: *6* function: restore_priv 

972 def restore_priv(prdata, topgnxes): 

973 vnodes = [] 

974 for row in prdata: 

975 (gnx, h, b, children, parents, iconVal, statusBits, ua) = row 

976 v = leoNodes.VNode(context=c, gnx=gnx) 

977 v._headString = h 

978 v._bodyString = b 

979 v.children = children 

980 v.parents = parents 

981 v.iconVal = iconVal 

982 v.statusBits = statusBits 

983 v.u = ua 

984 vnodes.append(v) 

985 pv = lambda x: fc.gnxDict.get(x, c.hiddenRootNode) 

986 for v in vnodes: 

987 v.children = [pv(x) for x in v.children] 

988 v.parents = [pv(x) for x in v.parents] 

989 for gnx in topgnxes: 

990 v = fc.gnxDict[gnx] 

991 c.hiddenRootNode.children.append(v) 

992 if gnx in pubgnxes: 

993 v.parents.append(c.hiddenRootNode) 

994 #@+node:vitalije.20170831144827.8: *6* function: priv_data 

995 def priv_data(gnxes): 

996 dbrow = lambda v: ( 

997 v.gnx, 

998 v.h, 

999 v.b, 

1000 [x.gnx for x in v.children], 

1001 [x.gnx for x in v.parents], 

1002 v.iconVal, 

1003 v.statusBits, 

1004 v.u 

1005 ) 

1006 return tuple(dbrow(fc.gnxDict[x]) for x in gnxes) 

1007 #@+node:vitalije.20170831144827.9: *6* function: nosqlite_commander 

1008 @contextmanager 

1009 def nosqlite_commander(fname): 

1010 oldname = c.mFileName 

1011 conn = getattr(c, 'sqlite_connection', None) 

1012 c.sqlite_connection = None 

1013 c.mFileName = fname 

1014 yield c 

1015 if c.sqlite_connection: 

1016 c.sqlite_connection.close() 

1017 c.mFileName = oldname 

1018 c.sqlite_connection = conn 

1019 #@-others 

1020 pubgnxes = set(pub_gnxes()) 

1021 privgnxes = set(priv_gnxes()) 

1022 privnodes = priv_data(privgnxes - pubgnxes) 

1023 toppriv = [v.gnx for v in priv_vnodes()] 

1024 fname = get_ref_filename() 

1025 with nosqlite_commander(fname): 

1026 theFile = open(fname, 'rb') 

1027 fc.initIvars() 

1028 fc.getLeoFile(theFile, fname, checkOpenFiles=False) 

1029 restore_priv(privnodes, toppriv) 

1030 c.redraw() 

1031 #@+node:ekr.20210316043902.1: *5* fc.read_leojs & helpers 

1032 def read_leojs(self, theFile, fileName): 

1033 """Read a JSON (.leojs) file and create the outline.""" 

1034 c = self.c 

1035 s = theFile.read() 

1036 try: 

1037 d = json.loads(s) 

1038 except Exception: 

1039 g.trace(f"Error reading .leojs file: {fileName}") 

1040 g.es_exception() 

1041 return None 

1042 # 

1043 # Get the top-level dicts. 

1044 tnodes_dict = d.get('tnodes') 

1045 vnodes_list = d.get('vnodes') 

1046 if not tnodes_dict: 

1047 g.trace(f"Bad .leojs file: no tnodes dict: {fileName}") 

1048 return None 

1049 if not vnodes_list: 

1050 g.trace(f"Bad .leojs file: no vnodes list: {fileName}") 

1051 return None 

1052 # 

1053 # Define function: create_vnode_from_dicts. 

1054 #@+others 

1055 #@+node:ekr.20210317155137.1: *6* function: create_vnode_from_dicts 

1056 def create_vnode_from_dicts(i, parent_v, v_dict): 

1057 """Create a new vnode as the i'th child of the parent vnode.""" 

1058 # 

1059 # Get the gnx. 

1060 gnx = v_dict.get('gnx') 

1061 if not gnx: 

1062 g.trace(f"Bad .leojs file: no gnx in v_dict: {fileName}") 

1063 g.printObj(v_dict) 

1064 return 

1065 # 

1066 # Create the vnode. 

1067 assert len(parent_v.children) == i, (i, parent_v, parent_v.children) 

1068 v = leoNodes.VNode(context=c, gnx=gnx) 

1069 parent_v.children.append(v) 

1070 v._headString = v_dict.get('vh', '') 

1071 v._bodyString = tnodes_dict.get(gnx, '') 

1072 # 

1073 # Recursively create the children. 

1074 for i2, v_dict2 in enumerate(v_dict.get('children', [])): 

1075 create_vnode_from_dicts(i2, v, v_dict2) 

1076 #@+node:ekr.20210318125522.1: *6* function: scan_leojs_globals 

1077 def scan_leojs_globals(json_d): 

1078 """Set the geometries from the globals dict.""" 

1079 

1080 def toInt(x, default): 

1081 try: 

1082 return int(x) 

1083 except Exception: 

1084 return default 

1085 

1086 # Priority 1: command-line args 

1087 windowSize = g.app.loadManager.options.get('windowSize') 

1088 windowSpot = g.app.loadManager.options.get('windowSpot') 

1089 # 

1090 # Priority 2: The cache. 

1091 db_top, db_left, db_height, db_width = c.db.get('window_position', (None, None, None, None)) 

1092 # 

1093 # Priority 3: The globals dict in the .leojs file. 

1094 # Leo doesn't write the globals element, but leoInteg might. 

1095 d = json_d.get('globals', {}) 

1096 # 

1097 # height & width 

1098 height, width = windowSize or (None, None) 

1099 if height is None: 

1100 height, width = d.get('height'), d.get('width') 

1101 if height is None: 

1102 height, width = db_height, db_width 

1103 height, width = toInt(height, 500), toInt(width, 800) 

1104 # 

1105 # top, left. 

1106 top, left = windowSpot or (None, None) 

1107 if top is None: 

1108 top, left = d.get('top'), d.get('left') 

1109 if top is None: 

1110 top, left = db_top, db_left 

1111 top, left = toInt(top, 50), toInt(left, 50) 

1112 # 

1113 # r1, r2. 

1114 r1 = float(c.db.get('body_outline_ratio', '0.5')) 

1115 r2 = float(c.db.get('body_secondary_ratio', '0.5')) 

1116 if 'size' in g.app.debug: 

1117 g.trace(width, height, left, top, c.shortFileName()) 

1118 # c.frame may be a NullFrame. 

1119 c.frame.setTopGeometry(width, height, left, top) 

1120 c.frame.resizePanesToRatio(r1, r2) 

1121 frameFactory = getattr(g.app.gui, 'frameFactory', None) 

1122 if not frameFactory: 

1123 return 

1124 assert frameFactory is not None 

1125 mf = frameFactory.masterFrame 

1126 if g.app.start_minimized: 

1127 mf.showMinimized() 

1128 elif g.app.start_maximized: 

1129 # #1189: fast.scanGlobals calls showMaximized later. 

1130 mf.showMaximized() 

1131 elif g.app.start_fullscreen: 

1132 mf.showFullScreen() 

1133 else: 

1134 mf.show() 

1135 #@-others 

1136 # 

1137 # Start the recursion by creating the top-level vnodes. 

1138 c.hiddenRootNode.children = [] # Necessary. 

1139 parent_v = c.hiddenRootNode 

1140 for i, v_dict in enumerate(vnodes_list): 

1141 create_vnode_from_dicts(i, parent_v, v_dict) 

1142 scan_leojs_globals(d) 

1143 return c.hiddenRootNode.children[0] 

1144 #@+node:ekr.20060919133249: *4* fc: Read Utils 

1145 # Methods common to both the sax and non-sax code. 

1146 #@+node:ekr.20061006104837.1: *5* fc.archivedPositionToPosition 

1147 def archivedPositionToPosition(self, s): 

1148 """Convert an archived position (a string) to a position.""" 

1149 return self.c.archivedPositionToPosition(s) 

1150 #@+node:ekr.20040701065235.1: *5* fc.getDescendentAttributes 

1151 def getDescendentAttributes(self, s, tag=""): 

1152 """s is a list of gnx's, separated by commas from a <v> or <t> element. 

1153 Parses s into a list. 

1154 

1155 This is used to record marked and expanded nodes. 

1156 """ 

1157 gnxs = s.split(',') 

1158 result = [gnx for gnx in gnxs if len(gnx) > 0] 

1159 return result 

1160 #@+node:EKR.20040627114602: *5* fc.getDescendentUnknownAttributes 

1161 # Pre Leo 4.5 Only @thin vnodes had the descendentTnodeUnknownAttributes field. 

1162 # New in Leo 4.5: @thin & @shadow vnodes have descendentVnodeUnknownAttributes field. 

1163 

1164 def getDescendentUnknownAttributes(self, s, v=None): 

1165 """Unhexlify and unpickle t/v.descendentUnknownAttribute field.""" 

1166 try: 

1167 # Changed in version 3.2: Accept only bytestring or bytearray objects as input. 

1168 s = g.toEncodedString(s) # 2011/02/22 

1169 # Throws a TypeError if val is not a hex string. 

1170 bin = binascii.unhexlify(s) 

1171 val = pickle.loads(bin) 

1172 return val 

1173 except Exception: 

1174 g.es_exception() 

1175 g.trace('Can not unpickle', type(s), v and v.h, s[:40]) 

1176 return None 

1177 #@+node:vitalije.20180304190953.1: *5* fc.getPos/VnodeFromClipboard 

1178 def getPosFromClipboard(self, s): 

1179 """A utility called from init_tree_abbrev.""" 

1180 v = self.getVnodeFromClipboard(s) 

1181 return leoNodes.Position(v) 

1182 

1183 def getVnodeFromClipboard(self, s): 

1184 """Called only from getPosFromClipboard.""" 

1185 c = self.c 

1186 self.initReadIvars() 

1187 oldGnxDict = self.gnxDict 

1188 self.gnxDict = {} # Fix #943 

1189 try: 

1190 # This encoding must match the encoding used in outline_to_clipboard_string. 

1191 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True) 

1192 v = FastRead(c, {}).readFileFromClipboard(s) 

1193 if not v: 

1194 return g.es("the clipboard is not valid ", color="blue") 

1195 finally: 

1196 self.gnxDict = oldGnxDict 

1197 return v 

1198 #@+node:ekr.20060919142200.1: *5* fc.initReadIvars 

1199 def initReadIvars(self): 

1200 self.descendentTnodeUaDictList = [] 

1201 self.descendentVnodeUaDictList = [] 

1202 self.descendentExpandedList = [] 

1203 # 2011/12/10: never re-init this dict. 

1204 # self.gnxDict = {} 

1205 self.descendentMarksList = [] 

1206 self.c.nodeConflictList = [] # 2010/01/05 

1207 self.c.nodeConflictFileName = None # 2010/01/05 

1208 #@+node:ekr.20100124110832.6212: *5* fc.propagateDirtyNodes 

1209 def propagateDirtyNodes(self): 

1210 c = self.c 

1211 aList = [z for z in c.all_positions() if z.isDirty()] 

1212 for p in aList: 

1213 p.setAllAncestorAtFileNodesDirty() 

1214 #@+node:ekr.20080805132422.3: *5* fc.resolveArchivedPosition 

1215 def resolveArchivedPosition(self, archivedPosition, root_v): 

1216 """ 

1217 Return a VNode corresponding to the archived position relative to root 

1218 node root_v. 

1219 """ 

1220 

1221 def oops(message): 

1222 """Give an error only if no file errors have been seen.""" 

1223 return None 

1224 

1225 try: 

1226 aList = [int(z) for z in archivedPosition.split('.')] 

1227 aList.reverse() 

1228 except Exception: 

1229 return oops(f'"{archivedPosition}"') 

1230 if not aList: 

1231 return oops('empty') 

1232 last_v = root_v 

1233 n = aList.pop() 

1234 if n != 0: 

1235 return oops(f'root index="{n}"') 

1236 while aList: 

1237 n = aList.pop() 

1238 children = last_v.children 

1239 if n < len(children): 

1240 last_v = children[n] 

1241 else: 

1242 return oops(f'bad index="{n}", len(children)="{len(children)}"') 

1243 return last_v 

1244 #@+node:EKR.20040627120120: *5* fc.restoreDescendentAttributes 

1245 def restoreDescendentAttributes(self): 

1246 """Called from fc.readExternalFiles.""" 

1247 c = self.c 

1248 for resultDict in self.descendentTnodeUaDictList: 

1249 for gnx in resultDict: 

1250 v = self.gnxDict.get(gnx) 

1251 if v: 

1252 v.unknownAttributes = resultDict[gnx] 

1253 v._p_changed = True 

1254 # New in Leo 4.5: keys are archivedPositions, values are attributes. 

1255 for root_v, resultDict in self.descendentVnodeUaDictList: 

1256 for key in resultDict: 

1257 v = self.resolveArchivedPosition(key, root_v) 

1258 if v: 

1259 v.unknownAttributes = resultDict[key] 

1260 v._p_changed = True 

1261 expanded, marks = {}, {} 

1262 for gnx in self.descendentExpandedList: 

1263 v = self.gnxDict.get(gnx) 

1264 if v: 

1265 expanded[v] = v 

1266 for gnx in self.descendentMarksList: 

1267 v = self.gnxDict.get(gnx) 

1268 if v: 

1269 marks[v] = v 

1270 if marks or expanded: 

1271 for p in c.all_unique_positions(): 

1272 if marks.get(p.v): 

1273 # This was the problem: was p.setMark. 

1274 # There was a big performance bug in the mark hook in the Node Navigator plugin. 

1275 p.v.initMarkedBit() 

1276 if expanded.get(p.v): 

1277 p.expand() 

1278 #@+node:ekr.20060919110638.13: *5* fc.setPositionsFromVnodes 

1279 def setPositionsFromVnodes(self): 

1280 

1281 c, root = self.c, self.c.rootPosition() 

1282 if c.sqlite_connection: 

1283 # position is already selected 

1284 return 

1285 current, str_pos = None, None 

1286 if c.mFileName: 

1287 str_pos = c.db.get('current_position') 

1288 if str_pos is None: 

1289 d = root.v.u 

1290 if d: 

1291 str_pos = d.get('str_leo_pos') 

1292 if str_pos is not None: 

1293 current = self.archivedPositionToPosition(str_pos) 

1294 c.setCurrentPosition(current or c.rootPosition()) 

1295 #@+node:ekr.20031218072017.3032: *3* fc: Writing 

1296 #@+node:ekr.20070413045221.2: *4* fc: Writing save* 

1297 #@+node:ekr.20031218072017.1720: *5* fc.save 

1298 def save(self, fileName, silent=False): 

1299 """fc.save: A helper for c.save.""" 

1300 c = self.c 

1301 p = c.p 

1302 # New in 4.2. Return ok flag so shutdown logic knows if all went well. 

1303 ok = g.doHook("save1", c=c, p=p, fileName=fileName) 

1304 if ok is None: 

1305 c.endEditing() # Set the current headline text. 

1306 self.setDefaultDirectoryForNewFiles(fileName) 

1307 g.app.commander_cacher.save(c, fileName) 

1308 ok = c.checkFileTimeStamp(fileName) 

1309 if ok: 

1310 if c.sqlite_connection: 

1311 c.sqlite_connection.close() 

1312 c.sqlite_connection = None 

1313 ok = self.write_Leo_file(fileName) 

1314 if ok: 

1315 if not silent: 

1316 self.putSavedMessage(fileName) 

1317 c.clearChanged() # Clears all dirty bits. 

1318 if c.config.save_clears_undo_buffer: 

1319 g.es("clearing undo") 

1320 c.undoer.clearUndoState() 

1321 c.redraw_after_icons_changed() 

1322 g.doHook("save2", c=c, p=p, fileName=fileName) 

1323 return ok 

1324 #@+node:vitalije.20170831135146.1: *5* fc.save_ref & helpers 

1325 def save_ref(self): 

1326 """Saves reference outline file""" 

1327 c = self.c 

1328 p = c.p 

1329 fc = self 

1330 #@+others 

1331 #@+node:vitalije.20170831135535.1: *6* function: put_v_elements 

1332 def put_v_elements(): 

1333 """ 

1334 Puts all <v> elements in the order in which they appear in the outline. 

1335 

1336 This is not the same as fc.put_v_elements! 

1337 """ 

1338 c.clearAllVisited() 

1339 fc.put("<vnodes>\n") 

1340 # Make only one copy for all calls. 

1341 fc.currentPosition = c.p 

1342 fc.rootPosition = c.rootPosition() 

1343 fc.vnodesDict = {} 

1344 ref_fname = None 

1345 for p in c.rootPosition().self_and_siblings(copy=False): 

1346 if p.h == PRIVAREA: 

1347 ref_fname = p.b.split('\n', 1)[0].strip() 

1348 break 

1349 # An optimization: Write the next top-level node. 

1350 fc.put_v_element(p, isIgnore=p.isAtIgnoreNode()) 

1351 fc.put("</vnodes>\n") 

1352 return ref_fname 

1353 #@+node:vitalije.20170831135447.1: *6* function: getPublicLeoFile 

1354 def getPublicLeoFile(): 

1355 fc.outputFile = io.StringIO() 

1356 fc.putProlog() 

1357 fc.putHeader() 

1358 fc.putGlobals() 

1359 fc.putPrefs() 

1360 fc.putFindSettings() 

1361 fname = put_v_elements() 

1362 put_t_elements() 

1363 fc.putPostlog() 

1364 return fname, fc.outputFile.getvalue() 

1365 

1366 #@+node:vitalije.20211218225014.1: *6* function: put_t_elements 

1367 def put_t_elements(): 

1368 """ 

1369 Write all <t> elements except those for vnodes appearing in @file, @edit or @auto nodes. 

1370 """ 

1371 

1372 def should_suppress(p): 

1373 return any(z.isAtFileNode() or z.isAtEditNode() or z.isAtAutoNode() 

1374 for z in p.self_and_parents()) 

1375 

1376 fc.put("<tnodes>\n") 

1377 suppress = {} 

1378 for p in c.all_positions(copy=False): 

1379 if should_suppress(p): 

1380 suppress[p.v] = True 

1381 

1382 toBeWritten = {} 

1383 for root in c.rootPosition().self_and_siblings(): 

1384 if root.h == PRIVAREA: 

1385 break 

1386 for p in root.self_and_subtree(): 

1387 if p.v not in suppress and p.v not in toBeWritten: 

1388 toBeWritten[p.v.fileIndex] = p.v 

1389 for gnx in sorted(toBeWritten): 

1390 v = toBeWritten[gnx] 

1391 fc.put_t_element(v) 

1392 fc.put("</tnodes>\n") 

1393 #@-others 

1394 c.endEditing() 

1395 for v in c.hiddenRootNode.children: 

1396 if v.h == PRIVAREA: 

1397 fileName = g.splitLines(v.b)[0].strip() 

1398 break 

1399 else: 

1400 fileName = c.mFileName 

1401 # New in 4.2. Return ok flag so shutdown logic knows if all went well. 

1402 ok = g.doHook("save1", c=c, p=p, fileName=fileName) 

1403 if ok is None: 

1404 fileName, content = getPublicLeoFile() 

1405 fileName = g.os_path_finalize_join(c.openDirectory, fileName) 

1406 with open(fileName, 'w', encoding="utf-8", newline='\n') as out: 

1407 out.write(content) 

1408 g.es('updated reference file:', 

1409 g.shortFileName(fileName)) 

1410 g.doHook("save2", c=c, p=p, fileName=fileName) 

1411 return ok 

1412 #@+node:ekr.20031218072017.3043: *5* fc.saveAs 

1413 def saveAs(self, fileName): 

1414 """fc.saveAs: A helper for c.saveAs.""" 

1415 c = self.c 

1416 p = c.p 

1417 if not g.doHook("save1", c=c, p=p, fileName=fileName): 

1418 c.endEditing() # Set the current headline text. 

1419 if c.sqlite_connection: 

1420 c.sqlite_connection.close() 

1421 c.sqlite_connection = None 

1422 self.setDefaultDirectoryForNewFiles(fileName) 

1423 g.app.commander_cacher.save(c, fileName) 

1424 # Disable path-changed messages in writeAllHelper. 

1425 c.ignoreChangedPaths = True 

1426 try: 

1427 if self.write_Leo_file(fileName): 

1428 c.clearChanged() # Clears all dirty bits. 

1429 self.putSavedMessage(fileName) 

1430 finally: 

1431 c.ignoreChangedPaths = False # #1367. 

1432 c.redraw_after_icons_changed() 

1433 g.doHook("save2", c=c, p=p, fileName=fileName) 

1434 #@+node:ekr.20031218072017.3044: *5* fc.saveTo 

1435 def saveTo(self, fileName, silent=False): 

1436 """fc.saveTo: A helper for c.saveTo.""" 

1437 c = self.c 

1438 p = c.p 

1439 if not g.doHook("save1", c=c, p=p, fileName=fileName): 

1440 c.endEditing() # Set the current headline text. 

1441 if c.sqlite_connection: 

1442 c.sqlite_connection.close() 

1443 c.sqlite_connection = None 

1444 self.setDefaultDirectoryForNewFiles(fileName) 

1445 g.app.commander_cacher.commit() # Commit, but don't save file name. 

1446 # Disable path-changed messages in writeAllHelper. 

1447 c.ignoreChangedPaths = True 

1448 try: 

1449 self.write_Leo_file(fileName) 

1450 finally: 

1451 c.ignoreChangedPaths = False 

1452 if not silent: 

1453 self.putSavedMessage(fileName) 

1454 c.redraw_after_icons_changed() 

1455 g.doHook("save2", c=c, p=p, fileName=fileName) 

1456 #@+node:ekr.20210316034237.1: *4* fc: Writing top-level 

1457 #@+node:vitalije.20170630172118.1: *5* fc.exportToSqlite & helpers 

1458 def exportToSqlite(self, fileName): 

1459 """Dump all vnodes to sqlite database. Returns True on success.""" 

1460 c, fc = self.c, self 

1461 if c.sqlite_connection is None: 

1462 c.sqlite_connection = sqlite3.connect(fileName, isolation_level='DEFERRED') 

1463 conn = c.sqlite_connection 

1464 

1465 def dump_u(v) -> bytes: 

1466 try: 

1467 s = pickle.dumps(v.u, protocol=1) 

1468 except pickle.PicklingError: 

1469 s = b'' # 2021/06/25: fixed via mypy complaint. 

1470 g.trace('unpickleable value', repr(v.u)) 

1471 return s 

1472 

1473 dbrow = lambda v: ( 

1474 v.gnx, 

1475 v.h, 

1476 v.b, 

1477 ' '.join(x.gnx for x in v.children), 

1478 ' '.join(x.gnx for x in v.parents), 

1479 v.iconVal, 

1480 v.statusBits, 

1481 dump_u(v) 

1482 ) 

1483 ok = False 

1484 try: 

1485 fc.prepareDbTables(conn) 

1486 fc.exportDbVersion(conn) 

1487 fc.exportVnodesToSqlite(conn, (dbrow(v) for v in c.all_unique_nodes())) 

1488 fc.exportGeomToSqlite(conn) 

1489 fc.exportHashesToSqlite(conn) 

1490 conn.commit() 

1491 ok = True 

1492 except sqlite3.Error as e: 

1493 g.internalError(e) 

1494 return ok 

1495 #@+node:vitalije.20170705075107.1: *6* fc.decodePosition 

1496 def decodePosition(self, s): 

1497 """Creates position from its string representation encoded by fc.encodePosition.""" 

1498 fc = self 

1499 if not s: 

1500 return fc.c.rootPosition() 

1501 sep = '<->' 

1502 comma = ',' 

1503 stack = [x.split(comma) for x in s.split(sep)] 

1504 stack = [(fc.gnxDict[x], int(y)) for x, y in stack] 

1505 v, ci = stack[-1] 

1506 p = leoNodes.Position(v, ci, stack[:-1]) 

1507 return p 

1508 #@+node:vitalije.20170705075117.1: *6* fc.encodePosition 

1509 def encodePosition(self, p): 

1510 """New schema for encoding current position hopefully simplier one.""" 

1511 jn = '<->' 

1512 mk = '%s,%s' 

1513 res = [mk % (x.gnx, y) for x, y in p.stack] 

1514 res.append(mk % (p.gnx, p._childIndex)) 

1515 return jn.join(res) 

1516 #@+node:vitalije.20170811130512.1: *6* fc.prepareDbTables 

1517 def prepareDbTables(self, conn): 

1518 conn.execute('''drop table if exists vnodes;''') 

1519 conn.execute( 

1520 ''' 

1521 create table if not exists vnodes( 

1522 gnx primary key, 

1523 head, 

1524 body, 

1525 children, 

1526 parents, 

1527 iconVal, 

1528 statusBits, 

1529 ua);''', 

1530 ) 

1531 conn.execute( 

1532 '''create table if not exists extra_infos(name primary key, value)''') 

1533 #@+node:vitalije.20170701161851.1: *6* fc.exportVnodesToSqlite 

1534 def exportVnodesToSqlite(self, conn, rows): 

1535 conn.executemany( 

1536 '''insert into vnodes 

1537 (gnx, head, body, children, parents, 

1538 iconVal, statusBits, ua) 

1539 values(?,?,?,?,?,?,?,?);''', 

1540 rows, 

1541 ) 

1542 #@+node:vitalije.20170701162052.1: *6* fc.exportGeomToSqlite 

1543 def exportGeomToSqlite(self, conn): 

1544 c = self.c 

1545 data = zip( 

1546 ( 

1547 'width', 'height', 'left', 'top', 

1548 'ratio', 'secondary_ratio', 

1549 'current_position' 

1550 ), 

1551 c.frame.get_window_info() + 

1552 ( 

1553 c.frame.ratio, c.frame.secondary_ratio, 

1554 self.encodePosition(c.p) 

1555 ) 

1556 ) 

1557 conn.executemany('replace into extra_infos(name, value) values(?, ?)', data) 

1558 #@+node:vitalije.20170811130559.1: *6* fc.exportDbVersion 

1559 def exportDbVersion(self, conn): 

1560 conn.execute( 

1561 "replace into extra_infos(name, value) values('dbversion', ?)", ('1.0',)) 

1562 #@+node:vitalije.20170701162204.1: *6* fc.exportHashesToSqlite 

1563 def exportHashesToSqlite(self, conn): 

1564 c = self.c 

1565 

1566 def md5(x): 

1567 try: 

1568 s = open(x, 'rb').read() 

1569 except Exception: 

1570 return '' 

1571 s = s.replace(b'\r\n', b'\n') 

1572 return hashlib.md5(s).hexdigest() 

1573 

1574 files = set() 

1575 

1576 p = c.rootPosition() 

1577 while p: 

1578 if p.isAtIgnoreNode(): 

1579 p.moveToNodeAfterTree() 

1580 elif p.isAtAutoNode() or p.isAtFileNode(): 

1581 fn = c.getNodeFileName(p) 

1582 files.add((fn, 'md5_' + p.gnx)) 

1583 p.moveToNodeAfterTree() 

1584 else: 

1585 p.moveToThreadNext() 

1586 conn.executemany( 

1587 'replace into extra_infos(name, value) values(?,?)', 

1588 map(lambda x: (x[1], md5(x[0])), files)) 

1589 #@+node:ekr.20031218072017.1573: *5* fc.outline_to_clipboard_string 

1590 def outline_to_clipboard_string(self, p=None): 

1591 """ 

1592 Return a string suitable for pasting to the clipboard. 

1593 """ 

1594 # Save 

1595 tua = self.descendentTnodeUaDictList 

1596 vua = self.descendentVnodeUaDictList 

1597 gnxDict = self.gnxDict 

1598 vnodesDict = self.vnodesDict 

1599 try: 

1600 self.outputFile = io.StringIO() 

1601 self.usingClipboard = True 

1602 self.putProlog() 

1603 self.putHeader() 

1604 self.put_v_elements(p or self.c.p) 

1605 self.put_t_elements() 

1606 self.putPostlog() 

1607 s = self.outputFile.getvalue() 

1608 self.outputFile = None 

1609 finally: # Restore 

1610 self.descendentTnodeUaDictList = tua 

1611 self.descendentVnodeUaDictList = vua 

1612 self.gnxDict = gnxDict 

1613 self.vnodesDict = vnodesDict 

1614 self.usingClipboard = False 

1615 return s 

1616 #@+node:ekr.20040324080819.1: *5* fc.outline_to_xml_string 

1617 def outline_to_xml_string(self): 

1618 """Write the outline in .leo (XML) format to a string.""" 

1619 self.outputFile = io.StringIO() 

1620 self.putProlog() 

1621 self.putHeader() 

1622 self.putGlobals() 

1623 self.putPrefs() 

1624 self.putFindSettings() 

1625 self.put_v_elements() 

1626 self.put_t_elements() 

1627 self.putPostlog() 

1628 s = self.outputFile.getvalue() 

1629 self.outputFile = None 

1630 return s 

1631 #@+node:ekr.20031218072017.3046: *5* fc.write_Leo_file 

1632 def write_Leo_file(self, fileName): 

1633 """ 

1634 Write all external files and the .leo file itself.""" 

1635 c, fc = self.c, self 

1636 if c.checkOutline(): 

1637 g.error('Structural errors in outline! outline not written') 

1638 return False 

1639 g.app.recentFilesManager.writeRecentFilesFile(c) 

1640 fc.writeAllAtFileNodes() # Ignore any errors. 

1641 return fc.writeOutline(fileName) 

1642 

1643 write_LEO_file = write_Leo_file # For compatibility with old plugins. 

1644 #@+node:ekr.20210316050301.1: *5* fc.write_leojs & helpers 

1645 def write_leojs(self, fileName): 

1646 """Write the outline in .leojs (JSON) format.""" 

1647 c = self.c 

1648 ok, backupName = self.createBackupFile(fileName) 

1649 if not ok: 

1650 return False 

1651 f = self.openOutlineForWriting(fileName) 

1652 if not f: 

1653 return False 

1654 try: 

1655 # Create the dict corresponding to the JSON. 

1656 d = self.leojs_file() 

1657 # Convert the dict to JSON. 

1658 json_s = json.dumps(d, indent=2) 

1659 s = bytes(json_s, self.leo_file_encoding, 'replace') 

1660 f.write(s) 

1661 f.close() 

1662 g.app.commander_cacher.save(c, fileName) 

1663 c.setFileTimeStamp(fileName) 

1664 # Delete backup file. 

1665 if backupName and g.os_path_exists(backupName): 

1666 self.deleteBackupFile(backupName) 

1667 self.mFileName = fileName 

1668 return True 

1669 except Exception: 

1670 self.handleWriteLeoFileException(fileName, backupName, f) 

1671 return False 

1672 #@+node:ekr.20210316095706.1: *6* fc.leojs_file 

1673 def leojs_file(self): 

1674 """Return a dict representing the outline.""" 

1675 c = self.c 

1676 return { 

1677 'leoHeader': {'fileFormat': 2}, 

1678 'globals': self.leojs_globals(), 

1679 'tnodes': {v.gnx: v._bodyString for v in c.all_unique_nodes()}, 

1680 'vnodes': [ 

1681 self.leojs_vnode(p.v) for p in c.rootPosition().self_and_siblings() 

1682 ], 

1683 } 

1684 #@+node:ekr.20210316092313.1: *6* fc.leojs_globals (sets window_position) 

1685 def leojs_globals(self): 

1686 """Put json representation of Leo's cached globals.""" 

1687 c = self.c 

1688 width, height, left, top = c.frame.get_window_info() 

1689 if 1: # Write to the cache, not the file. 

1690 d: Dict[str, str] = {} 

1691 c.db['body_outline_ratio'] = str(c.frame.ratio) 

1692 c.db['body_secondary_ratio'] = str(c.frame.secondary_ratio) 

1693 c.db['window_position'] = str(top), str(left), str(height), str(width) 

1694 if 'size' in g.app.debug: 

1695 g.trace('set window_position:', c.db['window_position'], c.shortFileName()) 

1696 else: 

1697 d = { 

1698 'body_outline_ratio': c.frame.ratio, 

1699 'body_secondary_ratio': c.frame.secondary_ratio, 

1700 'globalWindowPosition': { 

1701 'top': top, 

1702 'left': left, 

1703 'width': width, 

1704 'height': height, 

1705 }, 

1706 } 

1707 return d 

1708 #@+node:ekr.20210316085413.2: *6* fc.leojs_vnodes 

1709 def leojs_vnode(self, v): 

1710 """Return a jsonized vnode.""" 

1711 return { 

1712 'gnx': v.fileIndex, 

1713 'vh': v._headString, 

1714 'status': v.statusBits, 

1715 'children': [self.leojs_vnode(child) for child in v.children] 

1716 } 

1717 #@+node:ekr.20100119145629.6111: *5* fc.write_xml_file 

1718 def write_xml_file(self, fileName): 

1719 """Write the outline in .leo (XML) format.""" 

1720 c = self.c 

1721 ok, backupName = self.createBackupFile(fileName) 

1722 if not ok: 

1723 return False 

1724 f = self.openOutlineForWriting(fileName) 

1725 if not f: 

1726 return False 

1727 self.mFileName = fileName 

1728 try: 

1729 s = self.outline_to_xml_string() 

1730 s = bytes(s, self.leo_file_encoding, 'replace') 

1731 f.write(s) 

1732 f.close() 

1733 c.setFileTimeStamp(fileName) 

1734 # Delete backup file. 

1735 if backupName and g.os_path_exists(backupName): 

1736 self.deleteBackupFile(backupName) 

1737 return True 

1738 except Exception: 

1739 self.handleWriteLeoFileException(fileName, backupName, f) 

1740 return False 

1741 #@+node:ekr.20100119145629.6114: *5* fc.writeAllAtFileNodes 

1742 def writeAllAtFileNodes(self): 

1743 """Write all @<file> nodes and set orphan bits.""" 

1744 c = self.c 

1745 try: 

1746 # To allow Leo to quit properly, do *not* signal failure here. 

1747 c.atFileCommands.writeAll(all=False) 

1748 return True 

1749 except Exception: 

1750 # #1260415: https://bugs.launchpad.net/leo-editor/+bug/1260415 

1751 g.es_error("exception writing external files") 

1752 g.es_exception() 

1753 g.es('Internal error writing one or more external files.', color='red') 

1754 g.es('Please report this error to:', color='blue') 

1755 g.es('https://groups.google.com/forum/#!forum/leo-editor', color='blue') 

1756 g.es('All changes will be lost unless you', color='red') 

1757 g.es('can save each changed file.', color='red') 

1758 return False 

1759 #@+node:ekr.20210316041806.1: *5* fc.writeOutline (write switch) 

1760 def writeOutline(self, fileName): 

1761 

1762 c = self.c 

1763 if c.checkOutline(): 

1764 g.error('Structure errors in outline! outline not written') 

1765 return False 

1766 if self.isReadOnly(fileName): 

1767 return False 

1768 if fileName.endswith('.db'): 

1769 return self.exportToSqlite(fileName) 

1770 if fileName.endswith('.leojs'): 

1771 return self.write_leojs(fileName) 

1772 return self.write_xml_file(fileName) 

1773 #@+node:ekr.20070412095520: *5* fc.writeZipFile 

1774 def writeZipFile(self, s): 

1775 """Write string s as a .zip file.""" 

1776 # The name of the file in the archive. 

1777 contentsName = g.toEncodedString( 

1778 g.shortFileName(self.mFileName), 

1779 self.leo_file_encoding, reportErrors=True) 

1780 # The name of the archive itself. 

1781 fileName = g.toEncodedString( 

1782 self.mFileName, 

1783 self.leo_file_encoding, reportErrors=True) 

1784 # Write the archive. 

1785 # These mypy complaints look valid. 

1786 theFile = zipfile.ZipFile(fileName, 'w', zipfile.ZIP_DEFLATED) # type:ignore 

1787 theFile.writestr(contentsName, s) # type:ignore 

1788 theFile.close() 

1789 #@+node:ekr.20210316034532.1: *4* fc.Writing Utils 

1790 #@+node:ekr.20080805085257.2: *5* fc.pickle 

1791 def pickle(self, torv, val, tag): 

1792 """Pickle val and return the hexlified result.""" 

1793 try: 

1794 s = pickle.dumps(val, protocol=1) 

1795 s2 = binascii.hexlify(s) 

1796 s3 = g.toUnicode(s2, 'utf-8') 

1797 field = f' {tag}="{s3}"' 

1798 return field 

1799 except pickle.PicklingError: 

1800 if tag: # The caller will print the error if tag is None. 

1801 g.warning("ignoring non-pickleable value", val, "in", torv) 

1802 return '' 

1803 except Exception: 

1804 g.error("fc.pickle: unexpected exception in", torv) 

1805 g.es_exception() 

1806 return '' 

1807 #@+node:ekr.20031218072017.1470: *5* fc.put 

1808 def put(self, s): 

1809 """Put string s to self.outputFile. All output eventually comes here.""" 

1810 if s: 

1811 self.outputFile.write(s) 

1812 #@+node:ekr.20080805071954.2: *5* fc.putDescendentVnodeUas & helper 

1813 def putDescendentVnodeUas(self, p): 

1814 """ 

1815 Return the a uA field for descendent VNode attributes, 

1816 suitable for reconstituting uA's for anonymous vnodes. 

1817 """ 

1818 # 

1819 # Create aList of tuples (p,v) having a valid unknownAttributes dict. 

1820 # Create dictionary: keys are vnodes, values are corresonding archived positions. 

1821 aList = [] 

1822 pDict = {} 

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

1824 if hasattr(p2.v, "unknownAttributes"): 

1825 aList.append((p2.copy(), p2.v),) 

1826 pDict[p2.v] = p2.archivedPosition(root_p=p) 

1827 # Create aList of pairs (v,d) where d contains only pickleable entries. 

1828 if aList: 

1829 aList = self.createUaList(aList) 

1830 if not aList: 

1831 return '' 

1832 # Create d, an enclosing dict to hold all the inner dicts. 

1833 d = {} 

1834 for v, d2 in aList: 

1835 aList2 = [str(z) for z in pDict.get(v)] 

1836 key = '.'.join(aList2) 

1837 d[key] = d2 

1838 # Pickle and hexlify d 

1839 # pylint: disable=consider-using-ternary 

1840 return d and self.pickle( 

1841 torv=p.v, val=d, tag='descendentVnodeUnknownAttributes') or '' 

1842 #@+node:ekr.20080805085257.1: *6* fc.createUaList 

1843 def createUaList(self, aList): 

1844 """ 

1845 Given aList of pairs (p,torv), return a list of pairs (torv,d) 

1846 where d contains all picklable items of torv.unknownAttributes. 

1847 """ 

1848 result = [] 

1849 for p, torv in aList: 

1850 if isinstance(torv.unknownAttributes, dict): 

1851 # Create a new dict containing only entries that can be pickled. 

1852 d = dict(torv.unknownAttributes) # Copy the dict. 

1853 for key in d: 

1854 # Just see if val can be pickled. Suppress any error. 

1855 ok = self.pickle(torv=torv, val=d.get(key), tag=None) 

1856 if not ok: 

1857 del d[key] 

1858 g.warning("ignoring bad unknownAttributes key", key, "in", p.h) 

1859 if d: 

1860 result.append((torv, d),) 

1861 else: 

1862 g.warning("ignoring non-dictionary uA for", p) 

1863 return result 

1864 #@+node:ekr.20031218072017.3035: *5* fc.putFindSettings 

1865 def putFindSettings(self): 

1866 # New in 4.3: These settings never get written to the .leo file. 

1867 self.put("<find_panel_settings/>\n") 

1868 #@+node:ekr.20031218072017.3037: *5* fc.putGlobals (sets window_position) 

1869 def putGlobals(self): 

1870 """Put a vestigial <globals> element, and write global data to the cache.""" 

1871 trace = 'cache' in g.app.debug 

1872 c = self.c 

1873 self.put("<globals/>\n") 

1874 if not c.mFileName: 

1875 return 

1876 c.db['body_outline_ratio'] = str(c.frame.ratio) 

1877 c.db['body_secondary_ratio'] = str(c.frame.secondary_ratio) 

1878 w, h, l, t = c.frame.get_window_info() 

1879 c.db['window_position'] = str(t), str(l), str(h), str(w) 

1880 if trace: 

1881 g.trace(f"\nset c.db for {c.shortFileName()}") 

1882 print('window_position:', c.db['window_position']) 

1883 #@+node:ekr.20031218072017.3041: *5* fc.putHeader 

1884 def putHeader(self): 

1885 self.put('<leo_header file_format="2"/>\n') 

1886 #@+node:ekr.20031218072017.3042: *5* fc.putPostlog 

1887 def putPostlog(self): 

1888 self.put("</leo_file>\n") 

1889 #@+node:ekr.20031218072017.2066: *5* fc.putPrefs 

1890 def putPrefs(self): 

1891 # New in 4.3: These settings never get written to the .leo file. 

1892 self.put("<preferences/>\n") 

1893 #@+node:ekr.20031218072017.1246: *5* fc.putProlog 

1894 def putProlog(self): 

1895 """Put the prolog of the xml file.""" 

1896 tag = 'http://leoeditor.com/namespaces/leo-python-editor/1.1' 

1897 self.putXMLLine() 

1898 # Put "created by Leo" line. 

1899 self.put('<!-- Created by Leo: http://leoeditor.com/leo_toc.html -->\n') 

1900 self.putStyleSheetLine() 

1901 # Put the namespace 

1902 self.put(f'<leo_file xmlns:leo="{tag}" >\n') 

1903 #@+node:ekr.20070413061552: *5* fc.putSavedMessage 

1904 def putSavedMessage(self, fileName): 

1905 c = self.c 

1906 # #531: Optionally report timestamp... 

1907 if c.config.getBool('log-show-save-time', default=False): 

1908 format = c.config.getString('log-timestamp-format') or "%H:%M:%S" 

1909 timestamp = time.strftime(format) + ' ' 

1910 else: 

1911 timestamp = '' 

1912 g.es(f"{timestamp}saved: {g.shortFileName(fileName)}") 

1913 #@+node:ekr.20031218072017.1248: *5* fc.putStyleSheetLine 

1914 def putStyleSheetLine(self): 

1915 """ 

1916 Put the xml stylesheet line. 

1917 

1918 Leo 5.3: 

1919 - Use only the stylesheet setting, ignoreing c.frame.stylesheet. 

1920 - Write no stylesheet element if there is no setting. 

1921 

1922 The old way made it almost impossible to delete stylesheet element. 

1923 """ 

1924 c = self.c 

1925 sheet = (c.config.getString('stylesheet') or '').strip() 

1926 # sheet2 = c.frame.stylesheet and c.frame.stylesheet.strip() or '' 

1927 # sheet = sheet or sheet2 

1928 if sheet: 

1929 self.put(f"<?xml-stylesheet {sheet} ?>\n") 

1930 

1931 #@+node:ekr.20031218072017.1577: *5* fc.put_t_element 

1932 def put_t_element(self, v): 

1933 b, gnx = v.b, v.fileIndex 

1934 ua = self.putUnknownAttributes(v) 

1935 body = xml.sax.saxutils.escape(b) if b else '' 

1936 self.put(f'<t tx="{gnx}"{ua}>{body}</t>\n') 

1937 #@+node:ekr.20031218072017.1575: *5* fc.put_t_elements 

1938 def put_t_elements(self): 

1939 """Put all <t> elements as required for copy or save commands""" 

1940 self.put("<tnodes>\n") 

1941 self.putReferencedTElements() 

1942 self.put("</tnodes>\n") 

1943 #@+node:ekr.20031218072017.1576: *6* fc.putReferencedTElements 

1944 def putReferencedTElements(self): 

1945 """Put <t> elements for all referenced vnodes.""" 

1946 c = self.c 

1947 if self.usingClipboard: # write the current tree. 

1948 theIter = self.currentPosition.self_and_subtree(copy=False) 

1949 else: # write everything 

1950 theIter = c.all_unique_positions(copy=False) 

1951 # Populate the vnodes dict. 

1952 vnodes = {} 

1953 for p in theIter: 

1954 # Make *sure* the file index has the proper form. 

1955 # pylint: disable=unbalanced-tuple-unpacking 

1956 index = p.v.fileIndex 

1957 vnodes[index] = p.v 

1958 # Put all vnodes in index order. 

1959 for index in sorted(vnodes): 

1960 v = vnodes.get(index) 

1961 if v: 

1962 # Write <t> elements only for vnodes that will be written. 

1963 # For example, vnodes in external files will be written 

1964 # only if the vnodes are cloned outside the file. 

1965 if v.isWriteBit(): 

1966 self.put_t_element(v) 

1967 else: 

1968 g.trace('can not happen: no VNode for', repr(index)) 

1969 # This prevents the file from being written. 

1970 raise BadLeoFile(f"no VNode for {repr(index)}") 

1971 #@+node:ekr.20050418161620.2: *5* fc.putUaHelper 

1972 def putUaHelper(self, torv, key, val): 

1973 """Put attribute whose name is key and value is val to the output stream.""" 

1974 # New in 4.3: leave string attributes starting with 'str_' alone. 

1975 if key.startswith('str_'): 

1976 if isinstance(val, (str, bytes)): 

1977 val = g.toUnicode(val) 

1978 attr = f' {key}="{xml.sax.saxutils.escape(val)}"' 

1979 return attr 

1980 g.trace(type(val), repr(val)) 

1981 g.warning("ignoring non-string attribute", key, "in", torv) 

1982 return '' 

1983 return self.pickle(torv=torv, val=val, tag=key) 

1984 #@+node:EKR.20040526202501: *5* fc.putUnknownAttributes 

1985 def putUnknownAttributes(self, v): 

1986 """Put pickleable values for all keys in v.unknownAttributes dictionary.""" 

1987 if not hasattr(v, 'unknownAttributes'): 

1988 return '' 

1989 attrDict = v.unknownAttributes 

1990 if isinstance(attrDict, dict): 

1991 val = ''.join( 

1992 [self.putUaHelper(v, key, val) 

1993 for key, val in attrDict.items()]) 

1994 return val 

1995 g.warning("ignoring non-dictionary unknownAttributes for", v) 

1996 return '' 

1997 #@+node:ekr.20031218072017.1863: *5* fc.put_v_element & helper 

1998 def put_v_element(self, p, isIgnore=False): 

1999 """Write a <v> element corresponding to a VNode.""" 

2000 fc = self 

2001 v = p.v 

2002 # Precompute constants. 

2003 # Write the entire @edit tree if it has children. 

2004 isAuto = p.isAtAutoNode() and p.atAutoNodeName().strip() 

2005 isEdit = p.isAtEditNode() and p.atEditNodeName().strip() and not p.hasChildren() 

2006 isFile = p.isAtFileNode() 

2007 isShadow = p.isAtShadowFileNode() 

2008 isThin = p.isAtThinFileNode() 

2009 # Set forcewrite. 

2010 if isIgnore or p.isAtIgnoreNode(): 

2011 forceWrite = True 

2012 elif isAuto or isEdit or isFile or isShadow or isThin: 

2013 forceWrite = False 

2014 else: 

2015 forceWrite = True 

2016 # Set the write bit if necessary. 

2017 gnx = v.fileIndex 

2018 if forceWrite or self.usingClipboard: 

2019 v.setWriteBit() # 4.2: Indicate we wrote the body text. 

2020 

2021 attrs = fc.compute_attribute_bits(forceWrite, p) 

2022 # Write the node. 

2023 v_head = f'<v t="{gnx}"{attrs}>' 

2024 if gnx in fc.vnodesDict: 

2025 fc.put(v_head + '</v>\n') 

2026 else: 

2027 fc.vnodesDict[gnx] = True 

2028 v_head += f"<vh>{xml.sax.saxutils.escape(p.v.headString() or '')}</vh>" 

2029 # New in 4.2: don't write child nodes of @file-thin trees 

2030 # (except when writing to clipboard) 

2031 if p.hasChildren() and (forceWrite or self.usingClipboard): 

2032 fc.put(f"{v_head}\n") 

2033 # This optimization eliminates all "recursive" copies. 

2034 p.moveToFirstChild() 

2035 while 1: 

2036 fc.put_v_element(p, isIgnore) 

2037 if p.hasNext(): 

2038 p.moveToNext() 

2039 else: 

2040 break 

2041 p.moveToParent() # Restore p in the caller. 

2042 fc.put('</v>\n') 

2043 else: 

2044 fc.put(f"{v_head}</v>\n") # Call put only once. 

2045 #@+node:ekr.20031218072017.1865: *6* fc.compute_attribute_bits 

2046 def compute_attribute_bits(self, forceWrite, p): 

2047 """Return the initial values of v's attributes.""" 

2048 attrs = [] 

2049 if p.hasChildren() and not forceWrite and not self.usingClipboard: 

2050 # Fix #526: do this for @auto nodes as well. 

2051 attrs.append(self.putDescendentVnodeUas(p)) 

2052 return ''.join(attrs) 

2053 #@+node:ekr.20031218072017.1579: *5* fc.put_v_elements & helper 

2054 def put_v_elements(self, p=None): 

2055 """Puts all <v> elements in the order in which they appear in the outline.""" 

2056 c = self.c 

2057 c.clearAllVisited() 

2058 self.put("<vnodes>\n") 

2059 # Make only one copy for all calls. 

2060 self.currentPosition = p or c.p 

2061 self.rootPosition = c.rootPosition() 

2062 self.vnodesDict = {} 

2063 if self.usingClipboard: 

2064 self.expanded_gnxs, self.marked_gnxs = set(), set() # These will be ignored. 

2065 self.put_v_element(self.currentPosition) # Write only current tree. 

2066 else: 

2067 for p in c.rootPosition().self_and_siblings(): 

2068 self.put_v_element(p, isIgnore=p.isAtIgnoreNode()) 

2069 # #1018: scan *all* nodes. 

2070 self.setCachedBits() 

2071 self.put("</vnodes>\n") 

2072 #@+node:ekr.20190328160622.1: *6* fc.setCachedBits 

2073 def setCachedBits(self): 

2074 """ 

2075 Set the cached expanded and marked bits for *all* nodes. 

2076 Also cache the current position. 

2077 """ 

2078 trace = 'cache' in g.app.debug 

2079 c = self.c 

2080 if not c.mFileName: 

2081 return # New. 

2082 current = [str(z) for z in self.currentPosition.archivedPosition()] 

2083 expanded = [v.gnx for v in c.all_unique_nodes() if v.isExpanded()] 

2084 marked = [v.gnx for v in c.all_unique_nodes() if v.isMarked()] 

2085 c.db['expanded'] = ','.join(expanded) 

2086 c.db['marked'] = ','.join(marked) 

2087 c.db['current_position'] = ','.join(current) 

2088 if trace: 

2089 g.trace(f"\nset c.db for {c.shortFileName()}") 

2090 print('expanded:', expanded) 

2091 print('marked:', marked) 

2092 print('current_position:', current) 

2093 print('') 

2094 #@+node:ekr.20031218072017.1247: *5* fc.putXMLLine 

2095 def putXMLLine(self): 

2096 """Put the **properly encoded** <?xml> element.""" 

2097 # Use self.leo_file_encoding encoding. 

2098 self.put( 

2099 f"{g.app.prolog_prefix_string}" 

2100 f'"{self.leo_file_encoding}"' 

2101 f"{g.app.prolog_postfix_string}\n") 

2102 #@-others 

2103#@-others 

2104#@@language python 

2105#@@tabwidth -4 

2106#@@pagewidth 70 

2107#@-leo