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

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() 

29 

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. 

44 

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 

66 

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}") 

79 

80 # Always bind the command without a shortcut. 

81 # This will create the command bound to any existing settings. 

82 

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) 

95 

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) 

111 

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}") 

169 

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}") 

178 

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. 

206 

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. 

268 

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)}>" 

344 

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): 

368 

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