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

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