Coverage for C:\Repos\leo-editor\leo\core\leoCommands.py: 46%

2718 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20031218072017.2810: * @file leoCommands.py 

4#@@first 

5#@+<< imports >> 

6#@+node:ekr.20040712045933: ** << imports >> (leoCommands) 

7import itertools 

8import os 

9import re 

10import subprocess 

11import sys 

12import tabnanny 

13import tempfile 

14import time 

15import tokenize 

16from typing import Any, Dict, Callable, List, Optional, Set, Tuple 

17from leo.core import leoGlobals as g 

18# The leoCommands ctor now does most leo.core.leo* imports, 

19# thereby breaking circular dependencies. 

20from leo.core import leoNodes 

21#@-<< imports >> 

22Widget = Any 

23 

24def cmd(name) -> Callable: 

25 """Command decorator for the Commands class.""" 

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

27 

28#@+others 

29#@+node:ekr.20160514120615.1: ** class Commands 

30class Commands: 

31 """ 

32 A per-outline class that implements most of Leo's commands. The 

33 "c" predefined object is an instance of this class. 

34 

35 c.initObjects() creates sucommanders corresponding to files in the 

36 leo/core and leo/commands. All of Leo's core code is accessible 

37 via this class and its subcommanders. 

38 

39 g.app.pluginsController is Leo's plugins controller. Many plugins 

40 inject controllers objects into the Commands class. These are 

41 another kind of subcommander. 

42 

43 The @g..commander_command decorator injects methods into this class. 

44 """ 

45 #@+others 

46 #@+node:ekr.20031218072017.2811: *3* c.Birth & death 

47 #@+node:ekr.20031218072017.2812: *4* c.__init__ & helpers 

48 def __init__(self, fileName, 

49 gui=None, 

50 parentFrame=None, 

51 previousSettings=None, 

52 relativeFileName=None, 

53 ): 

54 t1 = time.process_time() 

55 c = self 

56 # Official ivars. 

57 self._currentPosition: Optional["leoNodes.Position"] = None 

58 self._topPosition: Optional["leoNodes.Position"] = None 

59 self.frame = None 

60 self.parentFrame = parentFrame # New in Leo 6.0. 

61 self.gui = gui or g.app.gui 

62 self.ipythonController = None # Set only by the ipython plugin. 

63 # The order of these calls does not matter. 

64 c.initCommandIvars() 

65 c.initDebugIvars() 

66 c.initDocumentIvars() 

67 c.initEventIvars() 

68 c.initFileIvars(fileName, relativeFileName) 

69 c.initOptionsIvars() 

70 c.initObjectIvars() 

71 # Init the settings *before* initing the objects. 

72 c.initSettings(previousSettings) 

73 # Initialize all subsidiary objects, including subcommanders. 

74 c.initObjects(self.gui) 

75 assert c.frame 

76 assert c.frame.c 

77 # Complete the init! 

78 t2 = time.process_time() 

79 c.finishCreate() # Slightly slow. 

80 t3 = time.process_time() 

81 if 'speed' in g.app.debug: 

82 print('c.__init__') 

83 print( 

84 f" 1: {t2-t1:5.2f}\n" # 0.00 sec. 

85 f" 2: {t3-t2:5.2f}\n" # 0.53 sec: c.finishCreate. 

86 f"total: {t3-t1:5.2f}" 

87 ) 

88 #@+node:ekr.20120217070122.10475: *5* c.computeWindowTitle 

89 def computeWindowTitle(self, fileName): 

90 """Set the window title and fileName.""" 

91 if fileName: 

92 title = g.computeWindowTitle(fileName) 

93 else: 

94 s = "untitled" 

95 n = g.app.numberOfUntitledWindows 

96 if n > 0: 

97 s += str(n) 

98 title = g.computeWindowTitle(s) 

99 g.app.numberOfUntitledWindows = n + 1 

100 return title 

101 #@+node:ekr.20120217070122.10473: *5* c.initCommandIvars 

102 def initCommandIvars(self): 

103 """Init ivars used while executing a command.""" 

104 self.commandsDict: dict[str, Callable] = {} # Keys are command names, values are functions. 

105 self.disableCommandsMessage = '' # The presence of this message disables all commands. 

106 self.hookFunction: Optional[Callable] = None # One of three places that g.doHook looks for hook functions. 

107 self.ignoreChangedPaths = False # True: disable path changed message in at.WriteAllHelper. 

108 self.inCommand = False # Interlocks to prevent premature closing of a window. 

109 self.outlineToNowebDefaultFileName: str = "noweb.nw" # For Outline To Noweb dialog. 

110 # For hoist/dehoist commands. 

111 # Affects drawing routines and find commands, but *not* generators. 

112 self.hoistStack: List[Any] = [] # Stack of g.Bunches to be root of drawn tree. 

113 # For outline navigation. 

114 self.navPrefix: str = '' # Must always be a string. 

115 self.navTime: Optional[float] = None 

116 self.recent_commands_list: List[str] = [] # List of command names. 

117 self.sqlite_connection = None 

118 #@+node:ekr.20120217070122.10466: *5* c.initDebugIvars 

119 def initDebugIvars(self): 

120 """Init Commander debugging ivars.""" 

121 self.command_count = 0 

122 self.scanAtPathDirectivesCount = 0 

123 self.trace_focus_count = 0 

124 #@+node:ekr.20120217070122.10471: *5* c.initDocumentIvars 

125 def initDocumentIvars(self): 

126 """Init per-document ivars.""" 

127 self.expansionLevel = 0 # The expansion level of this outline. 

128 self.expansionNode = None # The last node we expanded or contracted. 

129 self.nodeConflictList = [] # List of nodes with conflicting read-time data. 

130 self.nodeConflictFileName: Optional[str] = None # The fileName for c.nodeConflictList. 

131 self.user_dict = {} # Non-persistent dictionary for free use by scripts and plugins. 

132 #@+node:ekr.20120217070122.10467: *5* c.initEventIvars 

133 def initEventIvars(self): 

134 """Init ivars relating to gui events.""" 

135 self.configInited = False 

136 self.doubleClickFlag = False 

137 self.exists = True # Indicate that this class exists and has not been destroyed. 

138 self.in_qt_dialog = False # True: in a qt dialog. 

139 self.loading = False # True: we are loading a file: disables c.setChanged() 

140 self.promptingForClose = False # True: lock out additional closing dialogs. 

141 # 

142 # Flags for c.outerUpdate... 

143 self.enableRedrawFlag = True 

144 self.requestCloseWindow = False 

145 self.requestedFocusWidget = None 

146 self.requestLaterRedraw = False 

147 #@+node:ekr.20120217070122.10472: *5* c.initFileIvars 

148 def initFileIvars(self, fileName, relativeFileName): 

149 """Init file-related ivars of the commander.""" 

150 self.changed = False # True: the outline has changed since the last save. 

151 self.ignored_at_file_nodes: List["leoNodes.Position"] = [] # List of nodes for c.raise_error_dialogs. 

152 self.import_error_nodes: List["leoNodes.Position"] = [] # List of nodes for c.raise_error_dialogs. 

153 self.last_dir = None # The last used directory. 

154 self.mFileName: str = fileName or '' # Do _not_ use os_path_norm: it converts an empty path to '.' (!!) 

155 self.mRelativeFileName = relativeFileName or '' # 

156 self.openDirectory: Optional[str] = None # 

157 self.orphan_at_file_nodes: List["leoNodes.Position"] = [] # List of orphaned nodes for c.raise_error_dialogs. 

158 self.wrappedFileName: Optional[str] = None # The name of the wrapped file, for wrapper commanders. 

159 

160 #@+node:ekr.20120217070122.10469: *5* c.initOptionsIvars 

161 def initOptionsIvars(self): 

162 """Init Commander ivars corresponding to user options.""" 

163 self.fixed = False 

164 self.fixedWindowPosition = [] 

165 self.forceExecuteEntireBody = False 

166 self.focus_border_color = 'white' 

167 self.focus_border_width = 1 # pixels 

168 self.outlineHasInitialFocus = False 

169 self.page_width = 132 

170 self.sparse_find = True 

171 self.sparse_move = True 

172 self.sparse_spell = True 

173 self.sparse_goto_visible = False 

174 self.stayInTreeAfterSelect = False 

175 self.tab_width = -4 

176 self.tangle_batch_flag = False 

177 self.target_language = "python" 

178 self.untangle_batch_flag = False 

179 self.vim_mode = False 

180 #@+node:ekr.20120217070122.10468: *5* c.initObjectIvars 

181 def initObjectIvars(self): 

182 # These ivars are set later by leoEditCommands.createEditCommanders 

183 self.abbrevCommands = None 

184 self.editCommands = None 

185 self.db = {} # May be set to a PickleShare instance later. 

186 self.bufferCommands = None 

187 self.chapterCommands = None 

188 self.controlCommands = None 

189 self.convertCommands = None 

190 self.debugCommands = None 

191 self.editFileCommands = None 

192 self.evalController = None 

193 self.gotoCommands = None 

194 self.helpCommands = None 

195 self.keyHandler = self.k = None 

196 self.keyHandlerCommands = None 

197 self.killBufferCommands = None 

198 self.leoCommands = None 

199 self.macroCommands = None 

200 self.miniBufferWidget = None 

201 self.printingController = None 

202 self.queryReplaceCommands = None 

203 self.rectangleCommands = None 

204 self.searchCommands = None 

205 self.spellCommands = None 

206 self.leoTestManager = None 

207 self.vimCommands = None 

208 #@+node:ekr.20120217070122.10470: *5* c.initObjects 

209 #@@nobeautify 

210 

211 def initObjects(self, gui): 

212 

213 c = self 

214 self.hiddenRootNode = leoNodes.VNode(context=c, gnx='hidden-root-vnode-gnx') 

215 self.hiddenRootNode.h = '<hidden root vnode>' 

216 # Create the gui frame. 

217 title = c.computeWindowTitle(c.mFileName) 

218 if not g.app.initing: 

219 g.doHook("before-create-leo-frame", c=c) 

220 self.frame = gui.createLeoFrame(c, title) 

221 assert self.frame 

222 assert self.frame.c == c 

223 from leo.core import leoHistory 

224 self.nodeHistory = leoHistory.NodeHistory(c) 

225 self.initConfigSettings() 

226 c.setWindowPosition() # Do this after initing settings. 

227 

228 # Break circular import dependencies by doing imports here. 

229 # All these imports take almost 3/4 sec in the leoBridge. 

230 

231 from leo.core import leoAtFile 

232 from leo.core import leoBeautify # So decorators are executed. 

233 assert leoBeautify # for pyflakes. 

234 from leo.core import leoChapters 

235 # User commands... 

236 from leo.commands import abbrevCommands 

237 from leo.commands import bufferCommands 

238 from leo.commands import checkerCommands # The import *is* required to define commands. 

239 assert checkerCommands # To suppress a pyflakes warning. 

240 from leo.commands import controlCommands 

241 from leo.commands import convertCommands 

242 from leo.commands import debugCommands 

243 from leo.commands import editCommands 

244 from leo.commands import editFileCommands 

245 from leo.commands import gotoCommands 

246 from leo.commands import helpCommands 

247 from leo.commands import keyCommands 

248 from leo.commands import killBufferCommands 

249 from leo.commands import rectangleCommands 

250 from leo.commands import spellCommands 

251 # Import files to execute @g.commander_command decorators 

252 from leo.core import leoCompare 

253 assert leoCompare 

254 from leo.core import leoDebugger 

255 assert leoDebugger 

256 from leo.commands import commanderEditCommands 

257 assert commanderEditCommands 

258 from leo.commands import commanderFileCommands 

259 assert commanderFileCommands 

260 from leo.commands import commanderHelpCommands 

261 assert commanderHelpCommands 

262 from leo.commands import commanderOutlineCommands 

263 assert commanderOutlineCommands 

264 # Other subcommanders. 

265 from leo.core import leoFind # Leo 4.11.1 

266 from leo.core import leoKeys 

267 from leo.core import leoFileCommands 

268 from leo.core import leoImport 

269 from leo.core import leoMarkup 

270 from leo.core import leoPersistence 

271 from leo.core import leoPrinting 

272 from leo.core import leoRst 

273 from leo.core import leoShadow 

274 from leo.core import leoUndo 

275 from leo.core import leoVim 

276 # Import commands.testCommands to define commands. 

277 import leo.commands.testCommands as testCommands 

278 assert testCommands # For pylint. 

279 # Define the subcommanders. 

280 self.keyHandler = self.k = leoKeys.KeyHandlerClass(c) 

281 self.chapterController = leoChapters.ChapterController(c) 

282 self.shadowController = leoShadow.ShadowController(c) 

283 self.fileCommands = leoFileCommands.FileCommands(c) 

284 self.findCommands = leoFind.LeoFind(c) 

285 self.atFileCommands = leoAtFile.AtFile(c) 

286 self.importCommands = leoImport.LeoImportCommands(c) 

287 self.markupCommands = leoMarkup.MarkupCommands(c) 

288 self.persistenceController = leoPersistence.PersistenceDataController(c) 

289 self.printingController = leoPrinting.PrintingController(c) 

290 self.rstCommands = leoRst.RstCommands(c) 

291 self.vimCommands = leoVim.VimCommands(c) 

292 # User commands 

293 self.abbrevCommands = abbrevCommands.AbbrevCommandsClass(c) 

294 self.bufferCommands = bufferCommands.BufferCommandsClass(c) 

295 self.controlCommands = controlCommands.ControlCommandsClass(c) 

296 self.convertCommands = convertCommands.ConvertCommandsClass(c) 

297 self.debugCommands = debugCommands.DebugCommandsClass(c) 

298 self.editCommands = editCommands.EditCommandsClass(c) 

299 self.editFileCommands = editFileCommands.EditFileCommandsClass(c) 

300 self.gotoCommands = gotoCommands.GoToCommands(c) 

301 self.helpCommands = helpCommands.HelpCommandsClass(c) 

302 self.keyHandlerCommands = keyCommands.KeyHandlerCommandsClass(c) 

303 self.killBufferCommands = killBufferCommands.KillBufferCommandsClass(c) 

304 self.rectangleCommands = rectangleCommands.RectangleCommandsClass(c) 

305 self.spellCommands = spellCommands.SpellCommandsClass(c) 

306 self.undoer = leoUndo.Undoer(c) 

307 # Create the list of subcommanders. 

308 self.subCommanders = [ 

309 self.abbrevCommands, 

310 self.atFileCommands, 

311 self.bufferCommands, 

312 self.chapterController, 

313 self.controlCommands, 

314 self.convertCommands, 

315 self.debugCommands, 

316 self.editCommands, 

317 self.editFileCommands, 

318 self.fileCommands, 

319 self.findCommands, 

320 self.gotoCommands, 

321 self.helpCommands, 

322 self.importCommands, 

323 self.keyHandler, 

324 self.keyHandlerCommands, 

325 self.killBufferCommands, 

326 self.persistenceController, 

327 self.printingController, 

328 self.rectangleCommands, 

329 self.rstCommands, 

330 self.shadowController, 

331 self.spellCommands, 

332 self.vimCommands, 

333 self.undoer, 

334 ] 

335 # Other objects 

336 # A list of other classes that have a reloadSettings method 

337 c.configurables = c.subCommanders[:] 

338 c.db = g.app.commander_cacher.get_wrapper(c) 

339 # #2485: Load the free_layout plugin in the proper context. 

340 # g.app.pluginsController.loadOnePlugin won't work here. 

341 try: 

342 g.app.pluginsController.loadingModuleNameStack.append('leo.plugins.free_layout') 

343 from leo.plugins import free_layout 

344 c.free_layout = free_layout.FreeLayoutController(c) 

345 finally: 

346 g.app.pluginsController.loadingModuleNameStack.pop() 

347 if hasattr(g.app.gui, 'styleSheetManagerClass'): 

348 self.styleSheetManager = g.app.gui.styleSheetManagerClass(c) 

349 self.subCommanders.append(self.styleSheetManager) 

350 else: 

351 self.styleSheetManager = None 

352 #@+node:ekr.20140815160132.18837: *5* c.initSettings 

353 def initSettings(self, previousSettings): 

354 """Init the settings *before* initing the objects.""" 

355 c = self 

356 from leo.core import leoConfig 

357 c.config = leoConfig.LocalConfigManager(c, previousSettings) 

358 g.app.config.setIvarsFromSettings(c) 

359 #@+node:ekr.20031218072017.2814: *4* c.__repr__ & __str__ 

360 def __repr__(self): 

361 return f"Commander {id(self)}: {repr(self.mFileName)}" 

362 

363 __str__ = __repr__ 

364 #@+node:ekr.20050920093543: *4* c.finishCreate & helpers 

365 def finishCreate(self): 

366 """ 

367 Finish creating the commander and all sub-objects. 

368 This is the last step in the startup process. 

369 """ 

370 c, k = self, self.k 

371 assert c.gui 

372 assert k 

373 t1 = time.process_time() 

374 c.frame.finishCreate() # Slightly slow. 

375 t2 = time.process_time() 

376 c.miniBufferWidget = c.frame.miniBufferWidget # Will be None for nullGui. 

377 # Only c.abbrevCommands needs a finishCreate method. 

378 c.abbrevCommands.finishCreate() 

379 # Finish other objects... 

380 c.createCommandNames() 

381 k.finishCreate() 

382 c.findCommands.finishCreate() 

383 if not c.gui.isNullGui: 

384 # #2485: register idle_focus_helper in the proper context. 

385 try: 

386 g.app.pluginsController.loadingModuleNameStack.append('leo.core.leoCommands') 

387 g.registerHandler('idle', c.idle_focus_helper) 

388 finally: 

389 g.app.pluginsController.loadingModuleNameStack.pop() 

390 if getattr(c.frame, 'menu', None): 

391 c.frame.menu.finishCreate() 

392 if getattr(c.frame, 'log', None): 

393 c.frame.log.finishCreate() 

394 c.undoer.clearUndoState() 

395 if c.vimCommands and c.vim_mode: 

396 c.vimCommands.finishCreate() # Menus must exist at this point. 

397 # Do not call chapterController.finishCreate here: 

398 # It must be called after the first real redraw. 

399 g.check_cmd_instance_dict(c, g) 

400 c.bodyWantsFocus() 

401 t3 = time.process_time() 

402 if 'speed' in g.app.debug: 

403 print('c.finishCreate') 

404 print( 

405 f" 1: {t2-t1:5.2f}\n" # 0.20 sec: qtGui.finishCreate. 

406 f" 2: {t3-t2:5.2f}\n" # 0.16 sec: everything else. 

407 f"total: {t3-t1:5.2f}" 

408 ) 

409 #@+node:ekr.20140815160132.18835: *5* c.createCommandNames 

410 def createCommandNames(self): 

411 """ 

412 Create all entries in c.commandsDict. 

413 Do *not* clear c.commandsDict here. 

414 """ 

415 for commandName, func in g.global_commands_dict.items(): 

416 self.k.registerCommand(commandName, func) 

417 #@+node:ekr.20051007143620: *5* c.printCommandsDict 

418 def printCommandsDict(self): 

419 c = self 

420 print('Commands...') 

421 for key in sorted(c.commandsDict): 

422 command = c.commandsDict.get(key) 

423 print(f"{key:30} = {command.__name__ if command else '<None>'}") 

424 print('') 

425 #@+node:ekr.20041130173135: *4* c.hash 

426 # This is a bad idea. 

427 

428 def hash(self) -> str: # Leo 6.6.2: Always return a string. 

429 c = self 

430 if c.mFileName: 

431 return g.os_path_finalize(c.mFileName).lower() # #1341. 

432 return f"{id(self)!s}" 

433 #@+node:ekr.20110509064011.14563: *4* c.idle_focus_helper & helpers 

434 idle_focus_count = 0 

435 

436 def idle_focus_helper(self, tag, keys): 

437 """An idle-tme handler that ensures that focus is *somewhere*.""" 

438 trace = 'focus' in g.app.debug 

439 trace_inactive_focus = False # Too disruptive for --trace-focus 

440 trace_in_dialog = False # Not useful enough for --trace-focus 

441 c = self 

442 assert tag == 'idle' 

443 if g.unitTesting: 

444 return 

445 if keys.get('c') != c: 

446 if trace: 

447 g.trace('no c') 

448 return 

449 self.idle_focus_count += 1 

450 if c.in_qt_dialog: 

451 if trace and trace_in_dialog: 

452 g.trace('in_qt_dialog') 

453 return 

454 w = g.app.gui.get_focus(at_idle=True) 

455 if g.app.gui.active: 

456 # Always call trace_idle_focus. 

457 self.trace_idle_focus(w) 

458 if w and self.is_unusual_focus(w): 

459 if trace: 

460 w_class = w and w.__class__.__name__ 

461 g.trace('***** unusual focus', w_class) 

462 # Fix bug 270: Leo's keyboard events doesn't work after "Insert" 

463 # on headline and Alt+Tab, Alt+Tab 

464 # Presumably, intricate details of Qt event handling are involved. 

465 # The focus was in the tree, so put the focus back in the tree. 

466 c.treeWantsFocusNow() 

467 # elif not w and active: 

468 # c.bodyWantsFocusNow() 

469 elif trace and trace_inactive_focus: 

470 w_class = w and w.__class__.__name__ 

471 count = c.idle_focus_count 

472 g.trace(f"{count} inactive focus: {w_class}") 

473 #@+node:ekr.20160427062131.1: *5* c.is_unusual_focus 

474 def is_unusual_focus(self, w): 

475 """Return True if w is not in an expected place.""" 

476 # 

477 # #270: Leo's keyboard events doesn't work after "Insert" 

478 # on headline and Alt+Tab, Alt+Tab 

479 # 

480 # #276: Focus lost...in Nav text input 

481 from leo.plugins import qt_frame 

482 return isinstance(w, qt_frame.QtTabBarWrapper) 

483 #@+node:ekr.20150403063658.1: *5* c.trace_idle_focus 

484 last_unusual_focus = None 

485 # last_no_focus = False 

486 

