Coverage for C:\Repos\leo-editor\leo\plugins\mod_scripting.py: 10%
781 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#@+leo-ver=5-thin
2#@+node:ekr.20060328125248: * @file ../plugins/mod_scripting.py
3#@+<< mod_scripting docstring >>
4#@+node:ekr.20060328125248.1: ** << mod_scripting docstring >>
5r"""This plugin script buttons and eval* commands.
7Overview of script buttons
8--------------------------
10This plugin puts buttons in the icon area. Depending on settings the plugin will
11create the 'Run Script', the 'Script Button' and the 'Debug Script' buttons.
13The 'Run Script' button is simply another way of doing the Execute Script
14command: it executes the selected text of the presently selected node, or the
15entire text if no text is selected.
17The 'Script Button' button creates *another* button in the icon area every time
18you push it. The name of the button is the headline of the presently selected
19node. Hitting this *newly created* button executes the button's script.
21For example, to run a script on any part of an outline do the following:
231. Select the node containing the script.
242. Press the scriptButton button. This will create a new button.
253. Select the node on which you want to run the script.
264. Push the *new* button.
28Script buttons create commands
29------------------------------
31For every @button node, this plugin creates two new minibuffer commands: x and
32delete-x-button, where x is the 'cleaned' name of the button. The 'x' command is
33equivalent to pushing the script button.
36Global buttons and commands
37---------------------------
39You can specify **global buttons** in leoSettings.leo or myLeoSettings.leo by
40putting \@button nodes as children of an @buttons node in an \@settings trees.
41Such buttons are included in all open .leo (in a slightly different color).
42Actually, you can specify global buttons in any .leo file, but \@buttons nodes
43affect all later opened .leo files so usually you would define global buttons in
44leoSettings.leo or myLeoSettings.leo.
46The cleaned name of an @button node is the headline text of the button with:
48- Leading @button or @command removed,
49- @key and all following text removed,
50- @args and all following text removed,
51- @color and all following text removed,
52- all non-alphanumeric characters converted to a single '-' characters.
54Thus, cleaning headline text converts it to a valid minibuffer command name.
56You can delete a script button by right-clicking on it, or by
57executing the delete-x-button command.
59.. The 'Debug Script' button runs a script using an external debugger.
61This plugin optionally scans for @script nodes whenever a .leo file is opened.
62Such @script nodes cause a script to be executed when opening a .leo file.
63They are security risks, and are never enabled by default.
65Settings
66--------
68You can specify the following options in myLeoSettings.leo. See the node:
69@settings-->Plugins-->scripting plugin. Recommended defaults are shown::
71 @bool scripting-at-button-nodes = True
72 True: adds a button for every @button node.
74 @bool scripting-at-rclick-nodes = False
75 True: define a minibuffer command for every @rclick node.
77 @bool scripting-at-commands-nodes = True
78 True: define a minibuffer command for every @command node.
80 @bool scripting-at-plugin-nodes = False
81 True: dynamically loads plugins in @plugin nodes when a window is created.
83 @bool scripting-at-script-nodes = False
84 True: dynamically executes script in @script nodes when a window is created.
85 This is dangerous!
87 @bool scripting-create-debug-button = False
88 True: create Debug Script button.
90 @bool scripting-create-run-script-button = False
91 True: create Run Script button.
92 Note: The plugin creates the press-run-script-button regardless of this setting.
94 @bool scripting-create-script-button-button = True
95 True: create Script Button button in icon area.
96 Note: The plugin creates the press-script-button-button regardless of this setting.
98 @int scripting-max-button-size = 18
99 The maximum length of button names: longer names are truncated.
101Shortcuts for script buttons
102----------------------------
104You can bind key shortcuts to @button and @command nodes as follows:
106@button name @key=shortcut
108 Binds the shortcut to the script in the script button. The button's name is
109 'name', but you can see the full headline in the status line when you move the
110 mouse over the button.
112@command name @key=shortcut
114 Creates a new minibuffer command and binds shortcut to it. As with @buffer
115 nodes, the name of the command is the cleaned name of the headline.
117Binding arguments to script buttons with @args
118----------------------------------------------
120You can run @button and @command scripts with sys.argv initialized to string values using @args.
121For example::
123 @button test-args @args = a,b,c
125will set sys.argv to ['a', 'b', 'c'].
127You can set the background color of buttons created by @button nodes by using @color.
128For example::
130 @button my button @key=Ctrl+Alt+1 @color=white @args=a,b,c
132This creates a button named 'my-button', with a color of white, a keyboard shortcut
133of Ctrl+Alt+1, and sets sys.argv to ['a', 'b', 'c'] within the context of the script.
135Eval Commands
136-------------
138The mod_scripting plugin creates the following 5 eval* commands:
140eval
141----
143Evaluates the selected text, if any, and remember the result in c.vs, a global namespace.
144For example::
146 a = 10
148sets:
150 c.vs['a'] = 10
152This command prints the result of the last expression or assignment in the log pane
153and select the next line of the body pane. Handy for executing line by line.
155eval-last
156---------
158Inserts the result of the last eval in the body.
159Suppose you have this text::
161 The cat is 7 years, or 7*365 days old.
163To replace 7*365 with 2555, do the following::
165 select 7*367
166 eval
167 delete 7*365
168 do eval-last
170eval-replace
171------------
173Evaluates the expression and replaces it with the computed value.
174For example, the example above can be done as follows::
177 select 7*367
178 eval-replace
180eval-last-pretty
181----------------
183Like eval-last, but format with pprint.pformat.
185eval-block
186----------
188Evaluates a series of blocks of code in the body, separated like this::
190 # >>>
191 code to run
192 # <<<
193 output of code
194 # >>>
195 code to run
196 # <<<
197 output of code
198 ...
200For example::
202 import datetime
203 datetime.datetime.now()
204 # >>>
205 2018-03-21 21:46:13.582835
206 # <<<
207 datetime.datetime.now()+datetime.timedelta(days=1000)
208 # >>>
209 2020-12-15 21:46:34.403814
210 # <<<
212eval-block inserts the separators, blocks can be re-run by placing the cursor in
213them and doing eval-block, and the cursor is placed in the next block, so you
214can go back up, change something, then quickly re-execute everything.
216Acknowledgements
217----------------
219This plugin is based on ideas from e's dynabutton plugin, possibly the
220most brilliant idea in Leo's history.
221"""
222#@-<< mod_scripting docstring >>
223#@+<< imports >>
224#@+node:ekr.20060328125248.2: ** << imports >>
225import pprint
226import re
227import sys
228import textwrap
229from typing import Any, Dict, List
230from leo.core import leoGlobals as g
231from leo.core import leoColor
232from leo.core import leoGui
233#@-<< imports >>
235#@+others
236#@+node:ekr.20210228135810.1: ** cmd decorator
237def eval_cmd(name):
238 """Command decorator for the EvalController class."""
239 return g.new_cmd_decorator(name, ['c', 'evalController',])
240#@+node:ekr.20180328085010.1: ** Top level (mod_scripting)
241#@+node:tbrown.20140819100840.37719: *3* build_rclick_tree (mod_scripting.py)
242def build_rclick_tree(command_p, rclicks=None, top_level=False):
243 """
244 Return a list of top level RClicks for the button at command_p, which can be
245 used later to add the rclick menus.
247 After building a list of @rclick children and following siblings of the
248 @button this method applies itself recursively to each member of that list
249 to handle submenus.
251 :Parameters:
252 - `command_p`: node containing @button. May be None
253 - `rclicks`: list of RClicks to add to, created if needed
254 - `top_level`: is this the top level?
255 """
256 # representation of an rclick node
257 from collections import namedtuple
258 RClick = namedtuple('RClick', 'position,children')
260 at_others_pat = re.compile(r'^\s*@others\b', re.MULTILINE)
262 def has_at_others(p):
263 """Return True if p.b has a valid @others directive."""
264 # #2439: A much simplified version of g.get_directives_dict.
265 if 'others' in g.globalDirectiveList:
266 return bool(re.search(at_others_pat, p.b))
267 return False
269 # Called from QtIconBarClass.setCommandForButton.
270 if rclicks is None:
271 rclicks = []
272 if top_level:
273 # command_p will be None for leoSettings.leo and myLeoSettings.leo.
274 if command_p:
275 if not has_at_others(command_p):
276 rclicks.extend([
277 RClick(
278 position=i.copy(), # -2 for top level entries, i.e. before "Remove button"
279 children=[],
280 )
281 for i in command_p.children()
282 if i.h.startswith('@rclick ')
283 ])
284 for i in command_p.following_siblings():
285 if i.h.startswith('@rclick '):
286 rclicks.append(RClick(position=i.copy(), children=[]))
287 else:
288 break
289 for rc in rclicks:
290 build_rclick_tree(rc.position, rc.children, top_level=False)
291 else: # recursive mode below top level
292 if not command_p:
293 return []
294 if command_p.b.strip():
295 return [] # sub menus can't have body text
296 for child in command_p.children():
297 # pylint: disable=no-member
298 rc = RClick(position=child.copy(), children=[])
299 rclicks.append(rc)
300 build_rclick_tree(rc.position, rc.children, top_level=False)
301 return rclicks
302#@+node:ekr.20060328125248.4: *3* init
303def init():
304 """Return True if the plugin has loaded successfully."""
305 if g.app.gui is None:
306 g.app.createQtGui(__file__)
307 # This plugin is now gui-independent.
308 ok = g.app.gui and g.app.gui.guiName() in ('qt', 'nullGui')
309 if ok:
310 sc = 'ScriptingControllerClass'
311 if (not hasattr(g.app.gui, sc) or
312 getattr(g.app.gui, sc) is leoGui.NullScriptingControllerClass
313 ):
314 setattr(g.app.gui, sc, ScriptingController)
315 # Note: call onCreate _after_ reading the .leo file.
316 # That is, the 'after-create-leo-frame' hook is too early!
317 g.registerHandler(('new', 'open2'), onCreate)
318 g.plugin_signon(__name__)
319 return ok
320#@+node:ekr.20060328125248.5: *3* onCreate
321def onCreate(tag, keys):
322 """Handle the onCreate event in the mod_scripting plugin."""
323 c = keys.get('c')
324 if c:
325 sc = g.app.gui.ScriptingControllerClass(c)
326 c.theScriptingController = sc
327 sc.createAllButtons()
328 c.evalController = EvalController(c)
329#@+node:ekr.20141031053508.7: ** class AtButtonCallback
330class AtButtonCallback:
331 """A class whose __call__ method is a callback for @button nodes."""
332 #@+others
333 #@+node:ekr.20141031053508.9: *3* __init__ (AtButtonCallback)
334 def __init__(self, controller, b, c, buttonText, docstring, gnx, script):
335 """AtButtonCallback.__init__."""
336 self.b = b # A QButton.
337 self.buttonText = buttonText # The text of the button.
338 self.c = c # A Commander.
339 self.controller = controller # A ScriptingController instance.
340 self.gnx = gnx # Set if the script is defined in the local .leo file.
341 self.script = script # The script defined in myLeoSettings.leo or leoSettings.leo
342 self.source_c = c # For GetArgs.command_source.
343 self.__doc__ = docstring # The docstring for this callback for g.getDocStringForFunction.
344 #@+node:ekr.20141031053508.10: *3* __call__ (AtButtonCallback)
345 def __call__(self, event=None):
346 """AtButtonCallbgack.__call__. The callback for @button nodes."""
347 self.execute_script()
348 #@+node:ekr.20141031053508.13: *3* __repr__ (AtButtonCallback)
349 def __repr__(self):
350 """AtButtonCallback.__repr__."""
351 c = self.c
352 return 'AtButtonCallback %s gnx: %s len(script) %s' % (
353 c.shortFileName(), self.gnx, len(self.script or ''))
354 #@+node:ekr.20150512041758.1: *3* __getattr__ (AtButtonCallback)
355 def __getattr__(self, attr):
356 """AtButtonCallback.__getattr__. Implement __name__."""
357 if attr == '__name__':
358 return 'AtButtonCallback: %s' % self.gnx
359 raise AttributeError # Returning None is not correct.
360 #@+node:ekr.20170203043042.1: *3* AtButtonCallback.execute_script & helper
361 def execute_script(self):
362 """Execute the script associated with this button."""
363 script = self.find_script()
364 if script:
365 self.controller.executeScriptFromButton(
366 b=self.b,
367 buttonText=self.buttonText,
368 p=None,
369 script_gnx=self.gnx,
370 script=script,
371 )
372 #@+node:ekr.20180313171043.1: *4* AtButtonCallback.find_script
373 def find_script(self):
375 gnx = self.gnx
376 # First, search self.c for the gnx.
377 for p in self.c.all_positions():
378 if p.gnx == gnx:
379 script = self.controller.getScript(p)
380 return script
381 # See if myLeoSettings.leo is open.
382 for c in g.app.commanders():
383 if c.shortFileName().endswith('myLeoSettings.leo'):
384 break
385 else:
386 c = None
387 if c:
388 # Search myLeoSettings.leo file for the gnx.
389 for p in c.all_positions():
390 if p.gnx == gnx:
391 script = self.controller.getScript(p)
392 return script
393 return self.script
394 #@-others
395#@+node:ekr.20060328125248.6: ** class ScriptingController
396class ScriptingController:
397 """A class defining scripting commands."""
398 #@+others
399 #@+node:ekr.20060328125248.7: *3* sc.ctor
400 def __init__(self, c, iconBar=None):
401 self.c = c
402 self.gui = c.frame.gui
403 getBool = c.config.getBool
404 self.scanned = False
405 kind = c.config.getString('debugger-kind') or 'idle'
406 self.buttonsDict = {} # Keys are buttons, values are button names (strings).
407 self.debuggerKind = kind.lower()
408 # True: adds a button for every @button node.
409 self.atButtonNodes = getBool('scripting-at-button-nodes')
410 # True: define a minibuffer command for every @command node.
411 self.atCommandsNodes = getBool('scripting-at-commands-nodes')
412 # True: define a minibuffer command for every @rclick node.
413 self.atRclickNodes = getBool('scripting-at-rclick-nodes')
414 # True: dynamically loads plugins in @plugin nodes when a window is created.
415 self.atPluginNodes = getBool('scripting-at-plugin-nodes')
416 # # DANGEROUS! True: dynamically executes script in @script nodes when a window is created.
417 self.atScriptNodes = getBool('scripting-at-script-nodes')
418 # Do not allow this setting to be changed in local (non-settings) .leo files.
419 if self.atScriptNodes and c.config.isLocalSetting('scripting-at-script-nodes', 'bool'):
420 g.issueSecurityWarning('@bool scripting-at-script-nodes')
421 # Restore the value in myLeoSettings.leo
422 val = g.app.config.valueInMyLeoSettings('scripting-at-script-nodes')
423 if val is None:
424 val = False
425 g.es('Restoring value to', val, color='red')
426 self.atScriptNodes = val
427 # True: create Debug Script button.
428 self.createDebugButton = getBool('scripting-create-debug-button')
429 # True: create Run Script button.
430 self.createRunScriptButton = getBool('scripting-create-run-script-button')
431 # True: create Script Button button.
432 self.createScriptButtonButton = getBool('scripting-create-script-button-button')
433 # Maximum length of button names.
434 self.maxButtonSize = c.config.getInt('scripting-max-button-size') or 18
435 if not iconBar:
436 self.iconBar = c.frame.getIconBarObject()
437 else:
438 self.iconBar = iconBar
439 # #74: problems with @button if defined in myLeoSettings.leo
440 self.seen = set() # Set of gnx's (not vnodes!) that created buttons or commands.
441 #@+node:ekr.20150401113822.1: *3* sc.Callbacks
442 #@+node:ekr.20060328125248.23: *4* sc.addScriptButtonCommand
443 def addScriptButtonCommand(self, event=None):
444 """Called when the user presses the 'script-button' button or executes the script-button command."""
445 c = self.c
446 p = c.p
447 h = p.h
448 buttonText = self.getButtonText(h)
449 shortcut = self.getShortcut(h)
450 statusLine = "Run Script: %s" % buttonText
451 if shortcut:
452 statusLine = statusLine + " @key=" + shortcut
453 self.createLocalAtButtonHelper(p, h, statusLine, kind='script-button', verbose=True)
454 c.bodyWantsFocus()
455 #@+node:ekr.20060522105937.1: *4* sc.runDebugScriptCommand
456 def runDebugScriptCommand(self, event=None):
457 """Called when user presses the 'debug-script' button or executes the debug-script command."""
458 c = self.c
459 p = c.p
460 script = g.getScript(c, p, useSelectedText=True, useSentinels=False)
461 if script:
462 #@+<< set debugging if debugger is active >>
463 #@+node:ekr.20060523084441: *5* << set debugging if debugger is active >>
464 g.trace(self.debuggerKind)
465 if self.debuggerKind == 'winpdb':
466 try:
467 import rpdb2
468 debugging = rpdb2.g_debugger is not None
469 except ImportError:
470 debugging = False
471 elif self.debuggerKind == 'idle':
472 # import idlelib.Debugger.py as Debugger
473 # debugging = Debugger.interacting
474 debugging = True
475 else:
476 debugging = False
477 #@-<< set debugging if debugger is active >>
478 if debugging:
479 #@+<< create leoScriptModule >>
480 #@+node:ekr.20060524073716: *5* << create leoScriptModule >> (mod_scripting.py)
481 target = g.os_path_join(g.app.loadDir, 'leoScriptModule.py')
482 with open(target, 'w') as f:
483 f.write('# A module holding the script to be debugged.\n')
484 if self.debuggerKind == 'idle':
485 # This works, but uses the lame pdb debugger.
486 f.write('import pdb\n')
487 f.write('pdb.set_trace() # Hard breakpoint.\n')
488 elif self.debuggerKind == 'winpdb':
489 f.write('import rpdb2\n')
490 f.write('if rpdb2.g_debugger is not None: # don\'t hang if the debugger isn\'t running.\n')
491 f.write(' rpdb2.start_embedded_debugger(pwd="",fAllowUnencrypted=True) # Hard breakpoint.\n')
492 # f.write('# Remove all previous variables.\n')
493 f.write('# Predefine c, g and p.\n')
494 f.write('from leo.core import leoGlobals as g\n')
495 f.write('c = g.app.scriptDict.get("c")\n')
496 f.write('script_gnx = g.app.scriptDict.get("script_gnx")\n')
497 f.write('p = c.p\n')
498 f.write('# Actual script starts here.\n')
499 f.write(script + '\n')
500 #@-<< create leoScriptModule >>
501 # pylint: disable=no-name-in-module
502 g.app.scriptDict['c'] = c
503 g.app.scriptDict = {'script_gnx': p.gnx}
504 if 'leoScriptModule' in sys.modules.keys():
505 del sys.modules['leoScriptModule'] # Essential.
506 # pylint: disable=import-error
507 # This *will* exist.
508 from leo.core import leoScriptModule
509 assert leoScriptModule # for pyflakes.
510 else:
511 g.error('No debugger active')
512 c.bodyWantsFocus()
513 #@+node:ekr.20060328125248.21: *4* sc.runScriptCommand
514 def runScriptCommand(self, event=None):
515 """Called when user presses the 'run-script' button or executes the run-script command."""
516 c, p = self.c, self.c.p
517 args = self.getArgs(p)
518 g.app.scriptDict = {'script_gnx': p.gnx}
519 c.executeScript(args=args, p=p, useSelectedText=True, silent=True)
520 if 0:
521 # Do not assume the script will want to remain in this commander.
522 c.bodyWantsFocus()
523 #@+node:ekr.20060328125248.8: *3* sc.createAllButtons
524 def createAllButtons(self):
525 """Scan for @button, @rclick, @command, @plugin and @script nodes."""
526 c = self.c
527 if self.scanned:
528 return # Defensive.
529 self.scanned = True
530 #
531 # First, create standard buttons.
532 if self.createRunScriptButton:
533 self.createRunScriptIconButton()
534 if self.createScriptButtonButton:
535 self.createScriptButtonIconButton()
536 if self.createDebugButton:
537 self.createDebugIconButton()
538 #
539 # Next, create common buttons and commands.
540 self.createCommonButtons()
541 self.createCommonCommands()
542 #
543 # Handle all other nodes.
544 d = {
545 'button': self.handleAtButtonNode,
546 'command': self.handleAtCommandNode,
547 'plugin': self.handleAtPluginNode,
548 'rclick': self.handleAtRclickNode,
549 'script': self.handleAtScriptNode,
550 }
551 pattern = re.compile(r'^@(button|command|plugin|rclick|script)\b')
552 p = c.rootPosition()
553 while p:
554 gnx = p.v.gnx
555 if p.isAtIgnoreNode():
556 p.moveToNodeAfterTree()
557 elif gnx in self.seen:
558 # #657
559 # if g.match_word(p.h, 0, '@rclick'):
560 if p.h.startswith('@rlick'):
561 self.handleAtRclickNode(p)
562 p.moveToThreadNext()
563 else:
564 self.seen.add(gnx)
565 m = pattern.match(p.h)
566 if m:
567 func = d.get(m.group(1))
568 func(p)
569 p.moveToThreadNext()
570 #@+node:ekr.20060328125248.24: *3* sc.createLocalAtButtonHelper
571 def createLocalAtButtonHelper(self, p, h, statusLine,
572 kind='at-button',
573 verbose=True,
574 ):
575 """Create a button for a local @button node."""
576 c = self.c
577 buttonText = self.cleanButtonText(h, minimal=True)
578 args = self.getArgs(p)
579 # We must define the callback *after* defining b,
580 # so set both command and shortcut to None here.
581 bg = self.getColor(h)
582 b = self.createIconButton(
583 args=args,
584 text=h,
585 command=None,
586 statusLine=statusLine,
587 kind=kind,
588 bg=bg,
589 )
590 if not b:
591 return None
592 # Now that b is defined we can define the callback.
593 # Yes, executeScriptFromButton *does* use b (to delete b if requested by the script).
594 docstring = g.getDocString(p.b).strip()
595 cb = AtButtonCallback(
596 controller=self,
597 b=b,
598 c=c,
599 buttonText=buttonText,
600 docstring=docstring,
601 gnx=p.v.gnx,
602 script=None,
603 )
604 self.iconBar.setCommandForButton(
605 button=b,
606 command=cb, # This encapsulates the script.
607 command_p=p and p.copy(), # This does exist.
608 controller=self,
609 gnx=p and p.gnx,
610 script=None,
611 )
612 # At last we can define the command and use the shortcut.
613 # registerAllCommands recomputes the shortcut.
614 self.registerAllCommands(
615 args=self.getArgs(p),
616 func=cb,
617 h=h,
618 pane='button',
619 source_c=p.v.context,
620 tag='local @button')
621 return b
622 #@+node:ekr.20060328125248.17: *3* sc.createIconButton (creates all buttons)
623 def createIconButton(self, args, text, command, statusLine, bg=None, kind=None):
624 """
625 Create one icon button.
626 This method creates all scripting icon buttons.
628 - Creates the actual button and its balloon.
629 - Adds the button to buttonsDict.
630 - Registers command with the shortcut.
631 - Creates x amd delete-x-button commands, where x is the cleaned button name.
632 - Binds a right-click in the button to a callback that deletes the button.
633 """
634 c = self.c
635 # Create the button and add it to the buttons dict.
636 commandName = self.cleanButtonText(text)
637 # Truncate only the text of the button, not the command name.
638 truncatedText = self.truncateButtonText(commandName)
639 if not truncatedText.strip():
640 g.error('%s ignored: no cleaned text' % (text.strip() or ''))
641 return None
642 # Command may be None.
643 b = self.iconBar.add(text=truncatedText, command=command, kind=kind)
644 if not b:
645 return None
646 self.setButtonColor(b, bg)
647 self.buttonsDict[b] = truncatedText
648 if statusLine:
649 self.createBalloon(b, statusLine)
650 if command:
651 self.registerAllCommands(
652 args=args,
653 func=command,
654 h=text,
655 pane='button',
656 source_c=c,
657 tag='icon button')
659 def deleteButtonCallback(event=None, self=self, b=b):
660 self.deleteButton(b, event=event)
662 # Register the delete-x-button command.
663 deleteCommandName = 'delete-%s-button' % commandName
664 c.k.registerCommand(
665 # allowBinding=True,
666 commandName=deleteCommandName,
667 func=deleteButtonCallback,
668 pane='button',
669 shortcut=None,
670 )
671 # Reporting this command is way too annoying.
672 return b
673 #@+node:ekr.20060328125248.28: *3* sc.executeScriptFromButton
674 def executeScriptFromButton(self, b, buttonText, p, script, script_gnx=None):
675 """Execute an @button script in p.b or script."""
676 c = self.c
677 if c.disableCommandsMessage:
678 g.blue(c.disableCommandsMessage)
679 return
680 if not p and not script:
681 g.trace('can not happen: no p and no script')
682 return
683 g.app.scriptDict = {'script_gnx': script_gnx}
684 args = self.getArgs(p)
685 if not script:
686 script = self.getScript(p)
687 c.executeScript(args=args, p=p, script=script, silent=True)
688 # Remove the button if the script asks to be removed.
689 if g.app.scriptDict.get('removeMe'):
690 g.es("Removing '%s' button at its request" % buttonText)
691 self.deleteButton(b)
692 # Do *not* set focus here: the script may have changed the focus.
693 # c.bodyWantsFocus()
694 #@+node:ekr.20130912061655.11294: *3* sc.open_gnx
695 def open_gnx(self, c, gnx):
696 """
697 Find the node with the given gnx in c, myLeoSettings.leo and leoSettings.leo.
698 If found, open the tab/outline and select the specified node.
699 Return c,p of the found node.
701 Called only from a callback in QtIconBarClass.setCommandForButton.
702 """
703 if not gnx:
704 g.trace('can not happen: no gnx')
705 # First, look in commander c.
706 for p2 in c.all_positions():
707 if p2.gnx == gnx:
708 return c, p2
709 # Fix bug 74: problems with @button if defined in myLeoSettings.leo.
710 for f in (c.openMyLeoSettings, c.openLeoSettings):
711 c2 = f() # Open the settings file.
712 if c2:
713 for p2 in c2.all_positions():
714 if p2.gnx == gnx:
715 return c2, p2
716 c2.close()
717 # Fix bug 92: restore the previously selected tab.
718 if hasattr(c.frame, 'top'):
719 c.frame.top.leo_master.select(c)
720 return None, None # 2017/02/02.
721 #@+node:ekr.20150401130207.1: *3* sc.Scripts, common
722 # Important: common @button and @command nodes do **not** update dynamically!
723 #@+node:ekr.20080312071248.1: *4* sc.createCommonButtons
724 def createCommonButtons(self):
725 """Handle all global @button nodes."""
726 c = self.c
727 buttons = c.config.getButtons() or []
728 for z in buttons:
729 # #2011
730 p, script, rclicks = z
731 gnx = p.v.gnx
732 if gnx not in self.seen:
733 self.seen.add(gnx)
734 script = self.getScript(p)
735 self.createCommonButton(p, script, rclicks)
736 #@+node:ekr.20070926084600: *4* sc.createCommonButton (common @button)
737 def createCommonButton(self, p, script, rclicks=None):
738 """
739 Create a button in the icon area for a common @button node in an @setting
740 tree. Binds button presses to a callback that executes the script.
742 Important: Common @button and @command scripts now *do* update
743 dynamically provided that myLeoSettings.leo is open. Otherwise the
744 callback executes the static script.
746 See https://github.com/leo-editor/leo-editor/issues/171
747 """
748 c = self.c
749 gnx = p.gnx
750 args = self.getArgs(p)
751 # Fix bug #74: problems with @button if defined in myLeoSettings.leo
752 docstring = g.getDocString(p.b).strip()
753 statusLine = docstring or 'Global script button'
754 shortcut = self.getShortcut(p.h) # Get the shortcut from the @key field in the headline.
755 if shortcut:
756 statusLine = '%s = %s' % (statusLine.rstrip(), shortcut)
757 # We must define the callback *after* defining b,
758 # so set both command and shortcut to None here.
759 bg = self.getColor(p.h) # #2024
760 b = self.createIconButton(
761 args=args,
762 bg=bg, # #2024
763 text=p.h,
764 command=None,
765 statusLine=statusLine,
766 kind='at-button',
767 )
768 if not b:
769 return
770 # Now that b is defined we can define the callback.
771 # Yes, the callback *does* use b (to delete b if requested by the script).
772 buttonText = self.cleanButtonText(p.h)
773 cb = AtButtonCallback(
774 b=b,
775 buttonText=buttonText,
776 c=c,
777 controller=self,
778 docstring=docstring,
779 # #367: the gnx is needed for the Goto Script command.
780 # Use gnx to search myLeoSettings.leo if it is open.
781 gnx=gnx,
782 script=script,
783 )
784 # Now patch the button.
785 self.iconBar.setCommandForButton(
786 button=b,
787 command=cb, # This encapsulates the script.
788 command_p=p and p.copy(), # #567
789 controller=self,
790 gnx=gnx, # For the find-button function.
791 script=script,
792 )
793 self.handleRclicks(rclicks)
794 # At last we can define the command.
795 self.registerAllCommands(
796 args=args,
797 func=cb,
798 h=p.h,
799 pane='button',
800 source_c=p.v.context,
801 tag='@button')
802 #@+node:ekr.20080312071248.2: *4* sc.createCommonCommands
803 def createCommonCommands(self):
804 """Handle all global @command nodes."""
805 c = self.c
806 aList = c.config.getCommands() or []
807 for z in aList:
808 p, script = z
809 gnx = p.v.gnx
810 if gnx not in self.seen:
811 self.seen.add(gnx)
812 script = self.getScript(p)
813 self.createCommonCommand(p, script)
814 #@+node:ekr.20150401130818.1: *4* sc.createCommonCommand (common @command)
815 def createCommonCommand(self, p, script):
816 """
817 Handle a single @command node.
819 Important: Common @button and @command scripts now *do* update
820 dynamically provided that myLeoSettings.leo is open. Otherwise the
821 callback executes the static script.
823 See https://github.com/leo-editor/leo-editor/issues/171
824 """
825 c = self.c
826 args = self.getArgs(p)
827 commonCommandCallback = AtButtonCallback(
828 b=None,
829 buttonText=None,
830 c=c,
831 controller=self,
832 docstring=g.getDocString(p.b).strip(),
833 gnx=p.v.gnx, # Used to search myLeoSettings.leo if it is open.
834 script=script, # Fallback when myLeoSettings.leo is not open.
835 )
836 self.registerAllCommands(
837 args=args,
838 func=commonCommandCallback,
839 h=p.h,
840 pane='button', # Fix bug 416: use 'button', NOT 'command', and NOT 'all'
841 source_c=p.v.context,
842 tag='global @command',
843 )
844 #@+node:ekr.20150401130313.1: *3* sc.Scripts, individual
845 #@+node:ekr.20060328125248.12: *4* sc.handleAtButtonNode @button
846 def handleAtButtonNode(self, p):
847 """
848 Create a button in the icon area for an @button node.
850 An optional @key=shortcut defines a shortcut that is bound to the button's script.
851 The @key=shortcut does not appear in the button's name, but
852 it *does* appear in the statutus line shown when the mouse moves over the button.
854 An optional @color=colorname defines a color for the button's background. It does
855 not appear in the status line nor the button name.
856 """
857 h = p.h
858 shortcut = self.getShortcut(h)
859 docstring = g.getDocString(p.b).strip()
860 statusLine = docstring if docstring else 'Local script button'
861 if shortcut:
862 statusLine = '%s = %s' % (statusLine, shortcut)
863 g.app.config.atLocalButtonsList.append(p.copy())
864 # This helper is also called by the script-button callback.
865 self.createLocalAtButtonHelper(p, h, statusLine, verbose=False)
866 #@+node:ekr.20060328125248.10: *4* sc.handleAtCommandNode @command
867 def handleAtCommandNode(self, p):
868 """Handle @command name [@key[=]shortcut]."""
869 c = self.c
870 if not p.h.strip():
871 return
872 args = self.getArgs(p)
874 def atCommandCallback(event=None, args=args, c=c, p=p.copy()):
875 # pylint: disable=dangerous-default-value
876 c.executeScript(args=args, p=p, silent=True)
878 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252
879 # Minibuffer commands created by mod_scripting.py have no docstrings
881 atCommandCallback.__doc__ = g.getDocString(p.b).strip()
882 self.registerAllCommands(
883 args=args,
884 func=atCommandCallback,
885 h=p.h,
886 pane='button', # Fix # 416.
887 source_c=p.v.context,
888 tag='local @command')
889 g.app.config.atLocalCommandsList.append(p.copy())
890 #@+node:ekr.20060328125248.13: *4* sc.handleAtPluginNode @plugin
891 def handleAtPluginNode(self, p):
892 """Handle @plugin nodes."""
893 tag = "@plugin"
894 h = p.h
895 assert g.match(h, 0, tag)
896 # Get the name of the module.
897 moduleOrFileName = h[len(tag) :].strip()
898 if not self.atPluginNodes:
899 g.warning("disabled @plugin: %s" % (moduleOrFileName))
900 # elif theFile in g.app.loadedPlugins:
901 elif g.pluginIsLoaded(moduleOrFileName):
902 g.warning("plugin already loaded: %s" % (moduleOrFileName))
903 else:
904 g.loadOnePlugin(moduleOrFileName)
905 #@+node:peckj.20131113130420.6851: *4* sc.handleAtRclickNode @rclick
906 def handleAtRclickNode(self, p):
907 """Handle @rclick name [@key[=]shortcut]."""
908 c = self.c
909 if not p.h.strip():
910 return
911 args = self.getArgs(p)
913 def atCommandCallback(event=None, args=args, c=c, p=p.copy()):
914 # pylint: disable=dangerous-default-value
915 c.executeScript(args=args, p=p, silent=True)
916 if p.b.strip():
917 self.registerAllCommands(
918 args=args,
919 func=atCommandCallback,
920 h=p.h,
921 pane='all',
922 source_c=p.v.context,
923 tag='local @rclick')
924 g.app.config.atLocalCommandsList.append(p.copy())
925 #@+node:vitalije.20180224113123.1: *4* sc.handleRclicks
926 def handleRclicks(self, rclicks):
927 def handlerc(rc):
928 if rc.children:
929 for i in rc.children:
930 handlerc(i)
931 else:
932 self.handleAtRclickNode(rc.position)
933 for rc in rclicks:
934 handlerc(rc)
936 #@+node:ekr.20060328125248.14: *4* sc.handleAtScriptNode @script
937 def handleAtScriptNode(self, p):
938 """Handle @script nodes."""
939 c = self.c
940 tag = "@script"
941 assert g.match(p.h, 0, tag)
942 name = p.h[len(tag) :].strip()
943 args = self.getArgs(p)
944 if self.atScriptNodes:
945 g.blue("executing script %s" % (name))
946 c.executeScript(args=args, p=p, useSelectedText=False, silent=True)
947 else:
948 g.warning("disabled @script: %s" % (name))
949 if 0:
950 # Do not assume the script will want to remain in this commander.
951 c.bodyWantsFocus()
952 #@+node:ekr.20150401125747.1: *3* sc.Standard buttons
953 #@+node:ekr.20060522105937: *4* sc.createDebugIconButton 'debug-script'
954 def createDebugIconButton(self):
955 """Create the 'debug-script' button and the debug-script command."""
956 self.createIconButton(
957 args=None,
958 text='debug-script',
959 command=self.runDebugScriptCommand,
960 statusLine='Debug script in selected node',
961 kind='debug-script')
962 #@+node:ekr.20060328125248.20: *4* sc.createRunScriptIconButton 'run-script'
963 def createRunScriptIconButton(self):
964 """Create the 'run-script' button and the run-script command."""
965 self.createIconButton(
966 args=None,
967 text='run-script',
968 command=self.runScriptCommand,
969 statusLine='Run script in selected node',
970 kind='run-script',
971 )
972 #@+node:ekr.20060328125248.22: *4* sc.createScriptButtonIconButton 'script-button'
973 def createScriptButtonIconButton(self):
974 """Create the 'script-button' button and the script-button command."""
975 self.createIconButton(
976 args=None,
977 text='script-button',
978 command=self.addScriptButtonCommand,
979 statusLine='Make script button from selected node',
980 kind="script-button-button")
981 #@+node:ekr.20061014075212: *3* sc.Utils
982 #@+node:ekr.20060929135558: *4* sc.cleanButtonText
983 def cleanButtonText(self, s, minimal=False):
984 """
985 Clean the text following @button or @command so
986 that it is a valid name of a minibuffer command.
987 """
988 # #1121: Don't lowercase anything.
989 if minimal:
990 return s.replace(' ', '-').strip('-')
991 for tag in ('@key', '@args', '@color',):
992 i = s.find(tag)
993 if i > -1:
994 j = s.find('@', i + 1)
995 if i < j:
996 s = s[:i] + s[j:]
997 else:
998 s = s[:i]
999 s = s.strip()
1000 return s.replace(' ', '-').strip('-')
1001 #@+node:ekr.20060522104419.1: *4* sc.createBalloon (gui-dependent)
1002 def createBalloon(self, w, label):
1003 'Create a balloon for a widget.'
1004 if g.app.gui.guiName().startswith('qt'):
1005 # w is a leoIconBarButton.
1006 if hasattr(w, 'button'):
1007 w.button.setToolTip(label)
1008 #@+node:ekr.20060328125248.26: *4* sc.deleteButton
1009 def deleteButton(self, button, **kw):
1010 """Delete the given button.
1011 This is called from callbacks, it is not a callback."""
1012 w = button
1013 if button and self.buttonsDict.get(w):
1014 del self.buttonsDict[w]
1015 self.iconBar.deleteButton(w)
1016 self.c.bodyWantsFocus()
1017 #@+node:ekr.20080813064908.4: *4* sc.getArgs
1018 def getArgs(self, p):
1019 """Return the list of @args field of p.h."""
1020 args: List[str] = []
1021 if not p:
1022 return args
1023 h, tag = p.h, '@args'
1024 i = h.find(tag)
1025 if i > -1:
1026 j = g.skip_ws(h, i + len(tag))
1027 # 2011/10/16: Make '=' sign optional.
1028 if g.match(h, j, '='):
1029 j += 1
1030 if 0:
1031 s = h[j + 1 :].strip()
1032 else: # new logic 1/3/2014 Jake Peck
1033 k = h.find('@', j + 1)
1034 if k == -1:
1035 k = len(h)
1036 s = h[j:k].strip()
1037 args = s.split(',')
1038 args = [z.strip() for z in args]
1039 # if args: g.trace(args)
1040 return args
1041 #@+node:ekr.20060328125248.15: *4* sc.getButtonText
1042 def getButtonText(self, h):
1043 """Returns the button text found in the given headline string"""
1044 tag = "@button"
1045 if g.match_word(h, 0, tag):
1046 h = h[len(tag) :].strip()
1047 for tag in ('@key', '@args', '@color',):
1048 i = h.find(tag)
1049 if i > -1:
1050 j = h.find('@', i + 1)
1051 if i < j:
1052 h = h[:i] + h[j + 1 :]
1053 else:
1054 h = h[:i]
1055 h = h.strip()
1056 buttonText = h
1057 # fullButtonText = buttonText
1058 return buttonText
1059 #@+node:peckj.20140103101946.10404: *4* sc.getColor
1060 def getColor(self, h):
1061 """Returns the background color from the given headline string"""
1062 color = None
1063 tag = '@color'
1064 i = h.find(tag)
1065 if i > -1:
1066 j = g.skip_ws(h, i + len(tag))
1067 if g.match(h, j, '='):
1068 j += 1
1069 k = h.find('@', j + 1)
1070 if k == -1:
1071 k = len(h)
1072 color = h[j:k].strip()
1073 return color
1074 #@+node:ekr.20060328125248.16: *4* sc.getShortcut
1075 def getShortcut(self, h):
1076 """Return the keyboard shortcut from the given headline string"""
1077 shortcut = None
1078 i = h.find('@key')
1079 if i > -1:
1080 j = g.skip_ws(h, i + len('@key'))
1081 if g.match(h, j, '='):
1082 j += 1
1083 if 0:
1084 shortcut = h[j:].strip()
1085 else: # new logic 1/3/2014 Jake Peck
1086 k = h.find('@', j + 1)
1087 if k == -1:
1088 k = len(h)
1089 shortcut = h[j:k].strip()
1090 return shortcut
1091 #@+node:ekr.20150402042350.1: *4* sc.getScript
1092 def getScript(self, p):
1093 """Return the script composed from p and its descendants."""
1094 return (
1095 g.getScript(self.c, p,
1096 useSelectedText=False,
1097 forcePythonSentinels=True,
1098 useSentinels=True,
1099 ))
1100 #@+node:ekr.20120301114648.9932: *4* sc.registerAllCommands
1101 def registerAllCommands(self, args, func, h, pane, source_c=None, tag=None):
1102 """Register @button <name> and @rclick <name> and <name>"""
1103 c, k = self.c, self.c.k
1104 trace = False and not g.unitTesting
1105 shortcut = self.getShortcut(h) or ''
1106 commandName = self.cleanButtonText(h)
1107 if trace and not g.isascii(commandName):
1108 g.trace(commandName)
1109 # Register the original function.
1110 k.registerCommand(
1111 allowBinding=True,
1112 commandName=commandName,
1113 func=func,
1114 pane=pane,
1115 shortcut=shortcut,
1116 )
1118 # 2013/11/13 Jake Peck:
1119 # include '@rclick-' in list of tags
1120 for prefix in ('@button-', '@command-', '@rclick-'):
1121 if commandName.startswith(prefix):
1122 commandName2 = commandName[len(prefix) :].strip()
1123 # Create a *second* func, to avoid collision in c.commandsDict.
1125 def registerAllCommandsCallback(event=None, func=func):
1126 func()
1128 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252
1129 # Minibuffer commands created by mod_scripting.py have no docstrings.
1130 registerAllCommandsCallback.__doc__ = func.__doc__
1131 # Make sure we never redefine an existing commandName.
1132 if commandName2 in c.commandsDict:
1133 # A warning here would be annoying.
1134 if trace:
1135 g.trace('Already in commandsDict: %r' % commandName2)
1136 else:
1137 k.registerCommand(
1138 commandName=commandName2,
1139 func=registerAllCommandsCallback,
1140 pane=pane,
1141 shortcut=None,
1142 )
1143 #@+node:ekr.20150402021505.1: *4* sc.setButtonColor
1144 def setButtonColor(self, b, bg):
1145 """Set the background color of Qt button b to bg."""
1146 if not bg:
1147 return
1148 if not bg.startswith('#'):
1149 bg0 = bg
1150 d = leoColor.leo_color_database
1151 bg = d.get(bg.lower())
1152 if not bg:
1153 g.trace('bad color? %s' % bg0)
1154 return
1155 try:
1156 b.button.setStyleSheet("QPushButton{background-color: %s}" % (bg))
1157 except Exception:
1158 # g.es_exception()
1159 pass # Might not be a valid color.
1160 #@+node:ekr.20061015125212: *4* sc.truncateButtonText
1161 def truncateButtonText(self, s):
1162 # 2011/10/16: Remove @button here only.
1163 i = 0
1164 while g.match(s, i, '@'):
1165 i += 1
1166 if g.match_word(s, i, 'button'):
1167 i += 6
1168 s = s[i:]
1169 if self.maxButtonSize > 10:
1170 s = s[:self.maxButtonSize]
1171 if s.endswith('-'):
1172 s = s[:-1]
1173 s = s.strip('-')
1174 return s.strip()
1175 #@-others
1177scriptingController = ScriptingController
1178#@+node:ekr.20180328085038.1: ** class EvalController
1179class EvalController:
1180 """A class defining all eval-* commands."""
1181 #@+others
1182 #@+node:ekr.20180328130835.1: *3* eval.Birth
1183 def __init__(self, c):
1184 """Ctor for EvalController class."""
1185 self.answers = []
1186 self.c = c
1187 self.d: Dict[str, Any] = {}
1188 self.globals_d: Dict[str, Any] = {'c': c, 'g': g, 'p': c.p}
1189 self.locals_d: Dict[str, Any] = {}
1190 self.legacy = c.config.getBool('legacy-eval', default=True)
1191 if g.app.ipk:
1192 # Use the IPython namespace.
1193 self.c.vs = g.app.ipk.namespace
1194 elif self.legacy:
1195 self.c.vs = self.d
1196 else:
1197 self.c.vs = self.globals_d
1198 # allow the auto-completer to complete in this namespace
1199 # Updated by do_exec.
1200 self.c.keyHandler.autoCompleter.namespaces.append(self.c.vs)
1201 self.last_result = None
1202 self.old_stderr = None
1203 self.old_stdout = None
1204 #@+node:ekr.20180328092221.1: *3* eval.Commands
1205 #@+node:ekr.20180328085426.2: *4* eval
1206 @eval_cmd("eval")
1207 def eval_command(self, event):
1208 #@+<< eval docstring >>
1209 #@+node:ekr.20180328100519.1: *5* << eval docstring >>
1210 """
1211 Execute the selected text, if any, or the line containing the cursor.
1213 Select next line of text.
1215 Tries hard to capture the result of from the last expression in the
1216 selected text::
1218 import datetime
1219 today = datetime.date.today()
1221 will capture the value of ``today`` even though the last line is a
1222 statement, not an expression.
1224 Stores results in ``c.vs['_last']`` for insertion
1225 into body by ``eval-last`` or ``eval-last-pretty``.
1227 Removes common indentation (``textwrap.dedent()``) before executing,
1228 allowing execution of indented code.
1230 ``g``, ``c``, and ``p`` are available to executing code, assignments
1231 are made in the ``c.vs`` namespace and persist for the life of ``c``.
1232 """
1233 #@-<< eval docstring >>
1234 c = self.c
1235 if c == event.get('c'):
1236 s = self.get_selected_lines()
1237 if self.legacy and s is None:
1238 return
1239 # Updates self.last_answer if there is exactly one answer.
1240 self.eval_text(s)
1241 #@+node:ekr.20180328085426.3: *4* eval-block
1242 @eval_cmd("eval-block")
1243 def eval_block(self, event):
1244 #@+<< eval-block docstring >>
1245 #@+node:ekr.20180328100415.1: *5* << eval-block docstring >>
1246 """
1247 In the body, "# >>>" marks the end of a code block, and "# <<<" marks
1248 the end of an output block. E.g.::
1250 a = 2
1251 # >>>
1252 4
1253 # <<<
1254 b = 2.0*a
1255 # >>>
1256 4.0
1257 # <<<
1259 ``eval-block`` evaluates the current code block, either the code block
1260 the cursor's in, or the code block preceding the output block the cursor's
1261 in. Subsequent output blocks are marked "# >>> *" to show they may need
1262 re-evaluation.
1264 Note: you don't really need to type the "# >>>" and "# <<<" markers
1265 because ``eval-block`` will add them as needed. So just type the
1266 first code block and run ``eval-block``.
1268 """
1269 #@-<< eval-block docstring >>
1270 c = self.c
1271 if c != event.get('c'):
1272 return
1273 pos = 0
1274 lines = []
1275 current_seen = False
1276 for current, source, output in self.get_blocks():
1277 lines.append(source)
1278 lines.append("# >>>" + (" *" if current_seen else ""))
1279 if current:
1280 old_log = c.frame.log.logCtrl.getAllText()
1281 self.eval_text(source)
1282 new_log = c.frame.log.logCtrl.getAllText()[len(old_log) :]
1283 lines.append(new_log.strip())
1284 if not self.legacy:
1285 if self.last_result:
1286 lines.append(self.last_result)
1287 pos = len('\n'.join(lines)) + 7
1288 current_seen = True
1289 else:
1290 lines.append(output)
1291 lines.append("# <<<")
1292 c.p.b = '\n'.join(lines) + '\n'
1293 c.frame.body.wrapper.setInsertPoint(pos)
1294 c.redraw()
1295 c.bodyWantsFocusNow()
1296 #@+node:ekr.20180328085426.5: *4* eval-last
1297 @eval_cmd("eval-last")
1298 def eval_last(self, event, text=None):
1299 """
1300 Insert the last result from ``eval``.
1302 Inserted as a string, so ``"1\n2\n3\n4"`` will cover four lines and
1303 insert no quotes, for ``repr()`` style insertion use ``last-pretty``.
1304 """
1305 c = self.c
1306 if c != event.get('c'):
1307 return
1308 if self.legacy:
1309 text = str(c.vs.get('_last'))
1310 else:
1311 if not text and not self.last_result:
1312 return
1313 if not text:
1314 text = str(self.last_result)
1315 w = c.frame.body.wrapper
1316 i = w.getInsertPoint()
1317 w.insert(i, text + '\n')
1318 w.setInsertPoint(i + len(text) + 1)
1319 c.setChanged()
1320 #@+node:ekr.20180328085426.6: *4* eval-last-pretty
1321 @eval_cmd("eval-last-pretty")
1322 def vs_last_pretty(self, event):
1323 """
1324 Insert the last result from ``eval``.
1326 Formatted by ``pprint.pformat()``, so ``"1\n2\n3\n4"`` will appear as
1327 '``"1\n2\n3\n4"``', see all ``last``.
1328 """
1329 c = self.c
1330 if c != event.get('c'):
1331 return
1332 if self.legacy:
1333 text = str(c.vs.get('_last'))
1334 else:
1335 text = self.last_result
1336 if text:
1337 text = pprint.pformat(text)
1338 self.eval_last(event, text=text)
1339 #@+node:ekr.20180328085426.4: *4* eval-replace
1340 @eval_cmd("eval-replace")
1341 def eval_replace(self, event):
1342 """
1343 Execute the selected text, if any.
1344 Undoably replace it with the result.
1345 """
1346 c = self.c
1347 if c != event.get('c'):
1348 return
1349 w = c.frame.body.wrapper
1350 s = w.getSelectedText()
1351 if not s.strip():
1352 g.es_print('no selected text')
1353 return
1354 self.eval_text(s)
1355 if self.legacy:
1356 last = c.vs.get('_last')
1357 else:
1358 last = self.last_result
1359 if not last:
1360 return
1361 s = pprint.pformat(last)
1362 i, j = w.getSelectionRange()
1363 new_text = c.p.b[:i] + s + c.p.b[j:]
1364 bunch = c.undoer.beforeChangeNodeContents(c.p)
1365 w.setAllText(new_text)
1366 c.p.b = new_text
1367 w.setInsertPoint(i + len(s))
1368 c.undoer.afterChangeNodeContents(c.p, 'Insert result', bunch)
1369 c.setChanged()
1370 #@+node:ekr.20180328151652.1: *3* eval.Helpers
1371 #@+node:ekr.20180328090830.1: *4* eval.eval_text & helpers
1372 def eval_text(self, s):
1373 """Evaluate string s."""
1374 s = textwrap.dedent(s)
1375 if not s.strip():
1376 return None
1377 self.redirect()
1378 if self.legacy:
1379 blocks = re.split('\n(?=[^\\s])', s)
1380 ans = self.old_exec(blocks, s)
1381 self.show_legacy_answer(ans, blocks)
1382 return ans # needed by mod_http
1383 self.new_exec(s)
1384 self.show_answers()
1385 self.unredirect()
1386 return None
1387 #@+node:ekr.20180329130626.1: *5* eval.new_exec
1388 def new_exec(self, s):
1389 try:
1390 self.answers = []
1391 self.locals_d = {}
1392 exec(s, self.globals_d, self.locals_d)
1393 for key in self.locals_d:
1394 val = self.locals_d.get(key)
1395 self.globals_d[key] = val
1396 self.answers.append((key, val),)
1397 if len(self.answers) == 1:
1398 key, val = self.answers[0]
1399 self.last_result = val
1400 else:
1401 self.last_result = None
1402 except Exception:
1403 g.es_exception()
1404 #@+node:ekr.20180329130623.1: *5* eval.old_exec
1405 def old_exec(self, blocks, txt):
1407 # pylint: disable=eval-used
1408 c = self.c
1409 leo_globals = {'c': c, 'g': g, 'p': c.p}
1410 all_done, ans = False, None
1411 try:
1412 # Execute all but the last 'block'
1413 exec('\n'.join(blocks[:-1]), leo_globals, c.vs) # Compatible with Python 3.x.
1414 all_done = False
1415 except SyntaxError:
1416 # Splitting the last block caused syntax error
1417 try:
1418 # Is the whole thing a single expression?
1419 ans = eval(txt, leo_globals, c.vs)
1420 except SyntaxError:
1421 try:
1422 exec(txt, leo_globals, c.vs)
1423 except Exception:
1424 g.es_exception()
1425 all_done = True # Either way, the last block will be used.
1426 if not all_done: # last block still needs using
1427 try:
1428 ans = eval(blocks[-1], leo_globals, c.vs)
1429 except SyntaxError:
1430 try:
1431 exec(txt, leo_globals, c.vs)
1432 except Exception:
1433 g.es_exception()
1434 return ans
1435 #@+node:ekr.20180328130526.1: *5* eval.redirect & unredirect
1436 def redirect(self):
1437 c = self.c
1438 if c.config.getBool('eval-redirect'):
1439 self.old_stderr = g.stdErrIsRedirected()
1440 self.old_stdout = g.stdOutIsRedirected()
1441 if not self.old_stderr:
1442 g.redirectStderr()
1443 if not self.old_stdout:
1444 g.redirectStdout()
1446 def unredirect(self):
1447 c = self.c
1448 if c.config.getBool('eval-redirect'):
1449 if not self.old_stderr:
1450 g.restoreStderr()
1451 if not self.old_stdout:
1452 g.restoreStdout()
1453 #@+node:ekr.20180328132748.1: *5* eval.show_answers
1454 def show_answers(self):
1455 """ Show all new values computed by do_exec."""
1456 if len(self.answers) > 1:
1457 g.es('')
1458 for answer in self.answers:
1459 key, val = answer
1460 g.es('%s = %s' % (key, val))
1461 #@+node:ekr.20180329154232.1: *5* eval.show_legacy_answer
1462 def show_legacy_answer(self, ans, blocks):
1464 cvs = self.c.vs
1465 if ans is None: # see if last block was a simple "var =" assignment
1466 key = blocks[-1].split('=', 1)[0].strip()
1467 if key in cvs:
1468 ans = cvs[key]
1469 if ans is None: # see if whole text was a simple /multi-line/ "var =" assignment
1470 key = blocks[0].split('=', 1)[0].strip()
1471 if key in cvs:
1472 ans = cvs[key]
1473 cvs['_last'] = ans
1474 if ans is not None:
1475 # annoying to echo 'None' to the log during line by line execution
1476 txt = str(ans)
1477 lines = txt.split('\n')
1478 if len(lines) > 10:
1479 txt = '\n'.join(lines[:5] + ['<snip>'] + lines[-5:])
1480 if len(txt) > 500:
1481 txt = txt[:500] + ' <truncated>'
1482 g.es(txt)
1483 return ans
1484 #@+node:ekr.20180329125626.1: *4* eval.exec_then_eval (not used yet)
1485 def exec_then_eval(self, code, ns):
1486 # From Milan Melena.
1487 import ast
1488 block = ast.parse(code, mode='exec')
1489 if block.body and isinstance(block.body[-1], ast.Expr):
1490 last = ast.Expression(block.body.pop().value)
1491 exec(compile(block, '<string>', mode='exec'), ns)
1492 # pylint: disable=eval-used
1493 return eval(compile(last, '<string>', mode='eval'), ns)
1494 exec(compile(block, '<string>', mode='exec'), ns)
1495 return ""
1496 #@+node:tbrown.20170516194332.1: *4* eval.get_blocks
1497 def get_blocks(self):
1498 """get_blocks - iterate code blocks
1500 :return: (current, source, output)
1501 :rtype: (bool, str, str)
1502 """
1503 c = self.c
1504 pos = c.frame.body.wrapper.getInsertPoint()
1505 chrs = 0
1506 lines = c.p.b.split('\n')
1507 block: Dict[str, List] = {'source': [], 'output': []}
1508 reading = 'source'
1509 seeking_current = True
1510 # if the last non-blank line isn't the end of a possibly empty
1511 # output block, make it one
1512 if [i for i in lines if i.strip()][-1] != "# <<<":
1513 lines.append("# <<<")
1514 while lines:
1515 line = lines.pop(0)
1516 chrs += len(line) + 1
1517 if line.startswith("# >>>"):
1518 reading = 'output'
1519 continue
1520 if line.startswith("# <<<"):
1521 current = seeking_current and (chrs >= pos + 1)
1522 if current:
1523 seeking_current = False
1524 yield current, '\n'.join(block['source']), '\n'.join(block['output'])
1525 block = {'source': [], 'output': []}
1526 reading = 'source'
1527 continue
1528 block[reading].append(line)
1529 #@+node:ekr.20180328145035.1: *4* eval.get_selected_lines
1530 def get_selected_lines(self):
1532 c, p = self.c, self.c.p
1533 w = c.frame.body.wrapper
1534 body = w.getAllText()
1535 i = w.getInsertPoint()
1536 if w.hasSelection():
1537 if self.legacy:
1538 i1, i2 = w.getSelectionRange()
1539 else:
1540 j, k = w.getSelectionRange()
1541 i1, junk = g.getLine(body, j)
1542 junk, i2 = g.getLine(body, k)
1543 s = body[i1:i2]
1544 else:
1545 if self.legacy:
1546 k = w.getInsertPoint()
1547 junk, i2 = g.getLine(body, k)
1548 w.setSelectionRange(k, i2)
1549 return None
1550 i1, i2 = g.getLine(body, i)
1551 s = body[i1:i2].strip()
1552 # Select next line for next eval.
1553 if self.legacy:
1554 i = j = i2
1555 j += 1
1556 while j < len(body) and body[j] != '\n':
1557 j += 1
1558 w.setSelectionRange(i, j)
1559 else:
1560 if not body.endswith('\n'):
1561 if i >= len(p.b):
1562 i2 += 1
1563 p.b = p.b + '\n'
1564 ins = min(len(p.b), i2)
1565 w.setSelectionRange(i1, ins, insert=ins, s=p.b)
1566 return s
1567 #@-others
1568#@-others
1569#@@language python
1570#@@tabwidth -4
1571#@-leo