Coverage for C:\leo.repo\leo-editor\leo\plugins\mod_scripting.py : 10%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#@+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
337 # A QButton.
338 self.buttonText = buttonText
339 # The text of the button.
340 self.c = c
341 # A Commander.
342 self.controller = controller
343 # A ScriptingController instance.
344 self.gnx = gnx
345 # Set if the script is defined in the local .leo file.
346 self.script = script
347 # Set if the script is found defined in myLeoSettings.leo or leoSettings.leo
348 self.source_c = c
349 # For GetArgs.command_source.
350 self.__doc__ = docstring
351 # The docstring for this callback for g.getDocStringForFunction.
352 #@+node:ekr.20141031053508.10: *3* __call__ (AtButtonCallback)
353 def __call__(self, event=None):
354 """AtButtonCallbgack.__call__. The callback for @button nodes."""
355 self.execute_script()
356 #@+node:ekr.20141031053508.13: *3* __repr__ (AtButtonCallback)
357 def __repr__(self):
358 """AtButtonCallback.__repr__."""
359 c = self.c
360 return 'AtButtonCallback %s gnx: %s len(script) %s' % (
361 c.shortFileName(), self.gnx, len(self.script or ''))
362 #@+node:ekr.20150512041758.1: *3* __getattr__ (AtButtonCallback)
363 def __getattr__(self, attr):
364 """AtButtonCallback.__getattr__. Implement __name__."""
365 if attr == '__name__':
366 return 'AtButtonCallback: %s' % self.gnx
367 raise AttributeError
368 # Returning None is not correct.
369 #@+node:ekr.20170203043042.1: *3* AtButtonCallback.execute_script & helper
370 def execute_script(self):
371 """Execute the script associated with this button."""
372 script = self.find_script()
373 if script:
374 self.controller.executeScriptFromButton(
375 b=self.b,
376 buttonText=self.buttonText,
377 p=None,
378 script_gnx=self.gnx,
379 script=script,
380 )
381 #@+node:ekr.20180313171043.1: *4* AtButtonCallback.find_script
382 def find_script(self):
384 gnx = self.gnx
385 # First, search self.c for the gnx.
386 for p in self.c.all_positions():
387 if p.gnx == gnx:
388 script = self.controller.getScript(p)
389 return script
390 # See if myLeoSettings.leo is open.
391 for c in g.app.commanders():
392 if c.shortFileName().endswith('myLeoSettings.leo'):
393 break
394 else:
395 c = None
396 if c:
397 # Search myLeoSettings.leo file for the gnx.
398 for p in c.all_positions():
399 if p.gnx == gnx:
400 script = self.controller.getScript(p)
401 return script
402 return self.script
403 #@-others
404#@+node:ekr.20060328125248.6: ** class ScriptingController
405class ScriptingController:
406 """A class defining scripting commands."""
407 #@+others
408 #@+node:ekr.20060328125248.7: *3* sc.ctor
409 def __init__(self, c, iconBar=None):
410 self.c = c
411 self.gui = c.frame.gui
412 getBool = c.config.getBool
413 self.scanned = False
414 kind = c.config.getString('debugger-kind') or 'idle'
415 self.buttonsDict = {} # Keys are buttons, values are button names (strings).
416 self.debuggerKind = kind.lower()
417 self.atButtonNodes = getBool('scripting-at-button-nodes')
418 # True: adds a button for every @button node.
419 self.atCommandsNodes = getBool('scripting-at-commands-nodes')
420 # True: define a minibuffer command for every @command node.
421 self.atRclickNodes = getBool('scripting-at-rclick-nodes')
422 # True: define a minibuffer command for every @rclick node.
423 self.atPluginNodes = getBool('scripting-at-plugin-nodes')
424 # True: dynamically loads plugins in @plugin nodes when a window is created.
425 self.atScriptNodes = getBool('scripting-at-script-nodes')
426 # True: dynamically executes script in @script nodes when a window is created.
427 # DANGEROUS!
428 # Do not allow this setting to be changed in local (non-settings) .leo files.
429 if self.atScriptNodes and c.config.isLocalSetting('scripting-at-script-nodes', 'bool'):
430 g.issueSecurityWarning('@bool scripting-at-script-nodes')
431 # Restore the value in myLeoSettings.leo
432 val = g.app.config.valueInMyLeoSettings('scripting-at-script-nodes')
433 if val is None:
434 val = False
435 g.es('Restoring value to', val, color='red')
436 self.atScriptNodes = val
437 self.createDebugButton = getBool('scripting-create-debug-button')
438 # True: create Debug Script button.
439 self.createRunScriptButton = getBool('scripting-create-run-script-button')
440 # True: create Run Script button.
441 self.createScriptButtonButton = getBool('scripting-create-script-button-button')
442 # True: create Script Button button.
443 self.maxButtonSize = c.config.getInt('scripting-max-button-size') or 18
444 # Maximum length of button names.
445 if not iconBar:
446 self.iconBar = c.frame.getIconBarObject()
447 else:
448 self.iconBar = iconBar
449 self.seen = set()
450 # Fix bug 74: problems with @button if defined in myLeoSettings.leo
451 # Set of gnx's (not vnodes!) that created buttons or commands.
452 #@+node:ekr.20150401113822.1: *3* sc.Callbacks
453 #@+node:ekr.20060328125248.23: *4* sc.addScriptButtonCommand
454 def addScriptButtonCommand(self, event=None):
455 """Called when the user presses the 'script-button' button or executes the script-button command."""
456 c = self.c
457 p = c.p
458 h = p.h
459 buttonText = self.getButtonText(h)
460 shortcut = self.getShortcut(h)
461 statusLine = "Run Script: %s" % buttonText
462 if shortcut:
463 statusLine = statusLine + " @key=" + shortcut
464 self.createLocalAtButtonHelper(p, h, statusLine, kind='script-button', verbose=True)
465 c.bodyWantsFocus()
466 #@+node:ekr.20060522105937.1: *4* sc.runDebugScriptCommand
467 def runDebugScriptCommand(self, event=None):
468 """Called when user presses the 'debug-script' button or executes the debug-script command."""
469 c = self.c
470 p = c.p
471 script = g.getScript(c, p, useSelectedText=True, useSentinels=False)
472 if script:
473 #@+<< set debugging if debugger is active >>
474 #@+node:ekr.20060523084441: *5* << set debugging if debugger is active >>
475 g.trace(self.debuggerKind)
476 if self.debuggerKind == 'winpdb':
477 try:
478 import rpdb2
479 debugging = rpdb2.g_debugger is not None
480 except ImportError:
481 debugging = False
482 elif self.debuggerKind == 'idle':
483 # import idlelib.Debugger.py as Debugger
484 # debugging = Debugger.interacting
485 debugging = True
486 else:
487 debugging = False
488 #@-<< set debugging if debugger is active >>
489 if debugging:
490 #@+<< create leoScriptModule >>
491 #@+node:ekr.20060524073716: *5* << create leoScriptModule >> (mod_scripting.py)
492 target = g.os_path_join(g.app.loadDir, 'leoScriptModule.py')
493 with open(target, 'w') as f:
494 f.write('# A module holding the script to be debugged.\n')
495 if self.debuggerKind == 'idle':
496 # This works, but uses the lame pdb debugger.
497 f.write('import pdb\n')
498 f.write('pdb.set_trace() # Hard breakpoint.\n')
499 elif self.debuggerKind == 'winpdb':
500 f.write('import rpdb2\n')
501 f.write('if rpdb2.g_debugger is not None: # don\'t hang if the debugger isn\'t running.\n')
502 f.write(' rpdb2.start_embedded_debugger(pwd="",fAllowUnencrypted=True) # Hard breakpoint.\n')
503 # f.write('# Remove all previous variables.\n')
504 f.write('# Predefine c, g and p.\n')
505 f.write('from leo.core import leoGlobals as g\n')
506 f.write('c = g.app.scriptDict.get("c")\n')
507 f.write('script_gnx = g.app.scriptDict.get("script_gnx")\n')
508 f.write('p = c.p\n')
509 f.write('# Actual script starts here.\n')
510 f.write(script + '\n')
511 #@-<< create leoScriptModule >>
512 # pylint: disable=no-name-in-module
513 g.app.scriptDict['c'] = c
514 g.app.scriptDict = {'script_gnx': p.gnx}
515 if 'leoScriptModule' in sys.modules.keys():
516 del sys.modules['leoScriptModule'] # Essential.
517 # pylint: disable=import-error
518 # This *will* exist.
519 from leo.core import leoScriptModule
520 assert leoScriptModule # for pyflakes.
521 else:
522 g.error('No debugger active')
523 c.bodyWantsFocus()
524 #@+node:ekr.20060328125248.21: *4* sc.runScriptCommand
525 def runScriptCommand(self, event=None):
526 """Called when user presses the 'run-script' button or executes the run-script command."""
527 c, p = self.c, self.c.p
528 args = self.getArgs(p)
529 g.app.scriptDict = {'script_gnx': p.gnx}
530 c.executeScript(args=args, p=p, useSelectedText=True, silent=True)
531 if 0:
532 # Do not assume the script will want to remain in this commander.
533 c.bodyWantsFocus()
534 #@+node:ekr.20060328125248.8: *3* sc.createAllButtons
535 def createAllButtons(self):
536 """Scan for @button, @rclick, @command, @plugin and @script nodes."""
537 c = self.c
538 if self.scanned:
539 return # Defensive.
540 self.scanned = True
541 #
542 # First, create standard buttons.
543 if self.createRunScriptButton:
544 self.createRunScriptIconButton()
545 if self.createScriptButtonButton:
546 self.createScriptButtonIconButton()
547 if self.createDebugButton:
548 self.createDebugIconButton()
549 #
550 # Next, create common buttons and commands.
551 self.createCommonButtons()
552 self.createCommonCommands()
553 #
554 # Handle all other nodes.
555 d = {
556 'button': self.handleAtButtonNode,
557 'command': self.handleAtCommandNode,
558 'plugin': self.handleAtPluginNode,
559 'rclick': self.handleAtRclickNode,
560 'script': self.handleAtScriptNode,
561 }
562 pattern = re.compile(r'^@(button|command|plugin|rclick|script)\b')
563 p = c.rootPosition()
564 while p:
565 gnx = p.v.gnx
566 if p.isAtIgnoreNode():
567 p.moveToNodeAfterTree()
568 elif gnx in self.seen:
569 # #657
570 # if g.match_word(p.h, 0, '@rclick'):
571 if p.h.startswith('@rlick'):
572 self.handleAtRclickNode(p)
573 p.moveToThreadNext()
574 else:
575 self.seen.add(gnx)
576 m = pattern.match(p.h)
577 if m:
578 func = d.get(m.group(1))
579 func(p)
580 p.moveToThreadNext()
581 #@+node:ekr.20060328125248.24: *3* sc.createLocalAtButtonHelper
582 def createLocalAtButtonHelper(self, p, h, statusLine,
583 kind='at-button',
584 verbose=True,
585 ):
586 """Create a button for a local @button node."""
587 c = self.c
588 buttonText = self.cleanButtonText(h, minimal=True)
589 args = self.getArgs(p)
590 # We must define the callback *after* defining b,
591 # so set both command and shortcut to None here.
592 bg = self.getColor(h)
593 b = self.createIconButton(
594 args=args,
595 text=h,
596 command=None,
597 statusLine=statusLine,
598 kind=kind,
599 bg=bg,
600 )
601 if not b:
602 return None
603 # Now that b is defined we can define the callback.
604 # Yes, executeScriptFromButton *does* use b (to delete b if requested by the script).
605 docstring = g.getDocString(p.b).strip()
606 cb = AtButtonCallback(
607 controller=self,
608 b=b,
609 c=c,
610 buttonText=buttonText,
611 docstring=docstring,
612 gnx=p.v.gnx,
613 script=None,
614 )
615 self.iconBar.setCommandForButton(
616 button=b,
617 command=cb, # This encapsulates the script.
618 command_p=p and p.copy(), # This does exist.
619 controller=self,
620 gnx=p and p.gnx,
621 script=None,
622 )
623 # At last we can define the command and use the shortcut.
624 # registerAllCommands recomputes the shortcut.
625 self.registerAllCommands(
626 args=self.getArgs(p),
627 func=cb,
628 h=h,
629 pane='button',
630 source_c=p.v.context,
631 tag='local @button')
632 return b
633 #@+node:ekr.20060328125248.17: *3* sc.createIconButton (creates all buttons)
634 def createIconButton(self, args, text, command, statusLine, bg=None, kind=None):
635 """
636 Create one icon button.
637 This method creates all scripting icon buttons.
639 - Creates the actual button and its balloon.
640 - Adds the button to buttonsDict.
641 - Registers command with the shortcut.
642 - Creates x amd delete-x-button commands, where x is the cleaned button name.
643 - Binds a right-click in the button to a callback that deletes the button.
644 """
645 c = self.c
646 # Create the button and add it to the buttons dict.
647 commandName = self.cleanButtonText(text)
648 # Truncate only the text of the button, not the command name.
649 truncatedText = self.truncateButtonText(commandName)
650 if not truncatedText.strip():
651 g.error('%s ignored: no cleaned text' % (text.strip() or ''))
652 return None
653 # Command may be None.
654 b = self.iconBar.add(text=truncatedText, command=command, kind=kind)
655 if not b:
656 return None
657 self.setButtonColor(b, bg)
658 self.buttonsDict[b] = truncatedText
659 if statusLine:
660 self.createBalloon(b, statusLine)
661 if command:
662 self.registerAllCommands(
663 args=args,
664 func=command,
665 h=text,
666 pane='button',
667 source_c=c,
668 tag='icon button')
670 def deleteButtonCallback(event=None, self=self, b=b):
671 self.deleteButton(b, event=event)
672 # Register the delete-x-button command.
674 deleteCommandName = 'delete-%s-button' % commandName
675 c.k.registerCommand(
676 # allowBinding=True,
677 commandName=deleteCommandName,
678 func=deleteButtonCallback,
679 pane='button',
680 shortcut=None,
681 )
682 # Reporting this command is way too annoying.
683 return b
684 #@+node:ekr.20060328125248.28: *3* sc.executeScriptFromButton
685 def executeScriptFromButton(self, b, buttonText, p, script, script_gnx=None):
686 """Execute an @button script in p.b or script."""
687 c = self.c
688 if c.disableCommandsMessage:
689 g.blue(c.disableCommandsMessage)
690 return
691 if not p and not script:
692 g.trace('can not happen: no p and no script')
693 return
694 g.app.scriptDict = {'script_gnx': script_gnx}
695 args = self.getArgs(p)
696 if not script:
697 script = self.getScript(p)
698 c.executeScript(args=args, p=p, script=script, silent=True)
699 # Remove the button if the script asks to be removed.
700 if g.app.scriptDict.get('removeMe'):
701 g.es("Removing '%s' button at its request" % buttonText)
702 self.deleteButton(b)
703 # Do *not* set focus here: the script may have changed the focus.
704 # c.bodyWantsFocus()
705 #@+node:ekr.20130912061655.11294: *3* sc.open_gnx
706 def open_gnx(self, c, gnx):
707 """
708 Find the node with the given gnx in c, myLeoSettings.leo and leoSettings.leo.
709 If found, open the tab/outline and select the specified node.
710 Return c,p of the found node.
712 Called only from a callback in QtIconBarClass.setCommandForButton.
713 """
714 if not gnx:
715 g.trace('can not happen: no gnx')
716 # First, look in commander c.
717 for p2 in c.all_positions():
718 if p2.gnx == gnx:
719 return c, p2
720 # Fix bug 74: problems with @button if defined in myLeoSettings.leo.
721 for f in (c.openMyLeoSettings, c.openLeoSettings):
722 c2 = f() # Open the settings file.
723 if c2:
724 for p2 in c2.all_positions():
725 if p2.gnx == gnx:
726 return c2, p2
727 c2.close()
728 # Fix bug 92: restore the previously selected tab.
729 if hasattr(c.frame, 'top'):
730 c.frame.top.leo_master.select(c)
731 return None, None # 2017/02/02.
732 #@+node:ekr.20150401130207.1: *3* sc.Scripts, common
733 # Important: common @button and @command nodes do **not** update dynamically!
734 #@+node:ekr.20080312071248.1: *4* sc.createCommonButtons
735 def createCommonButtons(self):
736 """Handle all global @button nodes."""
737 c = self.c
738 buttons = c.config.getButtons() or []
739 for z in buttons:
740 # #2011
741 p, script, rclicks = z
742 gnx = p.v.gnx
743 if gnx not in self.seen:
744 self.seen.add(gnx)
745 script = self.getScript(p)
746 self.createCommonButton(p, script, rclicks)
747 #@+node:ekr.20070926084600: *4* sc.createCommonButton (common @button)
748 def createCommonButton(self, p, script, rclicks=None):
749 """
750 Create a button in the icon area for a common @button node in an @setting
751 tree. Binds button presses to a callback that executes the script.
753 Important: Common @button and @command scripts now *do* update
754 dynamically provided that myLeoSettings.leo is open. Otherwise the
755 callback executes the static script.
757 See https://github.com/leo-editor/leo-editor/issues/171
758 """
759 c = self.c
760 gnx = p.gnx
761 args = self.getArgs(p)
762 # Fix bug #74: problems with @button if defined in myLeoSettings.leo
763 docstring = g.getDocString(p.b).strip()
764 statusLine = docstring or 'Global script button'
765 shortcut = self.getShortcut(p.h)
766 # Get the shortcut from the @key field in the headline.
767 if shortcut:
768 statusLine = '%s = %s' % (statusLine.rstrip(), shortcut)
769 # We must define the callback *after* defining b,
770 # so set both command and shortcut to None here.
771 bg = self.getColor(p.h) # #2024
772 b = self.createIconButton(
773 args=args,
774 bg=bg, # #2024
775 text=p.h,
776 command=None,
777 statusLine=statusLine,
778 kind='at-button',
779 )
780 if not b:
781 return
782 # Now that b is defined we can define the callback.
783 # Yes, the callback *does* use b (to delete b if requested by the script).
784 buttonText = self.cleanButtonText(p.h)
785 cb = AtButtonCallback(
786 b=b,
787 buttonText=buttonText,
788 c=c,
789 controller=self,
790 docstring=docstring,
791 gnx=gnx,
792 # tag:#367: the gnx is needed for the Goto Script command.
793 # 2018/03/13: Use gnx to search myLeoSettings.leo if it is open.
794 script=script,
795 )
796 # Now patch the button.
797 self.iconBar.setCommandForButton(
798 button=b,
799 command=cb, # This encapsulates the script.
800 command_p=p and p.copy(), # tag:#567
801 controller=self,
802 gnx=gnx, # For the find-button function.
803 script=script,
804 )
805 self.handleRclicks(rclicks)
806 # At last we can define the command.
807 self.registerAllCommands(
808 args=args,
809 func=cb,
810 h=p.h,
811 pane='button',
812 source_c=p.v.context,
813 tag='@button')
814 #@+node:ekr.20080312071248.2: *4* sc.createCommonCommands
815 def createCommonCommands(self):
816 """Handle all global @command nodes."""
817 c = self.c
818 aList = c.config.getCommands() or []
819 for z in aList:
820 p, script = z
821 gnx = p.v.gnx
822 if gnx not in self.seen:
823 self.seen.add(gnx)
824 script = self.getScript(p)
825 self.createCommonCommand(p, script)
826 #@+node:ekr.20150401130818.1: *4* sc.createCommonCommand (common @command)
827 def createCommonCommand(self, p, script):
828 """
829 Handle a single @command node.
831 Important: Common @button and @command scripts now *do* update
832 dynamically provided that myLeoSettings.leo is open. Otherwise the
833 callback executes the static script.
835 See https://github.com/leo-editor/leo-editor/issues/171
836 """
837 c = self.c
838 args = self.getArgs(p)
839 commonCommandCallback = AtButtonCallback(
840 b=None,
841 buttonText=None,
842 c=c,
843 controller=self,
844 docstring=g.getDocString(p.b).strip(),
845 gnx=p.v.gnx, # Used to search myLeoSettings.leo if it is open.
846 script=script, # Fallback when myLeoSettings.leo is not open.
847 )
848 self.registerAllCommands(
849 args=args,
850 func=commonCommandCallback,
851 h=p.h,
852 pane='button', # Fix bug 416: use 'button', NOT 'command', and NOT 'all'
853 source_c=p.v.context,
854 tag='global @command',
855 )
856 #@+node:ekr.20150401130313.1: *3* sc.Scripts, individual
857 #@+node:ekr.20060328125248.12: *4* sc.handleAtButtonNode @button
858 def handleAtButtonNode(self, p):
859 """
860 Create a button in the icon area for an @button node.
862 An optional @key=shortcut defines a shortcut that is bound to the button's script.
863 The @key=shortcut does not appear in the button's name, but
864 it *does* appear in the statutus line shown when the mouse moves over the button.
866 An optional @color=colorname defines a color for the button's background. It does
867 not appear in the status line nor the button name.
868 """
869 h = p.h
870 shortcut = self.getShortcut(h)
871 docstring = g.getDocString(p.b).strip()
872 statusLine = docstring if docstring else 'Local script button'
873 if shortcut:
874 statusLine = '%s = %s' % (statusLine, shortcut)
875 g.app.config.atLocalButtonsList.append(p.copy())
876 # This helper is also called by the script-button callback.
877 self.createLocalAtButtonHelper(p, h, statusLine, verbose=False)
878 #@+node:ekr.20060328125248.10: *4* sc.handleAtCommandNode @command
879 def handleAtCommandNode(self, p):
880 """Handle @command name [@key[=]shortcut]."""
881 c = self.c
882 if not p.h.strip():
883 return
884 args = self.getArgs(p)
886 def atCommandCallback(event=None, args=args, c=c, p=p.copy()):
887 # pylint: disable=dangerous-default-value
888 c.executeScript(args=args, p=p, silent=True)
890 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252
891 # Minibuffer commands created by mod_scripting.py have no docstrings
893 atCommandCallback.__doc__ = g.getDocString(p.b).strip()
894 self.registerAllCommands(
895 args=args,
896 func=atCommandCallback,
897 h=p.h,
898 pane='button', # Fix # 416.
899 source_c=p.v.context,
900 tag='local @command')
901 g.app.config.atLocalCommandsList.append(p.copy())
902 #@+node:ekr.20060328125248.13: *4* sc.handleAtPluginNode @plugin
903 def handleAtPluginNode(self, p):
904 """Handle @plugin nodes."""
905 tag = "@plugin"
906 h = p.h
907 assert g.match(h, 0, tag)
908 # Get the name of the module.
909 moduleOrFileName = h[len(tag) :].strip()
910 if not self.atPluginNodes:
911 g.warning("disabled @plugin: %s" % (moduleOrFileName))
912 # elif theFile in g.app.loadedPlugins:
913 elif g.pluginIsLoaded(moduleOrFileName):
914 g.warning("plugin already loaded: %s" % (moduleOrFileName))
915 else:
916 g.loadOnePlugin(moduleOrFileName)
917 #@+node:peckj.20131113130420.6851: *4* sc.handleAtRclickNode @rclick
918 def handleAtRclickNode(self, p):
919 """Handle @rclick name [@key[=]shortcut]."""
920 c = self.c
921 if not p.h.strip():
922 return
923 args = self.getArgs(p)
925 def atCommandCallback(event=None, args=args, c=c, p=p.copy()):
926 # pylint: disable=dangerous-default-value
927 c.executeScript(args=args, p=p, silent=True)
928 if p.b.strip():
929 self.registerAllCommands(
930 args=args,
931 func=atCommandCallback,
932 h=p.h,
933 pane='all',
934 source_c=p.v.context,
935 tag='local @rclick')
936 g.app.config.atLocalCommandsList.append(p.copy())
937 #@+node:vitalije.20180224113123.1: *4* sc.handleRclicks
938 def handleRclicks(self, rclicks):
939 def handlerc(rc):
940 if rc.children:
941 for i in rc.children:
942 handlerc(i)
943 else:
944 self.handleAtRclickNode(rc.position)
945 for rc in rclicks:
946 handlerc(rc)
948 #@+node:ekr.20060328125248.14: *4* sc.handleAtScriptNode @script
949 def handleAtScriptNode(self, p):
950 """Handle @script nodes."""
951 c = self.c
952 tag = "@script"
953 assert g.match(p.h, 0, tag)
954 name = p.h[len(tag) :].strip()
955 args = self.getArgs(p)
956 if self.atScriptNodes:
957 g.blue("executing script %s" % (name))
958 c.executeScript(args=args, p=p, useSelectedText=False, silent=True)
959 else:
960 g.warning("disabled @script: %s" % (name))
961 if 0:
962 # Do not assume the script will want to remain in this commander.
963 c.bodyWantsFocus()
964 #@+node:ekr.20150401125747.1: *3* sc.Standard buttons
965 #@+node:ekr.20060522105937: *4* sc.createDebugIconButton 'debug-script'
966 def createDebugIconButton(self):
967 """Create the 'debug-script' button and the debug-script command."""
968 self.createIconButton(
969 args=None,
970 text='debug-script',
971 command=self.runDebugScriptCommand,
972 statusLine='Debug script in selected node',
973 kind='debug-script')
974 #@+node:ekr.20060328125248.20: *4* sc.createRunScriptIconButton 'run-script'
975 def createRunScriptIconButton(self):
976 """Create the 'run-script' button and the run-script command."""
977 self.createIconButton(
978 args=None,
979 text='run-script',
980 command=self.runScriptCommand,
981 statusLine='Run script in selected node',
982 kind='run-script',
983 )
984 #@+node:ekr.20060328125248.22: *4* sc.createScriptButtonIconButton 'script-button'
985 def createScriptButtonIconButton(self):
986 """Create the 'script-button' button and the script-button command."""
987 self.createIconButton(
988 args=None,
989 text='script-button',
990 command=self.addScriptButtonCommand,
991 statusLine='Make script button from selected node',
992 kind="script-button-button")
993 #@+node:ekr.20061014075212: *3* sc.Utils
994 #@+node:ekr.20060929135558: *4* sc.cleanButtonText
995 def cleanButtonText(self, s, minimal=False):
996 """
997 Clean the text following @button or @command so
998 that it is a valid name of a minibuffer command.
999 """
1000 # #1121: Don't lowercase anything.
1001 if minimal:
1002 return s.replace(' ', '-').strip('-')
1003 for tag in ('@key', '@args', '@color',):
1004 i = s.find(tag)
1005 if i > -1:
1006 j = s.find('@', i + 1)
1007 if i < j:
1008 s = s[:i] + s[j:]
1009 else:
1010 s = s[:i]
1011 s = s.strip()
1012 return s.replace(' ', '-').strip('-')
1013 #@+node:ekr.20060522104419.1: *4* sc.createBalloon (gui-dependent)
1014 def createBalloon(self, w, label):
1015 'Create a balloon for a widget.'
1016 if g.app.gui.guiName().startswith('qt'):
1017 # w is a leoIconBarButton.
1018 if hasattr(w, 'button'):
1019 w.button.setToolTip(label)
1020 #@+node:ekr.20060328125248.26: *4* sc.deleteButton
1021 def deleteButton(self, button, **kw):
1022 """Delete the given button.
1023 This is called from callbacks, it is not a callback."""
1024 w = button
1025 if button and self.buttonsDict.get(w):
1026 del self.buttonsDict[w]
1027 self.iconBar.deleteButton(w)
1028 self.c.bodyWantsFocus()
1029 #@+node:ekr.20080813064908.4: *4* sc.getArgs
1030 def getArgs(self, p):
1031 """Return the list of @args field of p.h."""
1032 args: List[str] = []
1033 if not p:
1034 return args
1035 h, tag = p.h, '@args'
1036 i = h.find(tag)
1037 if i > -1:
1038 j = g.skip_ws(h, i + len(tag))
1039 # 2011/10/16: Make '=' sign optional.
1040 if g.match(h, j, '='):
1041 j += 1
1042 if 0:
1043 s = h[j + 1 :].strip()
1044 else: # new logic 1/3/2014 Jake Peck
1045 k = h.find('@', j + 1)
1046 if k == -1:
1047 k = len(h)
1048 s = h[j:k].strip()
1049 args = s.split(',')
1050 args = [z.strip() for z in args]
1051 # if args: g.trace(args)
1052 return args
1053 #@+node:ekr.20060328125248.15: *4* sc.getButtonText
1054 def getButtonText(self, h):
1055 """Returns the button text found in the given headline string"""
1056 tag = "@button"
1057 if g.match_word(h, 0, tag):
1058 h = h[len(tag) :].strip()
1059 for tag in ('@key', '@args', '@color',):
1060 i = h.find(tag)
1061 if i > -1:
1062 j = h.find('@', i + 1)
1063 if i < j:
1064 h = h[:i] + h[j + 1 :]
1065 else:
1066 h = h[:i]
1067 h = h.strip()
1068 buttonText = h
1069 # fullButtonText = buttonText
1070 return buttonText
1071 #@+node:peckj.20140103101946.10404: *4* sc.getColor
1072 def getColor(self, h):
1073 """Returns the background color from the given headline string"""
1074 color = None
1075 tag = '@color'
1076 i = h.find(tag)
1077 if i > -1:
1078 j = g.skip_ws(h, i + len(tag))
1079 if g.match(h, j, '='):
1080 j += 1
1081 k = h.find('@', j + 1)
1082 if k == -1:
1083 k = len(h)
1084 color = h[j:k].strip()
1085 return color
1086 #@+node:ekr.20060328125248.16: *4* sc.getShortcut
1087 def getShortcut(self, h):
1088 """Return the keyboard shortcut from the given headline string"""
1089 shortcut = None
1090 i = h.find('@key')
1091 if i > -1:
1092 j = g.skip_ws(h, i + len('@key'))
1093 if g.match(h, j, '='):
1094 j += 1
1095 if 0:
1096 shortcut = h[j:].strip()
1097 else: # new logic 1/3/2014 Jake Peck
1098 k = h.find('@', j + 1)
1099 if k == -1:
1100 k = len(h)
1101 shortcut = h[j:k].strip()
1102 return shortcut
1103 #@+node:ekr.20150402042350.1: *4* sc.getScript
1104 def getScript(self, p):
1105 """Return the script composed from p and its descendants."""
1106 return (
1107 g.getScript(self.c, p,
1108 useSelectedText=False,
1109 forcePythonSentinels=True,
1110 useSentinels=True,
1111 ))
1112 #@+node:ekr.20120301114648.9932: *4* sc.registerAllCommands
1113 def registerAllCommands(self, args, func, h, pane, source_c=None, tag=None):
1114 """Register @button <name> and @rclick <name> and <name>"""
1115 c, k = self.c, self.c.k
1116 trace = False and not g.unitTesting
1117 shortcut = self.getShortcut(h) or ''
1118 commandName = self.cleanButtonText(h)
1119 if trace and not g.isascii(commandName):
1120 g.trace(commandName)
1121 # Register the original function.
1122 k.registerCommand(
1123 allowBinding=True,
1124 commandName=commandName,
1125 func=func,
1126 pane=pane,
1127 shortcut=shortcut,
1128 )
1130 # 2013/11/13 Jake Peck:
1131 # include '@rclick-' in list of tags
1132 for prefix in ('@button-', '@command-', '@rclick-'):
1133 if commandName.startswith(prefix):
1134 commandName2 = commandName[len(prefix) :].strip()
1135 # Create a *second* func, to avoid collision in c.commandsDict.
1137 def registerAllCommandsCallback(event=None, func=func):
1138 func()
1140 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252
1141 # Minibuffer commands created by mod_scripting.py have no docstrings.
1142 registerAllCommandsCallback.__doc__ = func.__doc__
1143 # Make sure we never redefine an existing commandName.
1144 if commandName2 in c.commandsDict:
1145 # A warning here would be annoying.
1146 if trace:
1147 g.trace('Already in commandsDict: %r' % commandName2)
1148 else:
1149 k.registerCommand(
1150 commandName=commandName2,
1151 func=registerAllCommandsCallback,
1152 pane=pane,
1153 shortcut=None
1154 )
1155 #@+node:ekr.20150402021505.1: *4* sc.setButtonColor
1156 def setButtonColor(self, b, bg):
1157 """Set the background color of Qt button b to bg."""
1158 if not bg:
1159 return
1160 if not bg.startswith('#'):
1161 bg0 = bg
1162 d = leoColor.leo_color_database
1163 bg = d.get(bg.lower())
1164 if not bg:
1165 g.trace('bad color? %s' % bg0)
1166 return
1167 try:
1168 b.button.setStyleSheet("QPushButton{background-color: %s}" % (bg))
1169 except Exception:
1170 # g.es_exception()
1171 pass # Might not be a valid color.
1172 #@+node:ekr.20061015125212: *4* sc.truncateButtonText
1173 def truncateButtonText(self, s):
1174 # 2011/10/16: Remove @button here only.
1175 i = 0
1176 while g.match(s, i, '@'):
1177 i += 1
1178 if g.match_word(s, i, 'button'):
1179 i += 6
1180 s = s[i:]
1181 if self.maxButtonSize > 10:
1182 s = s[:self.maxButtonSize]
1183 if s.endswith('-'):
1184 s = s[:-1]
1185 s = s.strip('-')
1186 return s.strip()
1187 #@-others
1189scriptingController = ScriptingController
1190#@+node:ekr.20180328085038.1: ** class EvalController
1191class EvalController:
1192 """A class defining all eval-* commands."""
1193 #@+others
1194 #@+node:ekr.20180328130835.1: *3* eval.Birth
1195 def __init__(self, c):
1196 """Ctor for EvalController class."""
1197 self.answers = []
1198 self.c = c
1199 self.d: Dict[str, Any] = {}
1200 self.globals_d: Dict[str, Any] = {'c': c, 'g': g, 'p': c.p}
1201 self.locals_d: Dict[str, Any] = {}
1202 self.legacy = c.config.getBool('legacy-eval', default=True)
1203 if g.app.ipk:
1204 # Use the IPython namespace.
1205 self.c.vs = g.app.ipk.namespace
1206 elif self.legacy:
1207 self.c.vs = self.d
1208 else:
1209 self.c.vs = self.globals_d
1210 # allow the auto-completer to complete in this namespace
1211 self.c.keyHandler.autoCompleter.namespaces.append(self.c.vs)
1212 # Updated by do_exec.
1213 self.last_result = None
1214 self.old_stderr = None
1215 self.old_stdout = None
1216 #@+node:ekr.20180328092221.1: *3* eval.Commands
1217 #@+node:ekr.20180328085426.2: *4* eval
1218 @eval_cmd("eval")
1219 def eval_command(self, event):
1220 #@+<< eval docstring >>
1221 #@+node:ekr.20180328100519.1: *5* << eval docstring >>
1222 """
1223 Execute the selected text, if any, or the line containing the cursor.
1225 Select next line of text.
1227 Tries hard to capture the result of from the last expression in the
1228 selected text::
1230 import datetime
1231 today = datetime.date.today()
1233 will capture the value of ``today`` even though the last line is a
1234 statement, not an expression.
1236 Stores results in ``c.vs['_last']`` for insertion
1237 into body by ``eval-last`` or ``eval-last-pretty``.
1239 Removes common indentation (``textwrap.dedent()``) before executing,
1240 allowing execution of indented code.
1242 ``g``, ``c``, and ``p`` are available to executing code, assignments
1243 are made in the ``c.vs`` namespace and persist for the life of ``c``.
1244 """
1245 #@-<< eval docstring >>
1246 c = self.c
1247 if c == event.get('c'):
1248 s = self.get_selected_lines()
1249 if self.legacy and s is None:
1250 return
1251 self.eval_text(s)
1252 # Updates self.last_answer if there is exactly one answer.
1253 #@+node:ekr.20180328085426.3: *4* eval-block
1254 @eval_cmd("eval-block")
1255 def eval_block(self, event):
1256 #@+<< eval-block docstring >>
1257 #@+node:ekr.20180328100415.1: *5* << eval-block docstring >>
1258 """
1259 In the body, "# >>>" marks the end of a code block, and "# <<<" marks
1260 the end of an output block. E.g.::
1262 a = 2
1263 # >>>
1264 4
1265 # <<<
1266 b = 2.0*a
1267 # >>>
1268 4.0
1269 # <<<
1271 ``eval-block`` evaluates the current code block, either the code block
1272 the cursor's in, or the code block preceding the output block the cursor's
1273 in. Subsequent output blocks are marked "# >>> *" to show they may need
1274 re-evaluation.
1276 Note: you don't really need to type the "# >>>" and "# <<<" markers
1277 because ``eval-block`` will add them as needed. So just type the
1278 first code block and run ``eval-block``.
1280 """
1281 #@-<< eval-block docstring >>
1282 c = self.c
1283 if c != event.get('c'):
1284 return
1285 pos = 0
1286 lines = []
1287 current_seen = False
1288 for current, source, output in self.get_blocks():
1289 lines.append(source)
1290 lines.append("# >>>" + (" *" if current_seen else ""))
1291 if current:
1292 old_log = c.frame.log.logCtrl.getAllText()
1293 self.eval_text(source)
1294 new_log = c.frame.log.logCtrl.getAllText()[len(old_log) :]
1295 lines.append(new_log.strip())
1296 if not self.legacy:
1297 if self.last_result:
1298 lines.append(self.last_result)
1299 pos = len('\n'.join(lines)) + 7
1300 current_seen = True
1301 else:
1302 lines.append(output)
1303 lines.append("# <<<")
1304 c.p.b = '\n'.join(lines) + '\n'
1305 c.frame.body.wrapper.setInsertPoint(pos)
1306 c.redraw()
1307 c.bodyWantsFocusNow()
1308 #@+node:ekr.20180328085426.5: *4* eval-last
1309 @eval_cmd("eval-last")
1310 def eval_last(self, event, text=None):
1311 """
1312 Insert the last result from ``eval``.
1314 Inserted as a string, so ``"1\n2\n3\n4"`` will cover four lines and
1315 insert no quotes, for ``repr()`` style insertion use ``last-pretty``.
1316 """
1317 c = self.c
1318 if c != event.get('c'):
1319 return
1320 if self.legacy:
1321 text = str(c.vs.get('_last'))
1322 else:
1323 if not text and not self.last_result:
1324 return
1325 if not text:
1326 text = str(self.last_result)
1327 w = c.frame.body.wrapper
1328 i = w.getInsertPoint()
1329 w.insert(i, text + '\n')
1330 w.setInsertPoint(i + len(text) + 1)
1331 c.setChanged()
1332 #@+node:ekr.20180328085426.6: *4* eval-last-pretty
1333 @eval_cmd("eval-last-pretty")
1334 def vs_last_pretty(self, event):
1335 """
1336 Insert the last result from ``eval``.
1338 Formatted by ``pprint.pformat()``, so ``"1\n2\n3\n4"`` will appear as
1339 '``"1\n2\n3\n4"``', see all ``last``.
1340 """
1341 c = self.c
1342 if c != event.get('c'):
1343 return
1344 if self.legacy:
1345 text = str(c.vs.get('_last'))
1346 else:
1347 text = self.last_result
1348 if text:
1349 text = pprint.pformat(text)
1350 self.eval_last(event, text=text)
1351 #@+node:ekr.20180328085426.4: *4* eval-replace
1352 @eval_cmd("eval-replace")
1353 def eval_replace(self, event):
1354 """
1355 Execute the selected text, if any.
1356 Undoably replace it with the result.
1357 """
1358 c = self.c
1359 if c != event.get('c'):
1360 return
1361 w = c.frame.body.wrapper
1362 s = w.getSelectedText()
1363 if not s.strip():
1364 g.es_print('no selected text')
1365 return
1366 self.eval_text(s)
1367 if self.legacy:
1368 last = c.vs.get('_last')
1369 else:
1370 last = self.last_result
1371 if not last:
1372 return
1373 s = pprint.pformat(last)
1374 i, j = w.getSelectionRange()
1375 new_text = c.p.b[:i] + s + c.p.b[j:]
1376 bunch = c.undoer.beforeChangeNodeContents(c.p)
1377 w.setAllText(new_text)
1378 c.p.b = new_text
1379 w.setInsertPoint(i + len(s))
1380 c.undoer.afterChangeNodeContents(c.p, 'Insert result', bunch)
1381 c.setChanged()
1382 #@+node:ekr.20180328151652.1: *3* eval.Helpers
1383 #@+node:ekr.20180328090830.1: *4* eval.eval_text & helpers
1384 def eval_text(self, s):
1385 """Evaluate string s."""
1386 s = textwrap.dedent(s)
1387 if not s.strip():
1388 return None
1389 self.redirect()
1390 if self.legacy:
1391 blocks = re.split('\n(?=[^\\s])', s)
1392 ans = self.old_exec(blocks, s)
1393 self.show_legacy_answer(ans, blocks)
1394 return ans # needed by mod_http
1395 self.new_exec(s)
1396 self.show_answers()
1397 self.unredirect()
1398 return None
1399 #@+node:ekr.20180329130626.1: *5* eval.new_exec
1400 def new_exec(self, s):
1401 try:
1402 self.answers = []
1403 self.locals_d = {}
1404 exec(s, self.globals_d, self.locals_d)
1405 for key in self.locals_d:
1406 val = self.locals_d.get(key)
1407 self.globals_d[key] = val
1408 self.answers.append((key, val),)
1409 if len(self.answers) == 1:
1410 key, val = self.answers[0]
1411 self.last_result = val
1412 else:
1413 self.last_result = None
1414 except Exception:
1415 g.es_exception()
1416 #@+node:ekr.20180329130623.1: *5* eval.old_exec
1417 def old_exec(self, blocks, txt):
1419 # pylint: disable=eval-used
1420 c = self.c
1421 leo_globals = {'c': c, 'g': g, 'p': c.p}
1422 all_done, ans = False, None
1423 try:
1424 # Execute all but the last 'block'
1425 exec('\n'.join(blocks[:-1]), leo_globals, c.vs) # Compatible with Python 3.x.
1426 all_done = False
1427 except SyntaxError:
1428 # Splitting the last block caused syntax error
1429 try:
1430 # Is the whole thing a single expression?
1431 ans = eval(txt, leo_globals, c.vs)
1432 except SyntaxError:
1433 try:
1434 exec(txt, leo_globals, c.vs)
1435 except Exception:
1436 g.es_exception()
1437 all_done = True # Either way, the last block will be used.
1438 if not all_done: # last block still needs using
1439 try:
1440 ans = eval(blocks[-1], leo_globals, c.vs)
1441 except SyntaxError:
1442 try:
1443 exec(txt, leo_globals, c.vs)
1444 except Exception:
1445 g.es_exception()
1446 return ans
1447 #@+node:ekr.20180328130526.1: *5* eval.redirect & unredirect
1448 def redirect(self):
1449 c = self.c
1450 if c.config.getBool('eval-redirect'):
1451 self.old_stderr = g.stdErrIsRedirected()
1452 self.old_stdout = g.stdOutIsRedirected()
1453 if not self.old_stderr:
1454 g.redirectStderr()
1455 if not self.old_stdout:
1456 g.redirectStdout()
1458 def unredirect(self):
1459 c = self.c
1460 if c.config.getBool('eval-redirect'):
1461 if not self.old_stderr:
1462 g.restoreStderr()
1463 if not self.old_stdout:
1464 g.restoreStdout()
1465 #@+node:ekr.20180328132748.1: *5* eval.show_answers
1466 def show_answers(self):
1467 """ Show all new values computed by do_exec."""
1468 if len(self.answers) > 1:
1469 g.es('')
1470 for answer in self.answers:
1471 key, val = answer
1472 g.es('%s = %s' % (key, val))
1473 #@+node:ekr.20180329154232.1: *5* eval.show_legacy_answer
1474 def show_legacy_answer(self, ans, blocks):
1476 cvs = self.c.vs
1477 if ans is None: # see if last block was a simple "var =" assignment
1478 key = blocks[-1].split('=', 1)[0].strip()
1479 if key in cvs:
1480 ans = cvs[key]
1481 if ans is None: # see if whole text was a simple /multi-line/ "var =" assignment
1482 key = blocks[0].split('=', 1)[0].strip()
1483 if key in cvs:
1484 ans = cvs[key]
1485 cvs['_last'] = ans
1486 if ans is not None:
1487 # annoying to echo 'None' to the log during line by line execution
1488 txt = str(ans)
1489 lines = txt.split('\n')
1490 if len(lines) > 10:
1491 txt = '\n'.join(lines[:5] + ['<snip>'] + lines[-5:])
1492 if len(txt) > 500:
1493 txt = txt[:500] + ' <truncated>'
1494 g.es(txt)
1495 return ans
1496 #@+node:ekr.20180329125626.1: *4* eval.exec_then_eval (not used yet)
1497 def exec_then_eval(self, code, ns):
1498 # From Milan Melena.
1499 import ast
1500 block = ast.parse(code, mode='exec')
1501 if block.body and isinstance(block.body[-1], ast.Expr):
1502 last = ast.Expression(block.body.pop().value)
1503 exec(compile(block, '<string>', mode='exec'), ns)
1504 # pylint: disable=eval-used
1505 return eval(compile(last, '<string>', mode='eval'), ns)
1506 exec(compile(block, '<string>', mode='exec'), ns)
1507 return ""
1508 #@+node:tbrown.20170516194332.1: *4* eval.get_blocks
1509 def get_blocks(self):
1510 """get_blocks - iterate code blocks
1512 :return: (current, source, output)
1513 :rtype: (bool, str, str)
1514 """
1515 c = self.c
1516 pos = c.frame.body.wrapper.getInsertPoint()
1517 chrs = 0
1518 lines = c.p.b.split('\n')
1519 block: Dict[str, List] = {'source': [], 'output': []}
1520 reading = 'source'
1521 seeking_current = True
1522 # if the last non-blank line isn't the end of a possibly empty
1523 # output block, make it one
1524 if [i for i in lines if i.strip()][-1] != "# <<<":
1525 lines.append("# <<<")
1526 while lines:
1527 line = lines.pop(0)
1528 chrs += len(line) + 1
1529 if line.startswith("# >>>"):
1530 reading = 'output'
1531 continue
1532 if line.startswith("# <<<"):
1533 current = seeking_current and (chrs >= pos + 1)
1534 if current:
1535 seeking_current = False
1536 yield current, '\n'.join(block['source']), '\n'.join(block['output'])
1537 block = {'source': [], 'output': []}
1538 reading = 'source'
1539 continue
1540 block[reading].append(line)
1541 #@+node:ekr.20180328145035.1: *4* eval.get_selected_lines
1542 def get_selected_lines(self):
1544 c, p = self.c, self.c.p
1545 w = c.frame.body.wrapper
1546 body = w.getAllText()
1547 i = w.getInsertPoint()
1548 if w.hasSelection():
1549 if self.legacy:
1550 i1, i2 = w.getSelectionRange()
1551 else:
1552 j, k = w.getSelectionRange()
1553 i1, junk = g.getLine(body, j)
1554 junk, i2 = g.getLine(body, k)
1555 s = body[i1:i2]
1556 else:
1557 if self.legacy:
1558 k = w.getInsertPoint()
1559 junk, i2 = g.getLine(body, k)
1560 w.setSelectionRange(k, i2)
1561 return None
1562 i1, i2 = g.getLine(body, i)
1563 s = body[i1:i2].strip()
1564 # Select next line for next eval.
1565 if self.legacy:
1566 i = j = i2
1567 j += 1
1568 while j < len(body) and body[j] != '\n':
1569 j += 1
1570 w.setSelectionRange(i, j)
1571 else:
1572 if not body.endswith('\n'):
1573 if i >= len(p.b):
1574 i2 += 1
1575 p.b = p.b + '\n'
1576 ins = min(len(p.b), i2)
1577 w.setSelectionRange(i1, ins, insert=ins, s=p.b)
1578 return s
1579 #@-others
1580#@-others
1581#@@language python
1582#@@tabwidth -4
1583#@-leo