487 def trace_idle_focus(self, w): 

488 """Trace the focus for w, minimizing chatter.""" 

489 from leo.core.leoQt import QtWidgets 

490 from leo.plugins import qt_frame 

491 trace = 'focus' in g.app.debug 

492 trace_known = False 

493 c = self 

494 table = (QtWidgets.QWidget, qt_frame.LeoQTreeWidget,) 

495 count = c.idle_focus_count 

496 if w: 

497 w_class = w and w.__class__.__name__ 

498 c.last_no_focus = False 

499 if self.is_unusual_focus(w): 

500 if trace: 

501 g.trace(f"{count} unusual focus: {w_class}") 

502 else: 

503 c.last_unusual_focus = None 

504 if isinstance(w, table): 

505 if trace and trace_known: 

506 g.trace(f"{count} known focus: {w_class}") 

507 elif trace: 

508 g.trace(f"{count} unknown focus: {w_class}") 

509 else: 

510 if trace: 

511 g.trace(f"{count:3} no focus") 

512 #@+node:ekr.20081005065934.1: *4* c.initAfterLoad 

513 def initAfterLoad(self): 

514 """Provide an official hook for late inits of the commander.""" 

515 pass 

516 #@+node:ekr.20090213065933.6: *4* c.initConfigSettings 

517 def initConfigSettings(self): 

518 """Init all cached commander config settings.""" 

519 c = self 

520 getBool = c.config.getBool 

521 getColor = c.config.getColor 

522 getData = c.config.getData 

523 getInt = c.config.getInt 

524 c.autoindent_in_nocolor = getBool('autoindent-in-nocolor-mode') 

525 c.collapse_nodes_after_move = getBool('collapse-nodes-after-move') 

526 c.collapse_on_lt_arrow = getBool('collapse-on-lt-arrow', default=True) 

527 c.contractVisitedNodes = getBool('contractVisitedNodes') 

528 c.fixedWindowPositionData = getData('fixedWindowPosition') 

529 c.focus_border_color = getColor('focus-border-color') or 'red' 

530 c.focus_border_command_state_color = getColor( 

531 'focus-border-command-state-color') or 'blue' 

532 c.focus_border_overwrite_state_color = getColor( 

533 'focus-border-overwrite-state-color') or 'green' 

534 c.focus_border_width = getInt('focus-border-width') or 1 # pixels 

535 c.forceExecuteEntireBody = getBool('force-execute-entire-body', default=False) 

536 c.make_node_conflicts_node = getBool('make-node-conflicts-node', default=True) 

537 c.outlineHasInitialFocus = getBool('outline-pane-has-initial-focus') 

538 c.page_width = getInt('page-width') or 132 

539 # c.putBitsFlag = getBool('put-expansion-bits-in-leo-files', default=True) 

540 c.sparse_move = getBool('sparse-move-outline-left') 

541 c.sparse_find = getBool('collapse-nodes-during-finds') 

542 c.sparse_spell = getBool('collapse-nodes-while-spelling') 

543 c.sparse_goto_visible = getBool('collapse-on-goto-first-last-visible', default=False) 

544 c.stayInTreeAfterSelect = getBool('stayInTreeAfterSelect') 

545 c.smart_tab = getBool('smart-tab') 

546 c.tab_width = getInt('tab-width') or -4 

547 c.verbose_check_outline = getBool('verbose-check-outline', default=False) 

548 c.vim_mode = getBool('vim-mode', default=False) 

549 c.write_script_file = getBool('write-script-file') 

550 #@+node:ekr.20090213065933.7: *4* c.setWindowPosition 

551 def setWindowPosition(self): 

552 c = self 

553 if c.fixedWindowPositionData: 

554 try: 

555 aList = [z.strip() for z in c.fixedWindowPositionData if z.strip()] 

556 w, h, l, t = aList 

557 c.fixedWindowPosition = int(w), int(h), int(l), int(t) # type:ignore 

558 except Exception: 

559 g.error('bad @data fixedWindowPosition', 

560 repr(self.fixedWindowPosition)) 

561 else: 

562 c.windowPosition = 500, 700, 50, 50 # width,height,left,top. 

563 #@+node:ekr.20210530065748.1: *3* @cmd c.execute-general-script 

564 @cmd('execute-general-script') 

565 def execute_general_script_command(self, event=None): 

566 """ 

567 Execute c.p and all its descendants as a script. 

568 

569 Create a temp file if c.p is not an @<file> node. 

570 

571 @data exec-script-commands associates commands with langauges. 

572 

573 @data exec-script-patterns provides patterns to create clickable 

574 links for error messages. 

575 

576 Set the cwd before calling the command. 

577 """ 

578 c, p, tag = self, self.p, 'execute-general-script' 

579 

580 def get_setting_for_language(setting: str): 

581 """ 

582 Return the setting from the given @data setting. 

583 The first colon ends each key. 

584 """ 

585 for s in c.config.getData(setting) or []: 

586 key, val = s.split(':', 1) 

587 if key.strip() == language: 

588 return val.strip() 

589 return None 

590 

591 # Get the language and extension. 

592 d = c.scanAllDirectives(p) 

593 language: str = d.get('language') 

594 if not language: 

595 print(f"{tag}: No language in effect at {p.h}") 

596 return 

597 ext = g.app.language_extension_dict.get(language) 

598 if not ext: 

599 print(f"{tag}: No extention for {language}") 

600 return 

601 # Get the command. 

602 command = get_setting_for_language('exec-script-commands') 

603 if not command: 

604 print(f"{tag}: No command for {language} in @data exec-script-commands") 

605 return 

606 # Get the optional pattern. 

607 regex = get_setting_for_language('exec-script-patterns') 

608 # Set the directory, if possible. 

609 if p.isAnyAtFileNode(): 

610 path = g.fullPath(c, p) 

611 directory = os.path.dirname(path) 

612 else: 

613 directory = None 

614 c.general_script_helper(command, ext, language, 

615 directory=directory, regex=regex, root=p) 

616 #@+node:vitalije.20190924191405.1: *3* @cmd execute-pytest 

617 @cmd('execute-pytest') 

618 def execute_pytest(self, event=None): 

619 """Using pytest, execute all @test nodes for p, p's parents and p's subtree.""" 

620 c = self 

621 

622 def it(p): 

623 for p1 in p.self_and_parents(): 

624 if p1.h.startswith('@test '): 

625 yield p1 

626 return 

627 for p1 in p.subtree(): 

628 if p1.h.startswith('@test '): 

629 yield p1 

630 

631 try: 

632 for p in it(c.p): 

633 self.execute_single_pytest(p) 

634 except ImportError: 

635 g.es('pytest needs to be installed') 

636 return 

637 

638 def execute_single_pytest(self, p): 

639 c = self 

640 from _pytest.config import get_config 

641 from _pytest.assertion.rewrite import rewrite_asserts 

642 import ast 

643 cfg = get_config() 

644 script = g.getScript(c, p, useSentinels=False) + ( 

645 '\n' 

646 'ls = dict(locals())\n' 

647 'failed = 0\n' 

648 'for x in ls:\n' 

649 ' if x.startswith("test_") and callable(ls[x]):\n' 

650 ' try:\n' 

651 ' ls[x]()\n' 

652 ' except AssertionError as e:\n' 

653 ' failed += 1\n' 

654 ' g.es(f"-------{p.h[6:].strip()}/{x} failed---------")\n' 

655 ' g.es(str(e))\n' 

656 'if failed == 0:\n' 

657 ' g.es("all tests passed")\n' 

658 'else:\n' 

659 ' g.es(f"failed:{failed} tests")\n') 

660 

661 fname = g.os_path_finalize_join(g.app.homeLeoDir, 'leoPytestScript.py') 

662 with open(fname, 'wt', encoding='utf8') as out: 

663 out.write(script) 

664 tree = ast.parse(script, filename=fname) 

665 # A mypy bug: the script can be str. 

666 rewrite_asserts(tree, script, config=cfg) # type:ignore 

667 co = compile(tree, fname, "exec", dont_inherit=True) 

668 sys.path.insert(0, '.') 

669 sys.path.insert(0, c.frame.openDirectory) 

670 try: 

671 exec(co, {'c': c, 'g': g, 'p': p}) 

672 except KeyboardInterrupt: 

673 g.es('interrupted') 

674 except Exception: 

675 g.handleScriptException(c, p, script, script) 

676 finally: 

677 del sys.path[:2] 

678 #@+node:ekr.20171123135625.4: *3* @cmd execute-script & public helpers 

679 @cmd('execute-script') 

680 def executeScript(self, event=None, 

681 args=None, p=None, script=None, useSelectedText=True, 

682 define_g=True, define_name='__main__', 

683 silent=False, namespace=None, raiseFlag=False, 

684 runPyflakes=True, 

685 ): 

686 """ 

687 Execute a *Leo* script, written in python. 

688 Keyword args: 

689 args=None Not None: set script_args in the execution environment. 

690 p=None Get the script from p.b, unless script is given. 

691 script=None None: use script in p.b or c.p.b 

692 useSelectedText=True False: use all the text in p.b or c.p.b. 

693 define_g=True True: define g for the script. 

694 define_name='__main__' Not None: define the name symbol. 

695 silent=False No longer used. 

696 namespace=None Not None: execute the script in this namespace. 

697 raiseFlag=False True: reraise any exceptions. 

698 runPyflakes=True True: run pyflakes if allowed by setting. 

699 """ 

700 c, script1 = self, script 

701 if runPyflakes: 

702 run_pyflakes = c.config.getBool('run-pyflakes-on-write', default=False) 

703 else: 

704 run_pyflakes = False 

705 if not script: 

706 if c.forceExecuteEntireBody: 

707 useSelectedText = False 

708 script = g.getScript(c, p or c.p, useSelectedText=useSelectedText) 

709 script_p = p or c.p # Only for error reporting below. 

710 # #532: check all scripts with pyflakes. 

711 if run_pyflakes and not g.unitTesting: 

712 from leo.commands import checkerCommands as cc 

713 prefix = ('c,g,p,script_gnx=None,None,None,None;' 

714 'assert c and g and p and script_gnx;\n') 

715 cc.PyflakesCommand(c).check_script(script_p, prefix + script) 

716 self.redirectScriptOutput() 

717 oldLog = g.app.log 

718 try: 

719 log = c.frame.log 

720 g.app.log = log 

721 if script.strip(): 

722 sys.path.insert(0, '.') # New in Leo 5.0 

723 sys.path.insert(0, c.frame.openDirectory) # per SegundoBob 

724 script += '\n' # Make sure we end the script properly. 

725 try: 

726 if not namespace or namespace.get('script_gnx') is None: 

727 namespace = namespace or {} 

728 namespace.update(script_gnx=script_p.gnx) 

729 # We *always* execute the script with p = c.p. 

730 c.executeScriptHelper(args, define_g, define_name, namespace, script) 

731 except KeyboardInterrupt: 

732 g.es('interrupted') 

733 except Exception: 

734 if raiseFlag: 

735 raise 

736 g.handleScriptException(c, script_p, script, script1) 

737 finally: 

738 del sys.path[0] 

739 del sys.path[0] 

740 else: 

741 tabName = log and hasattr(log, 'tabName') and log.tabName or 'Log' 

742 g.warning("no script selected", tabName=tabName) 

743 finally: 

744 g.app.log = oldLog 

745 self.unredirectScriptOutput() 

746 #@+node:ekr.20171123135625.5: *4* c.executeScriptHelper 

747 def executeScriptHelper(self, args, define_g, define_name, namespace, script): 

748 c = self 

749 if c.p: 

750 p = c.p.copy() # *Always* use c.p and pass c.p to script. 

751 c.setCurrentDirectoryFromContext(p) 

752 else: 

753 p = None 

754 d = {'c': c, 'g': g, 'input': g.input_, 'p': p} if define_g else {} 

755 if define_name: 

756 d['__name__'] = define_name 

757 d['script_args'] = args or [] 

758 d['script_gnx'] = g.app.scriptDict.get('script_gnx') 

759 if namespace: 

760 d.update(namespace) 

761 # A kludge: reset c.inCommand here to handle the case where we *never* return. 

762 # (This can happen when there are multiple event loops.) 

763 # This does not prevent zombie windows if the script puts up a dialog... 

764 try: 

765 c.inCommand = False 

766 g.inScript = g.app.inScript = True # g.inScript is a synonym for g.app.inScript. 

767 if c.write_script_file: 

768 scriptFile = self.writeScriptFile(script) 

769 exec(compile(script, scriptFile, 'exec'), d) 

770 else: 

771 exec(script, d) 

772 finally: 

773 g.inScript = g.app.inScript = False 

774 #@+node:ekr.20171123135625.6: *4* c.redirectScriptOutput 

775 def redirectScriptOutput(self): 

776 c = self 

777 if c.config.redirect_execute_script_output_to_log_pane: 

778 g.redirectStdout() # Redirect stdout 

779 g.redirectStderr() # Redirect stderr 

780 #@+node:ekr.20171123135625.7: *4* c.setCurrentDirectoryFromContext 

781 def setCurrentDirectoryFromContext(self, p): 

782 c = self 

783 aList = g.get_directives_dict_list(p) 

784 path = c.scanAtPathDirectives(aList) 

785 curDir = g.os_path_abspath(os.getcwd()) 

786 if path and path != curDir: 

787 try: 

788 os.chdir(path) 

789 except Exception: 

790 pass 

791 #@+node:ekr.20171123135625.8: *4* c.unredirectScriptOutput 

792 def unredirectScriptOutput(self): 

793 c = self 

794 if c.exists and c.config.redirect_execute_script_output_to_log_pane: 

795 g.restoreStderr() 

796 g.restoreStdout() 

797 #@+node:ekr.20080514131122.12: *3* @cmd recolor 

798 @cmd('recolor') 

799 def recolorCommand(self, event=None): 

800 """Force a full recolor.""" 

801 c = self 

802 wrapper = c.frame.body.wrapper 

803 # Setting all text appears to be the only way. 

804 i, j = wrapper.getSelectionRange() 

805 ins = wrapper.getInsertPoint() 

806 wrapper.setAllText(c.p.b) 

807 wrapper.setSelectionRange(i, j, insert=ins) 

808 #@+node:ekr.20171124100654.1: *3* c.API 

809 # These methods are a fundamental, unchanging, part of Leo's API. 

810 #@+node:ekr.20091001141621.6061: *4* c.Generators 

811 #@+node:ekr.20091001141621.6043: *5* c.all_nodes & all_unique_nodes 

812 def all_nodes(self): 

813 """A generator returning all vnodes in the outline, in outline order.""" 

814 c = self 

815 for p in c.all_positions(): 

816 yield p.v 

817 

818 def all_unique_nodes(self): 

819 """A generator returning each vnode of the outline.""" 

820 c = self 

821 for p in c.all_unique_positions(copy=False): 

822 yield p.v 

823 

824 # Compatibility with old code... 

825 

826 all_vnodes_iter = all_nodes 

827 all_unique_vnodes_iter = all_unique_nodes 

828 #@+node:ekr.20091001141621.6044: *5* c.all_positions 

829 def all_positions(self, copy=True): 

830 """A generator return all positions of the outline, in outline order.""" 

831 c = self 

832 p = c.rootPosition() 

833 while p: 

834 yield p.copy() if copy else p 

835 p.moveToThreadNext() 

836 

837 # Compatibility with old code... 

838 

839 all_positions_iter = all_positions 

840 allNodes_iter = all_positions 

841 #@+node:ekr.20191014093239.1: *5* c.all_positions_for_v 

842 def all_positions_for_v(self, v, stack=None): 

843 """ 

844 Generates all positions p in this outline where p.v is v. 

845 

846 Should be called with stack=None. 

847 

848 The generated positions are not necessarily in outline order. 

849 

850 By Виталије Милошевић (Vitalije Milosevic). 

851 """ 

852 c = self 

853 

854 if stack is None: 

855 stack = [] 

856 

857 if not isinstance(v, leoNodes.VNode): 

858 g.es_print(f"not a VNode: {v!r}") 

859 return # Stop the generator. 

860 

861 def allinds(v, target_v): 

862 """Yield all indices i such that v.children[i] == target_v.""" 

863 for i, x in enumerate(v.children): 

864 if x is target_v: 

865 yield i 

866 

867 def stack2pos(stack): 

868 """Convert the stack to a position.""" 

869 v, i = stack[-1] 

870 return leoNodes.Position(v, i, stack[:-1]) 

871 

872 for v2 in set(v.parents): 

873 for i in allinds(v2, v): 

874 stack.insert(0, (v, i)) 

875 if v2 is c.hiddenRootNode: 

876 yield stack2pos(stack) 

877 else: 

878 yield from c.all_positions_for_v(v2, stack) 

879 stack.pop(0) 

880 #@+node:ekr.20161120121226.1: *5* c.all_roots 

881 def all_roots(self, copy=True, predicate=None): 

882 """ 

883 A generator yielding *all* the root positions in the outline that 

884 satisfy the given predicate. p.isAnyAtFileNode is the default 

885 predicate. 

886 """ 

887 c = self 

888 if predicate is None: 

889 

890 # pylint: disable=function-redefined 

891 

892 def predicate(p): 

893 return p.isAnyAtFileNode() 

894 

895 p = c.rootPosition() 

896 while p: 

897 if predicate(p): 

898 yield p.copy() # 2017/02/19 

899 p.moveToNodeAfterTree() 

900 else: 

901 p.moveToThreadNext() 

902 #@+node:ekr.20091001141621.6062: *5* c.all_unique_positions 

903 def all_unique_positions(self, copy=True): 

904 """ 

905 A generator return all positions of the outline, in outline order. 

906 Returns only the first position for each vnode. 

907 """ 

908 c = self 

909 p = c.rootPosition() 

910 seen = set() 

911 while p: 

912 if p.v in seen: 

913 p.moveToNodeAfterTree() 

914 else: 

915 seen.add(p.v) 

916 yield p.copy() if copy else p 

917 p.moveToThreadNext() 

918 

919 # Compatibility with old code... 

920 

921 all_positions_with_unique_vnodes_iter = all_unique_positions 

922 #@+node:ekr.20161120125322.1: *5* c.all_unique_roots 

923 def all_unique_roots(self, copy=True, predicate=None): 

924 """ 

925 A generator yielding all unique root positions in the outline that 

926 satisfy the given predicate. p.isAnyAtFileNode is the default 

927 predicate. 

928 """ 

929 c = self 

930 if predicate is None: 

931 

932 # pylint: disable=function-redefined 

933 

934 def predicate(p): 

935 return p.isAnyAtFileNode() 

936 

937 seen = set() 

938 p = c.rootPosition() 

939 while p: 

940 if p.v not in seen and predicate(p): 

941 seen.add(p.v) 

942 yield p.copy() if copy else p 

943 p.moveToNodeAfterTree() 

944 else: 

945 p.moveToThreadNext() 

946 #@+node:ekr.20150316175921.5: *5* c.safe_all_positions 

947 def safe_all_positions(self, copy=True): 

948 """ 

949 A generator returning all positions of the outline. This generator does 

950 *not* assume that vnodes are never their own ancestors. 

951 """ 

952 c = self 

953 p = c.rootPosition() # Make one copy. 

954 while p: 

955 yield p.copy() if copy else p 

956 p.safeMoveToThreadNext() 

957 #@+node:ekr.20060906211747: *4* c.Getters 

958 #@+node:ekr.20040803140033: *5* c.currentPosition 

959 def currentPosition(self): 

960 """ 

961 Return a copy of the presently selected position or a new null 

962 position. So c.p.copy() is never necessary. 

963 """ 

964 c = self 

965 if hasattr(c, '_currentPosition') and getattr(c, '_currentPosition'): 

966 # *Always* return a copy. 

967 return c._currentPosition.copy() 

968 return c.rootPosition() 

969 

970 # For compatibility with old scripts... 

971 

972 currentVnode = currentPosition 

973 #@+node:ekr.20190506060937.1: *5* c.dumpExpanded 

974 @cmd('dump-expanded') 

975 def dump_expanded(self, event): 

976 """Print all non-empty v.expandedPositions lists.""" 

977 c = event.get('c') 

978 if not c: 

979 return 

980 g.es_print('dump-expanded...') 

981 for p in c.all_positions(): 

982 if p.v.expandedPositions: 

983 indent = ' ' * p.level() 

984 print(f"{indent}{p.h}") 

985 g.printObj(p.v.expandedPositions, indent=indent) 

986 #@+node:ekr.20040306220230.1: *5* c.edit_widget 

987 def edit_widget(self, p): 

988 c = self 

989 return p and c.frame.tree.edit_widget(p) 

990 #@+node:ekr.20031218072017.2986: *5* c.fileName & relativeFileName & shortFileName 

991 # Compatibility with scripts 

992 

993 def fileName(self): 

994 s = self.mFileName or "" 

995 if g.isWindows: 

996 s = s.replace('\\', '/') 

997 return s 

998 

999 def relativeFileName(self): 

1000 return self.mRelativeFileName or self.mFileName 

1001 

1002 def shortFileName(self): 

1003 return g.shortFileName(self.mFileName) 

1004 

1005 shortFilename = shortFileName 

1006 #@+node:ekr.20070615070925.1: *5* c.firstVisible 

1007 def firstVisible(self): 

1008 """Move to the first visible node of the present chapter or hoist.""" 

1009 c, p = self, self.p 

1010 while 1: 

1011 back = p.visBack(c) 

1012 if back and back.isVisible(c): 

1013 p = back 

1014 else: break 

1015 return p 

1016 #@+node:ekr.20171123135625.29: *5* c.getBodyLines 

1017 def getBodyLines(self): 

1018 """ 

1019 Return (head, lines, tail, oldSel, oldYview). 

1020 

1021 - head: string containg all the lines before the selected text (or the 

1022 text before the insert point if no selection) 

1023 - lines: list of lines containing the selected text 

1024 (or the line containing the insert point if no selection) 

1025 - after: string containing all lines after the selected text 

1026 (or the text after the insert point if no selection) 

1027 - oldSel: tuple containing the old selection range, or None. 

1028 - oldYview: int containing the old y-scroll value, or None. 

1029 """ 

