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
« 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):
62 def __init__(self, message):
63 self.message = message
64 super().__init__(message)
66 def __str__(self):
67 return "Bad Leo File:" + self.message
68#@+node:ekr.20180602062323.1: ** class FastRead
69class FastRead:
71 nativeVnodeAttributes = (
72 'a',
73 'descendentTnodeUnknownAttributes',
74 'descendentVnodeUnknownAttributes',
75 'expanded', 'marks', 't',
76 # 'tnodeList', # Removed in Leo 4.7.
77 )
79 def __init__(self, c, gnx2vnode):
80 self.c = c
81 self.gnx2vnode = gnx2vnode
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
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.
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'}
122 def readWithElementTree(self, path, s):
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):
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):
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)
366 #@-<< define v_element_visitor >>
367 #
368 # Create the hidden root vnode.
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)
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
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.
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.
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.
838 This method follows behavior of readSaxFile.
839 """
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
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
878 findNode = lambda x: fc.gnxDict.get(x, c.hiddenRootNode)
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
966 def pub_gnxes():
967 return sub_gnxes(pub_vnodes())
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."""
1080 def toInt(x, default):
1081 try:
1082 return int(x)
1083 except Exception:
1084 return default
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.
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.
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)
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 """
1221 def oops(message):
1222 """Give an error only if no file errors have been seen."""
1223 return None
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):
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.
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()
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 """
1372 def should_suppress(p):
1373 return any(z.isAtFileNode() or z.isAtEditNode() or z.isAtAutoNode()
1374 for z in p.self_and_parents())
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
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
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
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
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()
1574 files = set()
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)
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):
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.
1918 Leo 5.3:
1919 - Use only the stylesheet setting, ignoreing c.frame.stylesheet.
1920 - Write no stylesheet element if there is no setting.
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")
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.
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