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