1030 c = self 

1031 body = c.frame.body 

1032 w = body.wrapper 

1033 oldYview = w.getYScrollPosition() 

1034 # Note: lines is the entire line containing the insert point if no selection. 

1035 head, s, tail = body.getSelectionLines() 

1036 lines = g.splitLines(s) # Retain the newlines of each line. 

1037 # Expand the selection. 

1038 i = len(head) 

1039 j = len(head) + len(s) 

1040 oldSel = i, j 

1041 return head, lines, tail, oldSel, oldYview # string,list,string,tuple,int. 

1042 #@+node:ekr.20150417073117.1: *5* c.getTabWidth 

1043 def getTabWidth(self, p): 

1044 """Return the tab width in effect at p.""" 

1045 c = self 

1046 val = g.scanAllAtTabWidthDirectives(c, p) 

1047 return val 

1048 #@+node:ekr.20040803112200: *5* c.is...Position 

1049 #@+node:ekr.20040803155551: *6* c.currentPositionIsRootPosition 

1050 def currentPositionIsRootPosition(self): 

1051 """Return True if the current position is the root position. 

1052 

1053 This method is called during idle time, so not generating positions 

1054 here fixes a major leak. 

1055 """ 

1056 c = self 

1057 root = c.rootPosition() 

1058 return c._currentPosition and root and c._currentPosition == root 

1059 # return ( 

1060 # c._currentPosition and c._rootPosition and 

1061 # c._currentPosition == c._rootPosition) 

1062 #@+node:ekr.20040803160656: *6* c.currentPositionHasNext 

1063 def currentPositionHasNext(self): 

1064 """Return True if the current position is the root position. 

1065 

1066 This method is called during idle time, so not generating positions 

1067 here fixes a major leak. 

1068 """ 

1069 c = self 

1070 current = c._currentPosition 

1071 return current and current.hasNext() 

1072 #@+node:ekr.20040803112450: *6* c.isCurrentPosition 

1073 def isCurrentPosition(self, p): 

1074 c = self 

1075 if p is None or c._currentPosition is None: 

1076 return False 

1077 return p == c._currentPosition 

1078 #@+node:ekr.20040803112450.1: *6* c.isRootPosition 

1079 def isRootPosition(self, p): 

1080 c = self 

1081 root = c.rootPosition() 

1082 return p and root and p == root # 2011/03/03 

1083 #@+node:ekr.20031218072017.2987: *5* c.isChanged 

1084 def isChanged(self): 

1085 return self.changed 

1086 #@+node:ekr.20210901104900.1: *5* c.lastPosition 

1087 def lastPosition(self): 

1088 c = self 

1089 p = c.rootPosition() 

1090 while p.hasNext(): 

1091 p.moveToNext() 

1092 while p.hasThreadNext(): 

1093 p.moveToThreadNext() 

1094 return p 

1095 #@+node:ekr.20140106215321.16676: *5* c.lastTopLevel 

1096 def lastTopLevel(self): 

1097 """Return the last top-level position in the outline.""" 

1098 c = self 

1099 p = c.rootPosition() 

1100 while p.hasNext(): 

1101 p.moveToNext() 

1102 return p 

1103 #@+node:ekr.20031218072017.4146: *5* c.lastVisible 

1104 def lastVisible(self): 

1105 """Move to the last visible node of the present chapter or hoist.""" 

1106 c, p = self, self.p 

1107 while 1: 

1108 next = p.visNext(c) 

1109 if next and next.isVisible(c): 

1110 p = next 

1111 else: break 

1112 return p 

1113 #@+node:ekr.20040307104131.3: *5* c.positionExists 

1114 def positionExists(self, p, root=None, trace=False): 

1115 """Return True if a position exists in c's tree""" 

1116 if not p or not p.v: 

1117 return False 

1118 

1119 rstack = root.stack + [(root.v, root._childIndex)] if root else [] 

1120 pstack = p.stack + [(p.v, p._childIndex)] 

1121 

1122 if len(rstack) > len(pstack): 

1123 return False 

1124 

1125 par = self.hiddenRootNode 

1126 for j, x in enumerate(pstack): 

1127 if j < len(rstack) and x != rstack[j]: 

1128 return False 

1129 v, i = x 

1130 if i >= len(par.children) or v is not par.children[i]: 

1131 return False 

1132 par = v 

1133 return True 

1134 #@+node:ekr.20160427153457.1: *6* c.dumpPosition 

1135 def dumpPosition(self, p): 

1136 """Dump position p and it's ancestors.""" 

1137 g.trace('=====', p.h, p._childIndex) 

1138 for i, data in enumerate(p.stack): 

1139 v, childIndex = data 

1140 print(f"{i} {childIndex} {v._headString}") 

1141 #@+node:ekr.20040803140033.2: *5* c.rootPosition 

1142 _rootCount = 0 

1143 

1144 def rootPosition(self): 

1145 """Return the root position. 

1146 

1147 Root position is the first position in the document. Other 

1148 top level positions are siblings of this node. 

1149 """ 

1150 c = self 

1151 # 2011/02/25: Compute the position directly. 

1152 if c.hiddenRootNode.children: 

1153 v = c.hiddenRootNode.children[0] 

1154 return leoNodes.Position(v, childIndex=0, stack=None) 

1155 return None 

1156 

1157 # For compatibility with old scripts... 

1158 

1159 rootVnode = rootPosition 

1160 findRootPosition = rootPosition 

1161 #@+node:ekr.20131017174814.17480: *5* c.shouldBeExpanded 

1162 def shouldBeExpanded(self, p): 

1163 """Return True if the node at position p should be expanded.""" 

1164 c, v = self, p.v 

1165 if not p.hasChildren(): 

1166 return False 

1167 # Always clear non-existent positions. 

1168 v.expandedPositions = [z for z in v.expandedPositions if c.positionExists(z)] 

1169 if not p.isCloned(): 

1170 # Do not call p.isExpanded here! It calls this method. 

1171 return p.v.isExpanded() 

1172 if p.isAncestorOf(c.p): 

1173 return True 

1174 for p2 in v.expandedPositions: 

1175 if p == p2: 

1176 return True 

1177 return False 

1178 #@+node:ekr.20070609122713: *5* c.visLimit 

1179 def visLimit(self): 

1180 """ 

1181 Return the topmost visible node. 

1182 This is affected by chapters and hoists. 

1183 """ 

1184 c = self 

1185 cc = c.chapterController 

1186 if c.hoistStack: 

1187 bunch = c.hoistStack[-1] 

1188 p = bunch.p 

1189 limitIsVisible = not cc or not p.h.startswith('@chapter') 

1190 return p, limitIsVisible 

1191 return None, None 

1192 #@+node:tbrown.20091206142842.10296: *5* c.vnode2allPositions 

1193 def vnode2allPositions(self, v): 

1194 """Given a VNode v, find all valid positions p such that p.v = v. 

1195 

1196 Not really all, just all for each of v's distinct immediate parents. 

1197 """ 

1198 c = self 

1199 context = v.context # v's commander. 

1200 assert c == context 

1201 positions = [] 

1202 for immediate in v.parents: 

1203 if v in immediate.children: 

1204 n = immediate.children.index(v) 

1205 else: 

1206 continue 

1207 stack = [(v, n)] 

1208 while immediate.parents: 

1209 parent = immediate.parents[0] 

1210 if immediate in parent.children: 

1211 n = parent.children.index(immediate) 

1212 else: 

1213 break 

1214 stack.insert(0, (immediate, n),) 

1215 immediate = parent 

1216 else: 

1217 v, n = stack.pop() 

1218 p = leoNodes.Position(v, n, stack) 

1219 positions.append(p) 

1220 return positions 

1221 #@+node:ekr.20090107113956.1: *5* c.vnode2position 

1222 def vnode2position(self, v): 

1223 """Given a VNode v, construct a valid position p such that p.v = v. 

1224 """ 

1225 c = self 

1226 context = v.context # v's commander. 

1227 assert c == context 

1228 stack: List[Tuple[int, Tuple["leoNodes.VNode", int]]] = [] 

1229 while v.parents: 

1230 parent = v.parents[0] 

1231 if v in parent.children: 

1232 n = parent.children.index(v) 

1233 else: 

1234 return None 

1235 stack.insert(0, (v, n),) 

1236 v = parent 

1237 # v.parents includes the hidden root node. 

1238 if not stack: 

1239 # a VNode not in the tree 

1240 return None 

1241 v, n = stack.pop() 

1242 p = leoNodes.Position(v, n, stack) # type:ignore 

1243 return p 

1244 #@+node:ekr.20090130135126.1: *4* c.Properties 

1245 def __get_p(self): 

1246 c = self 

1247 return c.currentPosition() 

1248 

1249 p = property( 

1250 __get_p, # No setter. 

1251 doc="commander current position property") 

1252 #@+node:ekr.20060906211747.1: *4* c.Setters 

1253 #@+node:ekr.20040315032503: *5* c.appendStringToBody 

1254 def appendStringToBody(self, p, s): 

1255 

1256 if s: 

1257 p.b = p.b + g.toUnicode(s) 

1258 #@+node:ekr.20031218072017.2984: *5* c.clearAllMarked 

1259 def clearAllMarked(self): 

1260 c = self 

1261 for p in c.all_unique_positions(copy=False): 

1262 p.v.clearMarked() 

1263 #@+node:ekr.20031218072017.2985: *5* c.clearAllVisited 

1264 def clearAllVisited(self): 

1265 c = self 

1266 for p in c.all_unique_positions(copy=False): 

1267 p.v.clearVisited() 

1268 p.v.clearWriteBit() 

1269 #@+node:ekr.20191215044636.1: *5* c.clearChanged 

1270 def clearChanged(self): 

1271 """clear the marker that indicates that the .leo file has been changed.""" 

1272 c = self 

1273 if not c.frame: 

1274 return 

1275 c.changed = False 

1276 if c.loading: 

1277 return # don't update while loading. 

1278 # Clear all dirty bits _before_ setting the caption. 

1279 for v in c.all_unique_nodes(): 

1280 v.clearDirty() 

1281 c.changed = False 

1282 # Do nothing for null frames. 

1283 assert c.gui 

1284 if c.gui.guiName() == 'nullGui': 

1285 return 

1286 if not c.frame.top: 

1287 return 

1288 master = getattr(c.frame.top, 'leo_master', None) 

1289 if master: 

1290 master.setChanged(c, changed=False) # LeoTabbedTopLevel.setChanged. 

1291 s = c.frame.getTitle() 

1292 if len(s) > 2 and s[0:2] == "* ": 

1293 c.frame.setTitle(s[2:]) 

1294 #@+node:ekr.20060906211138: *5* c.clearMarked 

1295 def clearMarked(self, p): 

1296 c = self 

1297 p.v.clearMarked() 

1298 g.doHook("clear-mark", c=c, p=p) 

1299 #@+node:ekr.20040305223522: *5* c.setBodyString 

1300 def setBodyString(self, p, s): 

1301 """ 

1302 This is equivalent to p.b = s. 

1303 

1304 Warning: This method may call c.recolor() or c.redraw(). 

1305 """ 

1306 c, v = self, p.v 

1307 if not c or not v: 

1308 return 

1309 s = g.toUnicode(s) 

1310 current = c.p 

1311 # 1/22/05: Major change: the previous test was: 'if p == current:' 

1312 # This worked because commands work on the presently selected node. 

1313 # But setRecentFiles may change a _clone_ of the selected node! 

1314 if current and p.v == current.v: 

1315 w = c.frame.body.wrapper 

1316 w.setAllText(s) 

1317 v.setSelection(0, 0) 

1318 c.recolor() 

1319 # Keep the body text in the VNode up-to-date. 

1320 if v.b != s: 

1321 v.setBodyString(s) 

1322 v.setSelection(0, 0) 

1323 p.setDirty() 

1324 if not c.isChanged(): 

1325 c.setChanged() 

1326 c.redraw_after_icons_changed() 

1327 #@+node:ekr.20031218072017.2989: *5* c.setChanged 

1328 def setChanged(self): 

1329 """Set the marker that indicates that the .leo file has been changed.""" 

1330 c = self 

1331 if not c.frame: 

1332 return 

1333 c.changed = True 

1334 if c.loading: 

1335 return # don't update while loading. 

1336 # Do nothing for null frames. 

1337 assert c.gui 

1338 if c.gui.guiName() == 'nullGui': 

1339 return 

1340 if not c.frame.top: 

1341 return 

1342 master = getattr(c.frame.top, 'leo_master', None) 

1343 if master: 

1344 master.setChanged(c, changed=True) # LeoTabbedTopLevel.setChanged. 

1345 s = c.frame.getTitle() 

1346 if len(s) > 2 and s[0] != '*': 

1347 c.frame.setTitle("* " + s) 

1348 #@+node:ekr.20040803140033.1: *5* c.setCurrentPosition 

1349 _currentCount = 0 

1350 

1351 def setCurrentPosition(self, p): 

1352 """ 

1353 Set the presently selected position. For internal use only. 

1354 Client code should use c.selectPosition instead. 

1355 """ 

1356 c = self 

1357 if not p: 

1358 g.trace('===== no p', g.callers()) 

1359 return 

1360 if c.positionExists(p): 

1361 if c._currentPosition and p == c._currentPosition: 

1362 pass # We have already made a copy. 

1363 else: # Make a copy _now_ 

1364 c._currentPosition = p.copy() 

1365 else: 

1366 # Don't kill unit tests for this kind of problem. 

1367 c._currentPosition = c.rootPosition() 

1368 g.trace('Invalid position', repr(p)) 

1369 g.trace(g.callers()) 

1370 

1371 # For compatibility with old scripts. 

1372 

1373 setCurrentVnode = setCurrentPosition 

1374 #@+node:ekr.20040305223225: *5* c.setHeadString 

1375 def setHeadString(self, p, s): 

1376 """ 

1377 Set the p's headline and the corresponding tree widget to s. 

1378 

1379 This is used in by unit tests to restore the outline. 

1380 """ 

1381 c = self 

1382 p.initHeadString(s) 

1383 p.setDirty() 

1384 # Change the actual tree widget so 

1385 # A later call to c.endEditing or c.redraw will use s. 

1386 c.frame.tree.setHeadline(p, s) 

1387 #@+node:ekr.20060109164136: *5* c.setLog 

1388 def setLog(self): 

1389 c = self 

1390 if c.exists: 

1391 try: 

1392 # c.frame or c.frame.log may not exist. 

1393 g.app.setLog(c.frame.log) 

1394 except AttributeError: 

1395 pass 

1396 #@+node:ekr.20060906211138.1: *5* c.setMarked (calls hook) 

1397 def setMarked(self, p): 

1398 c = self 

1399 p.setMarked() 

1400 p.setDirty() # Defensive programming. 

1401 g.doHook("set-mark", c=c, p=p) 

1402 #@+node:ekr.20040803140033.3: *5* c.setRootPosition (A do-nothing) 

1403 def setRootPosition(self, unused_p=None): 

1404 """Set c._rootPosition.""" 

1405 # 2011/03/03: No longer used. 

1406 #@+node:ekr.20060906131836: *5* c.setRootVnode (A do-nothing) 

1407 def setRootVnode(self, v): 

1408 pass 

1409 # c = self 

1410 # # 2011/02/25: c.setRootPosition needs no arguments. 

1411 # c.setRootPosition() 

1412 #@+node:ekr.20040311173238: *5* c.topPosition & c.setTopPosition 

1413 def topPosition(self): 

1414 """Return the root position.""" 

1415 c = self 

1416 if c._topPosition: 

1417 return c._topPosition.copy() 

1418 return None 

1419 

1420 def setTopPosition(self, p): 

1421 """Set the root position.""" 

1422 c = self 

1423 if p: 

1424 c._topPosition = p.copy() 

1425 else: 

1426 c._topPosition = None 

1427 

1428 # Define these for compatibility with old scripts... 

1429 

1430 topVnode = topPosition 

1431 setTopVnode = setTopPosition 

1432 #@+node:ekr.20171124081419.1: *3* c.Check Outline... 

1433 #@+node:ekr.20141024211256.22: *4* c.checkGnxs 

1434 def checkGnxs(self): 

1435 """ 

1436 Check the consistency of all gnx's. 

1437 Reallocate gnx's for duplicates or empty gnx's. 

1438 Return the number of structure_errors found. 

1439 """ 

1440 c = self 

1441 # Keys are gnx's; values are sets of vnodes with that gnx. 

1442 d: Dict[str, Set["leoNodes.VNode"]] = {} 

1443 ni = g.app.nodeIndices 

1444 t1 = time.time() 

1445 

1446 def new_gnx(v): 

1447 """Set v.fileIndex.""" 

1448 v.fileIndex = ni.getNewIndex(v) 

1449 

1450 count, gnx_errors = 0, 0 

1451 for p in c.safe_all_positions(copy=False): 

1452 count += 1 

1453 v = p.v 

1454 gnx = v.fileIndex 

1455 if gnx: # gnx must be a string. 

1456 aSet: Set["leoNodes.VNode"] = d.get(gnx, set()) 

1457 aSet.add(v) 

1458 d[gnx] = aSet 

1459 else: 

1460 gnx_errors += 1 

1461 new_gnx(v) 

1462 g.es_print(f"empty v.fileIndex: {v} new: {p.v.gnx!r}", color='red') 

1463 for gnx in sorted(d.keys()): 

1464 aList = list(d.get(gnx)) 

1465 if len(aList) != 1: 

1466 print('\nc.checkGnxs...') 

1467 g.es_print(f"multiple vnodes with gnx: {gnx!r}", color='red') 

1468 for v in aList: 

1469 gnx_errors += 1 

1470 g.es_print(f"id(v): {id(v)} gnx: {v.fileIndex} {v.h}", color='red') 

1471 new_gnx(v) 

1472 ok = not gnx_errors and not g.app.structure_errors 

1473 t2 = time.time() 

1474 if not ok: 

1475 g.es_print( 

1476 f"check-outline ERROR! {c.shortFileName()} " 

1477 f"{count} nodes, " 

1478 f"{gnx_errors} gnx errors, " 

1479 f"{g.app.structure_errors} " 

1480 f"structure errors", 

1481 color='red' 

1482 ) 

1483 elif c.verbose_check_outline and not g.unitTesting: 

1484 print( 

1485 f"check-outline OK: {t2 - t1:4.2f} sec. " 

1486 f"{c.shortFileName()} {count} nodes") 

1487 return g.app.structure_errors 

1488 #@+node:ekr.20150318131947.7: *4* c.checkLinks & helpers 

1489 def checkLinks(self): 

1490 """Check the consistency of all links in the outline.""" 

1491 c = self 

1492 t1 = time.time() 

1493 count, errors = 0, 0 

1494 for p in c.safe_all_positions(): 

1495 count += 1 

1496 # try: 

1497 if not c.checkThreadLinks(p): 

1498 errors += 1 

1499 break 

1500 if not c.checkSiblings(p): 

1501 errors += 1 

1502 break 

1503 if not c.checkParentAndChildren(p): 

1504 errors += 1 

1505 break 

1506 # except AssertionError: 

1507 # errors += 1 

1508 # junk, value, junk = sys.exc_info() 

1509 # g.error("test failed at position %s\n%s" % (repr(p), value)) 

1510 t2 = time.time() 

1511 g.es_print( 

1512 f"check-links: {t2 - t1:4.2f} sec. " 

1513 f"{c.shortFileName()} {count} nodes", color='blue') 

1514 return errors 

1515 #@+node:ekr.20040314035615.2: *5* c.checkParentAndChildren 

1516 def checkParentAndChildren(self, p): 

1517 """Check consistency of parent and child data structures.""" 

1518 c = self 

1519 

1520 def _assert(condition): 

1521 return g._assert(condition, show_callers=False) 

1522 

1523 def dump(p): 

1524 if p and p.v: 

1525 p.v.dump() 

1526 elif p: 

1527 print('<no p.v>') 

1528 else: 

1529 print('<no p>') 

1530 if g.unitTesting: 

1531 assert False, g.callers() 

1532 

1533 if p.hasParent(): 

1534 n = p.childIndex() 

1535 if not _assert(p == p.parent().moveToNthChild(n)): 

1536 g.trace(f"p != parent().moveToNthChild({n})") 

1537 dump(p) 

1538 dump(p.parent()) 

1539 return False 

1540 if p.level() > 0 and not _assert(p.v.parents): 

1541 g.trace("no parents") 

1542 dump(p) 

1543 return False 

1544 for child in p.children(): 

1545 if not c.checkParentAndChildren(child): 

1546 return False 

1547 if not _assert(p == child.parent()): 

1548 g.trace("p != child.parent()") 

1549 dump(p) 

1550 dump(child.parent()) 

1551 return False 

1552 if p.hasNext(): 

1553 if not _assert(p.next().parent() == p.parent()): 

1554 g.trace("p.next().parent() != p.parent()") 

1555 dump(p.next().parent()) 

1556 dump(p.parent()) 

1557 return False 

1558 if p.hasBack(): 

1559 if not _assert(p.back().parent() == p.parent()): 

1560 g.trace("p.back().parent() != parent()") 

1561 dump(p.back().parent()) 

1562 dump(p.parent()) 

1563 return False 

1564 # Check consistency of parent and children arrays. 

1565 # Every nodes gets visited, so a strong test need only check consistency 

1566 # between p and its parent, not between p and its children. 

1567 parent_v = p._parentVnode() 

1568 n = p.childIndex() 

1569 if not _assert(parent_v.children[n] == p.v): 

1570 g.trace("parent_v.children[n] != p.v") 

1571 parent_v.dump() 

1572 p.v.dump() 

1573 return False 

1574 return True 

1575 #@+node:ekr.20040314035615.1: *5* c.checkSiblings 

1576 def checkSiblings(self, p): 

1577 """Check the consistency of next and back links.""" 

1578 back = p.back() 

1579 next = p.next() 

1580 if back: 

1581 if not g._assert(p == back.next()): 

