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

781 statements  

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. 

6 

7Overview of script buttons 

8-------------------------- 

9 

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. 

12 

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. 

16 

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. 

20 

21For example, to run a script on any part of an outline do the following: 

22 

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. 

27 

28Script buttons create commands 

29------------------------------ 

30 

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. 

34 

35 

36Global buttons and commands 

37--------------------------- 

38 

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. 

45 

46The cleaned name of an @button node is the headline text of the button with: 

47 

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. 

53 

54Thus, cleaning headline text converts it to a valid minibuffer command name. 

55 

56You can delete a script button by right-clicking on it, or by 

57executing the delete-x-button command. 

58 

59.. The 'Debug Script' button runs a script using an external debugger. 

60 

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. 

64 

65Settings 

66-------- 

67 

68You can specify the following options in myLeoSettings.leo. See the node: 

69@settings-->Plugins-->scripting plugin. Recommended defaults are shown:: 

70 

71 @bool scripting-at-button-nodes = True 

72 True: adds a button for every @button node. 

73 

74 @bool scripting-at-rclick-nodes = False 

75 True: define a minibuffer command for every @rclick node. 

76 

77 @bool scripting-at-commands-nodes = True 

78 True: define a minibuffer command for every @command node. 

79 

80 @bool scripting-at-plugin-nodes = False 

81 True: dynamically loads plugins in @plugin nodes when a window is created. 

82 

83 @bool scripting-at-script-nodes = False 

84 True: dynamically executes script in @script nodes when a window is created. 

85 This is dangerous! 

86 

87 @bool scripting-create-debug-button = False 

88 True: create Debug Script button. 

89 

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. 

93 

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. 

97 

98 @int scripting-max-button-size = 18 

99 The maximum length of button names: longer names are truncated. 

100 

101Shortcuts for script buttons 

102---------------------------- 

103 

104You can bind key shortcuts to @button and @command nodes as follows: 

105 

106@button name @key=shortcut 

107 

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. 

111 

112@command name @key=shortcut 

113 

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. 

116 

117Binding arguments to script buttons with @args 

118---------------------------------------------- 

119 

120You can run @button and @command scripts with sys.argv initialized to string values using @args. 

121For example:: 

122 

123 @button test-args @args = a,b,c 

124 

125will set sys.argv to ['a', 'b', 'c']. 

126 

127You can set the background color of buttons created by @button nodes by using @color. 

128For example:: 

129 

130 @button my button @key=Ctrl+Alt+1 @color=white @args=a,b,c 

131 

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. 

134 

135Eval Commands 

136------------- 

137 

138The mod_scripting plugin creates the following 5 eval* commands: 

139 

140eval 

141---- 

142 

143Evaluates the selected text, if any, and remember the result in c.vs, a global namespace. 

144For example:: 

145 

146 a = 10 

147 

148sets: 

149 

150 c.vs['a'] = 10 

151 

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. 

154 

155eval-last 

156--------- 

157 

158Inserts the result of the last eval in the body. 

159Suppose you have this text:: 

160 

161 The cat is 7 years, or 7*365 days old. 

162 

163To replace 7*365 with 2555, do the following:: 

164 

165 select 7*367 

166 eval 

167 delete 7*365 

168 do eval-last 

169 

170eval-replace 

171------------ 

172 

173Evaluates the expression and replaces it with the computed value. 

174For example, the example above can be done as follows:: 

175 

176 

177 select 7*367 

178 eval-replace 

179 

180eval-last-pretty 

181---------------- 

182 

183Like eval-last, but format with pprint.pformat. 

184 

185eval-block 

186---------- 

187 

188Evaluates a series of blocks of code in the body, separated like this:: 

189 

190 # >>> 

191 code to run 

192 # <<< 

193 output of code 

194 # >>> 

195 code to run 

196 # <<< 

197 output of code 

198 ... 

199 

200For example:: 

201 

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 # <<< 

211 

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. 

215 

216Acknowledgements 

217---------------- 

218 

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 >> 

234 

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. 

246 

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. 

250 

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') 

259 

260 at_others_pat = re.compile(r'^\s*@others\b', re.MULTILINE) 

261 

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 

268 

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): 

375 

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. 

628 

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') 

659 

660 def deleteButtonCallback(event=None, self=self, b=b): 

661 self.deleteButton(b, event=event) 

662 # Register the delete-x-button command. 

663 

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. 

701 

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. 

742 

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. 

746 

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. 

819 

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. 

823 

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. 

850 

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. 

854 

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) 

874 

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) 

878 

879 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252 

880 # Minibuffer commands created by mod_scripting.py have no docstrings 

881 

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) 

913 

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) 

936 

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 ) 

1118 

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. 

1125 

1126 def registerAllCommandsCallback(event=None, func=func): 

1127 func() 

1128 

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 

1177 

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. 

1213 

1214 Select next line of text. 

1215 

1216 Tries hard to capture the result of from the last expression in the 

1217 selected text:: 

1218 

1219 import datetime 

1220 today = datetime.date.today() 

1221 

1222 will capture the value of ``today`` even though the last line is a 

1223 statement, not an expression. 

1224 

1225 Stores results in ``c.vs['_last']`` for insertion 

1226 into body by ``eval-last`` or ``eval-last-pretty``. 

1227 

1228 Removes common indentation (``textwrap.dedent()``) before executing, 

1229 allowing execution of indented code. 

1230 

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.:: 

1250 

1251 a = 2 

1252 # >>> 

1253 4 

1254 # <<< 

1255 b = 2.0*a 

1256 # >>> 

1257 4.0 

1258 # <<< 

1259 

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. 

1264 

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``. 

1268 

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``. 

1302 

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``. 

1326 

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): 

1407 

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() 

1446 

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): 

1464 

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 

1500 

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): 

1532 

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