Coverage for C:\Repos\leo-editor\leo\core\leoChapters.py: 58%
327 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#@+leo-ver=5-thin
2#@+node:ekr.20070317085508.1: * @file leoChapters.py
3"""Classes that manage chapters in Leo's core."""
4import re
5import string
6from leo.core import leoGlobals as g
7#@+others
8#@+node:ekr.20150509030349.1: ** cc.cmd (decorator)
9def cmd(name):
10 """Command decorator for the ChapterController class."""
11 return g.new_cmd_decorator(name, ['c', 'chapterController',])
12#@+node:ekr.20070317085437: ** class ChapterController
13class ChapterController:
14 """A per-commander controller that manages chapters and related nodes."""
15 #@+others
16 #@+node:ekr.20070530075604: *3* Birth
17 #@+node:ekr.20070317085437.2: *4* cc.ctor
18 def __init__(self, c):
19 """Ctor for ChapterController class."""
20 self.c = c
21 # Note: chapter names never change, even if their @chapter node changes.
22 self.chaptersDict = {} # Keys are chapter names, values are chapters.
23 self.initing = True # #31: True: suppress undo when creating chapters.
24 self.re_chapter = None # Set where used.
25 self.selectedChapter = None
26 self.selectChapterLockout = False # True: cc.selectChapterForPosition does nothing.
27 self.tt = None # May be set in finishCreate.
28 self.reloadSettings()
30 def reloadSettings(self):
31 c = self.c
32 self.use_tabs = c.config.getBool('use-chapter-tabs')
33 #@+node:ekr.20160402024827.1: *4* cc.createIcon
34 def createIcon(self):
35 """Create chapter-selection Qt ListBox in the icon area."""
36 cc = self
37 c = cc.c
38 if cc.use_tabs:
39 if hasattr(c.frame.iconBar, 'createChaptersIcon'):
40 if not cc.tt:
41 cc.tt = c.frame.iconBar.createChaptersIcon()
42 #@+node:ekr.20070325104904: *4* cc.finishCreate
43 # This must be called late in the init process, after the first redraw.
45 def finishCreate(self):
46 """Create the box in the icon area."""
47 c, cc = self.c, self
48 cc.createIcon()
49 cc.setAllChapterNames() # Create all chapters.
50 # #31.
51 cc.initing = False
52 # Always select the main chapter.
53 # It can be alarming to open a small chapter in a large .leo file.
54 cc.selectChapterByName('main')
55 c.redraw()
56 #@+node:ekr.20160411145155.1: *4* cc.makeCommand
57 def makeCommand(self, chapterName, binding=None):
58 """Make chapter-select-<chapterName> command."""
59 c, cc = self.c, self
60 commandName = f"chapter-select-{chapterName}"
61 #
62 # For tracing:
63 # inverseBindingsDict = c.k.computeInverseBindingDict()
64 if commandName in c.commandsDict:
65 return
67 def select_chapter_callback(event, cc=cc, name=chapterName):
68 chapter = cc.chaptersDict.get(name)
69 if chapter:
70 try:
71 cc.selectChapterLockout = True
72 cc.selectChapterByNameHelper(chapter, collapse=True)
73 c.redraw(chapter.p) # 2016/04/20.
74 finally:
75 cc.selectChapterLockout = False
76 elif not g.unitTesting:
77 # Possible, but not likely.
78 cc.note(f"no such chapter: {name}")
80 # Always bind the command without a shortcut.
81 # This will create the command bound to any existing settings.
83 bindings = (None, binding) if binding else (None,)
84 for shortcut in bindings:
85 c.k.registerCommand(commandName, select_chapter_callback, shortcut=shortcut)
86 #@+node:ekr.20070604165126: *3* cc.selectChapter
87 @cmd('chapter-select')
88 def selectChapter(self, event=None):
89 """Use the minibuffer to get a chapter name, then create the chapter."""
90 cc, k = self, self.c.k
91 names = cc.setAllChapterNames()
92 g.es('Chapters:\n' + '\n'.join(names))
93 k.setLabelBlue('Select chapter: ')
94 k.get1Arg(event, handler=self.selectChapter1, tabList=names)
96 def selectChapter1(self, event):
97 cc, k = self, self.c.k
98 k.clearState()
99 k.resetLabel()
100 if k.arg:
101 cc.selectChapterByName(k.arg)
102 #@+node:ekr.20170202061705.1: *3* cc.selectNext/Back
103 @cmd('chapter-back')
104 def backChapter(self, event=None):
105 cc = self
106 names = sorted(cc.setAllChapterNames())
107 sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main'
108 i = names.index(sel_name)
109 new_name = names[i - 1 if i > 0 else len(names) - 1]
110 cc.selectChapterByName(new_name)
112 @cmd('chapter-next')
113 def nextChapter(self, event=None):
114 cc = self
115 names = sorted(cc.setAllChapterNames())
116 sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main'
117 i = names.index(sel_name)
118 new_name = names[i + 1 if i + 1 < len(names) else 0]
119 cc.selectChapterByName(new_name)
120 #@+node:ekr.20070317130250: *3* cc.selectChapterByName & helper
121 def selectChapterByName(self, name):
122 """Select a chapter without redrawing."""
123 cc = self
124 if self.selectChapterLockout:
125 return
126 if isinstance(name, int):
127 cc.note('PyQt5 chapters not supported')
128 return
129 chapter = cc.getChapter(name)
130 if not chapter:
131 if not g.unitTesting:
132 g.es_print(f"no such @chapter node: {name}")
133 return
134 try:
135 cc.selectChapterLockout = True
136 cc.selectChapterByNameHelper(chapter)
137 finally:
138 cc.selectChapterLockout = False
139 #@+node:ekr.20090306060344.2: *4* cc.selectChapterByNameHelper
140 def selectChapterByNameHelper(self, chapter, collapse=True):
141 """Select the chapter."""
142 cc, c = self, self.c
143 if not cc.selectedChapter and chapter.name == 'main':
144 chapter.p = c.p
145 return
146 if chapter == cc.selectedChapter:
147 chapter.p = c.p
148 return
149 if cc.selectedChapter:
150 cc.selectedChapter.unselect()
151 else:
152 main_chapter = cc.getChapter('main')
153 if main_chapter:
154 main_chapter.unselect()
155 if chapter.p and c.positionExists(chapter.p):
156 pass
157 elif chapter.name == 'main':
158 pass # Do not use c.p.
159 else:
160 chapter.p = chapter.findRootNode()
161 chapter.select()
162 c.contractAllHeadlines()
163 chapter.p.v.expand()
164 c.selectPosition(chapter.p)
165 #@+node:ekr.20070317130648: *3* cc.Utils
166 #@+node:ekr.20070320085610: *4* cc.error/note/warning
167 def error(self, s):
168 g.error(f"Error: {s}")
170 def note(self, s, killUnitTest=False):
171 if g.unitTesting:
172 if 0: # To trace cause of failed unit test.
173 g.trace('=====', s, g.callers())
174 if killUnitTest:
175 assert False, s
176 else:
177 g.note(f"Note: {s}")
179 def warning(self, s):
180 g.es_print(f"Warning: {s}")
181 #@+node:ekr.20160402025448.1: *4* cc.findAnyChapterNode
182 def findAnyChapterNode(self):
183 """Return True if the outline contains any @chapter node."""
184 cc = self
185 for p in cc.c.all_unique_positions():
186 if p.h.startswith('@chapter '):
187 return True
188 return False
189 #@+node:ekr.20071028091719: *4* cc.findChapterNameForPosition
190 def findChapterNameForPosition(self, p):
191 """Return the name of a chapter containing p or None if p does not exist."""
192 cc, c = self, self.c
193 if not p or not c.positionExists(p):
194 return None
195 for name in cc.chaptersDict:
196 if name != 'main':
197 theChapter = cc.chaptersDict.get(name)
198 if theChapter.positionIsInChapter(p):
199 return name
200 return 'main'
201 #@+node:ekr.20070325093617: *4* cc.findChapterNode
202 def findChapterNode(self, name):
203 """
204 Return the position of the first @chapter node with the given name
205 anywhere in the entire outline.
207 All @chapter nodes are created as children of the @chapters node,
208 but users may move them anywhere.
209 """
210 cc = self
211 name = g.checkUnicode(name)
212 for p in cc.c.all_positions():
213 chapterName, binding = self.parseHeadline(p)
214 if chapterName == name:
215 return p
216 return None # Not an error.
217 #@+node:ekr.20070318124004: *4* cc.getChapter
218 def getChapter(self, name):
219 cc = self
220 return cc.chaptersDict.get(name)
221 #@+node:ekr.20070318122708: *4* cc.getSelectedChapter
222 def getSelectedChapter(self):
223 cc = self
224 return cc.selectedChapter
225 #@+node:ekr.20070605124356: *4* cc.inChapter
226 def inChapter(self):
227 cc = self
228 theChapter = cc.getSelectedChapter()
229 return theChapter and theChapter.name != 'main'
230 #@+node:ekr.20160411152842.1: *4* cc.parseHeadline
231 def parseHeadline(self, p):
232 """Return the chapter name and key binding for p.h."""
233 if not self.re_chapter:
234 self.re_chapter = re.compile(
235 r'^@chapter\s+([^@]+)\s*(@key\s*=\s*(.+)\s*)?')
236 # @chapter (all up to @) (@key=(binding))?
237 # name=group(1), binding=group(3)
238 m = self.re_chapter.search(p.h)
239 if m:
240 chapterName, binding = m.group(1), m.group(3)
241 if chapterName:
242 chapterName = self.sanitize(chapterName)
243 if binding:
244 binding = binding.strip()
245 else:
246 chapterName = binding = None
247 return chapterName, binding
248 #@+node:ekr.20160414183716.1: *4* cc.sanitize
249 def sanitize(self, s):
250 """Convert s to a safe chapter name."""
251 # Similar to g.sanitize_filename, but simpler.
252 result = []
253 for ch in s.strip():
254 # pylint: disable=superfluous-parens
255 if ch in (string.ascii_letters + string.digits):
256 result.append(ch)
257 elif ch in ' \t':
258 result.append('-')
259 s = ''.join(result)
260 s = s.replace('--', '-')
261 return s[:128]
262 #@+node:ekr.20070615075643: *4* cc.selectChapterForPosition
263 def selectChapterForPosition(self, p, chapter=None):
264 """
265 Select a chapter containing position p.
266 New in Leo 4.11: prefer the given chapter if possible.
267 Do nothing if p if p does not exist or is in the presently selected chapter.
269 Note: this code calls c.redraw() if the chapter changes.
270 """
271 c, cc = self.c, self
272 # New in Leo 4.11
273 if cc.selectChapterLockout:
274 return
275 selChapter = cc.getSelectedChapter()
276 if not chapter and not selChapter:
277 return
278 if not p:
279 return
280 if not c.positionExists(p):
281 return
282 # New in Leo 4.11: prefer the given chapter if possible.
283 theChapter = chapter or selChapter
284 if not theChapter:
285 return
286 # First, try the presently selected chapter.
287 firstName = theChapter.name
288 if firstName == 'main':
289 return
290 if theChapter.positionIsInChapter(p):
291 cc.selectChapterByName(theChapter.name)
292 return
293 for name in cc.chaptersDict:
294 if name not in (firstName, 'main'):
295 theChapter = cc.chaptersDict.get(name)
296 if theChapter.positionIsInChapter(p):
297 cc.selectChapterByName(name)
298 break
299 else:
300 cc.selectChapterByName('main')
301 # Fix bug 869385: Chapters make the nav_qt.py plugin useless
302 assert not self.selectChapterLockout
303 # New in Leo 5.6: don't call c.redraw immediately.
304 c.redraw_later()
305 #@+node:ekr.20130915052002.11289: *4* cc.setAllChapterNames
306 def setAllChapterNames(self):
307 """Called early and often to discover all chapter names."""
308 c, cc = self.c, self
309 # sel_name = cc.selectedChapter and cc.selectedChapter.name or 'main'
310 if 'main' not in cc.chaptersDict:
311 cc.chaptersDict['main'] = Chapter(c, cc, 'main')
312 # This binds any existing bindings to chapter-select-main.
313 cc.makeCommand('main')
314 result, seen = ['main'], set()
315 for p in c.all_unique_positions():
316 chapterName, binding = self.parseHeadline(p)
317 if chapterName and p.v not in seen:
318 seen.add(p.v)
319 result.append(chapterName)
320 if chapterName not in cc.chaptersDict:
321 cc.chaptersDict[chapterName] = Chapter(c, cc, chapterName)
322 cc.makeCommand(chapterName, binding)
323 return result
324 #@-others
325#@+node:ekr.20070317085708: ** class Chapter
326class Chapter:
327 """A class representing the non-gui data of a single chapter."""
328 #@+others
329 #@+node:ekr.20070317085708.1: *3* chapter.__init__
330 def __init__(self, c, chapterController, name):
331 self.c = c
332 self.cc = cc = chapterController
333 self.name = g.checkUnicode(name)
334 self.selectLockout = False # True: in chapter.select logic.
335 # State variables: saved/restored when the chapter is unselected/selected.
336 self.p = c.p
337 self.root = self.findRootNode()
338 if cc.tt:
339 cc.tt.createTab(name)
340 #@+node:ekr.20070317085708.2: *3* chapter.__str__ and __repr__
341 def __str__(self):
342 """Chapter.__str__"""
343 return f"<chapter: {self.name}, p: {repr(self.p and self.p.h)}>"
345 __repr__ = __str__
346 #@+node:ekr.20110607182447.16464: *3* chapter.findRootNode
347 def findRootNode(self):
348 """Return the @chapter node for this chapter."""
349 if self.name == 'main':
350 return None
351 return self.cc.findChapterNode(self.name)
352 #@+node:ekr.20070317131205.1: *3* chapter.select & helpers
353 def select(self, w=None):
354 """Restore chapter information and redraw the tree when a chapter is selected."""
355 if self.selectLockout:
356 return
357 try:
358 tt = self.cc.tt
359 self.selectLockout = True
360 self.chapterSelectHelper(w)
361 if tt:
362 # A bad kludge: update all the chapter names *after* the selection.
363 tt.setTabLabel(self.name)
364 finally:
365 self.selectLockout = False
366 #@+node:ekr.20070423102603.1: *4* chapter.chapterSelectHelper
367 def chapterSelectHelper(self, w=None):
369 c, cc = self.c, self.cc
370 cc.selectedChapter = self
371 if self.name == 'main':
372 return # 2016/04/20
373 # Remember the root (it may have changed) for dehoist.
374 self.root = root = self.findRootNode()
375 if not root:
376 # Might happen during unit testing or startup.
377 return
378 if self.p and not c.positionExists(self.p):
379 self.p = p = root.copy()
380 # Next, recompute p and possibly select a new editor.
381 if w:
382 assert w == c.frame.body.wrapper
383 assert w.leo_p
384 self.p = p = self.findPositionInChapter(w.leo_p) or root.copy()
385 else:
386 # This must be done *after* switching roots.
387 self.p = p = self.findPositionInChapter(self.p) or root.copy()
388 # Careful: c.selectPosition would pop the hoist stack.
389 w = self.findEditorInChapter(p)
390 c.frame.body.selectEditor(w) # Switches text.
391 self.p = p # 2016/04/20: Apparently essential.
392 if g.match_word(p.h, 0, '@chapter'):
393 if p.hasChildren():
394 self.p = p = p.firstChild()
395 else:
396 # 2016/04/20: Create a dummy first child.
397 self.p = p = p.insertAsLastChild()
398 p.h = 'New Headline'
399 c.hoistStack.append(g.Bunch(p=root.copy(), expanded=True))
400 # Careful: c.selectPosition would pop the hoist stack.
401 c.setCurrentPosition(p)
402 g.doHook('hoist-changed', c=c)
403 #@+node:ekr.20070317131708: *4* chapter.findPositionInChapter
404 def findPositionInChapter(self, p1, strict=False):
405 """Return a valid position p such that p.v == v."""
406 c, name = self.c, self.name
407 # Bug fix: 2012/05/24: Search without root arg in the main chapter.
408 if name == 'main' and c.positionExists(p1):
409 return p1
410 if not p1:
411 return None
412 root = self.findRootNode()
413 if not root:
414 return None
415 if c.positionExists(p1, root=root.copy()):
416 return p1
417 if strict:
418 return None
419 if name == 'main':
420 theIter = c.all_unique_positions
421 else:
422 theIter = root.self_and_subtree
423 for p in theIter(copy=False):
424 if p.v == p1.v:
425 return p.copy()
426 return None
427 #@+node:ekr.20070425175522: *4* chapter.findEditorInChapter
428 def findEditorInChapter(self, p):
429 """return w, an editor displaying position p."""
430 chapter, c = self, self.c
431 w = c.frame.body.findEditorForChapter(chapter, p)
432 if w:
433 w.leo_chapter = chapter
434 w.leo_p = p and p.copy()
435 return w
436 #@+node:ekr.20070615065222: *4* chapter.positionIsInChapter
437 def positionIsInChapter(self, p):
438 p2 = self.findPositionInChapter(p, strict=True)
439 return p2
440 #@+node:ekr.20070320091806.1: *3* chapter.unselect
441 def unselect(self):
442 """Remember chapter info when a chapter is about to be unselected."""
443 c = self.c
444 # Always try to return to the same position.
445 self.p = c.p
446 if self.name == 'main':
447 return
448 root = None
449 while c.hoistStack:
450 bunch = c.hoistStack.pop()
451 root = bunch.p
452 if root == self.root:
453 break
454 # Re-institute the previous hoist.
455 if c.hoistStack:
456 p = c.hoistStack[-1].p
457 # Careful: c.selectPosition would pop the hoist stack.
458 c.setCurrentPosition(p)
459 else:
460 p = root or c.p
461 c.setCurrentPosition(p)
462 #@-others
463#@-others
464#@@language python
465#@@tabwidth -4
466#@@pagewidth 70
467#@-leo