1582 g.trace( 

1583 f"p!=p.back().next()\n" 

1584 f" back: {back}\n" 

1585 f"back.next: {back.next()}") 

1586 return False 

1587 if next: 

1588 if not g._assert(p == next.back()): 

1589 g.trace( 

1590 f"p!=p.next().back\n" 

1591 f" next: {next}\n" 

1592 f"next.back: {next.back()}") 

1593 return False 

1594 return True 

1595 #@+node:ekr.20040314035615: *5* c.checkThreadLinks 

1596 def checkThreadLinks(self, p): 

1597 """Check consistency of threadNext & threadBack links.""" 

1598 threadBack = p.threadBack() 

1599 threadNext = p.threadNext() 

1600 if threadBack: 

1601 if not g._assert(p == threadBack.threadNext()): 

1602 g.trace("p!=p.threadBack().threadNext()") 

1603 return False 

1604 if threadNext: 

1605 if not g._assert(p == threadNext.threadBack()): 

1606 g.trace("p!=p.threadNext().threadBack()") 

1607 return False 

1608 return True 

1609 #@+node:ekr.20031218072017.1760: *4* c.checkMoveWithParentWithWarning & c.checkDrag 

1610 #@+node:ekr.20070910105044: *5* c.checkMoveWithParentWithWarning 

1611 def checkMoveWithParentWithWarning(self, root, parent, warningFlag): 

1612 """ 

1613 Return False if root or any of root's descendents is a clone of parent 

1614 or any of parents ancestors. 

1615 """ 

1616 c = self 

1617 message = "Illegal move or drag: no clone may contain a clone of itself" 

1618 clonedVnodes = {} 

1619 for ancestor in parent.self_and_parents(copy=False): 

1620 if ancestor.isCloned(): 

1621 v = ancestor.v 

1622 clonedVnodes[v] = v 

1623 if not clonedVnodes: 

1624 return True 

1625 for p in root.self_and_subtree(copy=False): 

1626 if p.isCloned() and clonedVnodes.get(p.v): 

1627 if not g.unitTesting and warningFlag: 

1628 c.alert(message) 

1629 return False 

1630 return True 

1631 #@+node:ekr.20070910105044.1: *5* c.checkDrag 

1632 def checkDrag(self, root, target): 

1633 """Return False if target is any descendant of root.""" 

1634 c = self 

1635 message = "Can not drag a node into its descendant tree." 

1636 for z in root.subtree(): 

1637 if z == target: 

1638 if not g.unitTesting: 

1639 c.alert(message) 

1640 return False 

1641 return True 

1642 #@+node:ekr.20031218072017.2072: *4* c.checkOutline 

1643 def checkOutline(self, event=None, check_links=False): 

1644 """ 

1645 Check for errors in the outline. 

1646 Return the count of serious structure errors. 

1647 """ 

1648 # The check-outline command sets check_links = True. 

1649 c = self 

1650 g.app.structure_errors = 0 

1651 structure_errors = c.checkGnxs() 

1652 if check_links and not structure_errors: 

1653 structure_errors += c.checkLinks() 

1654 return structure_errors 

1655 #@+node:ekr.20031218072017.1765: *4* c.validateOutline 

1656 # Makes sure all nodes are valid. 

1657 

1658 def validateOutline(self, event=None): 

1659 c = self 

1660 if not g.app.validate_outline: 

1661 return True 

1662 root = c.rootPosition() 

1663 parent = None 

1664 if root: 

1665 return root.validateOutlineWithParent(parent) 

1666 return True 

1667 #@+node:ekr.20040723094220: *3* c.Check Python code 

1668 # This code is no longer used by any Leo command, 

1669 # but it will be retained for use of scripts. 

1670 #@+node:ekr.20040723094220.1: *4* c.checkAllPythonCode 

1671 def checkAllPythonCode(self, event=None, ignoreAtIgnore=True): 

1672 """Check all nodes in the selected tree for syntax and tab errors.""" 

1673 c = self 

1674 count = 0 

1675 result = "ok" 

1676 for p in c.all_unique_positions(): 

1677 count += 1 

1678 if not g.unitTesting: 

1679 #@+<< print dots >> 

1680 #@+node:ekr.20040723094220.2: *5* << print dots >> 

1681 if count % 100 == 0: 

1682 g.es('', '.', newline=False) 

1683 if count % 2000 == 0: 

1684 g.enl() 

1685 #@-<< print dots >> 

1686 if g.scanForAtLanguage(c, p) == "python": 

1687 if not g.scanForAtSettings(p) and ( 

1688 not ignoreAtIgnore or not g.scanForAtIgnore(c, p) 

1689 ): 

1690 try: 

1691 c.checkPythonNode(p) 

1692 except(SyntaxError, tokenize.TokenError, tabnanny.NannyNag): 

1693 result = "error" # Continue to check. 

1694 except Exception: 

1695 return "surprise" # abort 

1696 if result != 'ok': 

1697 g.pr(f"Syntax error in {p.h}") 

1698 return result # End the unit test: it has failed. 

1699 if not g.unitTesting: 

1700 g.blue("check complete") 

1701 return result 

1702 #@+node:ekr.20040723094220.3: *4* c.checkPythonCode 

1703 def checkPythonCode(self, 

1704 event=None, 

1705 ignoreAtIgnore=True, 

1706 checkOnSave=False 

1707 ): 

1708 """Check the selected tree for syntax and tab errors.""" 

1709 c = self 

1710 count = 0 

1711 result = "ok" 

1712 if not g.unitTesting: 

1713 g.es("checking Python code ") 

1714 for p in c.p.self_and_subtree(): 

1715 count += 1 

1716 if not g.unitTesting and not checkOnSave: 

1717 #@+<< print dots >> 

1718 #@+node:ekr.20040723094220.4: *5* << print dots >> 

1719 if count % 100 == 0: 

1720 g.es('', '.', newline=False) 

1721 if count % 2000 == 0: 

1722 g.enl() 

1723 #@-<< print dots >> 

1724 if g.scanForAtLanguage(c, p) == "python": 

1725 if not ignoreAtIgnore or not g.scanForAtIgnore(c, p): 

1726 try: 

1727 c.checkPythonNode(p) 

1728 except(SyntaxError, tokenize.TokenError, tabnanny.NannyNag): 

1729 result = "error" # Continue to check. 

1730 except Exception: 

1731 return "surprise" # abort 

1732 if not g.unitTesting: 

1733 g.blue("check complete") 

1734 # We _can_ return a result for unit tests because we aren't using doCommand. 

1735 return result 

1736 #@+node:ekr.20040723094220.5: *4* c.checkPythonNode 

1737 def checkPythonNode(self, p): 

1738 c, h = self, p.h 

1739 # Call getScript to ignore directives and section references. 

1740 body = g.getScript(c, p.copy()) 

1741 if not body: 

1742 return 

1743 try: 

1744 fn = f"<node: {p.h}>" 

1745 compile(body + '\n', fn, 'exec') 

1746 c.tabNannyNode(p, h, body) 

1747 except SyntaxError: 

1748 if g.unitTesting: 

1749 raise 

1750 g.warning(f"Syntax error in: {h}") 

1751 g.es_exception(full=False, color="black") 

1752 except Exception: 

1753 g.es_print('unexpected exception') 

1754 g.es_exception() 

1755 raise 

1756 #@+node:ekr.20040723094220.6: *4* c.tabNannyNode 

1757 # This code is based on tabnanny.check. 

1758 

1759 def tabNannyNode(self, p, headline, body): 

1760 """Check indentation using tabnanny.""" 

1761 try: 

1762 readline = g.ReadLinesClass(body).next 

1763 tabnanny.process_tokens(tokenize.generate_tokens(readline)) 

1764 except IndentationError: 

1765 if g.unitTesting: 

1766 raise 

1767 junk1, msg, junk2 = sys.exc_info() 

1768 g.warning("IndentationError in", headline) 

1769 g.es('', msg) 

1770 except tokenize.TokenError: 

1771 if g.unitTesting: 

1772 raise 

1773 junk1, msg, junk2 = sys.exc_info() 

1774 g.warning("TokenError in", headline) 

1775 g.es('', msg) 

1776 except tabnanny.NannyNag: 

1777 if g.unitTesting: 

1778 raise 

1779 junk1, nag, junk2 = sys.exc_info() 

1780 badline = nag.get_lineno() 

1781 line = nag.get_line() 

1782 message = nag.get_msg() 

1783 g.warning("indentation error in", headline, "line", badline) 

1784 g.es(message) 

1785 line2 = repr(str(line))[1:-1] 

1786 g.es("offending line:\n", line2) 

1787 except Exception: 

1788 g.trace("unexpected exception") 

1789 g.es_exception() 

1790 raise 

1791 #@+node:ekr.20171123200644.1: *3* c.Convenience methods 

1792 #@+node:ekr.20171123135625.39: *4* c.getTime 

1793 def getTime(self, body=True): 

1794 c = self 

1795 default_format = "%m/%d/%Y %H:%M:%S" # E.g., 1/30/2003 8:31:55 

1796 # Try to get the format string from settings. 

1797 if body: 

1798 format = c.config.getString("body-time-format-string") 

1799 gmt = c.config.getBool("body-gmt-time") 

1800 else: 

1801 format = c.config.getString("headline-time-format-string") 

1802 gmt = c.config.getBool("headline-gmt-time") 

1803 if format is None: 

1804 format = default_format 

1805 try: 

1806 # import time 

1807 if gmt: 

1808 s = time.strftime(format, time.gmtime()) 

1809 else: 

1810 s = time.strftime(format, time.localtime()) 

1811 except(ImportError, NameError): 

1812 g.warning("time.strftime not available on this platform") 

1813 return "" 

1814 except Exception: 

1815 g.es_exception() # Probably a bad format string in leoSettings.leo. 

1816 s = time.strftime(default_format, time.gmtime()) 

1817 return s 

1818 #@+node:ekr.20171123135625.10: *4* c.goToLineNumber & goToScriptLineNumber 

1819 def goToLineNumber(self, n): 

1820 """ 

1821 Go to line n (zero-based) of a script. 

1822 A convenience method called from g.handleScriptException. 

1823 """ 

1824 c = self 

1825 c.gotoCommands.find_file_line(n) 

1826 

1827 def goToScriptLineNumber(self, n, p): 

1828 """ 

1829 Go to line n (zero-based) of a script. 

1830 A convenience method called from g.handleScriptException. 

1831 """ 

1832 c = self 

1833 c.gotoCommands.find_script_line(n, p) 

1834 #@+node:ekr.20090103070824.9: *4* c.setFileTimeStamp 

1835 def setFileTimeStamp(self, fn): 

1836 """Update the timestamp for fn..""" 

1837 # c = self 

1838 if g.app.externalFilesController: 

1839 g.app.externalFilesController.set_time(fn) 

1840 #@+node:ekr.20031218072017.3000: *4* c.updateSyntaxColorer 

1841 def updateSyntaxColorer(self, v): 

1842 self.frame.body.updateSyntaxColorer(v) 

1843 #@+node:ekr.20180503110307.1: *4* c.interactive* 

1844 #@+node:ekr.20180504075937.1: *5* c.interactive 

1845 def interactive(self, callback, event, prompts): 

1846 #@+<< c.interactive docstring >> 

1847 #@+node:ekr.20180503131222.1: *6* << c.interactive docstring >> 

1848 """ 

1849 c.interactive: Prompt for up to three arguments from the minibuffer. 

1850 

1851 The number of prompts determines the number of arguments. 

1852 

1853 Use the @command decorator to define commands. Examples: 

1854 

1855 @g.command('i3') 

1856 def i3_command(event): 

1857 c = event.get('c') 

1858 if not c: return 

1859 

1860 def callback(args, c, event): 

1861 g.trace(args) 

1862 c.bodyWantsFocus() 

1863 

1864 c.interactive(callback, event, 

1865 prompts=['Arg1: ', ' Arg2: ', ' Arg3: ']) 

1866 """ 

1867 #@-<< c.interactive docstring >> 

1868 # 

1869 # This pathetic code should be generalized, 

1870 # but it's not as easy as one might imagine. 

1871 c = self 

1872 d = {1: c.interactive1, 2: c.interactive2, 3: c.interactive3,} 

1873 f = d.get(len(prompts)) 

1874 if f: 

1875 f(callback, event, prompts) 

1876 else: 

1877 g.trace('At most 3 arguments are supported.') 

1878 #@+node:ekr.20180503111213.1: *5* c.interactive1 

1879 def interactive1(self, callback, event, prompts): 

1880 

1881 c, k = self, self.k 

1882 prompt = prompts[0] 

1883 

1884 def state1(event): 

1885 callback(args=[k.arg], c=c, event=event) 

1886 k.clearState() 

1887 k.resetLabel() 

1888 k.showStateAndMode() 

1889 

1890 k.setLabelBlue(prompt) 

1891 k.get1Arg(event, handler=state1) 

1892 #@+node:ekr.20180503111249.1: *5* c.interactive2 

1893 def interactive2(self, callback, event, prompts): 

1894 

1895 c, d, k = self, {}, self.k 

1896 prompt1, prompt2 = prompts 

1897 

1898 def state1(event): 

1899 d['arg1'] = k.arg 

1900 k.extendLabel(prompt2, select=False, protect=True) 

1901 k.getNextArg(handler=state2) 

1902 

1903 def state2(event): 

1904 callback(args=[d.get('arg1'), k.arg], c=c, event=event) 

1905 k.clearState() 

1906 k.resetLabel() 

1907 k.showStateAndMode() 

1908 

1909 k.setLabelBlue(prompt1) 

1910 k.get1Arg(event, handler=state1) 

1911 #@+node:ekr.20180503111249.2: *5* c.interactive3 

1912 def interactive3(self, callback, event, prompts): 

1913 

1914 c, d, k = self, {}, self.k 

1915 prompt1, prompt2, prompt3 = prompts 

1916 

1917 def state1(event): 

1918 d['arg1'] = k.arg 

1919 k.extendLabel(prompt2, select=False, protect=True) 

1920 k.getNextArg(handler=state2) 

1921 

1922 def state2(event): 

1923 d['arg2'] = k.arg 

1924 k.extendLabel(prompt3, select=False, protect=True) 

1925 k.get1Arg(event, handler=state3) # Restart. 

1926 

1927 def state3(event): 

1928 args = [d.get('arg1'), d.get('arg2'), k.arg] 

1929 callback(args=args, c=c, event=event) 

1930 k.clearState() 

1931 k.resetLabel() 

1932 k.showStateAndMode() 

1933 

1934 k.setLabelBlue(prompt1) 

1935 k.get1Arg(event, handler=state1) 

1936 #@+node:ekr.20080901124540.1: *3* c.Directive scanning 

1937 # These are all new in Leo 4.5.1. 

1938 #@+node:ekr.20171123135625.33: *4* c.getLanguageAtCursor 

1939 def getLanguageAtCursor(self, p, language): 

1940 """ 

1941 Return the language in effect at the present insert point. 

1942 Use the language argument as a default if no @language directive seen. 

1943 """ 

1944 c = self 

1945 tag = '@language' 

1946 w = c.frame.body.wrapper 

1947 ins = w.getInsertPoint() 

1948 n = 0 

1949 for s in g.splitLines(p.b): 

1950 if g.match_word(s, 0, tag): 

1951 i = g.skip_ws(s, len(tag)) 

1952 j = g.skip_id(s, i) 

1953 language = s[i:j] 

1954 if n <= ins < n + len(s): 

1955 break 

1956 else: 

1957 n += len(s) 

1958 return language 

1959 #@+node:ekr.20081006100835.1: *4* c.getNodePath & c.getNodeFileName 

1960 def getNodePath(self, p): 

1961 """Return the path in effect at node p.""" 

1962 c = self 

1963 aList = g.get_directives_dict_list(p) 

1964 path = c.scanAtPathDirectives(aList) 

1965 return path 

1966 

1967 def getNodeFileName(self, p): 

1968 """ 

1969 Return the full file name at node p, 

1970 including effects of all @path directives. 

1971 Return '' if p is no kind of @file node. 

1972 """ 

1973 c = self 

1974 for p in p.self_and_parents(copy=False): 

1975 name = p.anyAtFileNodeName() 

1976 if name: 

1977 return g.fullPath(c, p) # #1914. 

1978 return '' 

1979 #@+node:ekr.20171123135625.32: *4* c.hasAmbiguousLanguage 

1980 def hasAmbiguousLanguage(self, p): 

1981 """Return True if p.b contains different @language directives.""" 

1982 # c = self 

1983 languages, tag = set(), '@language' 

1984 for s in g.splitLines(p.b): 

1985 if g.match_word(s, 0, tag): 

1986 i = g.skip_ws(s, len(tag)) 

1987 j = g.skip_id(s, i) 

1988 word = s[i:j] 

1989 languages.add(word) 

1990 return len(list(languages)) > 1 

1991 #@+node:ekr.20080827175609.39: *4* c.scanAllDirectives 

1992 #@@nobeautify 

1993 

1994 def scanAllDirectives(self, p): 

1995 """ 

1996 Scan p and ancestors for directives. 

1997 

1998 Returns a dict containing the results, including defaults. 

1999 """ 

2000 c = self 

2001 p = p or c.p 

2002 # Defaults... 

2003 default_language = g.getLanguageFromAncestorAtFileNode(p) or c.target_language or 'python' 

2004 default_delims = g.set_delims_from_language(default_language) 

2005 wrap = c.config.getBool("body-pane-wraps") 

2006 table = ( # type:ignore 

2007 ('encoding', None, g.scanAtEncodingDirectives), 

2008 ('lang-dict', {}, g.scanAtCommentAndAtLanguageDirectives), 

2009 ('lineending', None, g.scanAtLineendingDirectives), 

2010 ('pagewidth', c.page_width, g.scanAtPagewidthDirectives), 

2011 ('path', None, c.scanAtPathDirectives), 

2012 ('tabwidth', c.tab_width, g.scanAtTabwidthDirectives), 

2013 ('wrap', wrap, g.scanAtWrapDirectives), 

2014 ) 

2015 # Set d by scanning all directives. 

2016 aList = g.get_directives_dict_list(p) 

2017 d = {} 

2018 for key, default, func in table: 

2019 val = func(aList) # type:ignore 

2020 d[key] = default if val is None else val 

2021 # Post process: do *not* set commander ivars. 

2022 lang_dict = d.get('lang-dict') 

2023 d = { 

2024 "delims": lang_dict.get('delims') or default_delims, 

2025 "comment": lang_dict.get('comment'), # Leo 6.4: New. 

2026 "encoding": d.get('encoding'), 

2027 # Note: at.scanAllDirectives does not use the defaults for "language". 

2028 "language": lang_dict.get('language') or default_language, 

2029 "lang-dict": lang_dict, # Leo 6.4: New. 

2030 "lineending": d.get('lineending'), 

2031 "pagewidth": d.get('pagewidth'), 

2032 "path": d.get('path'), # Redundant: or g.getBaseDirectory(c), 

2033 "tabwidth": d.get('tabwidth'), 

2034 "wrap": d.get('wrap'), 

2035 } 

2036 return d 

2037 #@+node:ekr.20080828103146.15: *4* c.scanAtPathDirectives 

2038 def scanAtPathDirectives(self, aList): 

2039 """ 

2040 Scan aList for @path directives. 

2041 Return a reasonable default if no @path directive is found. 

2042 """ 

2043 c = self 

2044 c.scanAtPathDirectivesCount += 1 # An important statistic. 

2045 # Step 1: Compute the starting path. 

2046 # The correct fallback directory is the absolute path to the base. 

2047 if c.openDirectory: # Bug fix: 2008/9/18 

2048 base = c.openDirectory 

2049 else: 

2050 base = g.app.config.relative_path_base_directory 

2051 if base and base == "!": 

2052 base = g.app.loadDir 

2053 elif base and base == ".": 

2054 base = c.openDirectory 

2055 base = c.expand_path_expression(base) # #1341. 

2056 base = g.os_path_expanduser(base) # #1889. 

2057 absbase = g.os_path_finalize_join(g.app.loadDir, base) # #1341. 

2058 # Step 2: look for @path directives. 

2059 paths = [] 

2060 for d in aList: 

2061 # Look for @path directives. 

2062 path = d.get('path') 

2063 warning = d.get('@path_in_body') 

2064 if path is not None: # retain empty paths for warnings. 

2065 # Convert "path" or <path> to path. 

2066 path = g.stripPathCruft(path) 

2067 if path and not warning: 

2068 path = c.expand_path_expression(path) # #1341. 

2069 path = g.os_path_expanduser(path) # #1889. 

2070 paths.append(path) 

2071 # We will silently ignore empty @path directives. 

2072 # Add absbase and reverse the list. 

2073 paths.append(absbase) 

2074 paths.reverse() 

2075 # Step 3: Compute the full, effective, absolute path. 

2076 path = g.os_path_finalize_join(*paths) # #1341. 

2077 return path or g.getBaseDirectory(c) # 2010/10/22: A useful default. 

2078 #@+node:ekr.20171123201514.1: *3* c.Executing commands & scripts 

2079 #@+node:ekr.20110605040658.17005: *4* c.check_event 

2080 def check_event(self, event): 

2081 """Check an event object.""" 

2082 # c = self 

2083 from leo.core import leoGui 

2084 

2085 if not event: 

2086 return 

2087 stroke = event.stroke 

2088 got = event.char 

2089 if g.unitTesting: 

2090 return 

2091 if stroke and (stroke.find('Alt+') > -1 or stroke.find('Ctrl+') > -1): 

2092 # Alas, Alt and Ctrl bindings must *retain* the char field, 

2093 # so there is no way to know what char field to expect. 

2094 expected = event.char 

2095 else: 

2096 # disable the test. 

2097 # We will use the (weird) key value for, say, Ctrl-s, 

2098 # if there is no binding for Ctrl-s. 

2099 expected = event.char 

2100 if not isinstance(event, leoGui.LeoKeyEvent): 

2101 if g.app.gui.guiName() not in ('browser', 'console', 'curses'): # #1839. 

