Coverage for C:\leo.repo\leo-editor\leo\core\leoCommands.py: 46%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
18from leo.core import leoNodes
19 # The leoCommands ctor now does most leo.core.leo* imports,
20 # thereby breaking circular dependencies.
21#@-<< imports >>
23def cmd(name) -> Callable:
24 """Command decorator for the Commands class."""
25 return g.new_cmd_decorator(name, ['c',])
27#@+others
28#@+node:ekr.20160514120615.1: ** class Commands
29class Commands:
30 """
31 A per-outline class that implements most of Leo's commands. The
32 "c" predefined object is an instance of this class.
34 c.initObjects() creates sucommanders corresponding to files in the
35 leo/core and leo/commands. All of Leo's core code is accessible
36 via this class and its subcommanders.
38 g.app.pluginsController is Leo's plugins controller. Many plugins
39 inject controllers objects into the Commands class. These are
40 another kind of subcommander.
42 The @g..commander_command decorator injects methods into this class.
43 """
44 #@+others
45 #@+node:ekr.20031218072017.2811: *3* c.Birth & death
46 #@+node:ekr.20031218072017.2812: *4* c.__init__ & helpers
47 def __init__(self, fileName,
48 gui=None,
49 parentFrame=None,
50 previousSettings=None,
51 relativeFileName=None,
52 ):
53 t1 = time.process_time()
54 c = self
55 # Official ivars.
56 self._currentPosition: Optional["leoNodes.Position"] = None
57 self._topPosition: Optional["leoNodes.Position"] = None
58 self.frame = None
59 self.parentFrame = parentFrame # New in Leo 6.0.
60 self.gui = gui or g.app.gui
61 self.ipythonController = None # Set only by the ipython plugin.
62 # The order of these calls does not matter.
63 c.initCommandIvars()
64 c.initDebugIvars()
65 c.initDocumentIvars()
66 c.initEventIvars()
67 c.initFileIvars(fileName, relativeFileName)
68 c.initOptionsIvars()
69 c.initObjectIvars()
70 # Init the settings *before* initing the objects.
71 c.initSettings(previousSettings)
72 # Initialize all subsidiary objects, including subcommanders.
73 c.initObjects(self.gui)
74 assert c.frame
75 assert c.frame.c
76 # Complete the init!
77 t2 = time.process_time()
78 c.finishCreate() # Slightly slow.
79 t3 = time.process_time()
80 if 'speed' in g.app.debug:
81 print('c.__init__')
82 print(
83 f" 1: {t2-t1:5.2f}\n" # 0.00 sec.
84 f" 2: {t3-t2:5.2f}\n" # 0.53 sec: c.finishCreate.
85 f"total: {t3-t1:5.2f}"
86 )
87 #@+node:ekr.20120217070122.10475: *5* c.computeWindowTitle
88 def computeWindowTitle(self, fileName):
89 """Set the window title and fileName."""
90 if fileName:
91 title = g.computeWindowTitle(fileName)
92 else:
93 s = "untitled"
94 n = g.app.numberOfUntitledWindows
95 if n > 0:
96 s += str(n)
97 title = g.computeWindowTitle(s)
98 g.app.numberOfUntitledWindows = n + 1
99 return title
100 #@+node:ekr.20120217070122.10473: *5* c.initCommandIvars
101 def initCommandIvars(self):
102 """Init ivars used while executing a command."""
103 self.commandsDict: dict[str, Callable] = {} # Keys are command names, values are functions.
104 self.disableCommandsMessage = '' # The presence of this message disables all commands.
105 self.hookFunction: Optional[Callable] = None # One of three places that g.doHook looks for hook functions.
106 self.ignoreChangedPaths = False # True: disable path changed message in at.WriteAllHelper.
107 self.inCommand = False # Interlocks to prevent premature closing of a window.
108 self.outlineToNowebDefaultFileName: str = "noweb.nw" # For Outline To Noweb dialog.
109 # For hoist/dehoist commands.
110 # Affects drawing routines and find commands, but *not* generators.
111 self.hoistStack: List[Any] = [] # Stack of g.Bunches to be root of drawn tree.
112 # For outline navigation.
113 self.navPrefix: str = '' # Must always be a string.
114 self.navTime: Optional[float] = None
115 self.recent_commands_list: List[str] = [] # List of command names.
116 self.sqlite_connection = None
117 #@+node:ekr.20120217070122.10466: *5* c.initDebugIvars
118 def initDebugIvars(self):
119 """Init Commander debugging ivars."""
120 self.command_count = 0
121 self.scanAtPathDirectivesCount = 0
122 self.trace_focus_count = 0
123 #@+node:ekr.20120217070122.10471: *5* c.initDocumentIvars
124 def initDocumentIvars(self):
125 """Init per-document ivars."""
126 self.expansionLevel = 0 # The expansion level of this outline.
127 self.expansionNode = None # The last node we expanded or contracted.
128 self.nodeConflictList = [] # List of nodes with conflicting read-time data.
129 self.nodeConflictFileName: Optional[str] = None # The fileName for c.nodeConflictList.
130 self.user_dict = {} # Non-persistent dictionary for free use by scripts and plugins.
131 #@+node:ekr.20120217070122.10467: *5* c.initEventIvars
132 def initEventIvars(self):
133 """Init ivars relating to gui events."""
134 self.configInited = False
135 self.doubleClickFlag = False
136 self.exists = True # Indicate that this class exists and has not been destroyed.
137 self.in_qt_dialog = False # True: in a qt dialog.
138 self.loading = False # True: we are loading a file: disables c.setChanged()
139 self.promptingForClose = False # True: lock out additional closing dialogs.
140 #
141 # Flags for c.outerUpdate...
142 self.enableRedrawFlag = True
143 self.requestCloseWindow = False
144 self.requestedFocusWidget = None
145 self.requestLaterRedraw = False
146 #@+node:ekr.20120217070122.10472: *5* c.initFileIvars
147 def initFileIvars(self, fileName, relativeFileName):
148 """Init file-related ivars of the commander."""
149 self.changed = False # True: the outline has changed since the last save.
150 self.ignored_at_file_nodes: List["leoNodes.Position"] = [] # List of nodes for c.raise_error_dialogs.
151 self.import_error_nodes: List["leoNodes.Position"] = [] # List of nodes for c.raise_error_dialogs.
152 self.last_dir = None # The last used directory.
153 self.mFileName: str = fileName or '' # Do _not_ use os_path_norm: it converts an empty path to '.' (!!)
154 self.mRelativeFileName = relativeFileName or '' #
155 self.openDirectory: Optional[str] = None #
156 self.orphan_at_file_nodes: List["leoNodes.Position"] = [] # List of orphaned nodes for c.raise_error_dialogs.
157 self.wrappedFileName: Optional[str] = None # The name of the wrapped file, for wrapper commanders.
159 #@+node:ekr.20120217070122.10469: *5* c.initOptionsIvars
160 def initOptionsIvars(self):
161 """Init Commander ivars corresponding to user options."""
162 self.fixed = False
163 self.fixedWindowPosition = []
164 self.forceExecuteEntireBody = False
165 self.focus_border_color = 'white'
166 self.focus_border_width = 1 # pixels
167 self.outlineHasInitialFocus = False
168 self.page_width = 132
169 self.sparse_find = True
170 self.sparse_move = True
171 self.sparse_spell = True
172 self.stayInTreeAfterSelect = False
173 self.tab_width = -4
174 self.tangle_batch_flag = False
175 self.target_language = "python"
176 self.untangle_batch_flag = False
177 self.vim_mode = False
178 #@+node:ekr.20120217070122.10468: *5* c.initObjectIvars
179 def initObjectIvars(self):
180 # These ivars are set later by leoEditCommands.createEditCommanders
181 self.abbrevCommands = None
182 self.editCommands = None
183 self.db = {} # May be set to a PickleShare instance later.
184 self.bufferCommands = None
185 self.chapterCommands = None
186 self.controlCommands = None
187 self.convertCommands = None
188 self.debugCommands = None
189 self.editFileCommands = None
190 self.evalController = None
191 self.gotoCommands = None
192 self.helpCommands = None
193 self.keyHandler = self.k = None
194 self.keyHandlerCommands = None
195 self.killBufferCommands = None
196 self.leoCommands = None
197 self.macroCommands = None
198 self.miniBufferWidget = None
199 self.printingController = None
200 self.queryReplaceCommands = None
201 self.rectangleCommands = None
202 self.searchCommands = None
203 self.spellCommands = None
204 self.leoTestManager = None
205 self.vimCommands = None
206 #@+node:ekr.20120217070122.10470: *5* c.initObjects
207 #@@nobeautify
209 def initObjects(self, gui):
211 c = self
212 self.hiddenRootNode = leoNodes.VNode(context=c, gnx='hidden-root-vnode-gnx')
213 self.hiddenRootNode.h = '<hidden root vnode>'
214 # Create the gui frame.
215 title = c.computeWindowTitle(c.mFileName)
216 if not g.app.initing:
217 g.doHook("before-create-leo-frame", c=c)
218 self.frame = gui.createLeoFrame(c, title)
219 assert self.frame
220 assert self.frame.c == c
221 from leo.core import leoHistory
222 self.nodeHistory = leoHistory.NodeHistory(c)
223 self.initConfigSettings()
224 c.setWindowPosition() # Do this after initing settings.
226 # Break circular import dependencies by doing imports here.
227 # All these imports take almost 3/4 sec in the leoBridge.
229 from leo.core import leoAtFile
230 from leo.core import leoBeautify # So decorators are executed.
231 assert leoBeautify # for pyflakes.
232 from leo.core import leoChapters
233 # User commands...
234 from leo.commands import abbrevCommands
235 from leo.commands import bufferCommands
236 from leo.commands import checkerCommands # The import *is* required to define commands.
237 assert checkerCommands # To suppress a pyflakes warning.
238 from leo.commands import controlCommands
239 from leo.commands import convertCommands
240 from leo.commands import debugCommands
241 from leo.commands import editCommands
242 from leo.commands import editFileCommands
243 from leo.commands import gotoCommands
244 from leo.commands import helpCommands
245 from leo.commands import keyCommands
246 from leo.commands import killBufferCommands
247 from leo.commands import rectangleCommands
248 from leo.commands import spellCommands
249 # Import files to execute @g.commander_command decorators
250 from leo.core import leoCompare
251 assert leoCompare
252 from leo.core import leoDebugger
253 assert leoDebugger
254 from leo.commands import commanderEditCommands
255 assert commanderEditCommands
256 from leo.commands import commanderFileCommands
257 assert commanderFileCommands
258 from leo.commands import commanderHelpCommands
259 assert commanderHelpCommands
260 from leo.commands import commanderOutlineCommands
261 assert commanderOutlineCommands
262 # Other subcommanders.
263 from leo.core import leoFind # Leo 4.11.1
264 from leo.core import leoKeys
265 from leo.core import leoFileCommands
266 from leo.core import leoImport
267 from leo.core import leoMarkup
268 from leo.core import leoPersistence
269 from leo.core import leoPrinting
270 from leo.core import leoRst
271 from leo.core import leoShadow
272 from leo.core import leoUndo
273 from leo.core import leoVim
274 # Import commands.testCommands to define commands.
275 import leo.commands.testCommands as testCommands
276 assert testCommands # For pylint.
277 # Define the subcommanders.
278 self.keyHandler = self.k = leoKeys.KeyHandlerClass(c)
279 self.chapterController = leoChapters.ChapterController(c)
280 self.shadowController = leoShadow.ShadowController(c)
281 self.fileCommands = leoFileCommands.FileCommands(c)
282 self.findCommands = leoFind.LeoFind(c)
283 self.atFileCommands = leoAtFile.AtFile(c)
284 self.importCommands = leoImport.LeoImportCommands(c)
285 self.markupCommands = leoMarkup.MarkupCommands(c)
286 self.persistenceController = leoPersistence.PersistenceDataController(c)
287 self.printingController = leoPrinting.PrintingController(c)
288 self.rstCommands = leoRst.RstCommands(c)
289 self.vimCommands = leoVim.VimCommands(c)
290 # User commands
291 self.abbrevCommands = abbrevCommands.AbbrevCommandsClass(c)
292 self.bufferCommands = bufferCommands.BufferCommandsClass(c)
293 self.controlCommands = controlCommands.ControlCommandsClass(c)
294 self.convertCommands = convertCommands.ConvertCommandsClass(c)
295 self.debugCommands = debugCommands.DebugCommandsClass(c)
296 self.editCommands = editCommands.EditCommandsClass(c)
297 self.editFileCommands = editFileCommands.EditFileCommandsClass(c)
298 self.gotoCommands = gotoCommands.GoToCommands(c)
299 self.helpCommands = helpCommands.HelpCommandsClass(c)
300 self.keyHandlerCommands = keyCommands.KeyHandlerCommandsClass(c)
301 self.killBufferCommands = killBufferCommands.KillBufferCommandsClass(c)
302 self.rectangleCommands = rectangleCommands.RectangleCommandsClass(c)
303 self.spellCommands = spellCommands.SpellCommandsClass(c)
304 self.undoer = leoUndo.Undoer(c)
305 # Create the list of subcommanders.
306 self.subCommanders = [
307 self.abbrevCommands,
308 self.atFileCommands,
309 self.bufferCommands,
310 self.chapterController,
311 self.controlCommands,
312 self.convertCommands,
313 self.debugCommands,
314 self.editCommands,
315 self.editFileCommands,
316 self.fileCommands,
317 self.findCommands,
318 self.gotoCommands,
319 self.helpCommands,
320 self.importCommands,
321 self.keyHandler,
322 self.keyHandlerCommands,
323 self.killBufferCommands,
324 self.persistenceController,
325 self.printingController,
326 self.rectangleCommands,
327 self.rstCommands,
328 self.shadowController,
329 self.spellCommands,
330 self.vimCommands,
331 self.undoer,
332 ]
333 # Other objects
334 # A list of other classes that have a reloadSettings method
335 c.configurables = c.subCommanders[:]
336 c.db = g.app.commander_cacher.get_wrapper(c)
337 # #2485: Load the free_layout plugin in the proper context.
338 # g.app.pluginsController.loadOnePlugin won't work here.
339 try:
340 g.app.pluginsController.loadingModuleNameStack.append('leo.plugins.free_layout')
341 from leo.plugins import free_layout
342 c.free_layout = free_layout.FreeLayoutController(c)
343 finally:
344 g.app.pluginsController.loadingModuleNameStack.pop()
345 if hasattr(g.app.gui, 'styleSheetManagerClass'):
346 self.styleSheetManager = g.app.gui.styleSheetManagerClass(c)
347 self.subCommanders.append(self.styleSheetManager)
348 else:
349 self.styleSheetManager = None
350 #@+node:ekr.20140815160132.18837: *5* c.initSettings
351 def initSettings(self, previousSettings):
352 """Init the settings *before* initing the objects."""
353 c = self
354 from leo.core import leoConfig
355 c.config = leoConfig.LocalConfigManager(c, previousSettings)
356 g.app.config.setIvarsFromSettings(c)
357 #@+node:ekr.20031218072017.2814: *4* c.__repr__ & __str__
358 def __repr__(self):
359 return f"Commander {id(self)}: {repr(self.mFileName)}"
361 __str__ = __repr__
362 #@+node:ekr.20050920093543: *4* c.finishCreate & helpers
363 def finishCreate(self):
364 """
365 Finish creating the commander and all sub-objects.
366 This is the last step in the startup process.
367 """
368 c, k = self, self.k
369 assert c.gui
370 assert k
371 t1 = time.process_time()
372 c.frame.finishCreate() # Slightly slow.
373 t2 = time.process_time()
374 c.miniBufferWidget = c.frame.miniBufferWidget # Will be None for nullGui.
375 # Only c.abbrevCommands needs a finishCreate method.
376 c.abbrevCommands.finishCreate()
377 # Finish other objects...
378 c.createCommandNames()
379 k.finishCreate()
380 c.findCommands.finishCreate()
381 if not c.gui.isNullGui:
382 # #2485: register idle_focus_helper in the proper context.
383 try:
384 g.app.pluginsController.loadingModuleNameStack.append('leo.core.leoCommands')
385 g.registerHandler('idle', c.idle_focus_helper)
386 finally:
387 g.app.pluginsController.loadingModuleNameStack.pop()
388 if getattr(c.frame, 'menu', None):
389 c.frame.menu.finishCreate()
390 if getattr(c.frame, 'log', None):
391 c.frame.log.finishCreate()
392 c.undoer.clearUndoState()
393 if c.vimCommands and c.vim_mode:
394 c.vimCommands.finishCreate() # Menus must exist at this point.
395 # Do not call chapterController.finishCreate here:
396 # It must be called after the first real redraw.
397 g.check_cmd_instance_dict(c, g)
398 c.bodyWantsFocus()
399 t3 = time.process_time()
400 if 'speed' in g.app.debug:
401 print('c.finishCreate')
402 print(
403 f" 1: {t2-t1:5.2f}\n" # 0.20 sec: qtGui.finishCreate.
404 f" 2: {t3-t2:5.2f}\n" # 0.16 sec: everything else.
405 f"total: {t3-t1:5.2f}"
406 )
407 #@+node:ekr.20140815160132.18835: *5* c.createCommandNames
408 def createCommandNames(self):
409 """
410 Create all entries in c.commandsDict.
411 Do *not* clear c.commandsDict here.
412 """
413 for commandName, func in g.global_commands_dict.items():
414 self.k.registerCommand(commandName, func)
415 #@+node:ekr.20051007143620: *5* c.printCommandsDict
416 def printCommandsDict(self):
417 c = self
418 print('Commands...')
419 for key in sorted(c.commandsDict):
420 command = c.commandsDict.get(key)
421 print(f"{key:30} = {command.__name__ if command else '<None>'}")
422 print('')
423 #@+node:ekr.20041130173135: *4* c.hash
424 # This is a bad idea.
426 def hash(self):
427 c = self
428 if c.mFileName:
429 return g.os_path_finalize(c.mFileName).lower() # #1341.
430 return 0
431 #@+node:ekr.20110509064011.14563: *4* c.idle_focus_helper & helpers
432 idle_focus_count = 0
434 def idle_focus_helper(self, tag, keys):
435 """An idle-tme handler that ensures that focus is *somewhere*."""
436 trace = 'focus' in g.app.debug
437 trace_inactive_focus = False # Too disruptive for --trace-focus
438 trace_in_dialog = False # Not useful enough for --trace-focus
439 c = self
440 assert tag == 'idle'
441 if g.unitTesting:
442 return
443 if keys.get('c') != c:
444 if trace:
445 g.trace('no c')
446 return
447 self.idle_focus_count += 1
448 if c.in_qt_dialog:
449 if trace and trace_in_dialog:
450 g.trace('in_qt_dialog')
451 return
452 w = g.app.gui.get_focus(at_idle=True)
453 if g.app.gui.active:
454 # Always call trace_idle_focus.
455 self.trace_idle_focus(w)
456 if w and self.is_unusual_focus(w):
457 if trace:
458 w_class = w and w.__class__.__name__
459 g.trace('***** unusual focus', w_class)
460 # Fix bug 270: Leo's keyboard events doesn't work after "Insert"
461 # on headline and Alt+Tab, Alt+Tab
462 # Presumably, intricate details of Qt event handling are involved.
463 # The focus was in the tree, so put the focus back in the tree.
464 c.treeWantsFocusNow()
465 # elif not w and active:
466 # c.bodyWantsFocusNow()
467 elif trace and trace_inactive_focus:
468 w_class = w and w.__class__.__name__
469 count = c.idle_focus_count
470 g.trace(f"{count} inactive focus: {w_class}")
471 #@+node:ekr.20160427062131.1: *5* c.is_unusual_focus
472 def is_unusual_focus(self, w):
473 """Return True if w is not in an expected place."""
474 #
475 # #270: Leo's keyboard events doesn't work after "Insert"
476 # on headline and Alt+Tab, Alt+Tab
477 #
478 # #276: Focus lost...in Nav text input
479 from leo.plugins import qt_frame
480 return isinstance(w, qt_frame.QtTabBarWrapper)
481 #@+node:ekr.20150403063658.1: *5* c.trace_idle_focus
482 last_unusual_focus = None
483 # last_no_focus = False
485 def trace_idle_focus(self, w):
486 """Trace the focus for w, minimizing chatter."""
487 from leo.core.leoQt import QtWidgets
488 from leo.plugins import qt_frame
489 trace = 'focus' in g.app.debug
490 trace_known = False
491 c = self
492 table = (QtWidgets.QWidget, qt_frame.LeoQTreeWidget,)
493 count = c.idle_focus_count
494 if w:
495 w_class = w and w.__class__.__name__
496 c.last_no_focus = False
497 if self.is_unusual_focus(w):
498 if trace:
499 g.trace(f"{count} unusual focus: {w_class}")
500 else:
501 c.last_unusual_focus = None
502 if isinstance(w, table):
503 if trace and trace_known:
504 g.trace(f"{count} known focus: {w_class}")
505 elif trace:
506 g.trace(f"{count} unknown focus: {w_class}")
507 else:
508 if trace:
509 g.trace(f"{count:3} no focus")
510 #@+node:ekr.20081005065934.1: *4* c.initAfterLoad
511 def initAfterLoad(self):
512 """Provide an offical hook for late inits of the commander."""
513 pass
514 #@+node:ekr.20090213065933.6: *4* c.initConfigSettings
515 def initConfigSettings(self):
516 """Init all cached commander config settings."""
517 c = self
518 getBool = c.config.getBool
519 getColor = c.config.getColor
520 getData = c.config.getData
521 getInt = c.config.getInt
522 c.autoindent_in_nocolor = getBool('autoindent-in-nocolor-mode')
523 c.collapse_nodes_after_move = getBool('collapse-nodes-after-move')
524 c.collapse_on_lt_arrow = getBool('collapse-on-lt-arrow', default=True)
525 c.contractVisitedNodes = getBool('contractVisitedNodes')
526 c.fixedWindowPositionData = getData('fixedWindowPosition')
527 c.focus_border_color = getColor('focus-border-color') or 'red'
528 c.focus_border_command_state_color = getColor(
529 'focus-border-command-state-color') or 'blue'
530 c.focus_border_overwrite_state_color = getColor(
531 'focus-border-overwrite-state-color') or 'green'
532 c.focus_border_width = getInt('focus-border-width') or 1 # pixels
533 c.forceExecuteEntireBody = getBool('force-execute-entire-body', default=False)
534 c.make_node_conflicts_node = getBool('make-node-conflicts-node', default=True)
535 c.outlineHasInitialFocus = getBool('outline-pane-has-initial-focus')
536 c.page_width = getInt('page-width') or 132
537 # c.putBitsFlag = getBool('put-expansion-bits-in-leo-files', default=True)
538 c.sparse_move = getBool('sparse-move-outline-left')
539 c.sparse_find = getBool('collapse-nodes-during-finds')
540 c.sparce_spell = getBool('collapse-nodes-while-spelling')
541 c.stayInTreeAfterSelect = getBool('stayInTreeAfterSelect')
542 c.smart_tab = getBool('smart-tab')
543 c.tab_width = getInt('tab-width') or -4
544 c.verbose_check_outline = getBool('verbose-check-outline', default=False)
545 c.vim_mode = getBool('vim-mode', default=False)
546 c.write_script_file = getBool('write-script-file')
547 #@+node:ekr.20090213065933.7: *4* c.setWindowPosition
548 def setWindowPosition(self):
549 c = self
550 if c.fixedWindowPositionData:
551 try:
552 aList = [z.strip() for z in c.fixedWindowPositionData if z.strip()]
553 w, h, l, t = aList
554 c.fixedWindowPosition = int(w), int(h), int(l), int(t) # type:ignore
555 except Exception:
556 g.error('bad @data fixedWindowPosition',
557 repr(self.fixedWindowPosition))
558 else:
559 c.windowPosition = 500, 700, 50, 50 # width,height,left,top.
560 #@+node:ekr.20210530065748.1: *3* @cmd c.execute-general-script
561 @cmd('execute-general-script')
562 def execute_general_script_command(self, event=None):
563 """
564 Execute c.p and all its descendants as a script.
566 Create a temp file if c.p is not an @<file> node.
568 @data exec-script-commands associates commands with langauges.
570 @data exec-script-patterns provides patterns to create clickable
571 links for error messages.
573 Set the cwd before calling the command.
574 """
575 c, p, tag = self, self.p, 'execute-general-script'
577 def get_setting_for_language(setting: str):
578 """
579 Return the setting from the given @data setting.
580 The first colon ends each key.
581 """
582 for s in c.config.getData(setting) or []:
583 key, val = s.split(':', 1)
584 if key.strip() == language:
585 return val.strip()
586 return None
588 # Get the language and extension.
589 d = c.scanAllDirectives(p)
590 language: str = d.get('language')
591 if not language:
592 print(f"{tag}: No language in effect at {p.h}")
593 return
594 ext = g.app.language_extension_dict.get(language)
595 if not ext:
596 print(f"{tag}: No extention for {language}")
597 return
598 # Get the command.
599 command = get_setting_for_language('exec-script-commands')
600 if not command:
601 print(f"{tag}: No command for {language} in @data exec-script-commands")
602 return
603 # Get the optional pattern.
604 regex = get_setting_for_language('exec-script-patterns')
605 # Set the directory, if possible.
606 if p.isAnyAtFileNode():
607 path = g.fullPath(c, p)
608 directory = os.path.dirname(path)
609 else:
610 directory = None
611 c.general_script_helper(command, ext, language,
612 directory=directory, regex=regex, root=p)
613 #@+node:vitalije.20190924191405.1: *3* @cmd execute-pytest
614 @cmd('execute-pytest')
615 def execute_pytest(self, event=None):
616 """Using pytest, execute all @test nodes for p, p's parents and p's subtree."""
617 c = self
619 def it(p):
620 for p1 in p.self_and_parents():
621 if p1.h.startswith('@test '):
622 yield p1
623 return
624 for p1 in p.subtree():
625 if p1.h.startswith('@test '):
626 yield p1
628 try:
629 for p in it(c.p):
630 self.execute_single_pytest(p)
631 except ImportError:
632 g.es('pytest needs to be installed')
633 return
635 def execute_single_pytest(self, p):
636 c = self
637 from _pytest.config import get_config
638 from _pytest.assertion.rewrite import rewrite_asserts
639 import ast
640 cfg = get_config()
641 script = g.getScript(c, p, useSentinels=False) + (
642 '\n'
643 'ls = dict(locals())\n'
644 'failed = 0\n'
645 'for x in ls:\n'
646 ' if x.startswith("test_") and callable(ls[x]):\n'
647 ' try:\n'
648 ' ls[x]()\n'
649 ' except AssertionError as e:\n'
650 ' failed += 1\n'
651 ' g.es(f"-------{p.h[6:].strip()}/{x} failed---------")\n'
652 ' g.es(str(e))\n'
653 'if failed == 0:\n'
654 ' g.es("all tests passed")\n'
655 'else:\n'
656 ' g.es(f"failed:{failed} tests")\n')
658 fname = g.os_path_finalize_join(g.app.homeLeoDir, 'leoPytestScript.py')
659 with open(fname, 'wt', encoding='utf8') as out:
660 out.write(script)
661 tree = ast.parse(script, filename=fname)
662 # A mypy bug: the script can be str.
663 rewrite_asserts(tree, script, config=cfg) # type:ignore
664 co = compile(tree, fname, "exec", dont_inherit=True)
665 sys.path.insert(0, '.')
666 sys.path.insert(0, c.frame.openDirectory)
667 try:
668 exec(co, {'c': c, 'g': g, 'p': p})
669 except KeyboardInterrupt:
670 g.es('interrupted')
671 except Exception:
672 g.handleScriptException(c, p, script, script)
673 finally:
674 del sys.path[:2]
675 #@+node:ekr.20171123135625.4: *3* @cmd execute-script & public helpers
676 @cmd('execute-script')
677 def executeScript(self, event=None,
678 args=None, p=None, script=None, useSelectedText=True,
679 define_g=True, define_name='__main__',
680 silent=False, namespace=None, raiseFlag=False,
681 runPyflakes=True,
682 ):
683 """
684 Execute a *Leo* script, written in python.
685 Keyword args:
686 args=None Not None: set script_args in the execution environment.
687 p=None Get the script from p.b, unless script is given.
688 script=None None: use script in p.b or c.p.b
689 useSelectedText=True False: use all the text in p.b or c.p.b.
690 define_g=True True: define g for the script.
691 define_name='__main__' Not None: define the name symbol.
692 silent=False No longer used.
693 namespace=None Not None: execute the script in this namespace.
694 raiseFlag=False True: reraise any exceptions.
695 runPyflakes=True True: run pyflakes if allowed by setting.
696 """
697 c, script1 = self, script
698 if runPyflakes:
699 run_pyflakes = c.config.getBool('run-pyflakes-on-write', default=False)
700 else:
701 run_pyflakes = False
702 if not script:
703 if c.forceExecuteEntireBody:
704 useSelectedText = False
705 script = g.getScript(c, p or c.p, useSelectedText=useSelectedText)
706 script_p = p or c.p # Only for error reporting below.
707 # #532: check all scripts with pyflakes.
708 if run_pyflakes and not g.unitTesting:
709 from leo.commands import checkerCommands as cc
710 prefix = ('c,g,p,script_gnx=None,None,None,None;'
711 'assert c and g and p and script_gnx;\n')
712 cc.PyflakesCommand(c).check_script(script_p, prefix + script)
713 self.redirectScriptOutput()
714 oldLog = g.app.log
715 try:
716 log = c.frame.log
717 g.app.log = log
718 if script.strip():
719 sys.path.insert(0, '.') # New in Leo 5.0
720 sys.path.insert(0, c.frame.openDirectory) # per SegundoBob
721 script += '\n' # Make sure we end the script properly.
722 try:
723 if not namespace or namespace.get('script_gnx') is None:
724 namespace = namespace or {}
725 namespace.update(script_gnx=script_p.gnx)
726 # We *always* execute the script with p = c.p.
727 c.executeScriptHelper(args, define_g, define_name, namespace, script)
728 except KeyboardInterrupt:
729 g.es('interrupted')
730 except Exception:
731 if raiseFlag:
732 raise
733 g.handleScriptException(c, script_p, script, script1)
734 finally:
735 del sys.path[0]
736 del sys.path[0]
737 else:
738 tabName = log and hasattr(log, 'tabName') and log.tabName or 'Log'
739 g.warning("no script selected", tabName=tabName)
740 finally:
741 g.app.log = oldLog
742 self.unredirectScriptOutput()
743 #@+node:ekr.20171123135625.5: *4* c.executeScriptHelper
744 def executeScriptHelper(self, args, define_g, define_name, namespace, script):
745 c = self
746 if c.p:
747 p = c.p.copy() # *Always* use c.p and pass c.p to script.
748 c.setCurrentDirectoryFromContext(p)
749 else:
750 p = None
751 d = {'c': c, 'g': g, 'input': g.input_, 'p': p} if define_g else {}
752 if define_name:
753 d['__name__'] = define_name
754 d['script_args'] = args or []
755 d['script_gnx'] = g.app.scriptDict.get('script_gnx')
756 if namespace:
757 d.update(namespace)
758 # A kludge: reset c.inCommand here to handle the case where we *never* return.
759 # (This can happen when there are multiple event loops.)
760 # This does not prevent zombie windows if the script puts up a dialog...
761 try:
762 c.inCommand = False
763 g.inScript = g.app.inScript = True # g.inScript is a synonym for g.app.inScript.
764 if c.write_script_file:
765 scriptFile = self.writeScriptFile(script)
766 exec(compile(script, scriptFile, 'exec'), d)
767 else:
768 exec(script, d)
769 finally:
770 g.inScript = g.app.inScript = False
771 #@+node:ekr.20171123135625.6: *4* c.redirectScriptOutput
772 def redirectScriptOutput(self):
773 c = self
774 if c.config.redirect_execute_script_output_to_log_pane:
775 g.redirectStdout() # Redirect stdout
776 g.redirectStderr() # Redirect stderr
777 #@+node:ekr.20171123135625.7: *4* c.setCurrentDirectoryFromContext
778 def setCurrentDirectoryFromContext(self, p):
779 c = self
780 aList = g.get_directives_dict_list(p)
781 path = c.scanAtPathDirectives(aList)
782 curDir = g.os_path_abspath(os.getcwd())
783 if path and path != curDir:
784 try:
785 os.chdir(path)
786 except Exception:
787 pass
788 #@+node:ekr.20171123135625.8: *4* c.unredirectScriptOutput
789 def unredirectScriptOutput(self):
790 c = self
791 if c.exists and c.config.redirect_execute_script_output_to_log_pane:
792 g.restoreStderr()
793 g.restoreStdout()
794 #@+node:ekr.20080514131122.12: *3* @cmd recolor
795 @cmd('recolor')
796 def recolorCommand(self, event=None):
797 """Force a full recolor."""
798 c = self
799 wrapper = c.frame.body.wrapper
800 # Setting all text appears to be the only way.
801 i, j = wrapper.getSelectionRange()
802 ins = wrapper.getInsertPoint()
803 wrapper.setAllText(c.p.b)
804 wrapper.setSelectionRange(i, j, insert=ins)
805 #@+node:ekr.20171124100654.1: *3* c.API
806 # These methods are a fundamental, unchanging, part of Leo's API.
807 #@+node:ekr.20091001141621.6061: *4* c.Generators
808 #@+node:ekr.20091001141621.6043: *5* c.all_nodes & all_unique_nodes
809 def all_nodes(self):
810 """A generator returning all vnodes in the outline, in outline order."""
811 c = self
812 for p in c.all_positions():
813 yield p.v
815 def all_unique_nodes(self):
816 """A generator returning each vnode of the outline."""
817 c = self
818 for p in c.all_unique_positions(copy=False):
819 yield p.v
821 # Compatibility with old code...
823 all_vnodes_iter = all_nodes
824 all_unique_vnodes_iter = all_unique_nodes
825 #@+node:ekr.20091001141621.6044: *5* c.all_positions
826 def all_positions(self, copy=True):
827 """A generator return all positions of the outline, in outline order."""
828 c = self
829 p = c.rootPosition()
830 while p:
831 yield p.copy() if copy else p
832 p.moveToThreadNext()
834 # Compatibility with old code...
836 all_positions_iter = all_positions
837 allNodes_iter = all_positions
838 #@+node:ekr.20191014093239.1: *5* c.all_positions_for_v
839 def all_positions_for_v(self, v, stack=None):
840 """
841 Generates all positions p in this outline where p.v is v.
843 Should be called with stack=None.
845 The generated positions are not necessarily in outline order.
847 By Виталије Милошевић (Vitalije Milosevic).
848 """
849 c = self
851 if stack is None:
852 stack = []
854 if not isinstance(v, leoNodes.VNode):
855 g.es_print(f"not a VNode: {v!r}")
856 return # Stop the generator.
858 def allinds(v, target_v):
859 """Yield all indices i such that v.children[i] == target_v."""
860 for i, x in enumerate(v.children):
861 if x is target_v:
862 yield i
864 def stack2pos(stack):
865 """Convert the stack to a position."""
866 v, i = stack[-1]
867 return leoNodes.Position(v, i, stack[:-1])
869 for v2 in set(v.parents):
870 for i in allinds(v2, v):
871 stack.insert(0, (v, i))
872 if v2 is c.hiddenRootNode:
873 yield stack2pos(stack)
874 else:
875 yield from c.all_positions_for_v(v2, stack)
876 stack.pop(0)
877 #@+node:ekr.20161120121226.1: *5* c.all_roots
878 def all_roots(self, copy=True, predicate=None):
879 """
880 A generator yielding *all* the root positions in the outline that
881 satisfy the given predicate. p.isAnyAtFileNode is the default
882 predicate.
883 """
884 c = self
885 if predicate is None:
887 # pylint: disable=function-redefined
889 def predicate(p):
890 return p.isAnyAtFileNode()
892 p = c.rootPosition()
893 while p:
894 if predicate(p):
895 yield p.copy() # 2017/02/19
896 p.moveToNodeAfterTree()
897 else:
898 p.moveToThreadNext()
899 #@+node:ekr.20091001141621.6062: *5* c.all_unique_positions
900 def all_unique_positions(self, copy=True):
901 """
902 A generator return all positions of the outline, in outline order.
903 Returns only the first position for each vnode.
904 """
905 c = self
906 p = c.rootPosition()
907 seen = set()
908 while p:
909 if p.v in seen:
910 p.moveToNodeAfterTree()
911 else:
912 seen.add(p.v)
913 yield p.copy() if copy else p
914 p.moveToThreadNext()
916 # Compatibility with old code...
918 all_positions_with_unique_vnodes_iter = all_unique_positions
919 #@+node:ekr.20161120125322.1: *5* c.all_unique_roots
920 def all_unique_roots(self, copy=True, predicate=None):
921 """
922 A generator yielding all unique root positions in the outline that
923 satisfy the given predicate. p.isAnyAtFileNode is the default
924 predicate.
925 """
926 c = self
927 if predicate is None:
929 # pylint: disable=function-redefined
931 def predicate(p):
932 return p.isAnyAtFileNode()
934 seen = set()
935 p = c.rootPosition()
936 while p:
937 if p.v not in seen and predicate(p):
938 seen.add(p.v)
939 yield p.copy() if copy else p
940 p.moveToNodeAfterTree()
941 else:
942 p.moveToThreadNext()
943 #@+node:ekr.20150316175921.5: *5* c.safe_all_positions
944 def safe_all_positions(self, copy=True):
945 """
946 A generator returning all positions of the outline. This generator does
947 *not* assume that vnodes are never their own ancestors.
948 """
949 c = self
950 p = c.rootPosition() # Make one copy.
951 while p:
952 yield p.copy() if copy else p
953 p.safeMoveToThreadNext()
954 #@+node:ekr.20060906211747: *4* c.Getters
955 #@+node:ekr.20040803140033: *5* c.currentPosition
956 def currentPosition(self):
957 """
958 Return a copy of the presently selected position or a new null
959 position. So c.p.copy() is never necessary.
960 """
961 c = self
962 if hasattr(c, '_currentPosition') and getattr(c, '_currentPosition'):
963 # *Always* return a copy.
964 return c._currentPosition.copy()
965 return c.rootPosition()
967 # For compatibiility with old scripts...
969 currentVnode = currentPosition
970 #@+node:ekr.20190506060937.1: *5* c.dumpExpanded
971 @cmd('dump-expanded')
972 def dump_expanded(self, event):
973 """Print all non-empty v.expandedPositions lists."""
974 c = event.get('c')
975 if not c:
976 return
977 g.es_print('dump-expanded...')
978 for p in c.all_positions():
979 if p.v.expandedPositions:
980 indent = ' ' * p.level()
981 print(f"{indent}{p.h}")
982 g.printObj(p.v.expandedPositions, indent=indent)
983 #@+node:ekr.20040306220230.1: *5* c.edit_widget
984 def edit_widget(self, p):
985 c = self
986 return p and c.frame.tree.edit_widget(p)
987 #@+node:ekr.20031218072017.2986: *5* c.fileName & relativeFileName & shortFileName
988 # Compatibility with scripts
990 def fileName(self):
991 s = self.mFileName or ""
992 if g.isWindows:
993 s = s.replace('\\', '/')
994 return s
996 def relativeFileName(self):
997 return self.mRelativeFileName or self.mFileName
999 def shortFileName(self):
1000 return g.shortFileName(self.mFileName)
1002 shortFilename = shortFileName
1003 #@+node:ekr.20070615070925.1: *5* c.firstVisible
1004 def firstVisible(self):
1005 """Move to the first visible node of the present chapter or hoist."""
1006 c, p = self, self.p
1007 while 1:
1008 back = p.visBack(c)
1009 if back and back.isVisible(c):
1010 p = back
1011 else: break
1012 return p
1013 #@+node:ekr.20171123135625.29: *5* c.getBodyLines
1014 def getBodyLines(self):
1015 """
1016 Return (head, lines, tail, oldSel, oldYview).
1018 - head: string containg all the lines before the selected text (or the
1019 text before the insert point if no selection)
1020 - lines: list of lines containing the selected text
1021 (or the line containing the insert point if no selection)
1022 - after: string containing all lines after the selected text
1023 (or the text after the insert point if no selection)
1024 - oldSel: tuple containing the old selection range, or None.
1025 - oldYview: int containing the old y-scroll value, or None.
1026 """
1027 c = self
1028 body = c.frame.body
1029 w = body.wrapper
1030 oldVview = w.getYScrollPosition()
1031 # Note: lines is the entire line containing the insert point if no selection.
1032 head, s, tail = body.getSelectionLines()
1033 lines = g.splitLines(s) # Retain the newlines of each line.
1034 # Expand the selection.
1035 i = len(head)
1036 j = len(head) + len(s)
1037 oldSel = i, j
1038 return head, lines, tail, oldSel, oldVview # string,list,string,tuple,int.
1039 #@+node:ekr.20150417073117.1: *5* c.getTabWidth
1040 def getTabWidth(self, p):
1041 """Return the tab width in effect at p."""
1042 c = self
1043 val = g.scanAllAtTabWidthDirectives(c, p)
1044 return val
1045 #@+node:ekr.20040803112200: *5* c.is...Position
1046 #@+node:ekr.20040803155551: *6* c.currentPositionIsRootPosition
1047 def currentPositionIsRootPosition(self):
1048 """Return True if the current position is the root position.
1050 This method is called during idle time, so not generating positions
1051 here fixes a major leak.
1052 """
1053 c = self
1054 root = c.rootPosition()
1055 return c._currentPosition and root and c._currentPosition == root
1056 # return (
1057 # c._currentPosition and c._rootPosition and
1058 # c._currentPosition == c._rootPosition)
1059 #@+node:ekr.20040803160656: *6* c.currentPositionHasNext
1060 def currentPositionHasNext(self):
1061 """Return True if the current position is the root position.
1063 This method is called during idle time, so not generating positions
1064 here fixes a major leak.
1065 """
1066 c = self
1067 current = c._currentPosition
1068 return current and current.hasNext()
1069 #@+node:ekr.20040803112450: *6* c.isCurrentPosition
1070 def isCurrentPosition(self, p):
1071 c = self
1072 if p is None or c._currentPosition is None:
1073 return False
1074 return p == c._currentPosition
1075 #@+node:ekr.20040803112450.1: *6* c.isRootPosition
1076 def isRootPosition(self, p):
1077 c = self
1078 root = c.rootPosition()
1079 return p and root and p == root # 2011/03/03
1080 #@+node:ekr.20031218072017.2987: *5* c.isChanged
1081 def isChanged(self):
1082 return self.changed
1083 #@+node:ekr.20210901104900.1: *5* c.lastPosition
1084 def lastPosition(self):
1085 c = self
1086 p = c.rootPosition()
1087 while p.hasNext():
1088 p.moveToNext()
1089 while p.hasThreadNext():
1090 p.moveToThreadNext()
1091 return p
1092 #@+node:ekr.20140106215321.16676: *5* c.lastTopLevel
1093 def lastTopLevel(self):
1094 """Return the last top-level position in the outline."""
1095 c = self
1096 p = c.rootPosition()
1097 while p.hasNext():
1098 p.moveToNext()
1099 return p
1100 #@+node:ekr.20031218072017.4146: *5* c.lastVisible
1101 def lastVisible(self):
1102 """Move to the last visible node of the present chapter or hoist."""
1103 c, p = self, self.p
1104 while 1:
1105 next = p.visNext(c)
1106 if next and next.isVisible(c):
1107 p = next
1108 else: break
1109 return p
1110 #@+node:ekr.20040307104131.3: *5* c.positionExists
1111 def positionExists(self, p, root=None, trace=False):
1112 """Return True if a position exists in c's tree"""
1113 if not p or not p.v:
1114 return False
1116 rstack = root.stack + [(root.v, root._childIndex)] if root else []
1117 pstack = p.stack + [(p.v, p._childIndex)]
1119 if len(rstack) > len(pstack):
1120 return False
1122 par = self.hiddenRootNode
1123 for j, x in enumerate(pstack):
1124 if j < len(rstack) and x != rstack[j]:
1125 return False
1126 v, i = x
1127 if i >= len(par.children) or v is not par.children[i]:
1128 return False
1129 par = v
1130 return True
1131 #@+node:ekr.20160427153457.1: *6* c.dumpPosition
1132 def dumpPosition(self, p):
1133 """Dump position p and it's ancestors."""
1134 g.trace('=====', p.h, p._childIndex)
1135 for i, data in enumerate(p.stack):
1136 v, childIndex = data
1137 print(f"{i} {childIndex} {v._headString}")
1138 #@+node:ekr.20040803140033.2: *5* c.rootPosition
1139 _rootCount = 0
1141 def rootPosition(self):
1142 """Return the root position.
1144 Root position is the first position in the document. Other
1145 top level positions are siblings of this node.
1146 """
1147 c = self
1148 # 2011/02/25: Compute the position directly.
1149 if c.hiddenRootNode.children:
1150 v = c.hiddenRootNode.children[0]
1151 return leoNodes.Position(v, childIndex=0, stack=None)
1152 return None
1154 # For compatibiility with old scripts...
1156 rootVnode = rootPosition
1157 findRootPosition = rootPosition
1158 #@+node:ekr.20131017174814.17480: *5* c.shouldBeExpanded
1159 def shouldBeExpanded(self, p):
1160 """Return True if the node at position p should be expanded."""
1161 c, v = self, p.v
1162 if not p.hasChildren():
1163 return False
1164 # Always clear non-existent positions.
1165 v.expandedPositions = [z for z in v.expandedPositions if c.positionExists(z)]
1166 if not p.isCloned():
1167 # Do not call p.isExpanded here! It calls this method.
1168 return p.v.isExpanded()
1169 if p.isAncestorOf(c.p):
1170 return True
1171 for p2 in v.expandedPositions:
1172 if p == p2:
1173 return True
1174 return False
1175 #@+node:ekr.20070609122713: *5* c.visLimit
1176 def visLimit(self):
1177 """
1178 Return the topmost visible node.
1179 This is affected by chapters and hoists.
1180 """
1181 c = self
1182 cc = c.chapterController
1183 if c.hoistStack:
1184 bunch = c.hoistStack[-1]
1185 p = bunch.p
1186 limitIsVisible = not cc or not p.h.startswith('@chapter')
1187 return p, limitIsVisible
1188 return None, None
1189 #@+node:tbrown.20091206142842.10296: *5* c.vnode2allPositions
1190 def vnode2allPositions(self, v):
1191 """Given a VNode v, find all valid positions p such that p.v = v.
1193 Not really all, just all for each of v's distinct immediate parents.
1194 """
1195 c = self
1196 context = v.context # v's commander.
1197 assert c == context
1198 positions = []
1199 for immediate in v.parents:
1200 if v in immediate.children:
1201 n = immediate.children.index(v)
1202 else:
1203 continue
1204 stack = [(v, n)]
1205 while immediate.parents:
1206 parent = immediate.parents[0]
1207 if immediate in parent.children:
1208 n = parent.children.index(immediate)
1209 else:
1210 break
1211 stack.insert(0, (immediate, n),)
1212 immediate = parent
1213 else:
1214 v, n = stack.pop()
1215 p = leoNodes.Position(v, n, stack)
1216 positions.append(p)
1217 return positions
1218 #@+node:ekr.20090107113956.1: *5* c.vnode2position
1219 def vnode2position(self, v):
1220 """Given a VNode v, construct a valid position p such that p.v = v.
1221 """
1222 c = self
1223 context = v.context # v's commander.
1224 assert c == context
1225 stack: List[Tuple[int, Tuple["leoNodes.VNode", int]]] = []
1226 while v.parents:
1227 parent = v.parents[0]
1228 if v in parent.children:
1229 n = parent.children.index(v)
1230 else:
1231 return None
1232 stack.insert(0, (v, n),)
1233 v = parent
1234 # v.parents includes the hidden root node.
1235 if not stack:
1236 # a VNode not in the tree
1237 return None
1238 v, n = stack.pop()
1239 p = leoNodes.Position(v, n, stack) # type:ignore
1240 return p
1241 #@+node:ekr.20090130135126.1: *4* c.Properties
1242 def __get_p(self):
1243 c = self
1244 return c.currentPosition()
1246 p = property(
1247 __get_p, # No setter.
1248 doc="commander current position property")
1249 #@+node:ekr.20060906211747.1: *4* c.Setters
1250 #@+node:ekr.20040315032503: *5* c.appendStringToBody
1251 def appendStringToBody(self, p, s):
1253 if s:
1254 p.b = p.b + g.toUnicode(s)
1255 #@+node:ekr.20031218072017.2984: *5* c.clearAllMarked
1256 def clearAllMarked(self):
1257 c = self
1258 for p in c.all_unique_positions(copy=False):
1259 p.v.clearMarked()
1260 #@+node:ekr.20031218072017.2985: *5* c.clearAllVisited
1261 def clearAllVisited(self):
1262 c = self
1263 for p in c.all_unique_positions(copy=False):
1264 p.v.clearVisited()
1265 p.v.clearWriteBit()
1266 #@+node:ekr.20191215044636.1: *5* c.clearChanged
1267 def clearChanged(self):
1268 """clear the marker that indicates that the .leo file has been changed."""
1269 c = self
1270 if not c.frame:
1271 return
1272 c.changed = False
1273 if c.loading:
1274 return # don't update while loading.
1275 # Clear all dirty bits _before_ setting the caption.
1276 for v in c.all_unique_nodes():
1277 v.clearDirty()
1278 c.changed = False
1279 # Do nothing for null frames.
1280 assert c.gui
1281 if c.gui.guiName() == 'nullGui':
1282 return
1283 if not c.frame.top:
1284 return
1285 master = getattr(c.frame.top, 'leo_master', None)
1286 if master:
1287 master.setChanged(c, changed=False) # LeoTabbedTopLevel.setChanged.
1288 s = c.frame.getTitle()
1289 if len(s) > 2 and s[0:2] == "* ":
1290 c.frame.setTitle(s[2:])
1291 #@+node:ekr.20060906211138: *5* c.clearMarked
1292 def clearMarked(self, p):
1293 c = self
1294 p.v.clearMarked()
1295 g.doHook("clear-mark", c=c, p=p)
1296 #@+node:ekr.20040305223522: *5* c.setBodyString
1297 def setBodyString(self, p, s):
1298 """
1299 This is equivalent to p.b = s.
1301 Warning: This method may call c.recolor() or c.redraw().
1302 """
1303 c, v = self, p.v
1304 if not c or not v:
1305 return
1306 s = g.toUnicode(s)
1307 current = c.p
1308 # 1/22/05: Major change: the previous test was: 'if p == current:'
1309 # This worked because commands work on the presently selected node.
1310 # But setRecentFiles may change a _clone_ of the selected node!
1311 if current and p.v == current.v:
1312 w = c.frame.body.wrapper
1313 w.setAllText(s)
1314 v.setSelection(0, 0)
1315 c.recolor()
1316 # Keep the body text in the VNode up-to-date.
1317 if v.b != s:
1318 v.setBodyString(s)
1319 v.setSelection(0, 0)
1320 p.setDirty()
1321 if not c.isChanged():
1322 c.setChanged()
1323 c.redraw_after_icons_changed()
1324 #@+node:ekr.20031218072017.2989: *5* c.setChanged
1325 def setChanged(self):
1326 """Set the marker that indicates that the .leo file has been changed."""
1327 c = self
1328 if not c.frame:
1329 return
1330 c.changed = True
1331 if c.loading:
1332 return # don't update while loading.
1333 # Do nothing for null frames.
1334 assert c.gui
1335 if c.gui.guiName() == 'nullGui':
1336 return
1337 if not c.frame.top:
1338 return
1339 master = getattr(c.frame.top, 'leo_master', None)
1340 if master:
1341 master.setChanged(c, changed=True)
1342 # LeoTabbedTopLevel.setChanged.
1343 s = c.frame.getTitle()
1344 if len(s) > 2 and s[0] != '*':
1345 c.frame.setTitle("* " + s)
1346 #@+node:ekr.20040803140033.1: *5* c.setCurrentPosition
1347 _currentCount = 0
1349 def setCurrentPosition(self, p):
1350 """
1351 Set the presently selected position. For internal use only.
1352 Client code should use c.selectPosition instead.
1353 """
1354 c = self
1355 if not p:
1356 g.trace('===== no p', g.callers())
1357 return
1358 if c.positionExists(p):
1359 if c._currentPosition and p == c._currentPosition:
1360 pass # We have already made a copy.
1361 else: # Make a copy _now_
1362 c._currentPosition = p.copy()
1363 else:
1364 # Don't kill unit tests for this nkind of problem.
1365 c._currentPosition = c.rootPosition()
1366 g.trace('Invalid position', repr(p))
1367 g.trace(g.callers())
1369 # For compatibiility with old scripts.
1371 setCurrentVnode = setCurrentPosition
1372 #@+node:ekr.20040305223225: *5* c.setHeadString
1373 def setHeadString(self, p, s):
1374 """
1375 Set the p's headline and the corresponding tree widget to s.
1377 This is used in by unit tests to restore the outline.
1378 """
1379 c = self
1380 p.initHeadString(s)
1381 p.setDirty()
1382 # Change the actual tree widget so
1383 # A later call to c.endEditing or c.redraw will use s.
1384 c.frame.tree.setHeadline(p, s)
1385 #@+node:ekr.20060109164136: *5* c.setLog
1386 def setLog(self):
1387 c = self
1388 if c.exists:
1389 try:
1390 # c.frame or c.frame.log may not exist.
1391 g.app.setLog(c.frame.log)
1392 except AttributeError:
1393 pass
1394 #@+node:ekr.20060906211138.1: *5* c.setMarked (calls hook)
1395 def setMarked(self, p):
1396 c = self
1397 p.setMarked()
1398 p.setDirty() # Defensive programming.
1399 g.doHook("set-mark", c=c, p=p)
1400 #@+node:ekr.20040803140033.3: *5* c.setRootPosition (A do-nothing)
1401 def setRootPosition(self, unused_p=None):
1402 """Set c._rootPosition."""
1403 # 2011/03/03: No longer used.
1404 #@+node:ekr.20060906131836: *5* c.setRootVnode (A do-nothing)
1405 def setRootVnode(self, v):
1406 pass
1407 # c = self
1408 # # 2011/02/25: c.setRootPosition needs no arguments.
1409 # c.setRootPosition()
1410 #@+node:ekr.20040311173238: *5* c.topPosition & c.setTopPosition
1411 def topPosition(self):
1412 """Return the root position."""
1413 c = self
1414 if c._topPosition:
1415 return c._topPosition.copy()
1416 return None
1418 def setTopPosition(self, p):
1419 """Set the root positioin."""
1420 c = self
1421 if p:
1422 c._topPosition = p.copy()
1423 else:
1424 c._topPosition = None
1426 # Define these for compatibiility with old scripts...
1428 topVnode = topPosition
1429 setTopVnode = setTopPosition
1430 #@+node:ekr.20171124081419.1: *3* c.Check Outline...
1431 #@+node:ekr.20141024211256.22: *4* c.checkGnxs
1432 def checkGnxs(self):
1433 """
1434 Check the consistency of all gnx's.
1435 Reallocate gnx's for duplicates or empty gnx's.
1436 Return the number of structure_errors found.
1437 """
1438 c = self
1439 # Keys are gnx's; values are sets of vnodes with that gnx.
1440 d: Dict[str, Set["leoNodes.VNode"]] = {}
1441 ni = g.app.nodeIndices
1442 t1 = time.time()
1444 def new_gnx(v):
1445 """Set v.fileIndex."""
1446 v.fileIndex = ni.getNewIndex(v)
1448 count, gnx_errors = 0, 0
1449 for p in c.safe_all_positions(copy=False):
1450 count += 1
1451 v = p.v
1452 gnx = v.fileIndex
1453 if gnx: # gnx must be a string.
1454 aSet: Set["leoNodes.VNode"] = d.get(gnx, set())
1455 aSet.add(v)
1456 d[gnx] = aSet
1457 else:
1458 gnx_errors += 1
1459 new_gnx(v)
1460 g.es_print(f"empty v.fileIndex: {v} new: {p.v.gnx!r}", color='red')
1461 for gnx in sorted(d.keys()):
1462 aList = list(d.get(gnx))
1463 if len(aList) != 1:
1464 print('\nc.checkGnxs...')
1465 g.es_print(f"multiple vnodes with gnx: {gnx!r}", color='red')
1466 for v in aList:
1467 gnx_errors += 1
1468 g.es_print(f"id(v): {id(v)} gnx: {v.fileIndex} {v.h}", color='red')
1469 new_gnx(v)
1470 ok = not gnx_errors and not g.app.structure_errors
1471 t2 = time.time()
1472 if not ok:
1473 g.es_print(
1474 f"check-outline ERROR! {c.shortFileName()} "
1475 f"{count} nodes, "
1476 f"{gnx_errors} gnx errors, "
1477 f"{g.app.structure_errors} "
1478 f"structure errors",
1479 color='red'
1480 )
1481 elif c.verbose_check_outline and not g.unitTesting:
1482 print(
1483 f"check-outline OK: {t2 - t1:4.2f} sec. "
1484 f"{c.shortFileName()} {count} nodes")
1485 return g.app.structure_errors
1486 #@+node:ekr.20150318131947.7: *4* c.checkLinks & helpers
1487 def checkLinks(self):
1488 """Check the consistency of all links in the outline."""
1489 c = self
1490 t1 = time.time()
1491 count, errors = 0, 0
1492 for p in c.safe_all_positions():
1493 count += 1
1494 # try:
1495 if not c.checkThreadLinks(p):
1496 errors += 1
1497 break
1498 if not c.checkSiblings(p):
1499 errors += 1
1500 break
1501 if not c.checkParentAndChildren(p):
1502 errors += 1
1503 break
1504 # except AssertionError:
1505 # errors += 1
1506 # junk, value, junk = sys.exc_info()
1507 # g.error("test failed at position %s\n%s" % (repr(p), value))
1508 t2 = time.time()
1509 g.es_print(
1510 f"check-links: {t2 - t1:4.2f} sec. "
1511 f"{c.shortFileName()} {count} nodes", color='blue')
1512 return errors
1513 #@+node:ekr.20040314035615.2: *5* c.checkParentAndChildren
1514 def checkParentAndChildren(self, p):
1515 """Check consistency of parent and child data structures."""
1516 c = self
1518 def _assert(condition):
1519 return g._assert(condition, show_callers=False)
1521 def dump(p):
1522 if p and p.v:
1523 p.v.dump()
1524 elif p:
1525 print('<no p.v>')
1526 else:
1527 print('<no p>')
1528 if g.unitTesting:
1529 assert False, g.callers()
1531 if p.hasParent():
1532 n = p.childIndex()
1533 if not _assert(p == p.parent().moveToNthChild(n)):
1534 g.trace(f"p != parent().moveToNthChild({n})")
1535 dump(p)
1536 dump(p.parent())
1537 return False
1538 if p.level() > 0 and not _assert(p.v.parents):
1539 g.trace("no parents")
1540 dump(p)
1541 return False
1542 for child in p.children():
1543 if not c.checkParentAndChildren(child):
1544 return False
1545 if not _assert(p == child.parent()):
1546 g.trace("p != child.parent()")
1547 dump(p)
1548 dump(child.parent())
1549 return False
1550 if p.hasNext():
1551 if not _assert(p.next().parent() == p.parent()):
1552 g.trace("p.next().parent() != p.parent()")
1553 dump(p.next().parent())
1554 dump(p.parent())
1555 return False
1556 if p.hasBack():
1557 if not _assert(p.back().parent() == p.parent()):
1558 g.trace("p.back().parent() != parent()")
1559 dump(p.back().parent())
1560 dump(p.parent())
1561 return False
1562 # Check consistency of parent and children arrays.
1563 # Every nodes gets visited, so a strong test need only check consistency
1564 # between p and its parent, not between p and its children.
1565 parent_v = p._parentVnode()
1566 n = p.childIndex()
1567 if not _assert(parent_v.children[n] == p.v):
1568 g.trace("parent_v.children[n] != p.v")
1569 parent_v.dump()
1570 p.v.dump()
1571 return False
1572 return True
1573 #@+node:ekr.20040314035615.1: *5* c.checkSiblings
1574 def checkSiblings(self, p):
1575 """Check the consistency of next and back links."""
1576 back = p.back()
1577 next = p.next()
1578 if back:
1579 if not g._assert(p == back.next()):
1580 g.trace(
1581 f"p!=p.back().next()\n"
1582 f" back: {back}\n"
1583 f"back.next: {back.next()}")
1584 return False
1585 if next:
1586 if not g._assert(p == next.back()):
1587 g.trace(
1588 f"p!=p.next().back\n"
1589 f" next: {next}\n"
1590 f"next.back: {next.back()}")
1591 return False
1592 return True
1593 #@+node:ekr.20040314035615: *5* c.checkThreadLinks
1594 def checkThreadLinks(self, p):
1595 """Check consistency of threadNext & threadBack links."""
1596 threadBack = p.threadBack()
1597 threadNext = p.threadNext()
1598 if threadBack:
1599 if not g._assert(p == threadBack.threadNext()):
1600 g.trace("p!=p.threadBack().threadNext()")
1601 return False
1602 if threadNext:
1603 if not g._assert(p == threadNext.threadBack()):
1604 g.trace("p!=p.threadNext().threadBack()")
1605 return False
1606 return True
1607 #@+node:ekr.20031218072017.1760: *4* c.checkMoveWithParentWithWarning & c.checkDrag
1608 #@+node:ekr.20070910105044: *5* c.checkMoveWithParentWithWarning
1609 def checkMoveWithParentWithWarning(self, root, parent, warningFlag):
1610 """
1611 Return False if root or any of root's descendents is a clone of parent
1612 or any of parents ancestors.
1613 """
1614 c = self
1615 message = "Illegal move or drag: no clone may contain a clone of itself"
1616 clonedVnodes = {}
1617 for ancestor in parent.self_and_parents(copy=False):
1618 if ancestor.isCloned():
1619 v = ancestor.v
1620 clonedVnodes[v] = v
1621 if not clonedVnodes:
1622 return True
1623 for p in root.self_and_subtree(copy=False):
1624 if p.isCloned() and clonedVnodes.get(p.v):
1625 if not g.unitTesting and warningFlag:
1626 c.alert(message)
1627 return False
1628 return True
1629 #@+node:ekr.20070910105044.1: *5* c.checkDrag
1630 def checkDrag(self, root, target):
1631 """Return False if target is any descendant of root."""
1632 c = self
1633 message = "Can not drag a node into its descendant tree."
1634 for z in root.subtree():
1635 if z == target:
1636 if not g.unitTesting:
1637 c.alert(message)
1638 return False
1639 return True
1640 #@+node:ekr.20031218072017.2072: *4* c.checkOutline
1641 def checkOutline(self, event=None, check_links=False):
1642 """
1643 Check for errors in the outline.
1644 Return the count of serious structure errors.
1645 """
1646 # The check-outline command sets check_links = True.
1647 c = self
1648 g.app.structure_errors = 0
1649 structure_errors = c.checkGnxs()
1650 if check_links and not structure_errors:
1651 structure_errors += c.checkLinks()
1652 return structure_errors
1653 #@+node:ekr.20031218072017.1765: *4* c.validateOutline
1654 # Makes sure all nodes are valid.
1656 def validateOutline(self, event=None):
1657 c = self
1658 if not g.app.validate_outline:
1659 return True
1660 root = c.rootPosition()
1661 parent = None
1662 if root:
1663 return root.validateOutlineWithParent(parent)
1664 return True
1665 #@+node:ekr.20040723094220: *3* c.Check Python code
1666 # This code is no longer used by any Leo command,
1667 # but it will be retained for use of scripts.
1668 #@+node:ekr.20040723094220.1: *4* c.checkAllPythonCode
1669 def checkAllPythonCode(self, event=None, ignoreAtIgnore=True):
1670 """Check all nodes in the selected tree for syntax and tab errors."""
1671 c = self
1672 count = 0
1673 result = "ok"
1674 for p in c.all_unique_positions():
1675 count += 1
1676 if not g.unitTesting:
1677 #@+<< print dots >>
1678 #@+node:ekr.20040723094220.2: *5* << print dots >>
1679 if count % 100 == 0:
1680 g.es('', '.', newline=False)
1681 if count % 2000 == 0:
1682 g.enl()
1683 #@-<< print dots >>
1684 if g.scanForAtLanguage(c, p) == "python":
1685 if not g.scanForAtSettings(p) and (
1686 not ignoreAtIgnore or not g.scanForAtIgnore(c, p)
1687 ):
1688 try:
1689 c.checkPythonNode(p)
1690 except(SyntaxError, tokenize.TokenError, tabnanny.NannyNag):
1691 result = "error" # Continue to check.
1692 except Exception:
1693 return "surprise" # abort
1694 if result != 'ok':
1695 g.pr(f"Syntax error in {p.h}")
1696 return result # End the unit test: it has failed.
1697 if not g.unitTesting:
1698 g.blue("check complete")
1699 return result
1700 #@+node:ekr.20040723094220.3: *4* c.checkPythonCode
1701 def checkPythonCode(self,
1702 event=None,
1703 ignoreAtIgnore=True,
1704 checkOnSave=False
1705 ):
1706 """Check the selected tree for syntax and tab errors."""
1707 c = self
1708 count = 0
1709 result = "ok"
1710 if not g.unitTesting:
1711 g.es("checking Python code ")
1712 for p in c.p.self_and_subtree():
1713 count += 1
1714 if not g.unitTesting and not checkOnSave:
1715 #@+<< print dots >>
1716 #@+node:ekr.20040723094220.4: *5* << print dots >>
1717 if count % 100 == 0:
1718 g.es('', '.', newline=False)
1719 if count % 2000 == 0:
1720 g.enl()
1721 #@-<< print dots >>
1722 if g.scanForAtLanguage(c, p) == "python":
1723 if not ignoreAtIgnore or not g.scanForAtIgnore(c, p):
1724 try:
1725 c.checkPythonNode(p)
1726 except(SyntaxError, tokenize.TokenError, tabnanny.NannyNag):
1727 result = "error" # Continue to check.
1728 except Exception:
1729 return "surprise" # abort
1730 if not g.unitTesting:
1731 g.blue("check complete")
1732 # We _can_ return a result for unit tests because we aren't using doCommand.
1733 return result
1734 #@+node:ekr.20040723094220.5: *4* c.checkPythonNode
1735 def checkPythonNode(self, p):
1736 c, h = self, p.h
1737 # Call getScript to ignore directives and section references.
1738 body = g.getScript(c, p.copy())
1739 if not body:
1740 return
1741 try:
1742 fn = f"<node: {p.h}>"
1743 compile(body + '\n', fn, 'exec')
1744 c.tabNannyNode(p, h, body)
1745 except SyntaxError:
1746 if g.unitTesting:
1747 raise
1748 g.warning(f"Syntax error in: {h}")
1749 g.es_exception(full=False, color="black")
1750 except Exception:
1751 g.es_print('unexpected exception')
1752 g.es_exception()
1753 raise
1754 #@+node:ekr.20040723094220.6: *4* c.tabNannyNode
1755 # This code is based on tabnanny.check.
1757 def tabNannyNode(self, p, headline, body):
1758 """Check indentation using tabnanny."""
1759 try:
1760 readline = g.ReadLinesClass(body).next
1761 tabnanny.process_tokens(tokenize.generate_tokens(readline))
1762 except IndentationError:
1763 if g.unitTesting:
1764 raise
1765 junk1, msg, junk2 = sys.exc_info()
1766 g.warning("IndentationError in", headline)
1767 g.es('', msg)
1768 except tokenize.TokenError:
1769 if g.unitTesting:
1770 raise
1771 junk1, msg, junk2 = sys.exc_info()
1772 g.warning("TokenError in", headline)
1773 g.es('', msg)
1774 except tabnanny.NannyNag:
1775 if g.unitTesting:
1776 raise
1777 junk1, nag, junk2 = sys.exc_info()
1778 badline = nag.get_lineno()
1779 line = nag.get_line()
1780 message = nag.get_msg()
1781 g.warning("indentation error in", headline, "line", badline)
1782 g.es(message)
1783 line2 = repr(str(line))[1:-1]
1784 g.es("offending line:\n", line2)
1785 except Exception:
1786 g.trace("unexpected exception")
1787 g.es_exception()
1788 raise
1789 #@+node:ekr.20171123200644.1: *3* c.Convenience methods
1790 #@+node:ekr.20171123135625.39: *4* c.getTime
1791 def getTime(self, body=True):
1792 c = self
1793 default_format = "%m/%d/%Y %H:%M:%S" # E.g., 1/30/2003 8:31:55
1794 # Try to get the format string from settings.
1795 if body:
1796 format = c.config.getString("body-time-format-string")
1797 gmt = c.config.getBool("body-gmt-time")
1798 else:
1799 format = c.config.getString("headline-time-format-string")
1800 gmt = c.config.getBool("headline-gmt-time")
1801 if format is None:
1802 format = default_format
1803 try:
1804 # import time
1805 if gmt:
1806 s = time.strftime(format, time.gmtime())
1807 else:
1808 s = time.strftime(format, time.localtime())
1809 except(ImportError, NameError):
1810 g.warning("time.strftime not available on this platform")
1811 return ""
1812 except Exception:
1813 g.es_exception() # Probably a bad format string in leoSettings.leo.
1814 s = time.strftime(default_format, time.gmtime())
1815 return s
1816 #@+node:ekr.20171123135625.10: *4* c.goToLineNumber & goToScriptLineNumber
1817 def goToLineNumber(self, n):
1818 """
1819 Go to line n (zero-based) of a script.
1820 A convenience method called from g.handleScriptException.
1821 """
1822 c = self
1823 c.gotoCommands.find_file_line(n)
1825 def goToScriptLineNumber(self, n, p):
1826 """
1827 Go to line n (zero-based) of a script.
1828 A convenience method called from g.handleScriptException.
1829 """
1830 c = self
1831 c.gotoCommands.find_script_line(n, p)
1832 #@+node:ekr.20090103070824.9: *4* c.setFileTimeStamp
1833 def setFileTimeStamp(self, fn):
1834 """Update the timestamp for fn.."""
1835 # c = self
1836 if g.app.externalFilesController:
1837 g.app.externalFilesController.set_time(fn)
1838 #@+node:ekr.20031218072017.3000: *4* c.updateSyntaxColorer
1839 def updateSyntaxColorer(self, v):
1840 self.frame.body.updateSyntaxColorer(v)
1841 #@+node:ekr.20180503110307.1: *4* c.interactive*
1842 #@+node:ekr.20180504075937.1: *5* c.interactive
1843 def interactive(self, callback, event, prompts):
1844 #@+<< c.interactive docstring >>
1845 #@+node:ekr.20180503131222.1: *6* << c.interactive docstring >>
1846 """
1847 c.interactive: Prompt for up to three arguments from the minibuffer.
1849 The number of prompts determines the number of arguments.
1851 Use the @command decorator to define commands. Examples:
1853 @g.command('i3')
1854 def i3_command(event):
1855 c = event.get('c')
1856 if not c: return
1858 def callback(args, c, event):
1859 g.trace(args)
1860 c.bodyWantsFocus()
1862 c.interactive(callback, event,
1863 prompts=['Arg1: ', ' Arg2: ', ' Arg3: '])
1864 """
1865 #@-<< c.interactive docstring >>
1866 #
1867 # This pathetic code should be generalized,
1868 # but it's not as easy as one might imagine.
1869 c = self
1870 d = {1: c.interactive1, 2: c.interactive2, 3: c.interactive3,}
1871 f = d.get(len(prompts))
1872 if f:
1873 f(callback, event, prompts)
1874 else:
1875 g.trace('At most 3 arguments are supported.')
1876 #@+node:ekr.20180503111213.1: *5* c.interactive1
1877 def interactive1(self, callback, event, prompts):
1879 c, k = self, self.k
1880 prompt = prompts[0]
1882 def state1(event):
1883 callback(args=[k.arg], c=c, event=event)
1884 k.clearState()
1885 k.resetLabel()
1886 k.showStateAndMode()
1888 k.setLabelBlue(prompt)
1889 k.get1Arg(event, handler=state1)
1890 #@+node:ekr.20180503111249.1: *5* c.interactive2
1891 def interactive2(self, callback, event, prompts):
1893 c, d, k = self, {}, self.k
1894 prompt1, prompt2 = prompts
1896 def state1(event):
1897 d['arg1'] = k.arg
1898 k.extendLabel(prompt2, select=False, protect=True)
1899 k.getNextArg(handler=state2)
1901 def state2(event):
1902 callback(args=[d.get('arg1'), k.arg], c=c, event=event)
1903 k.clearState()
1904 k.resetLabel()
1905 k.showStateAndMode()
1907 k.setLabelBlue(prompt1)
1908 k.get1Arg(event, handler=state1)
1909 #@+node:ekr.20180503111249.2: *5* c.interactive3
1910 def interactive3(self, callback, event, prompts):
1912 c, d, k = self, {}, self.k
1913 prompt1, prompt2, prompt3 = prompts
1915 def state1(event):
1916 d['arg1'] = k.arg
1917 k.extendLabel(prompt2, select=False, protect=True)
1918 k.getNextArg(handler=state2)
1920 def state2(event):
1921 d['arg2'] = k.arg
1922 k.extendLabel(prompt3, select=False, protect=True)
1923 k.get1Arg(event, handler=state3)
1924 # Restart.
1926 def state3(event):
1927 args = [d.get('arg1'), d.get('arg2'), k.arg]
1928 callback(args=args, c=c, event=event)
1929 k.clearState()
1930 k.resetLabel()
1931 k.showStateAndMode()
1933 k.setLabelBlue(prompt1)
1934 k.get1Arg(event, handler=state1)
1935 #@+node:ekr.20080901124540.1: *3* c.Directive scanning
1936 # These are all new in Leo 4.5.1.
1937 #@+node:ekr.20171123135625.33: *4* c.getLanguageAtCursor
1938 def getLanguageAtCursor(self, p, language):
1939 """
1940 Return the language in effect at the present insert point.
1941 Use the language argument as a default if no @language directive seen.
1942 """
1943 c = self
1944 tag = '@language'
1945 w = c.frame.body.wrapper
1946 ins = w.getInsertPoint()
1947 n = 0
1948 for s in g.splitLines(p.b):
1949 if g.match_word(s, 0, tag):
1950 i = g.skip_ws(s, len(tag))
1951 j = g.skip_id(s, i)
1952 language = s[i:j]
1953 if n <= ins < n + len(s):
1954 break
1955 else:
1956 n += len(s)
1957 return language
1958 #@+node:ekr.20081006100835.1: *4* c.getNodePath & c.getNodeFileName
1959 def getNodePath(self, p):
1960 """Return the path in effect at node p."""
1961 c = self
1962 aList = g.get_directives_dict_list(p)
1963 path = c.scanAtPathDirectives(aList)
1964 return path
1966 def getNodeFileName(self, p):
1967 """
1968 Return the full file name at node p,
1969 including effects of all @path directives.
1970 Return '' if p is no kind of @file node.
1971 """
1972 c = self
1973 for p in p.self_and_parents(copy=False):
1974 name = p.anyAtFileNodeName()
1975 if name:
1976 return g.fullPath(c, p) # #1914.
1977 return ''
1978 #@+node:ekr.20171123135625.32: *4* c.hasAmbiguousLanguage
1979 def hasAmbiguousLanguage(self, p):
1980 """Return True if p.b contains different @language directives."""
1981 # c = self
1982 languages, tag = set(), '@language'
1983 for s in g.splitLines(p.b):
1984 if g.match_word(s, 0, tag):
1985 i = g.skip_ws(s, len(tag))
1986 j = g.skip_id(s, i)
1987 word = s[i:j]
1988 languages.add(word)
1989 return len(list(languages)) > 1
1990 #@+node:ekr.20080827175609.39: *4* c.scanAllDirectives
1991 #@@nobeautify
1993 def scanAllDirectives(self, p):
1994 """
1995 Scan p and ancestors for directives.
1997 Returns a dict containing the results, including defaults.
1998 """
1999 c = self
2000 p = p or c.p
2001 # Defaults...
2002 default_language = g.getLanguageFromAncestorAtFileNode(p) or c.target_language or 'python'
2003 default_delims = g.set_delims_from_language(default_language)
2004 wrap = c.config.getBool("body-pane-wraps")
2005 table = ( # type:ignore
2006 ('encoding', None, g.scanAtEncodingDirectives),
2007 ('lang-dict', {}, g.scanAtCommentAndAtLanguageDirectives),
2008 ('lineending', None, g.scanAtLineendingDirectives),
2009 ('pagewidth', c.page_width, g.scanAtPagewidthDirectives),
2010 ('path', None, c.scanAtPathDirectives),
2011 ('tabwidth', c.tab_width, g.scanAtTabwidthDirectives),
2012 ('wrap', wrap, g.scanAtWrapDirectives),
2013 )
2014 # Set d by scanning all directives.
2015 aList = g.get_directives_dict_list(p)
2016 d = {}
2017 for key, default, func in table:
2018 val = func(aList) # type:ignore
2019 d[key] = default if val is None else val
2020 # Post process: do *not* set commander ivars.
2021 lang_dict = d.get('lang-dict')
2022 d = {
2023 "delims": lang_dict.get('delims') or default_delims,
2024 "comment": lang_dict.get('comment'), # Leo 6.4: New.
2025 "encoding": d.get('encoding'),
2026 # Note: at.scanAllDirectives does not use the defaults for "language".
2027 "language": lang_dict.get('language') or default_language,
2028 "lang-dict": lang_dict, # Leo 6.4: New.
2029 "lineending": d.get('lineending'),
2030 "pagewidth": d.get('pagewidth'),
2031 "path": d.get('path'), # Redundant: or g.getBaseDirectory(c),
2032 "tabwidth": d.get('tabwidth'),
2033 "wrap": d.get('wrap'),
2034 }
2035 return d
2036 #@+node:ekr.20080828103146.15: *4* c.scanAtPathDirectives
2037 def scanAtPathDirectives(self, aList):
2038 """
2039 Scan aList for @path directives.
2040 Return a reasonable default if no @path directive is found.
2041 """
2042 c = self
2043 c.scanAtPathDirectivesCount += 1 # An important statistic.
2044 # Step 1: Compute the starting path.
2045 # The correct fallback directory is the absolute path to the base.
2046 if c.openDirectory: # Bug fix: 2008/9/18
2047 base = c.openDirectory
2048 else:
2049 base = g.app.config.relative_path_base_directory
2050 if base and base == "!":
2051 base = g.app.loadDir
2052 elif base and base == ".":
2053 base = c.openDirectory
2054 base = c.expand_path_expression(base) # #1341.
2055 base = g.os_path_expanduser(base) # #1889.
2056 absbase = g.os_path_finalize_join(g.app.loadDir, base) # #1341.
2057 # Step 2: look for @path directives.
2058 paths = []
2059 for d in aList:
2060 # Look for @path directives.
2061 path = d.get('path')
2062 warning = d.get('@path_in_body')
2063 if path is not None: # retain empty paths for warnings.
2064 # Convert "path" or <path> to path.
2065 path = g.stripPathCruft(path)
2066 if path and not warning:
2067 path = c.expand_path_expression(path) # #1341.
2068 path = g.os_path_expanduser(path) # #1889.
2069 paths.append(path)
2070 # We will silently ignore empty @path directives.
2071 # Add absbase and reverse the list.
2072 paths.append(absbase)
2073 paths.reverse()
2074 # Step 3: Compute the full, effective, absolute path.
2075 path = g.os_path_finalize_join(*paths) # #1341.
2076 return path or g.getBaseDirectory(c)
2077 # 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
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
2108 def doCommand(self, command_func, command_name, event):
2109 """
2110 Execute the given command function, invoking hooks and catching exceptions.
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.
2169 The caller must do any required keystroke-only tasks.
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
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.
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 tempory 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.
2212 Other features:
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.
2437 Create and return a callback that wraps a function with an rClick
2438 signature in a callback which adapts standard minibufer command
2439 callbacks to a compatible format.
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.
2445 A function wrapped in this wrapper can handle rclick generator
2446 and invocation commands and commands typed in the minibuffer.
2448 It will also be able to handle commands from the minibuffer even
2449 if rclick is not installed.
2450 """
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
2476 minibufferCallback.__doc__ = function.__doc__ # For g.getDocStringForFunction
2477 minibufferCallback.source_c = source_c # For GetArgs.command_source
2478 return minibufferCallback
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):
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] = []
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)
2628 # Issues saved message.
2629 # g.es('in', theDir)
2630 return path
2631 #@+node:ekr.20180210092235.1: *4* c.backup_helper
2632 def backup_helper(self,
2633 base_dir=None,
2634 env_key='LEO_BACKUP',
2635 sub_dir=None,
2636 use_git_prefix=True,
2637 ):
2638 """
2639 A helper for scripts that back up a .leo file.
2640 Use os.environ[env_key] as the base_dir only if base_dir is not given.
2641 Backup to base_dir or join(base_dir, sub_dir).
2642 """
2643 c = self
2644 old_cwd = os.getcwd()
2645 join = g.os_path_finalize_join
2646 if not base_dir:
2647 if env_key:
2648 try:
2649 base_dir = os.environ[env_key]
2650 except KeyError:
2651 print(f"No environment var: {env_key}")
2652 base_dir = None
2653 if base_dir and g.os_path_exists(base_dir):
2654 if use_git_prefix:
2655 git_branch, junk = g.gitInfo()
2656 else:
2657 git_branch = None
2658 theDir, fn = g.os_path_split(c.fileName())
2659 backup_dir = join(base_dir, sub_dir) if sub_dir else base_dir
2660 path = join(backup_dir, fn)
2661 if g.os_path_exists(backup_dir):
2662 written_fn = c.backup(
2663 path,
2664 prefix=git_branch,
2665 silent=True,
2666 useTimeStamp=True,
2667 )
2668 g.es_print(f"wrote: {written_fn}")
2669 else:
2670 g.es_print(f"backup_dir not found: {backup_dir!r}")
2671 else:
2672 g.es_print(f"base_dir not found: {base_dir!r}")
2673 os.chdir(old_cwd)
2674 #@+node:ekr.20090103070824.11: *4* c.checkFileTimeStamp
2675 def checkFileTimeStamp(self, fn):
2676 """
2677 Return True if the file given by fn has not been changed
2678 since Leo read it or if the user agrees to overwrite it.
2679 """
2680 c = self
2681 if g.app.externalFilesController:
2682 return g.app.externalFilesController.check_overwrite(c, fn)
2683 return True
2684 #@+node:ekr.20090212054250.9: *4* c.createNodeFromExternalFile
2685 def createNodeFromExternalFile(self, fn):
2686 """
2687 Read the file into a node.
2688 Return None, indicating that c.open should set focus.
2689 """
2690 c = self
2691 s, e = g.readFileIntoString(fn)
2692 if s is None:
2693 return
2694 head, ext = g.os_path_splitext(fn)
2695 if ext.startswith('.'):
2696 ext = ext[1:]
2697 language = g.app.extension_dict.get(ext)
2698 if language:
2699 prefix = f"@color\n@language {language}\n\n"
2700 else:
2701 prefix = '@killcolor\n\n'
2702 # pylint: disable=no-member
2703 # Defined in commanderOutlineCommands.py
2704 p2 = c.insertHeadline(op_name='Open File', as_child=False)
2705 p2.h = f"@edit {fn}"
2706 p2.b = prefix + s
2707 w = c.frame.body.wrapper
2708 if w:
2709 w.setInsertPoint(0)
2710 c.redraw()
2711 c.recolor()
2712 #@+node:ekr.20110530124245.18248: *4* c.looksLikeDerivedFile
2713 def looksLikeDerivedFile(self, fn):
2714 """
2715 Return True if fn names a file that looks like an
2716 external file written by Leo.
2717 """
2718 # c = self
2719 try:
2720 with open(fn, 'rb') as f: # 2020/11/14: Allow unicode characters!
2721 b = f.read()
2722 s = g.toUnicode(b)
2723 return s.find('@+leo-ver=') > -1
2724 except Exception:
2725 g.es_exception()
2726 return False
2727 #@+node:ekr.20031218072017.2925: *4* c.markAllAtFileNodesDirty
2728 def markAllAtFileNodesDirty(self, event=None):
2729 """Mark all @file nodes as changed."""
2730 c = self
2731 c.endEditing()
2732 p = c.rootPosition()
2733 while p:
2734 if p.isAtFileNode():
2735 p.setDirty()
2736 c.setChanged()
2737 p.moveToNodeAfterTree()
2738 else:
2739 p.moveToThreadNext()
2740 c.redraw_after_icons_changed()
2741 #@+node:ekr.20031218072017.2926: *4* c.markAtFileNodesDirty
2742 def markAtFileNodesDirty(self, event=None):
2743 """Mark all @file nodes in the selected tree as changed."""
2744 c = self
2745 p = c.p
2746 if not p:
2747 return
2748 c.endEditing()
2749 after = p.nodeAfterTree()
2750 while p and p != after:
2751 if p.isAtFileNode():
2752 p.setDirty()
2753 c.setChanged()
2754 p.moveToNodeAfterTree()
2755 else:
2756 p.moveToThreadNext()
2757 c.redraw_after_icons_changed()
2758 #@+node:ekr.20031218072017.2823: *4* c.openWith
2759 def openWith(self, event=None, d=None):
2760 """
2761 This is *not* a command.
2763 Handles the items in the Open With... menu.
2765 See ExternalFilesController.open_with for details about d.
2766 """
2767 c = self
2768 if d and g.app.externalFilesController:
2769 # Select an ancestor @<file> node if possible.
2770 if not d.get('p'):
2771 d['p'] = None
2772 p = c.p
2773 while p:
2774 if p.isAnyAtFileNode():
2775 d['p'] = p
2776 break
2777 p.moveToParent()
2778 g.app.externalFilesController.open_with(c, d)
2779 elif not d:
2780 g.trace('can not happen: no d', g.callers())
2781 #@+node:ekr.20140717074441.17770: *4* c.recreateGnxDict
2782 def recreateGnxDict(self):
2783 """Recreate the gnx dict prior to refreshing nodes from disk."""
2784 c, d = self, {}
2785 for v in c.all_unique_nodes():
2786 gnxString = v.fileIndex
2787 if isinstance(gnxString, str):
2788 d[gnxString] = v
2789 if 'gnx' in g.app.debug:
2790 g.trace(c.shortFileName(), gnxString, v)
2791 else:
2792 g.internalError(f"no gnx for vnode: {v}")
2793 c.fileCommands.gnxDict = d
2794 #@+node:ekr.20180508111544.1: *3* c.Git
2795 #@+node:ekr.20180510104805.1: *4* c.diff_file
2796 def diff_file(self, fn, rev1='HEAD', rev2=''):
2797 """
2798 Create an outline describing the git diffs for all files changed
2799 between rev1 and rev2.
2800 """
2801 from leo.commands import editFileCommands as efc
2802 x = efc.GitDiffController(c=self)
2803 x.diff_file(fn=fn, rev1=rev1, rev2=rev2)
2804 #@+node:ekr.20180508110755.1: *4* c.diff_two_revs
2805 def diff_two_revs(self, directory=None, rev1='', rev2=''):
2806 """
2807 Create an outline describing the git diffs for all files changed
2808 between rev1 and rev2.
2809 """
2810 from leo.commands import editFileCommands as efc
2811 efc.GitDiffController(c=self).diff_two_revs(rev1=rev1, rev2=rev2)
2812 #@+node:ekr.20180510103923.1: *4* c.diff_two_branches
2813 def diff_two_branches(self, branch1, branch2, fn):
2814 """
2815 Create an outline describing the git diffs for all files changed
2816 between rev1 and rev2.
2817 """
2818 from leo.commands import editFileCommands as efc
2819 efc.GitDiffController(c=self).diff_two_branches(
2820 branch1=branch1, branch2=branch2, fn=fn)
2821 #@+node:ekr.20180510105125.1: *4* c.git_diff
2822 def git_diff(self, rev1='HEAD', rev2=''):
2824 from leo.commands import editFileCommands as efc
2825 efc.GitDiffController(c=self).git_diff(rev1, rev2)
2826 #@+node:ekr.20171124100534.1: *3* c.Gui
2827 #@+node:ekr.20111217154130.10286: *4* c.Dialogs & messages
2828 #@+node:ekr.20110510052422.14618: *5* c.alert
2829 def alert(self, message):
2830 c = self
2831 # The unit tests just tests the args.
2832 if not g.unitTesting:
2833 g.es(message)
2834 g.app.gui.alert(c, message)
2835 #@+node:ekr.20111217154130.10284: *5* c.init_error_dialogs
2836 def init_error_dialogs(self):
2837 c = self
2838 c.import_error_nodes = []
2839 c.ignored_at_file_nodes = []
2840 c.orphan_at_file_nodes = []
2841 #@+node:ekr.20171123135805.1: *5* c.notValidInBatchMode
2842 def notValidInBatchMode(self, commandName):
2843 g.es('the', commandName, "command is not valid in batch mode")
2844 #@+node:ekr.20110530082209.18250: *5* c.putHelpFor
2845 def putHelpFor(self, s, short_title=''):
2846 """Helper for various help commands."""
2847 c = self
2848 g.app.gui.put_help(c, s, short_title)
2849 #@+node:ekr.20111217154130.10285: *5* c.raise_error_dialogs
2850 warnings_dict: Dict[str, bool] = {}
2852 def raise_error_dialogs(self, kind='read'):
2853 """Warn about read/write failures."""
2854 c = self
2855 use_dialogs = False
2856 if g.unitTesting:
2857 c.init_error_dialogs()
2858 return
2859 # Issue one or two dialogs or messages.
2860 saved_body = c.rootPosition().b # Save the root's body. The dialog destroys it!
2861 if c.import_error_nodes or c.ignored_at_file_nodes or c.orphan_at_file_nodes:
2862 g.app.gui.dismiss_splash_screen()
2863 else:
2864 # #1007: Exit now, so we don't have to restore c.rootPosition().b.
2865 c.init_error_dialogs()
2866 return
2867 if c.import_error_nodes:
2868 files = '\n'.join(sorted(set(c.import_error_nodes))) # type:ignore
2869 if files not in self.warnings_dict:
2870 self.warnings_dict[files] = True
2871 import_message1 = 'The following were not imported properly.'
2872 import_message2 = f"Inserted @ignore in...\n{files}"
2873 g.es_print(import_message1, color='red')
2874 g.es_print(import_message2)
2875 if use_dialogs:
2876 import_dialog_message = f"{import_message1}\n{import_message2}"
2877 g.app.gui.runAskOkDialog(c,
2878 message=import_dialog_message, title='Import errors')
2879 if c.ignored_at_file_nodes:
2880 files = '\n'.join(sorted(set(c.ignored_at_file_nodes))) # type:ignore
2881 if files not in self.warnings_dict:
2882 self.warnings_dict[files] = True
2883 kind_s = 'read' if kind == 'read' else 'written'
2884 ignored_message = f"The following were not {kind_s} because they contain @ignore:"
2885 kind = 'read' if kind.startswith('read') else 'written'
2886 g.es_print(ignored_message, color='red')
2887 g.es_print(files)
2888 if use_dialogs:
2889 ignored_dialog_message = f"{ignored_message}\n{files}"
2890 g.app.gui.runAskOkDialog(c,
2891 message=ignored_dialog_message, title=f"Not {kind.capitalize()}")
2892 # #1050: always raise a dialog for orphan @<file> nodes.
2893 if c.orphan_at_file_nodes:
2894 message = '\n'.join([
2895 'The following were not written because of errors:\n',
2896 '\n'.join(sorted(set(c.orphan_at_file_nodes))), # type:ignore
2897 '',
2898 'Warning: changes to these files will be lost\n'
2899 'unless you can save the files successfully.'
2900 ])
2901 g.app.gui.runAskOkDialog(c, message=message, title='Not Written')
2902 # Mark all the nodes dirty.
2903 for z in c.all_unique_positions():
2904 if z.isOrphan():
2905 z.setDirty()
2906 z.clearOrphan()
2907 c.setChanged()
2908 c.redraw()
2909 # Restore the root position's body.
2910 c.rootPosition().v.b = saved_body # #1007: just set v.b.
2911 c.init_error_dialogs()
2912 #@+node:ekr.20150710083827.1: *5* c.syntaxErrorDialog
2913 def syntaxErrorDialog(self):
2914 """Warn about syntax errors in files."""
2915 c = self
2916 if g.app.syntax_error_files and c.config.getBool(
2917 'syntax-error-popup', default=False):
2918 aList = sorted(set(g.app.syntax_error_files))
2919 g.app.syntax_error_files = []
2920 list_s = '\n'.join(aList)
2921 g.app.gui.runAskOkDialog(
2922 c,
2923 title='Python Errors',
2924 message=f"Python errors in:\n\n{list_s}",
2925 text="Ok",
2926 )
2927 #@+node:ekr.20031218072017.2945: *4* c.Dragging
2928 #@+node:ekr.20031218072017.2947: *5* c.dragToNthChildOf
2929 def dragToNthChildOf(self, p, parent, n):
2930 c, p, u = self, self.p, self.undoer
2931 if not c.checkDrag(p, parent):
2932 return
2933 if not c.checkMoveWithParentWithWarning(p, parent, True):
2934 return
2935 c.endEditing()
2936 undoData = u.beforeMoveNode(p)
2937 p.setDirty()
2938 p.moveToNthChildOf(parent, n)
2939 p.setDirty()
2940 c.setChanged()
2941 u.afterMoveNode(p, 'Drag', undoData)
2942 c.redraw(p)
2943 c.updateSyntaxColorer(p) # Dragging can change syntax coloring.
2944 #@+node:ekr.20031218072017.2353: *5* c.dragAfter
2945 def dragAfter(self, p, after):
2946 c, p, u = self, self.p, self.undoer
2947 if not c.checkDrag(p, after):
2948 return
2949 if not c.checkMoveWithParentWithWarning(p, after.parent(), True):
2950 return
2951 c.endEditing()
2952 undoData = u.beforeMoveNode(p)
2953 p.setDirty()
2954 p.moveAfter(after)
2955 p.setDirty()
2956 c.setChanged()
2957 u.afterMoveNode(p, 'Drag', undoData)
2958 c.redraw(p)
2959 c.updateSyntaxColorer(p) # Dragging can change syntax coloring.
2960 #@+node:ekr.20031218072017.2946: *5* c.dragCloneToNthChildOf
2961 def dragCloneToNthChildOf(self, p, parent, n):
2962 c = self
2963 u = c.undoer
2964 undoType = 'Clone Drag'
2965 current = c.p
2966 clone = p.clone() # Creates clone & dependents, does not set undo.
2967 if (
2968 not c.checkDrag(p, parent) or
2969 not c.checkMoveWithParentWithWarning(clone, parent, True)
2970 ):
2971 clone.doDelete(newNode=p) # Destroys clone and makes p the current node.
2972 c.selectPosition(p) # Also sets root position.
2973 return
2974 c.endEditing()
2975 undoData = u.beforeInsertNode(current)
2976 clone.setDirty()
2977 clone.moveToNthChildOf(parent, n)
2978 clone.setDirty()
2979 c.setChanged()
2980 u.afterInsertNode(clone, undoType, undoData)
2981 c.redraw(clone)
2982 c.updateSyntaxColorer(clone) # Dragging can change syntax coloring.
2983 #@+node:ekr.20031218072017.2948: *5* c.dragCloneAfter
2984 def dragCloneAfter(self, p, after):
2985 c = self
2986 u = c.undoer
2987 undoType = 'Clone Drag'
2988 current = c.p
2989 clone = p.clone() # Creates clone. Does not set undo.
2990 if c.checkDrag(
2991 p, after) and c.checkMoveWithParentWithWarning(clone, after.parent(), True):
2992 c.endEditing()
2993 undoData = u.beforeInsertNode(current)
2994 clone.setDirty()
2995 clone.moveAfter(after)
2996 clone.v.setDirty()
2997 c.setChanged()
2998 u.afterInsertNode(clone, undoType, undoData)
2999 p = clone
3000 else:
3001 clone.doDelete(newNode=p)
3002 c.redraw(p)
3003 c.updateSyntaxColorer(clone) # Dragging can change syntax coloring.
3004 #@+node:ekr.20031218072017.2949: *4* c.Drawing
3005 #@+node:ekr.20080514131122.8: *5* c.bringToFront
3006 def bringToFront(self, c2=None):
3007 c = self
3008 c2 = c2 or c
3009 g.app.gui.ensure_commander_visible(c2)
3011 BringToFront = bringToFront # Compatibility with old scripts
3012 #@+node:ekr.20040803072955.143: *5* c.expandAllAncestors
3013 def expandAllAncestors(self, p):
3014 """
3015 Expand all ancestors without redrawing.
3016 Return a flag telling whether a redraw is needed.
3017 """
3018 # c = self
3019 redraw_flag = False
3020 for p in p.parents():
3021 if not p.v.isExpanded():
3022 p.v.expand()
3023 p.expand()
3024 redraw_flag = True
3025 elif p.isExpanded():
3026 p.v.expand()
3027 else:
3028 p.expand()
3029 redraw_flag = True
3030 return redraw_flag
3031 #@+node:ekr.20080514131122.20: *5* c.outerUpdate
3032 def outerUpdate(self):
3033 """Handle delayed focus requests and modified events."""
3034 c = self
3035 if not c.exists or not c.k:
3036 return
3037 # New in Leo 5.6: Delayed redraws are useful in utility methods.
3038 if c.requestLaterRedraw:
3039 if c.enableRedrawFlag:
3040 c.requestLaterRedraw = False
3041 if 'drawing' in g.app.debug and not g.unitTesting:
3042 g.trace('\nDELAYED REDRAW')
3043 time.sleep(1.0)
3044 c.redraw()
3045 # Delayed focus requests will always be useful.
3046 if c.requestedFocusWidget:
3047 w = c.requestedFocusWidget
3048 if 'focus' in g.app.debug and not g.unitTesting:
3049 if hasattr(w, 'objectName'):
3050 name = w.objectName()
3051 else:
3052 name = w.__class__.__name__
3053 g.trace('DELAYED FOCUS', name)
3054 c.set_focus(w)
3055 c.requestedFocusWidget = None
3056 table = (
3057 ("childrenModified", g.childrenModifiedSet),
3058 ("contentModified", g.contentModifiedSet),
3059 )
3060 for kind, mods in table:
3061 if mods:
3062 g.doHook(kind, c=c, nodes=mods)
3063 mods.clear()
3064 #@+node:ekr.20080514131122.13: *5* c.recolor
3065 def recolor(self, p=None):
3066 # Support QScintillaColorizer.colorize.
3067 c = self
3068 colorizer = c.frame.body.colorizer
3069 if colorizer and hasattr(colorizer, 'colorize'):
3070 colorizer.colorize(p or c.p)
3072 recolor_now = recolor
3073 #@+node:ekr.20080514131122.14: *5* c.redrawing...
3074 #@+node:ekr.20170808014610.1: *6* c.enable/disable_redraw
3075 def disable_redraw(self):
3076 """Disable all redrawing until enabled."""
3077 c = self
3078 c.enableRedrawFlag = False
3080 def enable_redraw(self):
3081 c = self
3082 c.enableRedrawFlag = True
3083 #@+node:ekr.20090110073010.1: *6* c.redraw
3084 @cmd('redraw')
3085 def redraw_command(self, event):
3086 c = event.get('c')
3087 if c:
3088 c.redraw()
3090 def redraw(self, p=None):
3091 """
3092 Redraw the screen immediately.
3093 If p is given, set c.p to p.
3094 """
3095 c = self
3096 # New in Leo 5.6: clear the redraw request.
3097 c.requestLaterRedraw = False
3098 if not p:
3099 p = c.p or c.rootPosition()
3100 if not p:
3101 return
3102 c.expandAllAncestors(p)
3103 if p:
3104 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1183855
3105 # This looks redundant, but it is probably the only safe fix.
3106 c.frame.tree.select(p)
3107 # tree.redraw will change the position if p is a hoisted @chapter node.
3108 p2 = c.frame.tree.redraw(p)
3109 # Be careful. NullTree.redraw returns None.
3110 # #503: NullTree.redraw(p) now returns p.
3111 c.selectPosition(p2 or p)
3112 # Do not call treeFocusHelper here.
3113 # c.treeFocusHelper()
3114 # Clear the redraw request, again.
3115 c.requestLaterRedraw = False
3117 # Compatibility with old scripts
3119 force_redraw = redraw
3120 redraw_now = redraw
3121 #@+node:ekr.20090110073010.3: *6* c.redraw_afer_icons_changed
3122 def redraw_after_icons_changed(self):
3123 """Update the icon for the presently selected node"""
3124 c = self
3125 if c.enableRedrawFlag:
3126 c.frame.tree.redraw_after_icons_changed()
3127 # Do not call treeFocusHelper here.
3128 # c.treeFocusHelper()
3129 else:
3130 c.requestLaterRedraw = True
3131 #@+node:ekr.20090110131802.2: *6* c.redraw_after_contract
3132 def redraw_after_contract(self, p=None):
3133 c = self
3134 if c.enableRedrawFlag:
3135 if p:
3136 c.setCurrentPosition(p)
3137 else:
3138 p = c.currentPosition()
3139 c.frame.tree.redraw_after_contract(p)
3140 c.treeFocusHelper()
3141 else:
3142 c.requestLaterRedraw = True
3143 #@+node:ekr.20090112065525.1: *6* c.redraw_after_expand
3144 def redraw_after_expand(self, p):
3145 c = self
3146 if c.enableRedrawFlag:
3147 if p:
3148 c.setCurrentPosition(p)
3149 else:
3150 p = c.currentPosition()
3151 c.frame.tree.redraw_after_expand(p)
3152 c.treeFocusHelper()
3153 else:
3154 c.requestLaterRedraw = True
3155 #@+node:ekr.20090110073010.2: *6* c.redraw_after_head_changed
3156 def redraw_after_head_changed(self):
3157 """
3158 Redraw the screen (if needed) when editing ends.
3159 This may be a do-nothing for some gui's.
3160 """
3161 c = self
3162 if c.enableRedrawFlag:
3163 self.frame.tree.redraw_after_head_changed()
3164 else:
3165 c.requestLaterRedraw = True
3166 #@+node:ekr.20090110073010.4: *6* c.redraw_after_select
3167 def redraw_after_select(self, p):
3168 """Redraw the screen after node p has been selected."""
3169 c = self
3170 if c.enableRedrawFlag:
3171 flag = c.expandAllAncestors(p)
3172 if flag:
3173 c.frame.tree.redraw_after_select(p)
3174 # This is the same as c.frame.tree.full_redraw().
3175 else:
3176 c.requestLaterRedraw = True
3177 #@+node:ekr.20170908081918.1: *6* c.redraw_later
3178 def redraw_later(self):
3179 """
3180 Ensure that c.redraw() will be called eventually.
3182 c.outerUpdate will call c.redraw() only if no other code calls c.redraw().
3183 """
3184 c = self
3185 c.requestLaterRedraw = True
3186 if 'drawing' in g.app.debug:
3187 # g.trace('\n' + g.callers(8))
3188 g.trace(g.callers())
3189 #@+node:ekr.20080514131122.17: *5* c.widget_name
3190 def widget_name(self, widget):
3191 # c = self
3192 return g.app.gui.widget_name(widget) if g.app.gui else '<no widget>'
3193 #@+node:ekr.20171124101045.1: *4* c.Events
3194 #@+node:ekr.20060923202156: *5* c.onCanvasKey
3195 def onCanvasKey(self, event):
3196 """
3197 Navigate to the next headline starting with ch = event.char.
3198 If ch is uppercase, search all headlines; otherwise search only visible headlines.
3199 This is modelled on Windows explorer.
3200 """
3201 if not event or not event.char or not event.char.isalnum():
3202 return
3203 c, p = self, self.p
3204 p1 = p.copy()
3205 invisible = c.config.getBool('invisible-outline-navigation')
3206 ch = event.char if event else ''
3207 allFlag = ch.isupper() and invisible # all is a global (!?)
3208 if not invisible:
3209 ch = ch.lower()
3210 found = False
3211 extend = self.navQuickKey()
3212 attempts = (True, False) if extend else (False,)
3213 for extend2 in attempts:
3214 p = p1.copy()
3215 while 1:
3216 if allFlag:
3217 p.moveToThreadNext()
3218 else:
3219 p.moveToVisNext(c)
3220 if not p:
3221 p = c.rootPosition()
3222 if p == p1: # Never try to match the same position.
3223 found = False
3224 break
3225 newPrefix = c.navHelper(p, ch, extend2)
3226 if newPrefix:
3227 found = True
3228 break
3229 if found:
3230 break
3231 if found:
3232 c.selectPosition(p)
3233 c.redraw_after_select(p)
3234 c.navTime = time.time()
3235 c.navPrefix = newPrefix
3236 else:
3237 c.navTime = None
3238 c.navPrefix = ''
3239 c.treeWantsFocus()
3240 #@+node:ekr.20061002095711.1: *6* c.navQuickKey
3241 def navQuickKey(self) -> bool:
3242 """
3243 Return true if there are two quick outline navigation keys
3244 in quick succession.
3246 Returns False if @float outline_nav_extend_delay setting is 0.0 or unspecified.
3247 """
3248 c = self
3249 deltaTime = c.config.getFloat('outline-nav-extend-delay')
3250 if deltaTime in (None, 0.0):
3251 return False
3252 if c.navTime is None:
3253 return False # mypy.
3254 return time.time() - c.navTime < deltaTime
3255 #@+node:ekr.20061002095711: *6* c.navHelper
3256 def navHelper(self, p, ch, extend):
3257 c = self
3258 h = p.h.lower()
3259 if extend:
3260 prefix = c.navPrefix + ch
3261 return h.startswith(prefix.lower()) and prefix
3262 if h.startswith(ch):
3263 return ch
3264 # New feature: search for first non-blank character after @x for common x.
3265 if ch != '@' and h.startswith('@'):
3266 for s in ('button', 'command', 'file', 'thin', 'asis', 'nosent',):
3267 prefix = '@' + s
3268 if h.startswith('@' + s):
3269 while 1:
3270 n = len(prefix)
3271 ch2 = h[n] if n < len(h) else ''
3272 if ch2.isspace():
3273 prefix = prefix + ch2
3274 else: break
3275 if len(prefix) < len(h) and h.startswith(prefix + ch.lower()):
3276 return prefix + ch
3277 return ''
3278 #@+node:ekr.20031218072017.2909: *4* c.Expand/contract
3279 #@+node:ekr.20171124091426.1: *5* c.contractAllHeadlines
3280 def contractAllHeadlines(self, event=None):
3281 """Contract all nodes in the outline."""
3282 c = self
3283 for v in c.all_nodes():
3284 v.contract()
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):
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}")
3329 # bg='red', fg='red')
3330 #@+node:ekr.20141028061518.23: *4* c.Focus
3331 #@+node:ekr.20080514131122.9: *5* c.get/request/set_focus
3332 def get_focus(self):
3333 c = self
3334 w = g.app.gui and g.app.gui.get_focus(c)
3335 if 'focus' in g.app.debug:
3336 name = w.objectName() if hasattr(w, 'objectName') else w.__class__.__name__
3337 g.trace('(c)', name)
3338 # g.trace('\n(c)', w.__class__.__name__)
3339 # g.trace(g.callers(6))
3340 return w
3342 def get_requested_focus(self):
3343 c = self
3344 return c.requestedFocusWidget
3346 def request_focus(self, w):
3347 c = self
3348 if w and g.app.gui:
3349 if 'focus' in g.app.debug:
3350 # g.trace('\n(c)', repr(w))
3351 name = w.objectName(
3352 ) if hasattr(w, 'objectName') else w.__class__.__name__
3353 g.trace('(c)', name)
3354 c.requestedFocusWidget = w
3356 def set_focus(self, w):
3357 trace = 'focus' in g.app.debug
3358 c = self
3359 if w and g.app.gui:
3360 if trace:
3361 name = w.objectName(
3362 ) if hasattr(w, 'objectName') else w.__class__.__name__
3363 g.trace('(c)', name)
3364 g.app.gui.set_focus(c, w)
3365 else:
3366 if trace:
3367 g.trace('(c) no w')
3368 c.requestedFocusWidget = None
3369 #@+node:ekr.20080514131122.10: *5* c.invalidateFocus (do nothing)
3370 def invalidateFocus(self):
3371 """Indicate that the focus is in an invalid location, or is unknown."""
3372 # c = self
3373 # c.requestedFocusWidget = None
3374 pass
3375 #@+node:ekr.20080514131122.16: *5* c.traceFocus (not used)
3376 def traceFocus(self, w):
3377 c = self
3378 if 'focus' in g.app.debug:
3379 c.trace_focus_count += 1
3380 g.pr(f"{c.trace_focus_count:4d}", c.widget_name(w), g.callers(8))
3381 #@+node:ekr.20070226121510: *5* c.xFocusHelper & initialFocusHelper
3382 def treeFocusHelper(self):
3383 c = self
3384 if c.stayInTreeAfterSelect:
3385 c.treeWantsFocus()
3386 else:
3387 c.bodyWantsFocus()
3389 def initialFocusHelper(self):
3390 c = self
3391 if c.outlineHasInitialFocus:
3392 c.treeWantsFocus()
3393 else:
3394 c.bodyWantsFocus()
3395 #@+node:ekr.20080514131122.18: *5* c.xWantsFocus
3396 def bodyWantsFocus(self):
3397 c = self
3398 body = c.frame.body
3399 c.request_focus(body and body.wrapper)
3401 def logWantsFocus(self):
3402 c = self
3403 log = c.frame.log
3404 c.request_focus(log and log.logCtrl)
3406 def minibufferWantsFocus(self):
3407 c = self
3408 c.request_focus(c.miniBufferWidget)
3410 def treeWantsFocus(self):
3411 c = self
3412 tree = c.frame.tree
3413 c.request_focus(tree and tree.canvas)
3415 def widgetWantsFocus(self, w):
3416 c = self
3417 c.request_focus(w)
3418 #@+node:ekr.20080514131122.19: *5* c.xWantsFocusNow
3419 # widgetWantsFocusNow does an automatic update.
3421 def widgetWantsFocusNow(self, w):
3422 c = self
3423 if w:
3424 c.set_focus(w)
3425 c.requestedFocusWidget = None
3427 # New in 4.9: all FocusNow methods now *do* call c.outerUpdate().
3429 def bodyWantsFocusNow(self):
3430 c, body = self, self.frame.body
3431 c.widgetWantsFocusNow(body and body.wrapper)
3433 def logWantsFocusNow(self):
3434 c, log = self, self.frame.log
3435 c.widgetWantsFocusNow(log and log.logCtrl)
3437 def minibufferWantsFocusNow(self):
3438 c = self
3439 c.widgetWantsFocusNow(c.miniBufferWidget)
3441 def treeWantsFocusNow(self):
3442 c, tree = self, self.frame.tree
3443 c.widgetWantsFocusNow(tree and tree.canvas)
3444 #@+node:ekr.20031218072017.2955: *4* c.Menus
3445 #@+node:ekr.20080610085158.2: *5* c.add_command
3446 def add_command(self, menu, **keys):
3447 c = self
3448 command = keys.get('command')
3449 if command:
3450 # Command is always either:
3451 # one of two callbacks defined in createMenuEntries or
3452 # recentFilesCallback, defined in createRecentFilesMenuItems.
3454 def add_commandCallback(c=c, command=command):
3455 val = command()
3456 # Careful: func may destroy c.
3457 if c.exists:
3458 c.outerUpdate()
3459 return val
3461 keys['command'] = add_commandCallback
3462 menu.add_command(**keys)
3463 else:
3464 g.trace('can not happen: no "command" arg')
3465 #@+node:ekr.20171123203044.1: *5* c.Menu Enablers
3466 #@+node:ekr.20040131170659: *6* c.canClone
3467 def canClone(self):
3468 c = self
3469 if c.hoistStack:
3470 current = c.p
3471 bunch = c.hoistStack[-1]
3472 return current != bunch.p
3473 return True
3474 #@+node:ekr.20031218072017.2956: *6* c.canContractAllHeadlines
3475 def canContractAllHeadlines(self):
3476 """Contract all nodes in the tree."""
3477 c = self
3478 for p in c.all_positions(): # was c.all_unique_positions()
3479 if p.isExpanded():
3480 return True
3481 return False
3482 #@+node:ekr.20031218072017.2957: *6* c.canContractAllSubheads
3483 def canContractAllSubheads(self):
3484 current = self.p
3485 for p in current.subtree():
3486 if p != current and p.isExpanded():
3487 return True
3488 return False
3489 #@+node:ekr.20031218072017.2958: *6* c.canContractParent
3490 def canContractParent(self) -> bool:
3491 c = self
3492 return c.p.parent()
3493 #@+node:ekr.20031218072017.2959: *6* c.canContractSubheads
3494 def canContractSubheads(self):
3495 current = self.p
3496 for child in current.children():
3497 if child.isExpanded():
3498 return True
3499 return False
3500 #@+node:ekr.20031218072017.2960: *6* c.canCutOutline & canDeleteHeadline
3501 def canDeleteHeadline(self):
3502 c, p = self, self.p
3503 if c.hoistStack:
3504 bunch = c.hoistStack[0]
3505 if p == bunch.p:
3506 return False
3507 return p.hasParent() or p.hasThreadBack() or p.hasNext()
3509 canCutOutline = canDeleteHeadline
3510 #@+node:ekr.20031218072017.2961: *6* c.canDemote
3511 def canDemote(self) -> bool:
3512 c = self
3513 return c.p.hasNext()
3514 #@+node:ekr.20031218072017.2962: *6* c.canExpandAllHeadlines
3515 def canExpandAllHeadlines(self):
3516 """Return True if the Expand All Nodes menu item should be enabled."""
3517 c = self
3518 for p in c.all_positions(): # was c.all_unique_positions()
3519 if not p.isExpanded():
3520 return True
3521 return False
3522 #@+node:ekr.20031218072017.2963: *6* c.canExpandAllSubheads
3523 def canExpandAllSubheads(self):
3524 c = self
3525 for p in c.p.subtree():
3526 if not p.isExpanded():
3527 return True
3528 return False
3529 #@+node:ekr.20031218072017.2964: *6* c.canExpandSubheads
3530 def canExpandSubheads(self):
3531 current = self.p
3532 for p in current.children():
3533 if p != current and not p.isExpanded():
3534 return True
3535 return False
3536 #@+node:ekr.20031218072017.2287: *6* c.canExtract, canExtractSection & canExtractSectionNames
3537 def canExtract(self) -> bool:
3538 c = self
3539 w = c.frame.body.wrapper
3540 return w and w.hasSelection()
3542 canExtractSectionNames = canExtract
3544 def canExtractSection(self):
3545 c = self
3546 w = c.frame.body.wrapper
3547 if not w:
3548 return False
3549 s = w.getSelectedText()
3550 if not s:
3551 return False
3552 line = g.get_line(s, 0)
3553 i1 = line.find("<<")
3554 j1 = line.find(">>")
3555 i2 = line.find("@<")
3556 j2 = line.find("@>")
3557 return -1 < i1 < j1 or -1 < i2 < j2
3558 #@+node:ekr.20031218072017.2965: *6* c.canFindMatchingBracket
3559 #@@nobeautify
3561 def canFindMatchingBracket(self):
3562 c = self
3563 brackets = "()[]{}"
3564 w = c.frame.body.wrapper
3565 s = w.getAllText()
3566 ins = w.getInsertPoint()
3567 c1 = s[ins] if 0 <= ins < len(s) else ''
3568 c2 = s[ins-1] if 0 <= ins-1 < len(s) else ''
3569 val = (c1 and c1 in brackets) or (c2 and c2 in brackets)
3570 return bool(val)
3571 #@+node:ekr.20040303165342: *6* c.canHoist & canDehoist
3572 def canDehoist(self):
3573 """
3574 Return True if do-hoist should be enabled in a menu.
3575 Should not be used in any other context.
3576 """
3577 c = self
3578 return bool(c.hoistStack)
3580 def canHoist(self):
3581 # This is called at idle time, so minimizing positions is crucial!
3582 """
3583 Return True if hoist should be enabled in a menu.
3584 Should not be used in any other context.
3585 """
3586 return True
3587 #@+node:ekr.20031218072017.2970: *6* c.canMoveOutlineDown
3588 def canMoveOutlineDown(self) -> bool:
3589 c, p = self, self.p
3590 return p and p.visNext(c)
3591 #@+node:ekr.20031218072017.2971: *6* c.canMoveOutlineLeft
3592 def canMoveOutlineLeft(self) -> bool:
3593 c, p = self, self.p
3594 if c.hoistStack:
3595 bunch = c.hoistStack[-1]
3596 if p and p.hasParent():
3597 p.moveToParent()
3598 return p != bunch.p and bunch.p.isAncestorOf(p)
3599 return False
3600 return p and p.hasParent()
3601 #@+node:ekr.20031218072017.2972: *6* c.canMoveOutlineRight
3602 def canMoveOutlineRight(self) -> bool:
3603 c, p = self, self.p
3604 if c.hoistStack:
3605 bunch = c.hoistStack[-1]
3606 return p and p.hasBack() and p != bunch.p
3607 return p and p.hasBack()
3608 #@+node:ekr.20031218072017.2973: *6* c.canMoveOutlineUp
3609 def canMoveOutlineUp(self):
3610 c, current = self, self.p
3611 visBack = current and current.visBack(c)
3612 if not visBack:
3613 return False
3614 if visBack.visBack(c):
3615 return True
3616 if c.hoistStack:
3617 limit, limitIsVisible = c.visLimit()
3618 if limitIsVisible: # A hoist
3619 return current != limit
3620 # A chapter.
3621 return current != limit.firstChild()
3622 return current != c.rootPosition()
3623 #@+node:ekr.20031218072017.2974: *6* c.canPasteOutline
3624 def canPasteOutline(self, s=None):
3625 # c = self
3626 if not s:
3627 s = g.app.gui.getTextFromClipboard()
3628 if s and g.match(s, 0, g.app.prolog_prefix_string):
3629 return True
3630 return False
3631 #@+node:ekr.20031218072017.2975: *6* c.canPromote
3632 def canPromote(self) -> bool:
3633 p = self.p
3634 return p and p.hasChildren()
3635 #@+node:ekr.20031218072017.2977: *6* c.canSelect....
3636 def canSelectThreadBack(self):
3637 p = self.p
3638 return p.hasThreadBack()
3640 def canSelectThreadNext(self):
3641 p = self.p
3642 return p.hasThreadNext()
3644 def canSelectVisBack(self):
3645 c, p = self, self.p
3646 return p.visBack(c)
3648 def canSelectVisNext(self):
3649 c, p = self, self.p
3650 return p.visNext(c)
3651 #@+node:ekr.20031218072017.2978: *6* c.canShiftBodyLeft/Right
3652 def canShiftBodyLeft(self) -> bool:
3653 c = self
3654 w = c.frame.body.wrapper
3655 return w and w.getAllText()
3657 canShiftBodyRight = canShiftBodyLeft
3658 #@+node:ekr.20031218072017.2979: *6* c.canSortChildren, canSortSiblings
3659 def canSortChildren(self) -> bool:
3660 p = self.p
3661 return p and p.hasChildren()
3663 def canSortSiblings(self) -> bool:
3664 p = self.p
3665 return p and (p.hasNext() or p.hasBack())
3666 #@+node:ekr.20031218072017.2980: *6* c.canUndo & canRedo
3667 def canUndo(self) -> bool:
3668 c = self
3669 return c.undoer.canUndo()
3671 def canRedo(self) -> bool:
3672 c = self
3673 return c.undoer.canRedo()
3674 #@+node:ekr.20031218072017.2981: *6* c.canUnmarkAll
3675 def canUnmarkAll(self):
3676 c = self
3677 for p in c.all_unique_positions():
3678 if p.isMarked():
3679 return True
3680 return False
3681 #@+node:ekr.20040323172420: *6* Slow routines: no longer used
3682 #@+node:ekr.20031218072017.2966: *7* c.canGoToNextDirtyHeadline (slow)
3683 def canGoToNextDirtyHeadline(self):
3684 c, current = self, self.p
3685 for p in c.all_unique_positions():
3686 if p != current and p.isDirty():
3687 return True
3688 return False
3689 #@+node:ekr.20031218072017.2967: *7* c.canGoToNextMarkedHeadline (slow)
3690 def canGoToNextMarkedHeadline(self):
3691 c, current = self, self.p
3692 for p in c.all_unique_positions():
3693 if p != current and p.isMarked():
3694 return True
3695 return False
3696 #@+node:ekr.20031218072017.2968: *7* c.canMarkChangedHeadline (slow)
3697 def canMarkChangedHeadlines(self):
3698 c = self
3699 for p in c.all_unique_positions():
3700 if p.isDirty():
3701 return True
3702 return False
3703 #@+node:ekr.20031218072017.2969: *7* c.canMarkChangedRoots
3704 def canMarkChangedRoots(self):
3705 c = self
3706 for p in c.all_unique_positions():
3707 if p.isDirty() and p.isAnyAtFileNode():
3708 return True
3709 return False
3710 #@+node:ekr.20031218072017.2990: *4* c.Selecting
3711 #@+node:ekr.20031218072017.2992: *5* c.endEditing
3712 def endEditing(self):
3713 """End the editing of a headline."""
3714 c = self
3715 p = c.p
3716 if p:
3717 c.frame.tree.endEditLabel()
3718 #@+node:ville.20090525205736.12325: *5* c.getSelectedPositions
3719 def getSelectedPositions(self):
3720 """ Get list (PosList) of currently selected positions
3722 So far only makes sense on qt gui (which supports multiselection)
3723 """
3724 c = self
3725 return c.frame.tree.getSelectedPositions()
3726 #@+node:ekr.20031218072017.2991: *5* c.redrawAndEdit
3727 def redrawAndEdit(self, p, selectAll=False, selection=None, keepMinibuffer=False):
3728 """Redraw the screen and edit p's headline."""
3729 c, k = self, self.k
3730 c.redraw(p) # This *must* be done now.
3731 if p:
3732 # This should request focus.
3733 c.frame.tree.editLabel(p, selectAll=selectAll, selection=selection)
3734 if k and not keepMinibuffer:
3735 # Setting the input state has no effect on focus.
3736 if selectAll:
3737 k.setInputState('insert')
3738 else:
3739 k.setDefaultInputState()
3740 # This *does* affect focus.
3741 k.showStateAndMode()
3742 else:
3743 g.trace('** no p')
3744 # Update the focus immediately.
3745 if not keepMinibuffer:
3746 c.outerUpdate()
3747 #@+node:ekr.20031218072017.2997: *5* c.selectPosition (trace of unexpected de-hoists)
3748 def selectPosition(self, p, **kwargs):
3749 """
3750 Select a new position, redrawing the screen *only* if we must
3751 change chapters.
3752 """
3753 trace = False # For # 2167.
3754 if kwargs:
3755 print('c.selectPosition: all keyword args are ignored', g.callers())
3756 c = self
3757 cc = c.chapterController
3758 if not p:
3759 if not g.app.batchMode: # A serious error.
3760 g.trace('Warning: no p', g.callers())
3761 return
3762 if cc and not cc.selectChapterLockout:
3763 cc.selectChapterForPosition(p)
3764 # Calls c.redraw only if the chapter changes.
3765 # De-hoist as necessary to make p visible.
3766 if c.hoistStack:
3767 while c.hoistStack:
3768 bunch = c.hoistStack[-1]
3769 if c.positionExists(p, bunch.p):
3770 break
3771 if trace:
3772 # #2167: Give detailed trace.
3773 print('')
3774 print('pop hoist stack! callers:', g.callers())
3775 g.printObj(c.hoistStack, tag='c.hoistStack before pop')
3776 print('Recent keystrokes')
3777 for i, data in enumerate(reversed(g.app.lossage)):
3778 print(f"{i:>2} {data!r}")
3779 print('Recently-executed commands...')
3780 for i, command in enumerate(reversed(c.recent_commands_list)):
3781 print(f"{i:>2} {command}")
3782 c.hoistStack.pop()
3783 c.frame.tree.select(p)
3784 c.setCurrentPosition(p)
3785 # Do *not* test whether the position exists!
3786 # We may be in the midst of an undo.
3788 # Compatibility, but confusing.
3790 selectVnode = selectPosition
3791 #@+node:ekr.20080503055349.1: *5* c.setPositionAfterSort
3792 def setPositionAfterSort(self, sortChildren):
3793 """
3794 Return the position to be selected after a sort.
3795 """
3796 c = self
3797 p = c.p
3798 p_v = p.v
3799 parent = p.parent()
3800 parent_v = p._parentVnode()
3801 if sortChildren:
3802 return parent or c.rootPosition()
3803 if parent:
3804 p = parent.firstChild()
3805 else:
3806 p = leoNodes.Position(parent_v.children[0])
3807 while p and p.v != p_v:
3808 p.moveToNext()
3809 p = p or parent
3810 return p
3811 #@+node:ekr.20070226113916: *5* c.treeSelectHelper
3812 def treeSelectHelper(self, p):
3813 c = self
3814 if not p:
3815 p = c.p
3816 if p:
3817 # Do not call expandAllAncestors here.
3818 c.selectPosition(p)
3819 c.redraw_after_select(p)
3820 c.treeFocusHelper()
3821 # This is essential.
3822 #@+node:ekr.20130823083943.12559: *3* c.recursiveImport
3823 def recursiveImport(self, dir_, kind,
3824 add_context=None, # Override setting only if True/False
3825 add_file_context=None, # Override setting only if True/False
3826 add_path=True,
3827 recursive=True,
3828 safe_at_file=True,
3829 theTypes=None,
3830 # force_at_others=False, # tag:no-longer-used
3831 ignore_pattern=None,
3832 verbose=True, # legacy value.
3833 ):
3834 #@+<< docstring >>
3835 #@+node:ekr.20130823083943.12614: *4* << docstring >>
3836 """
3837 Recursively import all python files in a directory and clean the results.
3839 Parameters::
3840 dir_ The root directory or file to import.
3841 kind One of ('@clean','@edit','@file','@nosent').
3842 add_path=True True: add a full @path directive to @<file> nodes.
3843 recursive=True True: recurse into subdirectories.
3844 safe_at_file=True True: produce @@file nodes instead of @file nodes.
3845 theTypes=None A list of file extensions to import.
3846 None is equivalent to ['.py']
3848 This method cleans imported files as follows:
3850 - Replace backslashes with forward slashes in headlines.
3851 - Remove empty nodes.
3852 - Add @path directives that reduce the needed path specifiers in descendant nodes.
3853 - Add @file to nodes or replace @file with @@file.
3854 """
3855 #@-<< docstring >>
3856 c = self
3857 if g.os_path_exists(dir_):
3858 # Import all files in dir_ after c.p.
3859 try:
3860 from leo.core import leoImport
3861 cc = leoImport.RecursiveImportController(c, kind,
3862 add_context=add_context,
3863 add_file_context=add_file_context,
3864 add_path=add_path,
3865 ignore_pattern=ignore_pattern,
3866 recursive=recursive,
3867 safe_at_file=safe_at_file,
3868 theTypes=['.py'] if not theTypes else theTypes,
3869 verbose=verbose,
3870 )
3871 cc.run(dir_)
3872 finally:
3873 c.redraw()
3874 else:
3875 g.es_print(f"Does not exist: {dir_}")
3876 #@+node:ekr.20171124084149.1: *3* c.Scripting utils
3877 #@+node:ekr.20160201072634.1: *4* c.cloneFindByPredicate
3878 def cloneFindByPredicate(self,
3879 generator, # The generator used to traverse the tree.
3880 predicate, # A function of one argument p, returning True
3881 # if p should be included in the results.
3882 failMsg=None, # Failure message. Default is no message.
3883 flatten=False, # True: Put all matches at the top level.
3884 iconPath=None, # Full path to icon to attach to all matches.
3885 undoType=None, # The undo name, shown in the Edit:Undo menu.
3886 # The default is 'clone-find-predicate'
3887 ):
3888 """
3889 Traverse the tree given using the generator, cloning all positions for
3890 which predicate(p) is True. Undoably move all clones to a new node, created
3891 as the last top-level node. Returns the newly-created node. Arguments:
3893 generator, The generator used to traverse the tree.
3894 predicate, A function of one argument p returning true if p should be included.
3895 failMsg=None, Message given if nothing found. Default is no message.
3896 flatten=False, True: Move all node to be parents of the root node.
3897 iconPath=None, Full path to icon to attach to all matches.
3898 undo_type=None, The undo/redo name shown in the Edit:Undo menu.
3899 The default is 'clone-find-predicate'
3900 """
3901 c = self
3902 u, undoType = c.undoer, undoType or 'clone-find-predicate'
3903 clones, root, seen = [], None, set()
3904 for p in generator():
3905 if predicate(p) and p.v not in seen:
3906 c.setCloneFindByPredicateIcon(iconPath, p)
3907 if flatten:
3908 seen.add(p.v)
3909 else:
3910 for p2 in p.self_and_subtree(copy=False):
3911 seen.add(p2.v)
3912 clones.append(p.copy())
3913 if clones:
3914 undoData = u.beforeInsertNode(c.p)
3915 root = c.createCloneFindPredicateRoot(flatten, undoType)
3916 for p in clones:
3917 # Create the clone directly as a child of found.
3918 p2 = p.copy()
3919 n = root.numberOfChildren()
3920 p2._linkCopiedAsNthChild(root, n)
3921 u.afterInsertNode(root, undoType, undoData)
3922 c.selectPosition(root)
3923 c.setChanged()
3924 c.contractAllHeadlines()
3925 root.expand()
3926 elif failMsg:
3927 g.es(failMsg, color='red')
3928 return root
3929 #@+node:ekr.20160304054950.1: *5* c.setCloneFindByPredicateIcon
3930 def setCloneFindByPredicateIcon(self, iconPath, p):
3931 """Attach an icon to p.v.u."""
3932 if iconPath and g.os_path_exists(iconPath) and not g.os_path_isdir(iconPath):
3933 aList = p.v.u.get('icons', [])
3934 for d in aList:
3935 if d.get('file') == iconPath:
3936 break
3937 else:
3938 aList.append({
3939 'type': 'file',
3940 'file': iconPath,
3941 'on': 'VNode',
3942 # 'relPath': iconPath,
3943 'where': 'beforeHeadline',
3944 'xoffset': 2, 'xpad': 1,
3945 'yoffset': 0,
3947 })
3948 p.v.u['icons'] = aList
3949 elif iconPath:
3950 g.trace('bad icon path', iconPath)
3951 #@+node:ekr.20160201075438.1: *5* c.createCloneFindPredicateRoot
3952 def createCloneFindPredicateRoot(self, flatten, undoType):
3953 """Create a root node for clone-find-predicate."""
3954 c = self
3955 root = c.lastTopLevel().insertAfter()
3956 root.h = undoType + (' (flattened)' if flatten else '')
3957 return root
3958 #@+node:peckj.20131023115434.10114: *4* c.createNodeHierarchy
3959 def createNodeHierarchy(self, heads, parent=None, forcecreate=False):
3960 """ Create the proper hierarchy of nodes with headlines defined in
3961 'heads' under 'parent'
3963 params:
3964 parent - parent node to start from. Set to None for top-level nodes
3965 heads - list of headlines in order to create, i.e. ['foo','bar','baz']
3966 will create:
3967 parent
3968 -foo
3969 --bar
3970 ---baz
3971 forcecreate - If False (default), will not create nodes unless they don't exist
3972 If True, will create nodes regardless of existing nodes
3973 returns the final position ('baz' in the above example)
3974 """
3975 u = self.undoer
3976 undoType = 'Create Node Hierarchy'
3977 undoType2 = 'Insert Node In Hierarchy'
3978 u_node = parent or self.rootPosition()
3979 undoData = u.beforeChangeGroup(u_node, undoType)
3980 changed_node = False
3981 for idx, head in enumerate(heads):
3982 if parent is None and idx == 0: # if parent = None, create top level node for first head
3983 if not forcecreate:
3984 for pos in self.all_positions():
3985 if pos.h == head:
3986 parent = pos
3987 break
3988 if parent is None or forcecreate:
3989 u_d = u.beforeInsertNode(u_node)
3990 n = self.rootPosition().insertAfter()
3991 n.h = head
3992 u.afterInsertNode(n, undoType2, u_d)
3993 parent = n
3994 else: # else, simply create child nodes each round
3995 if not forcecreate:
3996 for ch in parent.children():
3997 if ch.h == head:
3998 parent = ch
3999 changed_node = True
4000 break
4001 if parent.h != head or not changed_node or forcecreate:
4002 u_d = u.beforeInsertNode(parent)
4003 n = parent.insertAsLastChild()
4004 n.h = head
4005 u.afterInsertNode(n, undoType2, u_d)
4006 parent = n
4007 changed_node = False
4008 u.afterChangeGroup(parent, undoType, undoData)
4009 return parent # actually the last created/found position
4010 #@+node:ekr.20100802121531.5804: *4* c.deletePositionsInList
4011 def deletePositionsInList(self, aList):
4012 """
4013 Delete all vnodes corresponding to the positions in aList.
4015 Set c.p if the old position no longer exists.
4017 See "Theory of operation of c.deletePositionsInList" in LeoDocs.leo.
4018 """
4019 # New implementation by Vitalije 2020-03-17 17:29
4020 c = self
4021 # Ensure all positions are valid.
4022 aList = [p for p in aList if c.positionExists(p)]
4023 if not aList:
4024 return []
4026 def p2link(p):
4027 parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode
4028 return p._childIndex, parent_v
4030 links_to_be_cut = sorted(set(map(p2link, aList)), key=lambda x: -x[0])
4031 undodata = []
4032 for i, v in links_to_be_cut:
4033 ch = v.children.pop(i)
4034 ch.parents.remove(v)
4035 undodata.append((v.gnx, i, ch.gnx))
4036 if not c.positionExists(c.p):
4037 c.selectPosition(c.rootPosition())
4038 return undodata
4040 #@+node:vitalije.20200318161844.1: *4* c.undoableDeletePositions
4041 def undoableDeletePositions(self, aList):
4042 """
4043 Deletes all vnodes corresponding to the positions in aList,
4044 and make changes undoable.
4045 """
4046 c = self
4047 u = c.undoer
4048 data = c.deletePositionsInList(aList)
4049 gnx2v = c.fileCommands.gnxDict
4050 def undo():
4051 for pgnx, i, chgnx in reversed(u.getBead(u.bead).data):
4052 v = gnx2v[pgnx]
4053 ch = gnx2v[chgnx]
4054 v.children.insert(i, ch)
4055 ch.parents.append(v)
4056 if not c.positionExists(c.p):
4057 c.setCurrentPosition(c.rootPosition())
4058 def redo():
4059 for pgnx, i, chgnx in u.getBead(u.bead + 1).data:
4060 v = gnx2v[pgnx]
4061 ch = v.children.pop(i)
4062 ch.parents.remove(v)
4063 if not c.positionExists(c.p):
4064 c.setCurrentPosition(c.rootPosition())
4065 u.pushBead(g.Bunch(
4066 data=data,
4067 undoType='delete nodes',
4068 undoHelper=undo,
4069 redoHelper=redo,
4070 ))
4071 #@+node:ekr.20091211111443.6265: *4* c.doBatchOperations & helpers
4072 def doBatchOperations(self, aList=None):
4073 # Validate aList and create the parents dict
4074 if aList is None:
4075 aList = []
4076 ok, d = self.checkBatchOperationsList(aList)
4077 if not ok:
4078 g.error('do-batch-operations: invalid list argument')
4079 return
4080 for v in list(d.keys()):
4081 aList2 = d.get(v, [])
4082 if aList2:
4083 aList.sort()
4084 #@+node:ekr.20091211111443.6266: *5* c.checkBatchOperationsList
4085 def checkBatchOperationsList(self, aList):
4086 ok = True
4087 d: Dict["leoNodes.VNode", List[Any]] = {}
4088 for z in aList:
4089 try:
4090 op, p, n = z
4091 ok = (op in ('insert', 'delete') and
4092 isinstance(p, leoNodes.position) and isinstance(n, int))
4093 if ok:
4094 aList2 = d.get(p.v, [])
4095 data = n, op
4096 aList2.append(data)
4097 d[p.v] = aList2
4098 except ValueError:
4099 ok = False
4100 if not ok:
4101 break
4102 return ok, d
4103 #@+node:ekr.20091002083910.6106: *4* c.find_b & find_h (PosList)
4104 #@+<< PosList doc >>
4105 #@+node:bob.20101215134608.5898: *5* << PosList doc >>
4106 #@@language rest
4107 #@+at
4108 # List of positions
4109 #
4110 # Functions find_h() and find_b() both return an instance of PosList.
4111 #
4112 # Methods filter_h() and filter_b() refine a PosList.
4113 #
4114 # Method children() generates a new PosList by descending one level from
4115 # all the nodes in a PosList.
4116 #
4117 # A chain of PosList method calls must begin with find_h() or find_b().
4118 # The rest of the chain can be any combination of filter_h(),
4119 # filter_b(), and children(). For example:
4120 #
4121 # pl = c.find_h('@file.*py').children().filter_h('class.*').filter_b('import (.*)')
4122 #
4123 # For each position, pos, in the PosList returned, find_h() and
4124 # filter_h() set attribute pos.mo to the match object (see Python
4125 # Regular Expression documentation) for the pattern match.
4126 #
4127 # Caution: The pattern given to find_h() or filter_h() must match zero
4128 # or more characters at the beginning of the headline.
4129 #
4130 # For each position, pos, the postlist returned, find_b() and filter_b()
4131 # set attribute pos.matchiter to an iterator that will return a match
4132 # object for each of the non-overlapping matches of the pattern in the
4133 # body of the node.
4134 #@-<< PosList doc >>
4135 #@+node:ville.20090311190405.70: *5* c.find_h
4136 def find_h(self, regex, flags=re.IGNORECASE):
4137 """ Return list (a PosList) of all nodes where zero or more characters at
4138 the beginning of the headline match regex
4139 """
4140 c = self
4141 pat = re.compile(regex, flags)
4142 res = leoNodes.PosList()
4143 for p in c.all_positions():
4144 m = re.match(pat, p.h)
4145 if m:
4146 pc = p.copy()
4147 pc.mo = m
4148 res.append(pc)
4149 return res
4150 #@+node:ville.20090311200059.1: *5* c.find_b
4151 def find_b(self, regex, flags=re.IGNORECASE | re.MULTILINE):
4152 """ Return list (a PosList) of all nodes whose body matches regex
4153 one or more times.
4155 """
4156 c = self
4157 pat = re.compile(regex, flags)
4158 res = leoNodes.PosList()
4159 for p in c.all_positions():
4160 m = re.finditer(pat, p.b)
4161 t1, t2 = itertools.tee(m, 2)
4162 try:
4163 t1.__next__()
4164 except StopIteration:
4165 continue
4166 pc = p.copy()
4167 pc.matchiter = t2
4168 res.append(pc)
4169 return res
4170 #@+node:ekr.20171124155725.1: *3* c.Settings
4171 #@+node:ekr.20171114114908.1: *4* c.registerReloadSettings
4172 def registerReloadSettings(self, obj):
4173 """Enter object into c.configurables."""
4174 c = self
4175 if obj not in c.configurables:
4176 c.configurables.append(obj)
4177 #@+node:ekr.20170221040621.1: *4* c.reloadConfigurableSettings
4178 def reloadConfigurableSettings(self):
4179 """
4180 Call all reloadSettings method in c.subcommanders, c.configurables and
4181 other known classes.
4182 """
4183 c = self
4184 table = [
4185 g.app.gui,
4186 g.app.pluginsController,
4187 c.k.autoCompleter,
4188 c.frame, c.frame.body, c.frame.log, c.frame.tree,
4189 c.frame.body.colorizer,
4190 getattr(c.frame.body.colorizer, 'highlighter', None),
4191 ]
4192 for obj in table:
4193 if obj:
4194 c.registerReloadSettings(obj)
4195 # Useful now that instances add themselves to c.configurables.
4196 c.configurables = list(set(c.configurables))
4197 c.configurables.sort(key=lambda obj: obj.__class__.__name__.lower())
4198 for obj in c.configurables:
4199 func = getattr(obj, 'reloadSettings', None)
4200 if func:
4201 # pylint: disable=not-callable
4202 try:
4203 func()
4204 except Exception:
4205 g.es_exception()
4206 c.configurables.remove(obj)
4207 #@-others
4208#@-others
4209#@@language python
4210#@@tabwidth -4
4211#@@pagewidth 70
4212#@-leo