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