2102 g.trace(f"not leo event: {event!r}, callers: {g.callers(8)}") 

2103 if expected != got: 

2104 g.trace(f"stroke: {stroke!r}, expected char: {expected!r}, got: {got!r}") 

2105 #@+node:ekr.20031218072017.2817: *4* c.doCommand 

2106 command_count = 0 

2107 

2108 def doCommand(self, command_func, command_name, event): 

2109 """ 

2110 Execute the given command function, invoking hooks and catching exceptions. 

2111 

2112 The code assumes that the "command1" hook has completely handled the 

2113 command func if g.doHook("command1") returns False. This provides a 

2114 simple mechanism for overriding commands. 

2115 """ 

2116 c, p = self, self.p 

2117 c.setLog() 

2118 self.command_count += 1 

2119 # New in Leo 6.2. Set command_function and command_name ivars. 

2120 self.command_function = command_func 

2121 self.command_name = command_name 

2122 # The presence of this message disables all commands. 

2123 if c.disableCommandsMessage: 

2124 g.blue(c.disableCommandsMessage) 

2125 return None 

2126 if c.exists and c.inCommand and not g.unitTesting: 

2127 g.app.commandInterruptFlag = True # For sc.make_slide_show_command. 

2128 # 1912: This message is annoying and unhelpful. 

2129 # g.error('ignoring command: already executing a command.') 

2130 return None 

2131 g.app.commandInterruptFlag = False 

2132 # #2256: Update the list of recent commands. 

2133 if len(c.recent_commands_list) > 99: 

2134 c.recent_commands_list.pop() 

2135 c.recent_commands_list.insert(0, command_name) 

2136 if not g.doHook("command1", c=c, p=p, label=command_name): 

2137 try: 

2138 c.inCommand = True 

2139 try: 

2140 return_value = command_func(event) 

2141 except Exception: 

2142 g.es_exception() 

2143 return_value = None 

2144 if c and c.exists: # Be careful: the command could destroy c. 

2145 c.inCommand = False 

2146 ## c.k.funcReturn = return_value 

2147 except Exception: 

2148 c.inCommand = False 

2149 if g.unitTesting: 

2150 raise 

2151 g.es_print("exception executing command") 

2152 g.es_exception(c=c) 

2153 if c and c.exists: 

2154 if c.requestCloseWindow: 

2155 c.requestCloseWindow = False 

2156 g.app.closeLeoWindow(c.frame) 

2157 else: 

2158 c.outerUpdate() 

2159 # Be careful: the command could destroy c. 

2160 if c and c.exists: 

2161 p = c.p 

2162 g.doHook("command2", c=c, p=p, label=command_name) 

2163 return return_value 

2164 #@+node:ekr.20200522075411.1: *4* c.doCommandByName 

2165 def doCommandByName(self, command_name, event): 

2166 """ 

2167 Execute one command, given the name of the command. 

2168 

2169 The caller must do any required keystroke-only tasks. 

2170 

2171 Return the result, if any, of the command. 

2172 """ 

2173 c = self 

2174 # Get the command's function. 

2175 command_func = c.commandsDict.get(command_name.replace('&', '')) 

2176 if not command_func: 

2177 message = f"no command function for {command_name!r}" 

2178 if g.unitTesting or g.app.inBridge: 

2179 raise AttributeError(message) 

2180 g.es_print(message, color='red') 

2181 g.trace(g.callers()) 

2182 return None 

2183 # Invoke the function. 

2184 val = c.doCommand(command_func, command_name, event) 

2185 if c.exists: 

2186 c.frame.updateStatusLine() 

2187 return val 

2188 #@+node:ekr.20200526074132.1: *4* c.executeMinibufferCommand 

2189 def executeMinibufferCommand(self, commandName): 

2190 """Call c.doCommandByName, creating the required event.""" 

2191 c = self 

2192 event = g.app.gui.create_key_event(c) 

2193 return c.doCommandByName(commandName, event) 

2194 #@+node:ekr.20210305133229.1: *4* c.general_script_helper & helpers 

2195 #@@nobeautify 

2196 

2197 def general_script_helper(self, command, ext, language, root, directory=None, regex=None): 

2198 """ 

2199 The official helper for the execute-general-script command. 

2200 

2201 c: The Commander of the outline. 

2202 command: The os command to execute the script. 

2203 directory: Optional: Change to this directory before executing command. 

2204 ext: The file extention for the temporary file. 

2205 language: The language name. 

2206 regex: Optional regular expression describing error messages. 

2207 If present, group(1) should evaluate to a line number. 

2208 May be a compiled regex expression or a string. 

2209 root: The root of the tree containing the script, 

2210 The script may contain section references and @others. 

2211 

2212 Other features: 

2213 

2214 - Create a temporary external file if `not root.isAnyAtFileNode()`. 

2215 - Compute the final command as follows. 

2216 1. If command contains <FILE>, replace <FILE> with the full path. 

2217 2. If command contains <NO-FILE>, just remove <NO-FILE>. 

2218 This allows, for example, `go run .` to work as expected. 

2219 3. Append the full path to the command. 

2220 """ 

2221 c, log = self, self.frame.log 

2222 #@+others # Define helper functions 

2223 #@+node:ekr.20210529142153.1: *5* function: put_line 

2224 def put_line(s): 

2225 """ 

2226 Put the line, creating a clickable link if the regex matches. 

2227 """ 

2228 if not regex: 

2229 g.es_print(s) 

2230 return 

2231 # Get the line number. 

2232 m = regex.match(s) 

2233 if not m: 

2234 g.es_print(s) 

2235 return 

2236 # If present, the regex should define two groups. 

2237 try: 

2238 s1 = m.group(1) 

2239 s2 = m.group(2) 

2240 except IndexError: 

2241 g.es_print(f"Regex {regex.pattern()} must define two groups") 

2242 return 

2243 if s1.isdigit(): 

2244 n = int(s1) 

2245 fn = s2 

2246 elif s2.isdigit(): 

2247 n = int(s2) 

2248 fn = s1 

2249 else: 

2250 # No line number. 

2251 g.es_print(s) 

2252 return 

2253 s = s.replace(root_path, root.h) 

2254 # Print to the console. 

2255 print(s) 

2256 # Find the node and offset corresponding to line n. 

2257 p, n2 = find_line(fn, n) 

2258 # Create the link. 

2259 unl = p.get_UNL() 

2260 if unl: 

2261 log.put(s + '\n', nodeLink=f"{unl}::{n2}") # local line. 

2262 else: 

2263 log.put(s + '\n') 

2264 #@+node:ekr.20210529164957.1: *5* function: find_line 

2265 def find_line(path, n): 

2266 """ 

2267 Return the node corresponding to line n of external file given by path. 

2268 """ 

2269 if path == root_path: 

2270 p, offset, found = c.gotoCommands.find_file_line(n, root) 

2271 else: 

2272 # Find an @<file> node with the given path. 

2273 found = False 

2274 for p in c.all_positions(): 

2275 if p.isAnyAtFileNode(): 

2276 norm_path = os.path.normpath(g.fullPath(c, p)) 

2277 if path == norm_path: 

2278 p, offset, found = c.gotoCommands.find_file_line(n, p) 

2279 break 

2280 if found: 

2281 return p, offset 

2282 return root, n 

2283 #@-others 

2284 # Compile and check the regex. 

2285 if regex: 

2286 if isinstance(regex, str): 

2287 try: 

2288 regex = re.compile(regex) 

2289 except Exception: 

2290 g.trace(f"Bad regex: {regex!s}") 

2291 return None 

2292 # Get the script. 

2293 script = g.getScript(c, root, 

2294 useSelectedText=False, 

2295 forcePythonSentinels=False, # language=='python', 

2296 useSentinels=True, 

2297 ) 

2298 # Create a temp file if root is not an @<file> node. 

2299 use_temp = not root.isAnyAtFileNode() 

2300 if use_temp: 

2301 fd, root_path = tempfile.mkstemp(suffix=ext, prefix="") 

2302 with os.fdopen(fd, 'w') as f: 

2303 f.write(script) 

2304 else: 

2305 root_path = g.fullPath(c, root) 

2306 # Compute the final command. 

2307 if '<FILE>' in command: 

2308 final_command = command.replace('<FILE>', root_path) 

2309 elif '<NO-FILE>' in command: 

2310 final_command = command.replace('<NO-FILE>', '').replace(root_path, '') 

2311 else: 

2312 final_command = f"{command} {root_path}" 

2313 # Change directory. 

2314 old_dir = os.path.abspath(os.path.curdir) 

2315 if not directory: 

2316 directory = os.path.dirname(root_path) 

2317 os.chdir(directory) 

2318 # Execute the final command. 

2319 try: 

2320 proc = subprocess.Popen(final_command, 

2321 shell=True, 

2322 stdout=subprocess.PIPE, 

2323 stderr=subprocess.PIPE) 

2324 out, err = proc.communicate() 

2325 for s in g.splitLines(g.toUnicode(out)): 

2326 print(s.rstrip()) 

2327 print('') 

2328 for s in g.splitLines(g.toUnicode(err)): 

2329 put_line(s.rstrip()) 

2330 finally: 

2331 if use_temp: 

2332 os.remove(root_path) 

2333 os.chdir(old_dir) 

2334 #@+node:ekr.20200523135601.1: *4* c.insertCharFromEvent 

2335 def insertCharFromEvent(self, event): 

2336 """ 

2337 Handle the character given by event, ignoring various special keys: 

2338 - getArg state: k.getArg. 

2339 - Tree: onCanvasKey or onHeadlineKey. 

2340 - Body: ec.selfInsertCommand 

2341 - Log: log_w.insert 

2342 """ 

2343 trace = all(z in g.app.debug for z in ('keys', 'verbose')) 

2344 c, k, w = self, self.k, event.widget 

2345 name = c.widget_name(w) 

2346 stroke = event.stroke 

2347 if trace: 

2348 g.trace('stroke', stroke, 'plain:', k.isPlainKey(stroke), 'widget', name) 

2349 if not stroke: 

2350 return 

2351 # 

2352 # Part 1: Very late special cases. 

2353 # 

2354 # #1448 

2355 if stroke.isNumPadKey() and k.state.kind == 'getArg': 

2356 stroke.removeNumPadModifier() 

2357 k.getArg(event, stroke=stroke) 

2358 return 

2359 # Handle all unbound characters in command mode. 

2360 if k.unboundKeyAction == 'command': 

2361 w = g.app.gui.get_focus(c) 

2362 if w and g.app.gui.widget_name(w).lower().startswith('canvas'): 

2363 c.onCanvasKey(event) 

2364 return 

2365 # 

2366 # Part 2: Filter out keys that should never be inserted by default. 

2367 # 

2368 # Ignore unbound F-keys. 

2369 if stroke.isFKey(): 

2370 return 

2371 # Ignore unbound Alt/Ctrl keys. 

2372 if stroke.isAltCtrl(): 

2373 if not k.enable_alt_ctrl_bindings: 

2374 return 

2375 if k.ignore_unbound_non_ascii_keys: 

2376 return 

2377 # #868 

2378 if stroke.isPlainNumPad(): 

2379 stroke.removeNumPadModifier() 

2380 event.stroke = stroke 

2381 # #868 

2382 if stroke.isNumPadKey(): 

2383 return 

2384 # Ignore unbound non-ascii character. 

2385 if k.ignore_unbound_non_ascii_keys and not stroke.isPlainKey(): 

2386 return 

2387 # Never insert escape or insert characters. 

2388 if 'Escape' in stroke.s or 'Insert' in stroke.s: 

2389 return 

2390 # 

2391 # Part 3: Handle the event depending on the pane and state. 

2392 # 

2393 # Handle events in the body pane. 

2394 if name.startswith('body'): 

2395 action = k.unboundKeyAction 

2396 if action in ('insert', 'overwrite'): 

2397 c.editCommands.selfInsertCommand(event, action=action) 

2398 c.frame.updateStatusLine() 

2399 return 

2400 # 

2401 # Handle events in headlines. 

2402 if name.startswith('head'): 

2403 c.frame.tree.onHeadlineKey(event) 

2404 return 

2405 # 

2406 # Handle events in the background tree (not headlines). 

2407 if name.startswith('canvas'): 

2408 if event.char: 

2409 k.searchTree(event.char) 

2410 # Not exactly right, but it seems to be good enough. 

2411 elif not stroke: 

2412 c.onCanvasKey(event) 

2413 return 

2414 # 

2415 # Ignore all events outside the log pane. 

2416 if not name.startswith('log'): 

2417 return 

2418 # 

2419 # Make sure we can insert into w. 

2420 log_w = event.widget 

2421 if not hasattr(log_w, 'supportsHighLevelInterface'): 

2422 return 

2423 # 

2424 # Send the event to the text widget, not the LeoLog instance. 

2425 i = log_w.getInsertPoint() 

2426 s = stroke.toGuiChar() 

2427 log_w.insert(i, s) 

2428 #@+node:ekr.20131016084446.16724: *4* c.setComplexCommand 

2429 def setComplexCommand(self, commandName): 

2430 """Make commandName the command to be executed by repeat-complex-command.""" 

2431 c = self 

2432 c.k.mb_history.insert(0, commandName) 

2433 #@+node:bobjack.20080509080123.2: *4* c.universalCallback & minibufferCallback 

2434 def universalCallback(self, source_c, function): 

2435 """Create a universal command callback. 

2436 

2437 Create and return a callback that wraps a function with an rClick 

2438 signature in a callback which adapts standard minibuffer command 

2439 callbacks to a compatible format. 

2440 

2441 This also serves to allow rClick callback functions to handle 

2442 minibuffer commands from sources other than rClick menus so allowing 

2443 a single function to handle calls from all sources. 

2444 

2445 A function wrapped in this wrapper can handle rclick generator 

2446 and invocation commands and commands typed in the minibuffer. 

2447 

2448 It will also be able to handle commands from the minibuffer even 

2449 if rclick is not installed. 

2450 """ 

2451 

2452 def minibufferCallback(event, function=function): 

2453 # Avoid a pylint complaint. 

2454 if hasattr(self, 'theContextMenuController'): 

2455 cm = getattr(self, 'theContextMenuController') 

2456 keywords = cm.mb_keywords 

2457 else: 

2458 cm = keywords = None 

2459 if not keywords: 

2460 # If rClick is not loaded or no keywords dict was provided 

2461 # then the command must have been issued in a minibuffer 

2462 # context. 

2463 keywords = {'c': self, 'rc_phase': 'minibuffer'} 

2464 keywords['mb_event'] = event 

2465 retval = None 

2466 try: 

2467 retval = function(keywords) 

2468 finally: 

2469 if cm: 

2470 # Even if there is an error: 

2471 # clear mb_keywords prior to next command and 

2472 # ensure mb_retval from last command is wiped 

2473 cm.mb_keywords = None 

2474 cm.mb_retval = retval 

2475 

2476 minibufferCallback.__doc__ = function.__doc__ # For g.getDocStringForFunction 

2477 minibufferCallback.source_c = source_c # For GetArgs.command_source 

2478 return minibufferCallback 

2479 

2480 # fix bobjack's spelling error. 

2481 universallCallback = universalCallback 

2482 #@+node:ekr.20070115135502: *4* c.writeScriptFile (changed: does not expand expressions) 

2483 def writeScriptFile(self, script): 

2484 

2485 # Get the path to the file. 

2486 c = self 

2487 path = c.config.getString('script-file-path') 

2488 if path: 

2489 isAbsPath = os.path.isabs(path) 

2490 driveSpec, path = os.path.splitdrive(path) 

2491 parts = path.split('/') 

2492 # xxx bad idea, loadDir is often read only! 

2493 path = g.app.loadDir 

2494 if isAbsPath: 

2495 # make the first element absolute 

2496 parts[0] = driveSpec + os.sep + parts[0] 

2497 allParts = [path] + parts 

2498 path = g.os_path_finalize_join(*allParts) # #1431 

2499 else: 

2500 path = g.os_path_finalize_join(g.app.homeLeoDir, 'scriptFile.py') # #1431 

2501 # 

2502 # Write the file. 

2503 try: 

2504 with open(path, encoding='utf-8', mode='w') as f: 

2505 f.write(script) 

2506 except Exception: 

2507 g.es_exception() 

2508 g.es(f"Failed to write script to {path}") 

2509 # g.es("Check your configuration of script_file_path, currently %s" % 

2510 # c.config.getString('script-file-path')) 

2511 path = None 

2512 return path 

2513 #@+node:ekr.20190921130036.1: *3* c.expand_path_expression 

2514 def expand_path_expression(self, s): 

2515 """Expand all {{anExpression}} in c's context.""" 

2516 c = self 

2517 if not s: 

2518 return '' 

2519 s = g.toUnicode(s) 

2520 # find and replace repeated path expressions 

2521 previ, aList = 0, [] 

2522 while previ < len(s): 

2523 i = s.find('{{', previ) 

2524 j = s.find('}}', previ) 

2525 if -1 < i < j: 

2526 # Add anything from previous index up to '{{' 

2527 if previ < i: 

2528 aList.append(s[previ:i]) 

2529 # Get expression and find substitute 

2530 exp = s[i + 2 : j].strip() 

2531 if exp: 

2532 try: 

2533 s2 = c.replace_path_expression(exp) 

2534 aList.append(s2) 

2535 except Exception: 

2536 g.es(f"Exception evaluating {{{{{exp}}}}} in {s.strip()}") 

2537 g.es_exception(full=True, c=c) 

2538 # Prepare to search again after the last '}}' 

2539 previ = j + 2 

2540 else: 

2541 # Add trailing fragment (fragile in case of mismatched '{{'/'}}') 

2542 aList.append(s[previ:]) 

2543 break 

2544 val = ''.join(aList) 

2545 if g.isWindows: 

2546 val = val.replace('\\', '/') 

2547 return val 

2548 #@+node:ekr.20190921130036.2: *4* c.replace_path_expression 

2549 replace_errors: List[str] = [] 

2550 

2551 def replace_path_expression(self, expr): 

2552 """ local function to replace a single path expression.""" 

2553 c = self 

2554 d = { 

2555 'c': c, 

2556 'g': g, 

2557 # 'getString': c.config.getString, 

2558 'p': c.p, 

2559 'os': os, 

2560 'sep': os.sep, 

2561 'sys': sys, 

2562 } 

2563 # #1338: Don't report errors when called by g.getUrlFromNode. 

2564 try: 

2565 # pylint: disable=eval-used 

2566 path = eval(expr, d) 

2567 return g.toUnicode(path, encoding='utf-8') 

2568 except Exception as e: 

2569 message = ( 

2570 f"{c.shortFileName()}: {c.p.h}\n" 

2571 f"expression: {expr!s}\n" 

2572 f" error: {e!s}") 

2573 if message not in self.replace_errors: 

2574 self.replace_errors.append(message) 

2575 g.trace(message) 

2576 return expr 

2577 #@+node:ekr.20171124101444.1: *3* c.File 

2578 #@+node:ekr.20200305104646.1: *4* c.archivedPositionToPosition (new) 

2579 def archivedPositionToPosition(self, s): 

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

2581 c = self 

2582 s = g.toUnicode(s) 

2583 aList = s.split(',') 

2584 try: 

2585 aList = [int(z) for z in aList] 

2586 except Exception: 

2587 aList = None 

2588 if not aList: 

2589 return None 

2590 p = c.rootPosition() 

2591 level = 0 

2592 while level < len(aList): 

2593 i = aList[level] 

2594 while i > 0: 

2595 if p.hasNext(): 

2596 p.moveToNext() 

2597 i -= 1 

2598 else: 

2599 return None 

2600 level += 1 

2601 if level < len(aList): 

2602 p.moveToFirstChild() 

2603 return p 

2604 #@+node:ekr.20150422080541.1: *4* c.backup 

2605 def backup(self, fileName=None, prefix=None, silent=False, useTimeStamp=True): 

2606 """ 

2607 Back up given fileName or c.fileName(). 

2608 If useTimeStamp is True, append a timestamp to the filename. 

2609 """ 

2610 c = self 

2611 fn = fileName or c.fileName() 

2612 if not fn: 

2613 return None 

2614 theDir, base = g.os_path_split(fn) 

2615 if useTimeStamp: 

2616 if base.endswith('.leo'): 

2617 base = base[:-4] 

2618 stamp = time.strftime("%Y%m%d-%H%M%S") 

2619 branch = prefix + '-' if prefix else '' 

2620 fn = f"{branch}{base}-{stamp}.leo" 

2621 path = g.os_path_finalize_join(theDir, fn) 

2622 else: 

2623 path = fn 

2624 if path: 

2625 # pylint: disable=no-member 

2626 # Defined in commanderFileCommands.py. 

2627 c.saveTo(fileName=path, silent=silent) # Issues saved message. 

2628 # g.es('in', theDir) 

2629 return path 

2630 #@+node:ekr.20180210092235.1: *4* c.backup_helper 

2631 def backup_helper(self, 

2632 base_dir=None, 

2633 env_key='LEO_BACKUP', 

2634 sub_dir=None, 

2635 use_git_prefix=True, 

2636 ): 

2637 """ 

2638 A helper for scripts that back up a .leo file. 

2639 Use os.environ[env_key] as the base_dir only if base_dir is not given. 

2640 Backup to base_dir or join(base_dir, sub_dir). 

2641 """ 

2642 c = self 

2643 old_cwd = os.getcwd() 

2644 join = g.os_path_finalize_join 

2645 if not base_dir: 

2646 if env_key: 

2647 try: 

2648 base_dir = os.environ[env_key] 

2649 except KeyError: 

2650 print(f"No environment var: {env_key}") 

2651 base_dir = None 

2652 if base_dir and g.os_path_exists(base_dir): 

2653 if use_git_prefix: 

2654 git_branch, junk = g.gitInfo() 

2655 else: 

2656 git_branch = None 

2657 theDir, fn = g.os_path_split(c.fileName()) 

2658 backup_dir = join(base_dir, sub_dir) if sub_dir else base_dir 

2659 path = join(backup_dir, fn) 

2660 if g.os_path_exists(backup_dir): 

2661 written_fn = c.backup( 

2662 path, 

2663 prefix=git_branch, 

2664 silent=True, 

2665 useTimeStamp=True, 

2666 ) 

2667 g.es_print(f"wrote: {written_fn}") 

2668 else: 

2669 g.es_print(f"backup_dir not found: {backup_dir!r}") 

2670 else: 

2671 g.es_print(f"base_dir not found: {base_dir!r}") 

2672 os.chdir(old_cwd) 

2673 #@+node:ekr.20090103070824.11: *4* c.checkFileTimeStamp 

2674 def checkFileTimeStamp(self, fn): 

2675 """ 

2676 Return True if the file given by fn has not been changed 

2677 since Leo read it or if the user agrees to overwrite it. 

2678 """ 

2679 c = self 

2680 if g.app.externalFilesController: 

2681 return g.app.externalFilesController.check_overwrite(c, fn) 

2682 return True 

2683 #@+node:ekr.20090212054250.9: *4* c.createNodeFromExternalFile 

2684 def createNodeFromExternalFile(self, fn): 

2685 """ 

2686 Read the file into a node. 

2687 Return None, indicating that c.open should set focus. 

2688 """ 

2689 c = self 

2690 s, e = g.readFileIntoString(fn) 

2691 if s is None: 

2692 return 

2693 head, ext = g.os_path_splitext(fn) 

2694 if ext.startswith('.'): 

2695 ext = ext[1:] 

2696 language = g.app.extension_dict.get(ext) 

2697 if language: 

2698 prefix = f"@color\n@language {language}\n\n" 

2699 else: 

2700 prefix = '@killcolor\n\n' 

2701 # pylint: disable=no-member 

2702 # Defined in commanderOutlineCommands.py 

2703 p2 = c.insertHeadline(op_name='Open File', as_child=False) 

2704 p2.h = f"@edit {fn}" 

2705 p2.b = prefix + s 

2706 w = c.frame.body.wrapper 

2707 if w: 

2708 w.setInsertPoint(0) 

2709 c.redraw() 

2710 c.recolor() 

2711 #@+node:ekr.20110530124245.18248: *4* c.looksLikeDerivedFile 

2712 def looksLikeDerivedFile(self, fn): 

2713 """ 

2714 Return True if fn names a file that looks like an 

2715 external file written by Leo. 

2716 """ 

2717 # c = self 

2718 try: 

2719 with open(fn, 'rb') as f: # 2020/11/14: Allow unicode characters! 

2720 b = f.read() 

2721 s = g.toUnicode(b) 

2722 return s.find('@+leo-ver=') > -1 

2723 except Exception: 

2724 g.es_exception() 

2725 return False 

2726 #@+node:ekr.20031218072017.2925: *4* c.markAllAtFileNodesDirty 

2727 def markAllAtFileNodesDirty(self, event=None): 

2728 """Mark all @file nodes as changed.""" 

2729 c = self 

2730 c.endEditing() 

2731 p = c.rootPosition() 

2732 while p: 

2733 if p.isAtFileNode(): 

2734 p.setDirty() 

2735 c.setChanged() 

2736 p.moveToNodeAfterTree() 

2737 else: 

2738 p.moveToThreadNext() 

2739 c.redraw_after_icons_changed() 

2740 #@+node:ekr.20031218072017.2926: *4* c.markAtFileNodesDirty 

2741 def markAtFileNodesDirty(self, event=None): 

2742 """Mark all @file nodes in the selected tree as changed.""" 

2743 c = self 

2744 p = c.p 

2745 if not p: 

2746 return 

2747 c.endEditing() 

2748 after = p.nodeAfterTree() 

2749 while p and p != after: 

2750 if p.isAtFileNode(): 

2751 p.setDirty() 

2752 c.setChanged() 

2753 p.moveToNodeAfterTree() 

2754 else: 

2755 p.moveToThreadNext() 

2756 c.redraw_after_icons_changed() 

2757 #@+node:ekr.20031218072017.2823: *4* c.openWith 

2758 def openWith(self, event=None, d=None): 

2759 """ 

2760 This is *not* a command. 

2761 

2762 Handles the items in the Open With... menu. 

2763 

2764 See ExternalFilesController.open_with for details about d. 

2765 """ 

2766 c = self 

2767 if d and g.app.externalFilesController: 

2768 # Select an ancestor @<file> node if possible. 

2769 if not d.get('p'): 

2770 d['p'] = None 

2771 p = c.p 

2772 while p: 

2773 if p.isAnyAtFileNode(): 

2774 d['p'] = p 

2775 break 

2776 p.moveToParent() 

2777 g.app.externalFilesController.open_with(c, d) 

2778 elif not d: 

2779 g.trace('can not happen: no d', g.callers()) 

2780 #@+node:ekr.20140717074441.17770: *4* c.recreateGnxDict 

2781 def recreateGnxDict(self): 

2782 """Recreate the gnx dict prior to refreshing nodes from disk.""" 

2783 c, d = self, {} 

2784 for v in c.all_unique_nodes(): 

2785 gnxString = v.fileIndex 

2786 if isinstance(gnxString, str): 

2787 d[gnxString] = v 

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

2789 g.trace(c.shortFileName(), gnxString, v) 

2790 else: 

2791 g.internalError(f"no gnx for vnode: {v}") 

2792 c.fileCommands.gnxDict = d 

2793 #@+node:ekr.20180508111544.1: *3* c.Git 

2794 #@+node:ekr.20180510104805.1: *4* c.diff_file 

2795 def diff_file(self, fn, rev1='HEAD', rev2=''): 

2796 """ 

2797 Create an outline describing the git diffs for all files changed 

2798 between rev1 and rev2. 

2799 """ 

2800 from leo.commands import editFileCommands as efc 

2801 x = efc.GitDiffController(c=self) 

2802 x.diff_file(fn=fn, rev1=rev1, rev2=rev2) 

2803 #@+node:ekr.20180508110755.1: *4* c.diff_two_revs 

2804 def diff_two_revs(self, directory=None, rev1='', rev2=''): 

2805 """ 

2806 Create an outline describing the git diffs for all files changed 

2807 between rev1 and rev2. 

2808 """ 

2809 from leo.commands import editFileCommands as efc 

2810 efc.GitDiffController(c=self).diff_two_revs(rev1=rev1, rev2=rev2) 

2811 #@+node:ekr.20180510103923.1: *4* c.diff_two_branches 

2812 def diff_two_branches(self, branch1, branch2, fn): 

2813 """ 

2814 Create an outline describing the git diffs for all files changed 

2815 between rev1 and rev2. 

2816 """ 

2817 from leo.commands import editFileCommands as efc 

2818 efc.GitDiffController(c=self).diff_two_branches( 

2819 branch1=branch1, branch2=branch2, fn=fn) 

2820 #@+node:ekr.20180510105125.1: *4* c.git_diff 

2821 def git_diff(self, rev1='HEAD', rev2=''): 

2822 

2823 from leo.commands import editFileCommands as efc 

2824 efc.GitDiffController(c=self).git_diff(rev1, rev2) 

2825 #@+node:ekr.20171124100534.1: *3* c.Gui 

2826 #@+node:ekr.20111217154130.10286: *4* c.Dialogs & messages 

2827 #@+node:ekr.20110510052422.14618: *5* c.alert 

2828 def alert(self, message): 

2829 c = self 

2830 # The unit tests just tests the args. 

2831 if not g.unitTesting: 

2832 g.es(message) 

2833 g.app.gui.alert(c, message) 

2834 #@+node:ekr.20111217154130.10284: *5* c.init_error_dialogs 

2835 def init_error_dialogs(self): 

2836 c = self 

2837 c.import_error_nodes = [] 

2838 c.ignored_at_file_nodes = [] 

2839 c.orphan_at_file_nodes = [] 

2840 #@+node:ekr.20171123135805.1: *5* c.notValidInBatchMode 

2841 def notValidInBatchMode(self, commandName): 

2842 g.es('the', commandName, "command is not valid in batch mode") 

2843 #@+node:ekr.20110530082209.18250: *5* c.putHelpFor 

2844 def putHelpFor(self, s, short_title=''): 

2845 """Helper for various help commands.""" 

2846 c = self 

2847 g.app.gui.put_help(c, s, short_title) 

2848 #@+node:ekr.20111217154130.10285: *5* c.raise_error_dialogs 

2849 warnings_dict: Dict[str, bool] = {} 

2850 

2851 def raise_error_dialogs(self, kind='read'): 

2852 """Warn about read/write failures.""" 

2853 c = self 

2854 use_dialogs = False 

2855 if g.unitTesting: 

2856 c.init_error_dialogs() 

2857 return 

2858 # Issue one or two dialogs or messages. 

2859 saved_body = c.rootPosition().b # Save the root's body. The dialog destroys it! 

2860 if c.import_error_nodes or c.ignored_at_file_nodes or c.orphan_at_file_nodes: 

2861 g.app.gui.dismiss_splash_screen() 

2862 else: 

2863 # #1007: Exit now, so we don't have to restore c.rootPosition().b. 

2864 c.init_error_dialogs() 

2865 return 

2866 if c.import_error_nodes: 

2867 files = '\n'.join(sorted(set(c.import_error_nodes))) # type:ignore 

2868 if files not in self.warnings_dict: 

2869 self.warnings_dict[files] = True 

2870 import_message1 = 'The following were not imported properly.' 

2871 import_message2 = f"Inserted @ignore in...\n{files}" 

2872 g.es_print(import_message1, color='red') 

2873 g.es_print(import_message2) 

2874 if use_dialogs: 

2875 import_dialog_message = f"{import_message1}\n{import_message2}" 

2876 g.app.gui.runAskOkDialog(c, 

2877 message=import_dialog_message, title='Import errors') 

2878 if c.ignored_at_file_nodes: 

2879 files = '\n'.join(sorted(set(c.ignored_at_file_nodes))) # type:ignore 

2880 if files not in self.warnings_dict: 

2881 self.warnings_dict[files] = True 

2882 kind_s = 'read' if kind == 'read' else 'written' 

2883 ignored_message = f"The following were not {kind_s} because they contain @ignore:" 

2884 kind = 'read' if kind.startswith('read') else 'written' 

2885 g.es_print(ignored_message, color='red') 

2886 g.es_print(files) 

2887 if use_dialogs: 

2888 ignored_dialog_message = f"{ignored_message}\n{files}" 

2889 g.app.gui.runAskOkDialog(c, 

2890 message=ignored_dialog_message, title=f"Not {kind.capitalize()}") 

2891 # #1050: always raise a dialog for orphan @<file> nodes. 

2892 if c.orphan_at_file_nodes: 

2893 message = '\n'.join([ 

2894 'The following were not written because of errors:\n', 

2895 '\n'.join(sorted(set(c.orphan_at_file_nodes))), # type:ignore 

2896 '', 

2897 'Warning: changes to these files will be lost\n' 

2898 'unless you can save the files successfully.' 

2899 ]) 

2900 g.app.gui.runAskOkDialog(c, message=message, title='Not Written') 

2901 # Mark all the nodes dirty. 

2902 for z in c.all_unique_positions(): 

2903 if z.isOrphan(): 

2904 z.setDirty() 

2905 z.clearOrphan() 

2906 c.setChanged() 

2907 c.redraw() 

2908 # Restore the root position's body. 

2909 c.rootPosition().v.b = saved_body # #1007: just set v.b. 

2910 c.init_error_dialogs() 

2911 #@+node:ekr.20150710083827.1: *5* c.syntaxErrorDialog 

2912 def syntaxErrorDialog(self): 

2913 """Warn about syntax errors in files.""" 

2914 c = self 

2915 if g.app.syntax_error_files and c.config.getBool( 

2916 'syntax-error-popup', default=False): 

2917 aList = sorted(set(g.app.syntax_error_files)) 

2918 g.app.syntax_error_files = [] 

2919 list_s = '\n'.join(aList) 

2920 g.app.gui.runAskOkDialog( 

2921 c, 

2922 title='Python Errors', 

2923 message=f"Python errors in:\n\n{list_s}", 

2924 text="Ok", 

2925 ) 

2926 #@+node:ekr.20031218072017.2945: *4* c.Dragging 

2927 #@+node:ekr.20031218072017.2947: *5* c.dragToNthChildOf 

2928 def dragToNthChildOf(self, p, parent, n): 

2929 c, p, u = self, self.p, self.undoer 

2930 if not c.checkDrag(p, parent): 

2931 return 

2932 if not c.checkMoveWithParentWithWarning(p, parent, True): 

2933 return 

2934 c.endEditing() 

2935 undoData = u.beforeMoveNode(p) 

2936 p.setDirty() 

2937 p.moveToNthChildOf(parent, n) 

2938 p.setDirty() 

2939 c.setChanged() 

2940 u.afterMoveNode(p, 'Drag', undoData) 

2941 c.redraw(p) 

2942 c.updateSyntaxColorer(p) # Dragging can change syntax coloring. 

2943 #@+node:ekr.20031218072017.2353: *5* c.dragAfter 

2944 def dragAfter(self, p, after): 

2945 c, p, u = self, self.p, self.undoer 

2946 if not c.checkDrag(p, after): 

2947 return 

2948 if not c.checkMoveWithParentWithWarning(p, after.parent(), True): 

2949 return 

2950 c.endEditing() 

2951 undoData = u.beforeMoveNode(p) 

2952 p.setDirty() 

2953 p.moveAfter(after) 

2954 p.setDirty() 

2955 c.setChanged() 

2956 u.afterMoveNode(p, 'Drag', undoData) 

2957 c.redraw(p) 

2958 c.updateSyntaxColorer(p) # Dragging can change syntax coloring. 

2959 #@+node:ekr.20031218072017.2946: *5* c.dragCloneToNthChildOf 

2960 def dragCloneToNthChildOf(self, p, parent, n): 

2961 c = self 

2962 u = c.undoer 

2963 undoType = 'Clone Drag' 

2964 current = c.p 

2965 clone = p.clone() # Creates clone & dependents, does not set undo. 

2966 if ( 

2967 not c.checkDrag(p, parent) or 

2968 not c.checkMoveWithParentWithWarning(clone, parent, True) 

2969 ): 

2970 clone.doDelete(newNode=p) # Destroys clone and makes p the current node. 

2971 c.selectPosition(p) # Also sets root position. 

2972 return 

2973 c.endEditing() 

2974 undoData = u.beforeInsertNode(current) 

2975 clone.setDirty() 

2976 clone.moveToNthChildOf(parent, n) 

2977 clone.setDirty() 

2978 c.setChanged() 

2979 u.afterInsertNode(clone, undoType, undoData) 

2980 c.redraw(clone) 

2981 c.updateSyntaxColorer(clone) # Dragging can change syntax coloring. 

2982 #@+node:ekr.20031218072017.2948: *5* c.dragCloneAfter 

2983 def dragCloneAfter(self, p, after): 

2984 c = self 

2985 u = c.undoer 

2986 undoType = 'Clone Drag' 

2987 current = c.p 

2988 clone = p.clone() # Creates clone. Does not set undo. 

2989 if c.checkDrag( 

2990 p, after) and c.checkMoveWithParentWithWarning(clone, after.parent(), True): 

2991 c.endEditing() 

2992 undoData = u.beforeInsertNode(current) 

2993 clone.setDirty() 

2994 clone.moveAfter(after) 

2995 clone.v.setDirty() 

2996 c.setChanged() 

2997 u.afterInsertNode(clone, undoType, undoData) 

2998 p = clone 

2999 else: 

3000 clone.doDelete(newNode=p) 

3001 c.redraw(p) 

3002 c.updateSyntaxColorer(clone) # Dragging can change syntax coloring. 

3003 #@+node:ekr.20031218072017.2949: *4* c.Drawing 

3004 #@+node:ekr.20080514131122.8: *5* c.bringToFront 

3005 def bringToFront(self, c2=None): 

3006 c = self 

3007 c2 = c2 or c 

3008 g.app.gui.ensure_commander_visible(c2) 

3009 

3010 BringToFront = bringToFront # Compatibility with old scripts 

3011 #@+node:ekr.20040803072955.143: *5* c.expandAllAncestors 

3012 def expandAllAncestors(self, p): 

3013 """ 

3014 Expand all ancestors without redrawing. 

3015 Return a flag telling whether a redraw is needed. 

3016 """ 

3017 # c = self 

3018 redraw_flag = False 

3019 for p in p.parents(): 

3020 if not p.v.isExpanded(): 

3021 p.v.expand() 

3022 p.expand() 

3023 redraw_flag = True 

3024 elif p.isExpanded(): 

3025 p.v.expand() 

3026 else: 

3027 p.expand() 

3028 redraw_flag = True 

3029 return redraw_flag 

3030 #@+node:ekr.20080514131122.20: *5* c.outerUpdate 

3031 def outerUpdate(self): 

3032 """Handle delayed focus requests and modified events.""" 

3033 c = self 

3034 if not c.exists or not c.k: 

3035 return 

3036 # New in Leo 5.6: Delayed redraws are useful in utility methods. 

3037 if c.requestLaterRedraw: 

3038 if c.enableRedrawFlag: 

3039 c.requestLaterRedraw = False 

3040 if 'drawing' in g.app.debug and not g.unitTesting: 

3041 g.trace('\nDELAYED REDRAW') 

3042 time.sleep(1.0) 

3043 c.redraw() 

3044 # Delayed focus requests will always be useful. 

3045 if c.requestedFocusWidget: 

3046 w = c.requestedFocusWidget 

3047 if 'focus' in g.app.debug and not g.unitTesting: 

3048 if hasattr(w, 'objectName'): 

3049 name = w.objectName() 

3050 else: 

3051 name = w.__class__.__name__ 

3052 g.trace('DELAYED FOCUS', name) 

3053 c.set_focus(w) 

3054 c.requestedFocusWidget = None 

3055 table = ( 

3056 ("childrenModified", g.childrenModifiedSet), 

3057 ("contentModified", g.contentModifiedSet), 

3058 ) 

3059 for kind, mods in table: 

3060 if mods: 

3061 g.doHook(kind, c=c, nodes=mods) 

3062 mods.clear() 

3063 #@+node:ekr.20080514131122.13: *5* c.recolor 

3064 def recolor(self, p=None): 

3065 # Support QScintillaColorizer.colorize. 

3066 c = self 

3067 colorizer = c.frame.body.colorizer 

3068 if colorizer and hasattr(colorizer, 'colorize'): 

3069 colorizer.colorize(p or c.p) 

3070 

3071 recolor_now = recolor 

3072 #@+node:ekr.20080514131122.14: *5* c.redrawing... 

3073 #@+node:ekr.20170808014610.1: *6* c.enable/disable_redraw 

3074 def disable_redraw(self): 

3075 """Disable all redrawing until enabled.""" 

3076 c = self 

3077 c.enableRedrawFlag = False 

3078 

3079 def enable_redraw(self): 

3080 c = self 

3081 c.enableRedrawFlag = True 

3082 #@+node:ekr.20090110073010.1: *6* c.redraw 

3083 @cmd('redraw') 

3084 def redraw_command(self, event): 

3085 c = event.get('c') 

3086 if c: 

3087 c.redraw() 

3088 

3089 def redraw(self, p=None): 

3090 """ 

3091 Redraw the screen immediately. 

3092 If p is given, set c.p to p. 

3093 """ 

3094 c = self 

3095 # New in Leo 5.6: clear the redraw request. 

3096 c.requestLaterRedraw = False 

3097 if not p: 

3098 p = c.p or c.rootPosition() 

3099 if not p: 

3100 return 

3101 c.expandAllAncestors(p) 

3102 if p: 

3103 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1183855 

3104 # This looks redundant, but it is probably the only safe fix. 

3105 c.frame.tree.select(p) 

3106 # tree.redraw will change the position if p is a hoisted @chapter node. 

3107 p2 = c.frame.tree.redraw(p) 

3108 # Be careful. NullTree.redraw returns None. 

3109 # #503: NullTree.redraw(p) now returns p. 

3110 c.selectPosition(p2 or p) 

3111 # Do not call treeFocusHelper here. 

3112 # c.treeFocusHelper() 

3113 # Clear the redraw request, again. 

3114 c.requestLaterRedraw = False 

3115 

3116 # Compatibility with old scripts 

3117 

3118 force_redraw = redraw 

3119 redraw_now = redraw 

3120 #@+node:ekr.20090110073010.3: *6* c.redraw_after_icons_changed 

3121 def redraw_after_icons_changed(self): 

3122 """Update the icon for the presently selected node""" 

3123 c = self 

3124 if c.enableRedrawFlag: 

3125 c.frame.tree.redraw_after_icons_changed() 

3126 # Do not call treeFocusHelper here. 

3127 # c.treeFocusHelper() 

3128 else: 

3129 c.requestLaterRedraw = True 

3130 #@+node:ekr.20090110131802.2: *6* c.redraw_after_contract 

3131 def redraw_after_contract(self, p=None): 

3132 c = self 

3133 if c.enableRedrawFlag: 

3134 if p: 

3135 c.setCurrentPosition(p) 

3136 else: 

3137 p = c.currentPosition() 

3138 c.frame.tree.redraw_after_contract(p) 

3139 c.treeFocusHelper() 

3140 else: 

3141 c.requestLaterRedraw = True 

3142 #@+node:ekr.20090112065525.1: *6* c.redraw_after_expand 

3143 def redraw_after_expand(self, p): 

3144 c = self 

3145 if c.enableRedrawFlag: 

3146 if p: 

3147 c.setCurrentPosition(p) 

3148 else: 

3149 p = c.currentPosition() 

3150 c.frame.tree.redraw_after_expand(p) 

3151 c.treeFocusHelper() 

3152 else: 

3153 c.requestLaterRedraw = True 

3154 #@+node:ekr.20090110073010.2: *6* c.redraw_after_head_changed 

3155 def redraw_after_head_changed(self): 

3156 """ 

3157 Redraw the screen (if needed) when editing ends. 

3158 This may be a do-nothing for some gui's. 

3159 """ 

3160 c = self 

3161 if c.enableRedrawFlag: 

3162 self.frame.tree.redraw_after_head_changed() 

3163 else: 

3164 c.requestLaterRedraw = True 

3165 #@+node:ekr.20090110073010.4: *6* c.redraw_after_select 

3166 def redraw_after_select(self, p): 

3167 """Redraw the screen after node p has been selected.""" 

3168 c = self 

3169 if c.enableRedrawFlag: 

3170 flag = c.expandAllAncestors(p) 

3171 if flag: 

3172 # This is the same as c.frame.tree.full_redraw(). 

3173 c.frame.tree.redraw_after_select(p) 

3174 else: 

3175 c.requestLaterRedraw = True 

3176 #@+node:ekr.20170908081918.1: *6* c.redraw_later 

3177 def redraw_later(self): 

3178 """ 

3179 Ensure that c.redraw() will be called eventually. 

3180 

3181 c.outerUpdate will call c.redraw() only if no other code calls c.redraw(). 

3182 """ 

3183 c = self 

3184 c.requestLaterRedraw = True 

3185 if 'drawing' in g.app.debug: 

3186 # g.trace('\n' + g.callers(8)) 

3187 g.trace(g.callers()) 

3188 #@+node:ekr.20080514131122.17: *5* c.widget_name 

3189 def widget_name(self, widget): 

3190 # c = self 

3191 return g.app.gui.widget_name(widget) if g.app.gui else '<no widget>' 

3192 #@+node:ekr.20171124101045.1: *4* c.Events 

3193 #@+node:ekr.20060923202156: *5* c.onCanvasKey 

3194 def onCanvasKey(self, event): 

3195 """ 

3196 Navigate to the next headline starting with ch = event.char. 

3197 If ch is uppercase, search all headlines; otherwise search only visible headlines. 

3198 This is modelled on Windows explorer. 

3199 """ 

3200 if not event or not event.char or not event.char.isalnum(): 

3201 return 

3202 c, p = self, self.p 

3203 p1 = p.copy() 

3204 invisible = c.config.getBool('invisible-outline-navigation') 

3205 ch = event.char if event else '' 

3206 allFlag = ch.isupper() and invisible # all is a global (!?) 

3207 if not invisible: 

3208 ch = ch.lower() 

3209 found = False 

3210 extend = self.navQuickKey() 

3211 attempts = (True, False) if extend else (False,) 

3212 for extend2 in attempts: 

3213 p = p1.copy() 

3214 while 1: 

3215 if allFlag: 

3216 p.moveToThreadNext() 

3217 else: 

3218 p.moveToVisNext(c) 

3219 if not p: 

3220 p = c.rootPosition() 

3221 if p == p1: # Never try to match the same position. 

3222 found = False 

3223 break 

3224 newPrefix = c.navHelper(p, ch, extend2) 

3225 if newPrefix: 

3226 found = True 

3227 break 

3228 if found: 

3229 break 

3230 if found: 

3231 c.selectPosition(p) 

3232 c.redraw_after_select(p) 

3233 c.navTime = time.time() 

3234 c.navPrefix = newPrefix 

3235 else: 

3236 c.navTime = None 

3237 c.navPrefix = '' 

3238 c.treeWantsFocus() 

3239 #@+node:ekr.20061002095711.1: *6* c.navQuickKey 

3240 def navQuickKey(self) -> bool: 

3241 """ 

3242 Return true if there are two quick outline navigation keys 

3243 in quick succession. 

3244 

3245 Returns False if @float outline_nav_extend_delay setting is 0.0 or unspecified. 

3246 """ 

3247 c = self 

3248 deltaTime = c.config.getFloat('outline-nav-extend-delay') 

3249 if deltaTime in (None, 0.0): 

3250 return False 

3251 if c.navTime is None: 

3252 return False # mypy. 

3253 return time.time() - c.navTime < deltaTime 

3254 #@+node:ekr.20061002095711: *6* c.navHelper 

3255 def navHelper(self, p, ch, extend): 

3256 c = self 

3257 h = p.h.lower() 

3258 if extend: 

3259 prefix = c.navPrefix + ch 

3260 return h.startswith(prefix.lower()) and prefix 

3261 if h.startswith(ch): 

3262 return ch 

3263 # New feature: search for first non-blank character after @x for common x. 

3264 if ch != '@' and h.startswith('@'): 

3265 for s in ('button', 'command', 'file', 'thin', 'asis', 'nosent',): 

3266 prefix = '@' + s 

3267 if h.startswith('@' + s): 

3268 while 1: 

3269 n = len(prefix) 

3270 ch2 = h[n] if n < len(h) else '' 

3271 if ch2.isspace(): 

3272 prefix = prefix + ch2 

3273 else: break 

3274 if len(prefix) < len(h) and h.startswith(prefix + ch.lower()): 

3275 return prefix + ch 

3276 return '' 

3277 #@+node:ekr.20031218072017.2909: *4* c.Expand/contract 

3278 #@+node:ekr.20171124091426.1: *5* c.contractAllHeadlines 

3279 def contractAllHeadlines(self, event=None): 

3280 """Contract all nodes in the outline.""" 

3281 c = self 

3282 for v in c.all_nodes(): 

3283 v.contract() 

3284 v.expandedPositions = [] # #2571 

3285 if c.hoistStack: 

3286 # #2380: Handle hoists properly. 

3287 bunch = c.hoistStack[-1] 

3288 p = bunch.p 

3289 else: 

3290 # Select the topmost ancestor of the presently selected node. 

3291 p = c.p 

3292 while p and p.hasParent(): 

3293 p.moveToParent() 

3294 c.selectPosition(p) # #2380: Don't redraw here. 

3295 c.expansionLevel = 1 # Reset expansion level. 

3296 #@+node:ekr.20031218072017.2910: *5* c.contractSubtree 

3297 def contractSubtree(self, p): 

3298 for p in p.subtree(): 

3299 p.contract() 

3300 #@+node:ekr.20031218072017.2911: *5* c.expandSubtree 

3301 def expandSubtree(self, p): 

3302 # c = self 

3303 last = p.lastNode() 

3304 p = p.copy() 

3305 while p and p != last: 

3306 p.expand() 

3307 p = p.moveToThreadNext() 

3308 #@+node:ekr.20031218072017.2912: *5* c.expandToLevel 

3309 def expandToLevel(self, level): 

3310 

3311 c = self 

3312 n = c.p.level() 

3313 old_expansion_level = c.expansionLevel 

3314 max_level = 0 

3315 for p in c.p.self_and_subtree(copy=False): 

3316 if p.level() - n + 1 < level: 

3317 p.expand() 

3318 max_level = max(max_level, p.level() - n + 1) 

3319 else: 

3320 p.contract() 

3321 c.expansionNode = c.p.copy() 

3322 c.expansionLevel = max_level + 1 

3323 if c.expansionLevel != old_expansion_level: 

3324 c.redraw() 

3325 # It's always useful to announce the level. 

3326 # c.k.setLabelBlue('level: %s' % (max_level+1)) 

3327 # g.es('level', max_level + 1) 

3328 c.frame.putStatusLine(f"level: {max_level + 1}") # bg='red', fg='red') 

3329 #@+node:ekr.20141028061518.23: *4* c.Focus 

3330 #@+node:ekr.20080514131122.9: *5* c.get/request/set_focus 

3331 def get_focus(self) -> Widget: 

3332 c = self 

3333 w = g.app.gui and g.app.gui.get_focus(c) 

3334 if 'focus' in g.app.debug: 

3335 name = w.objectName() if hasattr(w, 'objectName') else w.__class__.__name__ 

3336 g.trace('(c)', name) 

3337 # g.trace('\n(c)', w.__class__.__name__) 

3338 # g.trace(g.callers(6)) 

3339 return w 

3340 

3341 def get_requested_focus(self): 

3342 c = self 

3343 return c.requestedFocusWidget 

3344 

3345 def request_focus(self, w): 

3346 c = self 

3347 if w and g.app.gui: 

3348 if 'focus' in g.app.debug: 

3349 # g.trace('\n(c)', repr(w)) 

3350 name = w.objectName( 

3351 ) if hasattr(w, 'objectName') else w.__class__.__name__ 

3352 g.trace('(c)', name) 

3353 c.requestedFocusWidget = w 

3354 

3355 def set_focus(self, w): 

3356 trace = 'focus' in g.app.debug 

3357 c = self 

3358 if w and g.app.gui: 

3359 if trace: 

3360 name = w.objectName( 

3361 ) if hasattr(w, 'objectName') else w.__class__.__name__ 

3362 g.trace('(c)', name) 

3363 g.app.gui.set_focus(c, w) 

3364 else: 

3365 if trace: 

3366 g.trace('(c) no w') 

3367 c.requestedFocusWidget = None 

3368 #@+node:ekr.20080514131122.10: *5* c.invalidateFocus (do nothing) 

3369 def invalidateFocus(self): 

3370 """Indicate that the focus is in an invalid location, or is unknown.""" 

3371 # c = self 

3372 # c.requestedFocusWidget = None 

3373 pass 

3374 #@+node:ekr.20080514131122.16: *5* c.traceFocus (not used) 

3375 def traceFocus(self, w): 

3376 c = self 

3377 if 'focus' in g.app.debug: 

3378 c.trace_focus_count += 1 

3379 g.pr(f"{c.trace_focus_count:4d}", c.widget_name(w), g.callers(8)) 

3380 #@+node:ekr.20070226121510: *5* c.xFocusHelper & initialFocusHelper 

3381 def treeFocusHelper(self): 

3382 c = self 

3383 if c.stayInTreeAfterSelect: 

3384 c.treeWantsFocus() 

3385 else: 

3386 c.bodyWantsFocus() 

3387 

3388 def initialFocusHelper(self): 

3389 c = self 

3390 if c.outlineHasInitialFocus: 

3391 c.treeWantsFocus() 

3392 else: 

3393 c.bodyWantsFocus() 

3394 #@+node:ekr.20080514131122.18: *5* c.xWantsFocus 

3395 def bodyWantsFocus(self): 

3396 c = self 

3397 body = c.frame.body 

3398 c.request_focus(body and body.wrapper) 

3399 

3400 def logWantsFocus(self): 

3401 c = self 

3402 log = c.frame.log 

3403 c.request_focus(log and log.logCtrl) 

3404 

3405 def minibufferWantsFocus(self): 

3406 c = self 

3407 c.request_focus(c.miniBufferWidget) 

3408 

3409 def treeWantsFocus(self): 

3410 c = self 

3411 tree = c.frame.tree 

3412 c.request_focus(tree and tree.canvas) 

3413 

3414 def widgetWantsFocus(self, w): 

3415 c = self 

3416 c.request_focus(w) 

3417 #@+node:ekr.20080514131122.19: *5* c.xWantsFocusNow 

3418 # widgetWantsFocusNow does an automatic update. 

3419 

3420 def widgetWantsFocusNow(self, w): 

3421 c = self 

3422 if w: 

3423 c.set_focus(w) 

3424 c.requestedFocusWidget = None 

3425 

3426 # New in 4.9: all FocusNow methods now *do* call c.outerUpdate(). 

3427 

3428 def bodyWantsFocusNow(self): 

3429 c, body = self, self.frame.body 

3430 c.widgetWantsFocusNow(body and body.wrapper) 

3431 

3432 def logWantsFocusNow(self): 

3433 c, log = self, self.frame.log 

3434 c.widgetWantsFocusNow(log and log.logCtrl) 

3435 

3436 def minibufferWantsFocusNow(self): 

3437 c = self 

3438 c.widgetWantsFocusNow(c.miniBufferWidget) 

3439 

3440 def treeWantsFocusNow(self): 

3441 c, tree = self, self.frame.tree 

3442 c.widgetWantsFocusNow(tree and tree.canvas) 

3443 #@+node:ekr.20031218072017.2955: *4* c.Menus 

3444 #@+node:ekr.20080610085158.2: *5* c.add_command 

3445 def add_command(self, menu: Widget, 

3446 accelerator: str='', # Not used. 

3447 command: Callable=None, 

3448 commandName: str=None, # Not used. 

3449 label: str=None, # Not used. 

3450 underline: int=0, 

3451 ) -> None: 

3452 c = self 

3453 if command: 

3454 # Command is always either: 

3455 # one of two callbacks defined in createMenuEntries or 

3456 # recentFilesCallback, defined in createRecentFilesMenuItems. 

3457 

3458 def add_commandCallback(c=c, command=command): 

3459 val = command() 

3460 # Careful: func may destroy c. 

3461 if c.exists: 

3462 c.outerUpdate() 

3463 return val 

3464 

3465 menu.add_command(menu, 

3466 accelerator=accelerator, command=command, commandName=commandName, label=label, underline=underline) 

3467 else: 

3468 g.trace('can not happen: no "command" arg') 

3469 #@+node:ekr.20171123203044.1: *5* c.Menu Enablers 

3470 #@+node:ekr.20040131170659: *6* c.canClone 

3471 def canClone(self): 

3472 c = self 

3473 if c.hoistStack: 

3474 current = c.p 

3475 bunch = c.hoistStack[-1] 

3476 return current != bunch.p 

3477 return True 

3478 #@+node:ekr.20031218072017.2956: *6* c.canContractAllHeadlines 

3479 def canContractAllHeadlines(self): 

3480 """Contract all nodes in the tree.""" 

3481 c = self 

3482 for p in c.all_positions(): # was c.all_unique_positions() 

3483 if p.isExpanded(): 

3484 return True 

3485 return False 

3486 #@+node:ekr.20031218072017.2957: *6* c.canContractAllSubheads 

3487 def canContractAllSubheads(self): 

3488 current = self.p 

3489 for p in current.subtree(): 

3490 if p != current and p.isExpanded(): 

3491 return True 

3492 return False 

3493 #@+node:ekr.20031218072017.2958: *6* c.canContractParent 

3494 def canContractParent(self) -> bool: 

3495 c = self 

3496 return c.p.parent() 

3497 #@+node:ekr.20031218072017.2959: *6* c.canContractSubheads 

3498 def canContractSubheads(self): 

3499 current = self.p 

3500 for child in current.children(): 

3501 if child.isExpanded(): 

3502 return True 

3503 return False 

3504 #@+node:ekr.20031218072017.2960: *6* c.canCutOutline & canDeleteHeadline 

3505 def canDeleteHeadline(self): 

3506 c, p = self, self.p 

3507 if c.hoistStack: 

3508 bunch = c.hoistStack[0] 

3509 if p == bunch.p: 

3510 return False 

3511 return p.hasParent() or p.hasThreadBack() or p.hasNext() 

3512 

3513 canCutOutline = canDeleteHeadline 

3514 #@+node:ekr.20031218072017.2961: *6* c.canDemote 

3515 def canDemote(self) -> bool: 

3516 c = self 

3517 return c.p.hasNext() 

3518 #@+node:ekr.20031218072017.2962: *6* c.canExpandAllHeadlines 

3519 def canExpandAllHeadlines(self): 

3520 """Return True if the Expand All Nodes menu item should be enabled.""" 

3521 c = self 

3522 for p in c.all_positions(): # was c.all_unique_positions() 

3523 if not p.isExpanded(): 

3524 return True 

3525 return False 

3526 #@+node:ekr.20031218072017.2963: *6* c.canExpandAllSubheads 

3527 def canExpandAllSubheads(self): 

3528 c = self 

3529 for p in c.p.subtree(): 

3530 if not p.isExpanded(): 

3531 return True 

3532 return False 

3533 #@+node:ekr.20031218072017.2964: *6* c.canExpandSubheads 

3534 def canExpandSubheads(self): 

3535 current = self.p 

3536 for p in current.children(): 

3537 if p != current and not p.isExpanded(): 

3538 return True 

3539 return False 

3540 #@+node:ekr.20031218072017.2287: *6* c.canExtract, canExtractSection & canExtractSectionNames 

3541 def canExtract(self) -> bool: 

3542 c = self 

3543 w = c.frame.body.wrapper 

3544 return w and w.hasSelection() 

3545 

3546 canExtractSectionNames = canExtract 

3547 

3548 def canExtractSection(self): 

3549 c = self 

3550 w = c.frame.body.wrapper 

3551 if not w: 

3552 return False 

3553 s = w.getSelectedText() 

3554 if not s: 

3555 return False 

3556 line = g.get_line(s, 0) 

3557 i1 = line.find("<<") 

3558 j1 = line.find(">>") 

3559 i2 = line.find("@<") 

3560 j2 = line.find("@>") 

3561 return -1 < i1 < j1 or -1 < i2 < j2 

3562 #@+node:ekr.20031218072017.2965: *6* c.canFindMatchingBracket 

3563 #@@nobeautify 

3564 

3565 def canFindMatchingBracket(self): 

3566 c = self 

3567 brackets = "()[]{}" 

3568 w = c.frame.body.wrapper 

3569 s = w.getAllText() 

3570 ins = w.getInsertPoint() 

3571 c1 = s[ins] if 0 <= ins < len(s) else '' 

3572 c2 = s[ins-1] if 0 <= ins-1 < len(s) else '' 

3573 val = (c1 and c1 in brackets) or (c2 and c2 in brackets) 

3574 return bool(val) 

3575 #@+node:ekr.20040303165342: *6* c.canHoist & canDehoist 

3576 def canDehoist(self): 

3577 """ 

3578 Return True if do-hoist should be enabled in a menu. 

3579 Should not be used in any other context. 

3580 """ 

3581 c = self 

3582 return bool(c.hoistStack) 

3583 

3584 def canHoist(self): 

3585 # This is called at idle time, so minimizing positions is crucial! 

3586 """ 

3587 Return True if hoist should be enabled in a menu. 

3588 Should not be used in any other context. 

3589 """ 

3590 return True 

3591 #@+node:ekr.20031218072017.2970: *6* c.canMoveOutlineDown 

3592 def canMoveOutlineDown(self) -> bool: 

3593 c, p = self, self.p 

3594 return p and p.visNext(c) 

3595 #@+node:ekr.20031218072017.2971: *6* c.canMoveOutlineLeft 

3596 def canMoveOutlineLeft(self) -> bool: 

3597 c, p = self, self.p 

3598 if c.hoistStack: 

3599 bunch = c.hoistStack[-1] 

3600 if p and p.hasParent(): 

3601 p.moveToParent() 

3602 return p != bunch.p and bunch.p.isAncestorOf(p) 

3603 return False 

3604 return p and p.hasParent() 

3605 #@+node:ekr.20031218072017.2972: *6* c.canMoveOutlineRight 

3606 def canMoveOutlineRight(self) -> bool: 

3607 c, p = self, self.p 

3608 if c.hoistStack: 

3609 bunch = c.hoistStack[-1] 

3610 return p and p.hasBack() and p != bunch.p 

3611 return p and p.hasBack() 

3612 #@+node:ekr.20031218072017.2973: *6* c.canMoveOutlineUp 

3613 def canMoveOutlineUp(self): 

3614 c, current = self, self.p 

3615 visBack = current and current.visBack(c) 

3616 if not visBack: 

3617 return False 

3618 if visBack.visBack(c): 

3619 return True 

3620 if c.hoistStack: 

3621 limit, limitIsVisible = c.visLimit() 

3622 if limitIsVisible: # A hoist 

3623 return current != limit 

3624 # A chapter. 

3625 return current != limit.firstChild() 

3626 return current != c.rootPosition() 

3627 #@+node:ekr.20031218072017.2974: *6* c.canPasteOutline 

3628 def canPasteOutline(self, s=None): 

3629 # c = self 

3630 if not s: 

3631 s = g.app.gui.getTextFromClipboard() 

3632 if s and g.match(s, 0, g.app.prolog_prefix_string): 

3633 return True 

3634 return False 

3635 #@+node:ekr.20031218072017.2975: *6* c.canPromote 

3636 def canPromote(self) -> bool: 

3637 p = self.p 

3638 return p and p.hasChildren() 

3639 #@+node:ekr.20031218072017.2977: *6* c.canSelect.... 

3640 def canSelectThreadBack(self): 

3641 p = self.p 

3642 return p.hasThreadBack() 

3643 

3644 def canSelectThreadNext(self): 

3645 p = self.p 

3646 return p.hasThreadNext() 

3647 

3648 def canSelectVisBack(self): 

3649 c, p = self, self.p 

3650 return p.visBack(c) 

3651 

3652 def canSelectVisNext(self): 

3653 c, p = self, self.p 

3654 return p.visNext(c) 

3655 #@+node:ekr.20031218072017.2978: *6* c.canShiftBodyLeft/Right 

3656 def canShiftBodyLeft(self) -> bool: 

3657 c = self 

3658 w = c.frame.body.wrapper 

3659 return w and w.getAllText() 

3660 

3661 canShiftBodyRight = canShiftBodyLeft 

3662 #@+node:ekr.20031218072017.2979: *6* c.canSortChildren, canSortSiblings 

3663 def canSortChildren(self) -> bool: 

3664 p = self.p 

3665 return p and p.hasChildren() 

3666 

3667 def canSortSiblings(self) -> bool: 

3668 p = self.p 

3669 return p and (p.hasNext() or p.hasBack()) 

3670 #@+node:ekr.20031218072017.2980: *6* c.canUndo & canRedo 

3671 def canUndo(self) -> bool: 

3672 c = self 

3673 return c.undoer.canUndo() 

3674 

3675 def canRedo(self) -> bool: 

3676 c = self 

3677 return c.undoer.canRedo() 

3678 #@+node:ekr.20031218072017.2981: *6* c.canUnmarkAll 

3679 def canUnmarkAll(self): 

3680 c = self 

3681 for p in c.all_unique_positions(): 

3682 if p.isMarked(): 

3683 return True 

3684 return False 

3685 #@+node:ekr.20040323172420: *6* Slow routines: no longer used 

3686 #@+node:ekr.20031218072017.2966: *7* c.canGoToNextDirtyHeadline (slow) 

3687 def canGoToNextDirtyHeadline(self): 

3688 c, current = self, self.p 

3689 for p in c.all_unique_positions(): 

3690 if p != current and p.isDirty(): 

3691 return True 

3692 return False 

3693 #@+node:ekr.20031218072017.2967: *7* c.canGoToNextMarkedHeadline (slow) 

3694 def canGoToNextMarkedHeadline(self): 

3695 c, current = self, self.p 

3696 for p in c.all_unique_positions(): 

3697 if p != current and p.isMarked(): 

3698 return True 

3699 return False 

3700 #@+node:ekr.20031218072017.2968: *7* c.canMarkChangedHeadline (slow) 

3701 def canMarkChangedHeadlines(self): 

3702 c = self 

3703 for p in c.all_unique_positions(): 

3704 if p.isDirty(): 

3705 return True 

3706 return False 

3707 #@+node:ekr.20031218072017.2969: *7* c.canMarkChangedRoots 

3708 def canMarkChangedRoots(self): 

3709 c = self 

3710 for p in c.all_unique_positions(): 

3711 if p.isDirty() and p.isAnyAtFileNode(): 

3712 return True 

3713 return False 

3714 #@+node:ekr.20031218072017.2990: *4* c.Selecting 

3715 #@+node:ekr.20031218072017.2992: *5* c.endEditing 

3716 def endEditing(self): 

3717 """End the editing of a headline.""" 

3718 c = self 

3719 p = c.p 

3720 if p: 

3721 c.frame.tree.endEditLabel() 

3722 #@+node:ville.20090525205736.12325: *5* c.getSelectedPositions 

3723 def getSelectedPositions(self): 

3724 """ Get list (PosList) of currently selected positions 

3725 

3726 So far only makes sense on qt gui (which supports multiselection) 

3727 """ 

3728 c = self 

3729 return c.frame.tree.getSelectedPositions() 

3730 #@+node:ekr.20031218072017.2991: *5* c.redrawAndEdit 

3731 def redrawAndEdit(self, p, selectAll=False, selection=None, keepMinibuffer=False): 

3732 """Redraw the screen and edit p's headline.""" 

3733 c, k = self, self.k 

3734 c.redraw(p) # This *must* be done now. 

3735 if p: 

3736 # This should request focus. 

3737 c.frame.tree.editLabel(p, selectAll=selectAll, selection=selection) 

3738 if k and not keepMinibuffer: 

3739 # Setting the input state has no effect on focus. 

3740 if selectAll: 

3741 k.setInputState('insert') 

3742 else: 

3743 k.setDefaultInputState() 

3744 # This *does* affect focus. 

3745 k.showStateAndMode() 

3746 else: 

3747 g.trace('** no p') 

3748 # Update the focus immediately. 

3749 if not keepMinibuffer: 

3750 c.outerUpdate() 

3751 #@+node:ekr.20031218072017.2997: *5* c.selectPosition (trace of unexpected de-hoists) 

3752 def selectPosition(self, p, **kwargs): 

3753 """ 

3754 Select a new position, redrawing the screen *only* if we must 

3755 change chapters. 

3756 """ 

3757 trace = False # For # 2167. 

3758 if kwargs: 

3759 print('c.selectPosition: all keyword args are ignored', g.callers()) 

3760 c = self 

3761 cc = c.chapterController 

3762 if not p: 

3763 if not g.app.batchMode: # A serious error. 

3764 g.trace('Warning: no p', g.callers()) 

3765 return 

3766 if cc and not cc.selectChapterLockout: 

3767 # Calls c.redraw only if the chapter changes. 

3768 cc.selectChapterForPosition(p) 

3769 # De-hoist as necessary to make p visible. 

3770 if c.hoistStack: 

3771 while c.hoistStack: 

3772 bunch = c.hoistStack[-1] 

3773 if c.positionExists(p, bunch.p): 

3774 break 

3775 if trace: 

3776 # #2167: Give detailed trace. 

3777 print('') 

3778 print('pop hoist stack! callers:', g.callers()) 

3779 g.printObj(c.hoistStack, tag='c.hoistStack before pop') 

3780 print('Recent keystrokes') 

3781 for i, data in enumerate(reversed(g.app.lossage)): 

3782 print(f"{i:>2} {data!r}") 

3783 print('Recently-executed commands...') 

3784 for i, command in enumerate(reversed(c.recent_commands_list)): 

3785 print(f"{i:>2} {command}") 

3786 c.hoistStack.pop() 

3787 c.frame.tree.select(p) 

3788 # Do *not* test whether the position exists! 

3789 # We may be in the midst of an undo. 

3790 c.setCurrentPosition(p) 

3791 

3792 # Compatibility, but confusing. 

3793 

3794 selectVnode = selectPosition 

3795 #@+node:ekr.20080503055349.1: *5* c.setPositionAfterSort 

3796 def setPositionAfterSort(self, sortChildren): 

3797 """ 

3798 Return the position to be selected after a sort. 

3799 """ 

3800 c = self 

3801 p = c.p 

3802 p_v = p.v 

3803 parent = p.parent() 

3804 parent_v = p._parentVnode() 

3805 if sortChildren: 

3806 return parent or c.rootPosition() 

3807 if parent: 

3808 p = parent.firstChild() 

3809 else: 

3810 p = leoNodes.Position(parent_v.children[0]) 

3811 while p and p.v != p_v: 

3812 p.moveToNext() 

3813 p = p or parent 

3814 return p 

3815 #@+node:ekr.20070226113916: *5* c.treeSelectHelper 

3816 def treeSelectHelper(self, p): 

3817 c = self 

3818 if not p: 

3819 p = c.p 

3820 if p: 

3821 # Do not call expandAllAncestors here. 

3822 c.selectPosition(p) 

3823 c.redraw_after_select(p) 

3824 c.treeFocusHelper() # This is essential. 

3825 #@+node:ekr.20130823083943.12559: *3* c.recursiveImport 

3826 def recursiveImport(self, dir_, kind, 

3827 add_context=None, # Override setting only if True/False 

3828 add_file_context=None, # Override setting only if True/False 

3829 add_path=True, 

3830 recursive=True, 

3831 safe_at_file=True, 

3832 theTypes=None, 

3833 # force_at_others=False, # tag:no-longer-used 

3834 ignore_pattern=None, 

3835 verbose=True, # legacy value. 

3836 ): 

3837 #@+<< docstring >> 

3838 #@+node:ekr.20130823083943.12614: *4* << docstring >> 

3839 """ 

3840 Recursively import all python files in a directory and clean the results. 

3841 

3842 Parameters:: 

3843 dir_ The root directory or file to import. 

3844 kind One of ('@clean','@edit','@file','@nosent'). 

3845 add_path=True True: add a full @path directive to @<file> nodes. 

3846 recursive=True True: recurse into subdirectories. 

3847 safe_at_file=True True: produce @@file nodes instead of @file nodes. 

3848 theTypes=None A list of file extensions to import. 

3849 None is equivalent to ['.py'] 

3850 

3851 This method cleans imported files as follows: 

3852 

3853 - Replace backslashes with forward slashes in headlines. 

3854 - Remove empty nodes. 

3855 - Add @path directives that reduce the needed path specifiers in descendant nodes. 

3856 - Add @file to nodes or replace @file with @@file. 

3857 """ 

3858 #@-<< docstring >> 

3859 c = self 

3860 if g.os_path_exists(dir_): 

3861 # Import all files in dir_ after c.p. 

3862 try: 

3863 from leo.core import leoImport 

3864 cc = leoImport.RecursiveImportController(c, kind, 

3865 add_context=add_context, 

3866 add_file_context=add_file_context, 

3867 add_path=add_path, 

3868 ignore_pattern=ignore_pattern, 

3869 recursive=recursive, 

3870 safe_at_file=safe_at_file, 

3871 theTypes=['.py'] if not theTypes else theTypes, 

3872 verbose=verbose, 

3873 ) 

3874 cc.run(dir_) 

3875 finally: 

3876 c.redraw() 

3877 else: 

3878 g.es_print(f"Does not exist: {dir_}") 

3879 #@+node:ekr.20171124084149.1: *3* c.Scripting utils 

3880 #@+node:ekr.20160201072634.1: *4* c.cloneFindByPredicate 

3881 def cloneFindByPredicate(self, 

3882 generator, # The generator used to traverse the tree. 

3883 predicate, # A function of one argument p, returning True 

3884 # if p should be included in the results. 

3885 failMsg=None, # Failure message. Default is no message. 

3886 flatten=False, # True: Put all matches at the top level. 

3887 iconPath=None, # Full path to icon to attach to all matches. 

3888 undoType=None, # The undo name, shown in the Edit:Undo menu. 

3889 # The default is 'clone-find-predicate' 

3890 ): 

3891 """ 

3892 Traverse the tree given using the generator, cloning all positions for 

3893 which predicate(p) is True. Undoably move all clones to a new node, created 

3894 as the last top-level node. Returns the newly-created node. Arguments: 

3895 

3896 generator, The generator used to traverse the tree. 

3897 predicate, A function of one argument p returning true if p should be included. 

3898 failMsg=None, Message given if nothing found. Default is no message. 

3899 flatten=False, True: Move all node to be parents of the root node. 

3900 iconPath=None, Full path to icon to attach to all matches. 

3901 undo_type=None, The undo/redo name shown in the Edit:Undo menu. 

3902 The default is 'clone-find-predicate' 

3903 """ 

3904 c = self 

3905 u, undoType = c.undoer, undoType or 'clone-find-predicate' 

3906 clones, root, seen = [], None, set() 

3907 for p in generator(): 

3908 if predicate(p) and p.v not in seen: 

3909 c.setCloneFindByPredicateIcon(iconPath, p) 

3910 if flatten: 

3911 seen.add(p.v) 

3912 else: 

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

3914 seen.add(p2.v) 

3915 clones.append(p.copy()) 

3916 if clones: 

3917 undoData = u.beforeInsertNode(c.p) 

3918 root = c.createCloneFindPredicateRoot(flatten, undoType) 

3919 for p in clones: 

3920 # Create the clone directly as a child of found. 

3921 p2 = p.copy() 

3922 n = root.numberOfChildren() 

3923 p2._linkCopiedAsNthChild(root, n) 

3924 u.afterInsertNode(root, undoType, undoData) 

3925 c.selectPosition(root) 

3926 c.setChanged() 

3927 c.contractAllHeadlines() 

3928 root.expand() 

3929 elif failMsg: 

3930 g.es(failMsg, color='red') 

3931 return root 

3932 #@+node:ekr.20160304054950.1: *5* c.setCloneFindByPredicateIcon 

3933 def setCloneFindByPredicateIcon(self, iconPath, p): 

3934 """Attach an icon to p.v.u.""" 

3935 if iconPath and g.os_path_exists(iconPath) and not g.os_path_isdir(iconPath): 

3936 aList = p.v.u.get('icons', []) 

3937 for d in aList: 

3938 if d.get('file') == iconPath: 

3939 break 

3940 else: 

3941 aList.append({ 

3942 'type': 'file', 

3943 'file': iconPath, 

3944 'on': 'VNode', 

3945 # 'relPath': iconPath, 

3946 'where': 'beforeHeadline', 

3947 'xoffset': 2, 'xpad': 1, 

3948 'yoffset': 0, 

3949 

3950 }) 

3951 p.v.u['icons'] = aList 

3952 elif iconPath: 

3953 g.trace('bad icon path', iconPath) 

3954 #@+node:ekr.20160201075438.1: *5* c.createCloneFindPredicateRoot 

3955 def createCloneFindPredicateRoot(self, flatten, undoType): 

3956 """Create a root node for clone-find-predicate.""" 

3957 c = self 

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

3959 root.h = undoType + (' (flattened)' if flatten else '') 

3960 return root 

3961 #@+node:peckj.20131023115434.10114: *4* c.createNodeHierarchy 

3962 def createNodeHierarchy(self, heads, parent=None, forcecreate=False): 

3963 """ Create the proper hierarchy of nodes with headlines defined in 

3964 'heads' under 'parent' 

3965 

3966 params: 

3967 parent - parent node to start from. Set to None for top-level nodes 

3968 heads - list of headlines in order to create, i.e. ['foo','bar','baz'] 

3969 will create: 

3970 parent 

3971 -foo 

3972 --bar 

3973 ---baz 

3974 forcecreate - If False (default), will not create nodes unless they don't exist 

3975 If True, will create nodes regardless of existing nodes 

3976 returns the final position ('baz' in the above example) 

3977 """ 

3978 u = self.undoer 

3979 undoType = 'Create Node Hierarchy' 

3980 undoType2 = 'Insert Node In Hierarchy' 

3981 u_node = parent or self.rootPosition() 

3982 undoData = u.beforeChangeGroup(u_node, undoType) 

3983 changed_node = False 

3984 for idx, head in enumerate(heads): 

3985 if parent is None and idx == 0: # if parent = None, create top level node for first head 

3986 if not forcecreate: 

3987 for pos in self.all_positions(): 

3988 if pos.h == head: 

3989 parent = pos 

3990 break 

3991 if parent is None or forcecreate: 

3992 u_d = u.beforeInsertNode(u_node) 

3993 n = self.rootPosition().insertAfter() 

3994 n.h = head 

3995 u.afterInsertNode(n, undoType2, u_d) 

3996 parent = n 

3997 else: # else, simply create child nodes each round 

3998 if not forcecreate: 

3999 for ch in parent.children(): 

4000 if ch.h == head: 

4001 parent = ch 

4002 changed_node = True 

4003 break 

4004 if parent.h != head or not changed_node or forcecreate: 

4005 u_d = u.beforeInsertNode(parent) 

4006 n = parent.insertAsLastChild() 

4007 n.h = head 

4008 u.afterInsertNode(n, undoType2, u_d) 

4009 parent = n 

4010 changed_node = False 

4011 u.afterChangeGroup(parent, undoType, undoData) 

4012 return parent # actually the last created/found position 

4013 #@+node:ekr.20100802121531.5804: *4* c.deletePositionsInList 

4014 def deletePositionsInList(self, aList): 

4015 """ 

4016 Delete all vnodes corresponding to the positions in aList. 

4017 

4018 Set c.p if the old position no longer exists. 

4019 

4020 See "Theory of operation of c.deletePositionsInList" in LeoDocs.leo. 

4021 """ 

4022 # New implementation by Vitalije 2020-03-17 17:29 

4023 c = self 

4024 # Ensure all positions are valid. 

4025 aList = [p for p in aList if c.positionExists(p)] 

4026 if not aList: 

4027 return [] 

4028 

4029 def p2link(p): 

4030 parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode 

4031 return p._childIndex, parent_v 

4032 

4033 links_to_be_cut = sorted(set(map(p2link, aList)), key=lambda x: -x[0]) 

4034 undodata = [] 

4035 for i, v in links_to_be_cut: 

4036 ch = v.children.pop(i) 

4037 ch.parents.remove(v) 

4038 undodata.append((v.gnx, i, ch.gnx)) 

4039 if not c.positionExists(c.p): 

4040 c.selectPosition(c.rootPosition()) 

4041 return undodata 

4042 

4043 #@+node:vitalije.20200318161844.1: *4* c.undoableDeletePositions 

4044 def undoableDeletePositions(self, aList): 

4045 """ 

4046 Deletes all vnodes corresponding to the positions in aList, 

4047 and make changes undoable. 

4048 """ 

4049 c = self 

4050 u = c.undoer 

4051 data = c.deletePositionsInList(aList) 

4052 gnx2v = c.fileCommands.gnxDict 

4053 def undo(): 

4054 for pgnx, i, chgnx in reversed(u.getBead(u.bead).data): 

4055 v = gnx2v[pgnx] 

4056 ch = gnx2v[chgnx] 

4057 v.children.insert(i, ch) 

4058 ch.parents.append(v) 

4059 if not c.positionExists(c.p): 

4060 c.setCurrentPosition(c.rootPosition()) 

4061 def redo(): 

4062 for pgnx, i, chgnx in u.getBead(u.bead + 1).data: 

4063 v = gnx2v[pgnx] 

4064 ch = v.children.pop(i) 

4065 ch.parents.remove(v) 

4066 if not c.positionExists(c.p): 

4067 c.setCurrentPosition(c.rootPosition()) 

4068 u.pushBead(g.Bunch( 

4069 data=data, 

4070 undoType='delete nodes', 

4071 undoHelper=undo, 

4072 redoHelper=redo, 

4073 )) 

4074 #@+node:ekr.20091211111443.6265: *4* c.doBatchOperations & helpers 

4075 def doBatchOperations(self, aList=None): 

4076 # Validate aList and create the parents dict 

4077 if aList is None: 

4078 aList = [] 

4079 ok, d = self.checkBatchOperationsList(aList) 

4080 if not ok: 

4081 g.error('do-batch-operations: invalid list argument') 

4082 return 

4083 for v in list(d.keys()): 

4084 aList2 = d.get(v, []) 

4085 if aList2: 

4086 aList.sort() 

4087 #@+node:ekr.20091211111443.6266: *5* c.checkBatchOperationsList 

4088 def checkBatchOperationsList(self, aList): 

4089 ok = True 

4090 d: Dict["leoNodes.VNode", List[Any]] = {} 

4091 for z in aList: 

4092 try: 

4093 op, p, n = z 

4094 ok = (op in ('insert', 'delete') and 

4095 isinstance(p, leoNodes.position) and isinstance(n, int)) 

4096 if ok: 

4097 aList2 = d.get(p.v, []) 

4098 data = n, op 

4099 aList2.append(data) 

4100 d[p.v] = aList2 

4101 except ValueError: 

4102 ok = False 

4103 if not ok: 

4104 break 

4105 return ok, d 

4106 #@+node:ekr.20091002083910.6106: *4* c.find_b & find_h (PosList) 

4107 #@+<< PosList doc >> 

4108 #@+node:bob.20101215134608.5898: *5* << PosList doc >> 

4109 #@@language rest 

4110 #@+at 

4111 # List of positions 

4112 # 

4113 # Functions find_h() and find_b() both return an instance of PosList. 

4114 # 

4115 # Methods filter_h() and filter_b() refine a PosList. 

4116 # 

4117 # Method children() generates a new PosList by descending one level from 

4118 # all the nodes in a PosList. 

4119 # 

4120 # A chain of PosList method calls must begin with find_h() or find_b(). 

4121 # The rest of the chain can be any combination of filter_h(), 

4122 # filter_b(), and children(). For example: 

4123 # 

4124 # pl = c.find_h('@file.*py').children().filter_h('class.*').filter_b('import (.*)') 

4125 # 

4126 # For each position, pos, in the PosList returned, find_h() and 

4127 # filter_h() set attribute pos.mo to the match object (see Python 

4128 # Regular Expression documentation) for the pattern match. 

4129 # 

4130 # Caution: The pattern given to find_h() or filter_h() must match zero 

4131 # or more characters at the beginning of the headline. 

4132 # 

4133 # For each position, pos, the postlist returned, find_b() and filter_b() 

4134 # set attribute pos.matchiter to an iterator that will return a match 

4135 # object for each of the non-overlapping matches of the pattern in the 

4136 # body of the node. 

4137 #@-<< PosList doc >> 

4138 #@+node:ville.20090311190405.70: *5* c.find_h 

4139 def find_h(self, regex, flags=re.IGNORECASE): 

4140 """ Return list (a PosList) of all nodes where zero or more characters at 

4141 the beginning of the headline match regex 

4142 """ 

4143 c = self 

4144 pat = re.compile(regex, flags) 

4145 res = leoNodes.PosList() 

4146 for p in c.all_positions(): 

4147 m = re.match(pat, p.h) 

4148 if m: 

4149 pc = p.copy() 

4150 pc.mo = m 

4151 res.append(pc) 

4152 return res 

4153 #@+node:ville.20090311200059.1: *5* c.find_b 

4154 def find_b(self, regex, flags=re.IGNORECASE | re.MULTILINE): 

4155 """ Return list (a PosList) of all nodes whose body matches regex 

4156 one or more times. 

4157 

4158 """ 

4159 c = self 

4160 pat = re.compile(regex, flags) 

4161 res = leoNodes.PosList() 

4162 for p in c.all_positions(): 

4163 m = re.finditer(pat, p.b) 

4164 t1, t2 = itertools.tee(m, 2) 

4165 try: 

4166 t1.__next__() 

4167 except StopIteration: 

4168 continue 

4169 pc = p.copy() 

4170 pc.matchiter = t2 

4171 res.append(pc) 

4172 return res 

4173 #@+node:ekr.20171124155725.1: *3* c.Settings 

4174 #@+node:ekr.20171114114908.1: *4* c.registerReloadSettings 

4175 def registerReloadSettings(self, obj): 

4176 """Enter object into c.configurables.""" 

4177 c = self 

4178 if obj not in c.configurables: 

4179 c.configurables.append(obj) 

4180 #@+node:ekr.20170221040621.1: *4* c.reloadConfigurableSettings 

4181 def reloadConfigurableSettings(self): 

4182 """ 

4183 Call all reloadSettings method in c.subcommanders, c.configurables and 

4184 other known classes. 

4185 """ 

4186 c = self 

4187 table = [ 

4188 g.app.gui, 

4189 g.app.pluginsController, 

4190 c.k.autoCompleter, 

4191 c.frame, c.frame.body, c.frame.log, c.frame.tree, 

4192 c.frame.body.colorizer, 

4193 getattr(c.frame.body.colorizer, 'highlighter', None), 

4194 ] 

4195 for obj in table: 

4196 if obj: 

4197 c.registerReloadSettings(obj) 

4198 # Useful now that instances add themselves to c.configurables. 

4199 c.configurables = list(set(c.configurables)) 

4200 c.configurables.sort(key=lambda obj: obj.__class__.__name__.lower()) 

4201 for obj in c.configurables: 

4202 func = getattr(obj, 'reloadSettings', None) 

4203 if func: 

4204 # pylint: disable=not-callable 

4205 try: 

4206 func() 

4207 except Exception: 

4208 g.es_exception() 

4209 c.configurables.remove(obj) 

4210 #@-others 

4211#@-others 

4212#@@language python 

4213#@@tabwidth -4 

4214#@@pagewidth 70 

4215#@-leo