Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#@+leo-ver=5-thin 

2#@+node:ekr.20060328125248: * @file ../plugins/mod_scripting.py 

3#@+<< mod_scripting docstring >> 

4#@+node:ekr.20060328125248.1: ** << mod_scripting docstring >> 

5r"""This plugin script buttons and eval* commands. 

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 

337 # A QButton. 

338 self.buttonText = buttonText 

339 # The text of the button. 

340 self.c = c 

341 # A Commander. 

342 self.controller = controller 

343 # A ScriptingController instance. 

344 self.gnx = gnx 

345 # Set if the script is defined in the local .leo file. 

346 self.script = script 

347 # Set if the script is found defined in myLeoSettings.leo or leoSettings.leo 

348 self.source_c = c 

349 # For GetArgs.command_source. 

350 self.__doc__ = docstring 

351 # The docstring for this callback for g.getDocStringForFunction. 

352 #@+node:ekr.20141031053508.10: *3* __call__ (AtButtonCallback) 

353 def __call__(self, event=None): 

354 """AtButtonCallbgack.__call__. The callback for @button nodes.""" 

355 self.execute_script() 

356 #@+node:ekr.20141031053508.13: *3* __repr__ (AtButtonCallback) 

357 def __repr__(self): 

358 """AtButtonCallback.__repr__.""" 

359 c = self.c 

360 return 'AtButtonCallback %s gnx: %s len(script) %s' % ( 

361 c.shortFileName(), self.gnx, len(self.script or '')) 

362 #@+node:ekr.20150512041758.1: *3* __getattr__ (AtButtonCallback) 

363 def __getattr__(self, attr): 

364 """AtButtonCallback.__getattr__. Implement __name__.""" 

365 if attr == '__name__': 

366 return 'AtButtonCallback: %s' % self.gnx 

367 raise AttributeError 

368 # Returning None is not correct. 

369 #@+node:ekr.20170203043042.1: *3* AtButtonCallback.execute_script & helper 

370 def execute_script(self): 

371 """Execute the script associated with this button.""" 

372 script = self.find_script() 

373 if script: 

374 self.controller.executeScriptFromButton( 

375 b=self.b, 

376 buttonText=self.buttonText, 

377 p=None, 

378 script_gnx=self.gnx, 

379 script=script, 

380 ) 

381 #@+node:ekr.20180313171043.1: *4* AtButtonCallback.find_script 

382 def find_script(self): 

383 

384 gnx = self.gnx 

385 # First, search self.c for the gnx. 

386 for p in self.c.all_positions(): 

387 if p.gnx == gnx: 

388 script = self.controller.getScript(p) 

389 return script 

390 # See if myLeoSettings.leo is open. 

391 for c in g.app.commanders(): 

392 if c.shortFileName().endswith('myLeoSettings.leo'): 

393 break 

394 else: 

395 c = None 

396 if c: 

397 # Search myLeoSettings.leo file for the gnx. 

398 for p in c.all_positions(): 

399 if p.gnx == gnx: 

400 script = self.controller.getScript(p) 

401 return script 

402 return self.script 

403 #@-others 

404#@+node:ekr.20060328125248.6: ** class ScriptingController 

405class ScriptingController: 

406 """A class defining scripting commands.""" 

407 #@+others 

408 #@+node:ekr.20060328125248.7: *3* sc.ctor 

409 def __init__(self, c, iconBar=None): 

410 self.c = c 

411 self.gui = c.frame.gui 

412 getBool = c.config.getBool 

413 self.scanned = False 

414 kind = c.config.getString('debugger-kind') or 'idle' 

415 self.buttonsDict = {} # Keys are buttons, values are button names (strings). 

416 self.debuggerKind = kind.lower() 

417 self.atButtonNodes = getBool('scripting-at-button-nodes') 

418 # True: adds a button for every @button node. 

419 self.atCommandsNodes = getBool('scripting-at-commands-nodes') 

420 # True: define a minibuffer command for every @command node. 

421 self.atRclickNodes = getBool('scripting-at-rclick-nodes') 

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

423 self.atPluginNodes = getBool('scripting-at-plugin-nodes') 

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

425 self.atScriptNodes = getBool('scripting-at-script-nodes') 

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

427 # DANGEROUS! 

428 # Do not allow this setting to be changed in local (non-settings) .leo files. 

429 if self.atScriptNodes and c.config.isLocalSetting('scripting-at-script-nodes', 'bool'): 

430 g.issueSecurityWarning('@bool scripting-at-script-nodes') 

431 # Restore the value in myLeoSettings.leo 

432 val = g.app.config.valueInMyLeoSettings('scripting-at-script-nodes') 

433 if val is None: 

434 val = False 

435 g.es('Restoring value to', val, color='red') 

436 self.atScriptNodes = val 

437 self.createDebugButton = getBool('scripting-create-debug-button') 

438 # True: create Debug Script button. 

439 self.createRunScriptButton = getBool('scripting-create-run-script-button') 

440 # True: create Run Script button. 

441 self.createScriptButtonButton = getBool('scripting-create-script-button-button') 

442 # True: create Script Button button. 

443 self.maxButtonSize = c.config.getInt('scripting-max-button-size') or 18 

444 # Maximum length of button names. 

445 if not iconBar: 

446 self.iconBar = c.frame.getIconBarObject() 

447 else: 

448 self.iconBar = iconBar 

449 self.seen = set() 

450 # Fix bug 74: problems with @button if defined in myLeoSettings.leo 

451 # Set of gnx's (not vnodes!) that created buttons or commands. 

452 #@+node:ekr.20150401113822.1: *3* sc.Callbacks 

453 #@+node:ekr.20060328125248.23: *4* sc.addScriptButtonCommand 

454 def addScriptButtonCommand(self, event=None): 

455 """Called when the user presses the 'script-button' button or executes the script-button command.""" 

456 c = self.c 

457 p = c.p 

458 h = p.h 

459 buttonText = self.getButtonText(h) 

460 shortcut = self.getShortcut(h) 

461 statusLine = "Run Script: %s" % buttonText 

462 if shortcut: 

463 statusLine = statusLine + " @key=" + shortcut 

464 self.createLocalAtButtonHelper(p, h, statusLine, kind='script-button', verbose=True) 

465 c.bodyWantsFocus() 

466 #@+node:ekr.20060522105937.1: *4* sc.runDebugScriptCommand 

467 def runDebugScriptCommand(self, event=None): 

468 """Called when user presses the 'debug-script' button or executes the debug-script command.""" 

469 c = self.c 

470 p = c.p 

471 script = g.getScript(c, p, useSelectedText=True, useSentinels=False) 

472 if script: 

473 #@+<< set debugging if debugger is active >> 

474 #@+node:ekr.20060523084441: *5* << set debugging if debugger is active >> 

475 g.trace(self.debuggerKind) 

476 if self.debuggerKind == 'winpdb': 

477 try: 

478 import rpdb2 

479 debugging = rpdb2.g_debugger is not None 

480 except ImportError: 

481 debugging = False 

482 elif self.debuggerKind == 'idle': 

483 # import idlelib.Debugger.py as Debugger 

484 # debugging = Debugger.interacting 

485 debugging = True 

486 else: 

487 debugging = False 

488 #@-<< set debugging if debugger is active >> 

489 if debugging: 

490 #@+<< create leoScriptModule >> 

491 #@+node:ekr.20060524073716: *5* << create leoScriptModule >> (mod_scripting.py) 

492 target = g.os_path_join(g.app.loadDir, 'leoScriptModule.py') 

493 with open(target, 'w') as f: 

494 f.write('# A module holding the script to be debugged.\n') 

495 if self.debuggerKind == 'idle': 

496 # This works, but uses the lame pdb debugger. 

497 f.write('import pdb\n') 

498 f.write('pdb.set_trace() # Hard breakpoint.\n') 

499 elif self.debuggerKind == 'winpdb': 

500 f.write('import rpdb2\n') 

501 f.write('if rpdb2.g_debugger is not None: # don\'t hang if the debugger isn\'t running.\n') 

502 f.write(' rpdb2.start_embedded_debugger(pwd="",fAllowUnencrypted=True) # Hard breakpoint.\n') 

503 # f.write('# Remove all previous variables.\n') 

504 f.write('# Predefine c, g and p.\n') 

505 f.write('from leo.core import leoGlobals as g\n') 

506 f.write('c = g.app.scriptDict.get("c")\n') 

507 f.write('script_gnx = g.app.scriptDict.get("script_gnx")\n') 

508 f.write('p = c.p\n') 

509 f.write('# Actual script starts here.\n') 

510 f.write(script + '\n') 

511 #@-<< create leoScriptModule >> 

512 # pylint: disable=no-name-in-module 

513 g.app.scriptDict['c'] = c 

514 g.app.scriptDict = {'script_gnx': p.gnx} 

515 if 'leoScriptModule' in sys.modules.keys(): 

516 del sys.modules['leoScriptModule'] # Essential. 

517 # pylint: disable=import-error 

518 # This *will* exist. 

519 from leo.core import leoScriptModule 

520 assert leoScriptModule # for pyflakes. 

521 else: 

522 g.error('No debugger active') 

523 c.bodyWantsFocus() 

524 #@+node:ekr.20060328125248.21: *4* sc.runScriptCommand 

525 def runScriptCommand(self, event=None): 

526 """Called when user presses the 'run-script' button or executes the run-script command.""" 

527 c, p = self.c, self.c.p 

528 args = self.getArgs(p) 

529 g.app.scriptDict = {'script_gnx': p.gnx} 

530 c.executeScript(args=args, p=p, useSelectedText=True, silent=True) 

531 if 0: 

532 # Do not assume the script will want to remain in this commander. 

533 c.bodyWantsFocus() 

534 #@+node:ekr.20060328125248.8: *3* sc.createAllButtons 

535 def createAllButtons(self): 

536 """Scan for @button, @rclick, @command, @plugin and @script nodes.""" 

537 c = self.c 

538 if self.scanned: 

539 return # Defensive. 

540 self.scanned = True 

541 # 

542 # First, create standard buttons. 

543 if self.createRunScriptButton: 

544 self.createRunScriptIconButton() 

545 if self.createScriptButtonButton: 

546 self.createScriptButtonIconButton() 

547 if self.createDebugButton: 

548 self.createDebugIconButton() 

549 # 

550 # Next, create common buttons and commands. 

551 self.createCommonButtons() 

552 self.createCommonCommands() 

553 # 

554 # Handle all other nodes. 

555 d = { 

556 'button': self.handleAtButtonNode, 

557 'command': self.handleAtCommandNode, 

558 'plugin': self.handleAtPluginNode, 

559 'rclick': self.handleAtRclickNode, 

560 'script': self.handleAtScriptNode, 

561 } 

562 pattern = re.compile(r'^@(button|command|plugin|rclick|script)\b') 

563 p = c.rootPosition() 

564 while p: 

565 gnx = p.v.gnx 

566 if p.isAtIgnoreNode(): 

567 p.moveToNodeAfterTree() 

568 elif gnx in self.seen: 

569 # #657 

570 # if g.match_word(p.h, 0, '@rclick'): 

571 if p.h.startswith('@rlick'): 

572 self.handleAtRclickNode(p) 

573 p.moveToThreadNext() 

574 else: 

575 self.seen.add(gnx) 

576 m = pattern.match(p.h) 

577 if m: 

578 func = d.get(m.group(1)) 

579 func(p) 

580 p.moveToThreadNext() 

581 #@+node:ekr.20060328125248.24: *3* sc.createLocalAtButtonHelper 

582 def createLocalAtButtonHelper(self, p, h, statusLine, 

583 kind='at-button', 

584 verbose=True, 

585 ): 

586 """Create a button for a local @button node.""" 

587 c = self.c 

588 buttonText = self.cleanButtonText(h, minimal=True) 

589 args = self.getArgs(p) 

590 # We must define the callback *after* defining b, 

591 # so set both command and shortcut to None here. 

592 bg = self.getColor(h) 

593 b = self.createIconButton( 

594 args=args, 

595 text=h, 

596 command=None, 

597 statusLine=statusLine, 

598 kind=kind, 

599 bg=bg, 

600 ) 

601 if not b: 

602 return None 

603 # Now that b is defined we can define the callback. 

604 # Yes, executeScriptFromButton *does* use b (to delete b if requested by the script). 

605 docstring = g.getDocString(p.b).strip() 

606 cb = AtButtonCallback( 

607 controller=self, 

608 b=b, 

609 c=c, 

610 buttonText=buttonText, 

611 docstring=docstring, 

612 gnx=p.v.gnx, 

613 script=None, 

614 ) 

615 self.iconBar.setCommandForButton( 

616 button=b, 

617 command=cb, # This encapsulates the script. 

618 command_p=p and p.copy(), # This does exist. 

619 controller=self, 

620 gnx=p and p.gnx, 

621 script=None, 

622 ) 

623 # At last we can define the command and use the shortcut. 

624 # registerAllCommands recomputes the shortcut. 

625 self.registerAllCommands( 

626 args=self.getArgs(p), 

627 func=cb, 

628 h=h, 

629 pane='button', 

630 source_c=p.v.context, 

631 tag='local @button') 

632 return b 

633 #@+node:ekr.20060328125248.17: *3* sc.createIconButton (creates all buttons) 

634 def createIconButton(self, args, text, command, statusLine, bg=None, kind=None): 

635 """ 

636 Create one icon button. 

637 This method creates all scripting icon buttons. 

638 

639 - Creates the actual button and its balloon. 

640 - Adds the button to buttonsDict. 

641 - Registers command with the shortcut. 

642 - Creates x amd delete-x-button commands, where x is the cleaned button name. 

643 - Binds a right-click in the button to a callback that deletes the button. 

644 """ 

645 c = self.c 

646 # Create the button and add it to the buttons dict. 

647 commandName = self.cleanButtonText(text) 

648 # Truncate only the text of the button, not the command name. 

649 truncatedText = self.truncateButtonText(commandName) 

650 if not truncatedText.strip(): 

651 g.error('%s ignored: no cleaned text' % (text.strip() or '')) 

652 return None 

653 # Command may be None. 

654 b = self.iconBar.add(text=truncatedText, command=command, kind=kind) 

655 if not b: 

656 return None 

657 self.setButtonColor(b, bg) 

658 self.buttonsDict[b] = truncatedText 

659 if statusLine: 

660 self.createBalloon(b, statusLine) 

661 if command: 

662 self.registerAllCommands( 

663 args=args, 

664 func=command, 

665 h=text, 

666 pane='button', 

667 source_c=c, 

668 tag='icon button') 

669 

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

671 self.deleteButton(b, event=event) 

672 # Register the delete-x-button command. 

673 

674 deleteCommandName = 'delete-%s-button' % commandName 

675 c.k.registerCommand( 

676 # allowBinding=True, 

677 commandName=deleteCommandName, 

678 func=deleteButtonCallback, 

679 pane='button', 

680 shortcut=None, 

681 ) 

682 # Reporting this command is way too annoying. 

683 return b 

684 #@+node:ekr.20060328125248.28: *3* sc.executeScriptFromButton 

685 def executeScriptFromButton(self, b, buttonText, p, script, script_gnx=None): 

686 """Execute an @button script in p.b or script.""" 

687 c = self.c 

688 if c.disableCommandsMessage: 

689 g.blue(c.disableCommandsMessage) 

690 return 

691 if not p and not script: 

692 g.trace('can not happen: no p and no script') 

693 return 

694 g.app.scriptDict = {'script_gnx': script_gnx} 

695 args = self.getArgs(p) 

696 if not script: 

697 script = self.getScript(p) 

698 c.executeScript(args=args, p=p, script=script, silent=True) 

699 # Remove the button if the script asks to be removed. 

700 if g.app.scriptDict.get('removeMe'): 

701 g.es("Removing '%s' button at its request" % buttonText) 

702 self.deleteButton(b) 

703 # Do *not* set focus here: the script may have changed the focus. 

704 # c.bodyWantsFocus() 

705 #@+node:ekr.20130912061655.11294: *3* sc.open_gnx 

706 def open_gnx(self, c, gnx): 

707 """ 

708 Find the node with the given gnx in c, myLeoSettings.leo and leoSettings.leo. 

709 If found, open the tab/outline and select the specified node. 

710 Return c,p of the found node. 

711 

712 Called only from a callback in QtIconBarClass.setCommandForButton. 

713 """ 

714 if not gnx: 

715 g.trace('can not happen: no gnx') 

716 # First, look in commander c. 

717 for p2 in c.all_positions(): 

718 if p2.gnx == gnx: 

719 return c, p2 

720 # Fix bug 74: problems with @button if defined in myLeoSettings.leo. 

721 for f in (c.openMyLeoSettings, c.openLeoSettings): 

722 c2 = f() # Open the settings file. 

723 if c2: 

724 for p2 in c2.all_positions(): 

725 if p2.gnx == gnx: 

726 return c2, p2 

727 c2.close() 

728 # Fix bug 92: restore the previously selected tab. 

729 if hasattr(c.frame, 'top'): 

730 c.frame.top.leo_master.select(c) 

731 return None, None # 2017/02/02. 

732 #@+node:ekr.20150401130207.1: *3* sc.Scripts, common 

733 # Important: common @button and @command nodes do **not** update dynamically! 

734 #@+node:ekr.20080312071248.1: *4* sc.createCommonButtons 

735 def createCommonButtons(self): 

736 """Handle all global @button nodes.""" 

737 c = self.c 

738 buttons = c.config.getButtons() or [] 

739 for z in buttons: 

740 # #2011 

741 p, script, rclicks = z 

742 gnx = p.v.gnx 

743 if gnx not in self.seen: 

744 self.seen.add(gnx) 

745 script = self.getScript(p) 

746 self.createCommonButton(p, script, rclicks) 

747 #@+node:ekr.20070926084600: *4* sc.createCommonButton (common @button) 

748 def createCommonButton(self, p, script, rclicks=None): 

749 """ 

750 Create a button in the icon area for a common @button node in an @setting 

751 tree. Binds button presses to a callback that executes the script. 

752 

753 Important: Common @button and @command scripts now *do* update 

754 dynamically provided that myLeoSettings.leo is open. Otherwise the 

755 callback executes the static script. 

756 

757 See https://github.com/leo-editor/leo-editor/issues/171 

758 """ 

759 c = self.c 

760 gnx = p.gnx 

761 args = self.getArgs(p) 

762 # Fix bug #74: problems with @button if defined in myLeoSettings.leo 

763 docstring = g.getDocString(p.b).strip() 

764 statusLine = docstring or 'Global script button' 

765 shortcut = self.getShortcut(p.h) 

766 # Get the shortcut from the @key field in the headline. 

767 if shortcut: 

768 statusLine = '%s = %s' % (statusLine.rstrip(), shortcut) 

769 # We must define the callback *after* defining b, 

770 # so set both command and shortcut to None here. 

771 bg = self.getColor(p.h) # #2024 

772 b = self.createIconButton( 

773 args=args, 

774 bg=bg, # #2024 

775 text=p.h, 

776 command=None, 

777 statusLine=statusLine, 

778 kind='at-button', 

779 ) 

780 if not b: 

781 return 

782 # Now that b is defined we can define the callback. 

783 # Yes, the callback *does* use b (to delete b if requested by the script). 

784 buttonText = self.cleanButtonText(p.h) 

785 cb = AtButtonCallback( 

786 b=b, 

787 buttonText=buttonText, 

788 c=c, 

789 controller=self, 

790 docstring=docstring, 

791 gnx=gnx, 

792 # tag:#367: the gnx is needed for the Goto Script command. 

793 # 2018/03/13: Use gnx to search myLeoSettings.leo if it is open. 

794 script=script, 

795 ) 

796 # Now patch the button. 

797 self.iconBar.setCommandForButton( 

798 button=b, 

799 command=cb, # This encapsulates the script. 

800 command_p=p and p.copy(), # tag:#567 

801 controller=self, 

802 gnx=gnx, # For the find-button function. 

803 script=script, 

804 ) 

805 self.handleRclicks(rclicks) 

806 # At last we can define the command. 

807 self.registerAllCommands( 

808 args=args, 

809 func=cb, 

810 h=p.h, 

811 pane='button', 

812 source_c=p.v.context, 

813 tag='@button') 

814 #@+node:ekr.20080312071248.2: *4* sc.createCommonCommands 

815 def createCommonCommands(self): 

816 """Handle all global @command nodes.""" 

817 c = self.c 

818 aList = c.config.getCommands() or [] 

819 for z in aList: 

820 p, script = z 

821 gnx = p.v.gnx 

822 if gnx not in self.seen: 

823 self.seen.add(gnx) 

824 script = self.getScript(p) 

825 self.createCommonCommand(p, script) 

826 #@+node:ekr.20150401130818.1: *4* sc.createCommonCommand (common @command) 

827 def createCommonCommand(self, p, script): 

828 """ 

829 Handle a single @command node. 

830 

831 Important: Common @button and @command scripts now *do* update 

832 dynamically provided that myLeoSettings.leo is open. Otherwise the 

833 callback executes the static script. 

834 

835 See https://github.com/leo-editor/leo-editor/issues/171 

836 """ 

837 c = self.c 

838 args = self.getArgs(p) 

839 commonCommandCallback = AtButtonCallback( 

840 b=None, 

841 buttonText=None, 

842 c=c, 

843 controller=self, 

844 docstring=g.getDocString(p.b).strip(), 

845 gnx=p.v.gnx, # Used to search myLeoSettings.leo if it is open. 

846 script=script, # Fallback when myLeoSettings.leo is not open. 

847 ) 

848 self.registerAllCommands( 

849 args=args, 

850 func=commonCommandCallback, 

851 h=p.h, 

852 pane='button', # Fix bug 416: use 'button', NOT 'command', and NOT 'all' 

853 source_c=p.v.context, 

854 tag='global @command', 

855 ) 

856 #@+node:ekr.20150401130313.1: *3* sc.Scripts, individual 

857 #@+node:ekr.20060328125248.12: *4* sc.handleAtButtonNode @button 

858 def handleAtButtonNode(self, p): 

859 """ 

860 Create a button in the icon area for an @button node. 

861 

862 An optional @key=shortcut defines a shortcut that is bound to the button's script. 

863 The @key=shortcut does not appear in the button's name, but 

864 it *does* appear in the statutus line shown when the mouse moves over the button. 

865 

866 An optional @color=colorname defines a color for the button's background. It does 

867 not appear in the status line nor the button name. 

868 """ 

869 h = p.h 

870 shortcut = self.getShortcut(h) 

871 docstring = g.getDocString(p.b).strip() 

872 statusLine = docstring if docstring else 'Local script button' 

873 if shortcut: 

874 statusLine = '%s = %s' % (statusLine, shortcut) 

875 g.app.config.atLocalButtonsList.append(p.copy()) 

876 # This helper is also called by the script-button callback. 

877 self.createLocalAtButtonHelper(p, h, statusLine, verbose=False) 

878 #@+node:ekr.20060328125248.10: *4* sc.handleAtCommandNode @command 

879 def handleAtCommandNode(self, p): 

880 """Handle @command name [@key[=]shortcut].""" 

881 c = self.c 

882 if not p.h.strip(): 

883 return 

884 args = self.getArgs(p) 

885 

886 def atCommandCallback(event=None, args=args, c=c, p=p.copy()): 

887 # pylint: disable=dangerous-default-value 

888 c.executeScript(args=args, p=p, silent=True) 

889 

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

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

892 

893 atCommandCallback.__doc__ = g.getDocString(p.b).strip() 

894 self.registerAllCommands( 

895 args=args, 

896 func=atCommandCallback, 

897 h=p.h, 

898 pane='button', # Fix # 416. 

899 source_c=p.v.context, 

900 tag='local @command') 

901 g.app.config.atLocalCommandsList.append(p.copy()) 

902 #@+node:ekr.20060328125248.13: *4* sc.handleAtPluginNode @plugin 

903 def handleAtPluginNode(self, p): 

904 """Handle @plugin nodes.""" 

905 tag = "@plugin" 

906 h = p.h 

907 assert g.match(h, 0, tag) 

908 # Get the name of the module. 

909 moduleOrFileName = h[len(tag) :].strip() 

910 if not self.atPluginNodes: 

911 g.warning("disabled @plugin: %s" % (moduleOrFileName)) 

912 # elif theFile in g.app.loadedPlugins: 

913 elif g.pluginIsLoaded(moduleOrFileName): 

914 g.warning("plugin already loaded: %s" % (moduleOrFileName)) 

915 else: 

916 g.loadOnePlugin(moduleOrFileName) 

917 #@+node:peckj.20131113130420.6851: *4* sc.handleAtRclickNode @rclick 

918 def handleAtRclickNode(self, p): 

919 """Handle @rclick name [@key[=]shortcut].""" 

920 c = self.c 

921 if not p.h.strip(): 

922 return 

923 args = self.getArgs(p) 

924 

925 def atCommandCallback(event=None, args=args, c=c, p=p.copy()): 

926 # pylint: disable=dangerous-default-value 

927 c.executeScript(args=args, p=p, silent=True) 

928 if p.b.strip(): 

929 self.registerAllCommands( 

930 args=args, 

931 func=atCommandCallback, 

932 h=p.h, 

933 pane='all', 

934 source_c=p.v.context, 

935 tag='local @rclick') 

936 g.app.config.atLocalCommandsList.append(p.copy()) 

937 #@+node:vitalije.20180224113123.1: *4* sc.handleRclicks 

938 def handleRclicks(self, rclicks): 

939 def handlerc(rc): 

940 if rc.children: 

941 for i in rc.children: 

942 handlerc(i) 

943 else: 

944 self.handleAtRclickNode(rc.position) 

945 for rc in rclicks: 

946 handlerc(rc) 

947 

948 #@+node:ekr.20060328125248.14: *4* sc.handleAtScriptNode @script 

949 def handleAtScriptNode(self, p): 

950 """Handle @script nodes.""" 

951 c = self.c 

952 tag = "@script" 

953 assert g.match(p.h, 0, tag) 

954 name = p.h[len(tag) :].strip() 

955 args = self.getArgs(p) 

956 if self.atScriptNodes: 

957 g.blue("executing script %s" % (name)) 

958 c.executeScript(args=args, p=p, useSelectedText=False, silent=True) 

959 else: 

960 g.warning("disabled @script: %s" % (name)) 

961 if 0: 

962 # Do not assume the script will want to remain in this commander. 

963 c.bodyWantsFocus() 

964 #@+node:ekr.20150401125747.1: *3* sc.Standard buttons 

965 #@+node:ekr.20060522105937: *4* sc.createDebugIconButton 'debug-script' 

966 def createDebugIconButton(self): 

967 """Create the 'debug-script' button and the debug-script command.""" 

968 self.createIconButton( 

969 args=None, 

970 text='debug-script', 

971 command=self.runDebugScriptCommand, 

972 statusLine='Debug script in selected node', 

973 kind='debug-script') 

974 #@+node:ekr.20060328125248.20: *4* sc.createRunScriptIconButton 'run-script' 

975 def createRunScriptIconButton(self): 

976 """Create the 'run-script' button and the run-script command.""" 

977 self.createIconButton( 

978 args=None, 

979 text='run-script', 

980 command=self.runScriptCommand, 

981 statusLine='Run script in selected node', 

982 kind='run-script', 

983 ) 

984 #@+node:ekr.20060328125248.22: *4* sc.createScriptButtonIconButton 'script-button' 

985 def createScriptButtonIconButton(self): 

986 """Create the 'script-button' button and the script-button command.""" 

987 self.createIconButton( 

988 args=None, 

989 text='script-button', 

990 command=self.addScriptButtonCommand, 

991 statusLine='Make script button from selected node', 

992 kind="script-button-button") 

993 #@+node:ekr.20061014075212: *3* sc.Utils 

994 #@+node:ekr.20060929135558: *4* sc.cleanButtonText 

995 def cleanButtonText(self, s, minimal=False): 

996 """ 

997 Clean the text following @button or @command so 

998 that it is a valid name of a minibuffer command. 

999 """ 

1000 # #1121: Don't lowercase anything. 

1001 if minimal: 

1002 return s.replace(' ', '-').strip('-') 

1003 for tag in ('@key', '@args', '@color',): 

1004 i = s.find(tag) 

1005 if i > -1: 

1006 j = s.find('@', i + 1) 

1007 if i < j: 

1008 s = s[:i] + s[j:] 

1009 else: 

1010 s = s[:i] 

1011 s = s.strip() 

1012 return s.replace(' ', '-').strip('-') 

1013 #@+node:ekr.20060522104419.1: *4* sc.createBalloon (gui-dependent) 

1014 def createBalloon(self, w, label): 

1015 'Create a balloon for a widget.' 

1016 if g.app.gui.guiName().startswith('qt'): 

1017 # w is a leoIconBarButton. 

1018 if hasattr(w, 'button'): 

1019 w.button.setToolTip(label) 

1020 #@+node:ekr.20060328125248.26: *4* sc.deleteButton 

1021 def deleteButton(self, button, **kw): 

1022 """Delete the given button. 

1023 This is called from callbacks, it is not a callback.""" 

1024 w = button 

1025 if button and self.buttonsDict.get(w): 

1026 del self.buttonsDict[w] 

1027 self.iconBar.deleteButton(w) 

1028 self.c.bodyWantsFocus() 

1029 #@+node:ekr.20080813064908.4: *4* sc.getArgs 

1030 def getArgs(self, p): 

1031 """Return the list of @args field of p.h.""" 

1032 args: List[str] = [] 

1033 if not p: 

1034 return args 

1035 h, tag = p.h, '@args' 

1036 i = h.find(tag) 

1037 if i > -1: 

1038 j = g.skip_ws(h, i + len(tag)) 

1039 # 2011/10/16: Make '=' sign optional. 

1040 if g.match(h, j, '='): 

1041 j += 1 

1042 if 0: 

1043 s = h[j + 1 :].strip() 

1044 else: # new logic 1/3/2014 Jake Peck 

1045 k = h.find('@', j + 1) 

1046 if k == -1: 

1047 k = len(h) 

1048 s = h[j:k].strip() 

1049 args = s.split(',') 

1050 args = [z.strip() for z in args] 

1051 # if args: g.trace(args) 

1052 return args 

1053 #@+node:ekr.20060328125248.15: *4* sc.getButtonText 

1054 def getButtonText(self, h): 

1055 """Returns the button text found in the given headline string""" 

1056 tag = "@button" 

1057 if g.match_word(h, 0, tag): 

1058 h = h[len(tag) :].strip() 

1059 for tag in ('@key', '@args', '@color',): 

1060 i = h.find(tag) 

1061 if i > -1: 

1062 j = h.find('@', i + 1) 

1063 if i < j: 

1064 h = h[:i] + h[j + 1 :] 

1065 else: 

1066 h = h[:i] 

1067 h = h.strip() 

1068 buttonText = h 

1069 # fullButtonText = buttonText 

1070 return buttonText 

1071 #@+node:peckj.20140103101946.10404: *4* sc.getColor 

1072 def getColor(self, h): 

1073 """Returns the background color from the given headline string""" 

1074 color = None 

1075 tag = '@color' 

1076 i = h.find(tag) 

1077 if i > -1: 

1078 j = g.skip_ws(h, i + len(tag)) 

1079 if g.match(h, j, '='): 

1080 j += 1 

1081 k = h.find('@', j + 1) 

1082 if k == -1: 

1083 k = len(h) 

1084 color = h[j:k].strip() 

1085 return color 

1086 #@+node:ekr.20060328125248.16: *4* sc.getShortcut 

1087 def getShortcut(self, h): 

1088 """Return the keyboard shortcut from the given headline string""" 

1089 shortcut = None 

1090 i = h.find('@key') 

1091 if i > -1: 

1092 j = g.skip_ws(h, i + len('@key')) 

1093 if g.match(h, j, '='): 

1094 j += 1 

1095 if 0: 

1096 shortcut = h[j:].strip() 

1097 else: # new logic 1/3/2014 Jake Peck 

1098 k = h.find('@', j + 1) 

1099 if k == -1: 

1100 k = len(h) 

1101 shortcut = h[j:k].strip() 

1102 return shortcut 

1103 #@+node:ekr.20150402042350.1: *4* sc.getScript 

1104 def getScript(self, p): 

1105 """Return the script composed from p and its descendants.""" 

1106 return ( 

1107 g.getScript(self.c, p, 

1108 useSelectedText=False, 

1109 forcePythonSentinels=True, 

1110 useSentinels=True, 

1111 )) 

1112 #@+node:ekr.20120301114648.9932: *4* sc.registerAllCommands 

1113 def registerAllCommands(self, args, func, h, pane, source_c=None, tag=None): 

1114 """Register @button <name> and @rclick <name> and <name>""" 

1115 c, k = self.c, self.c.k 

1116 trace = False and not g.unitTesting 

1117 shortcut = self.getShortcut(h) or '' 

1118 commandName = self.cleanButtonText(h) 

1119 if trace and not g.isascii(commandName): 

1120 g.trace(commandName) 

1121 # Register the original function. 

1122 k.registerCommand( 

1123 allowBinding=True, 

1124 commandName=commandName, 

1125 func=func, 

1126 pane=pane, 

1127 shortcut=shortcut, 

1128 ) 

1129 

1130 # 2013/11/13 Jake Peck: 

1131 # include '@rclick-' in list of tags 

1132 for prefix in ('@button-', '@command-', '@rclick-'): 

1133 if commandName.startswith(prefix): 

1134 commandName2 = commandName[len(prefix) :].strip() 

1135 # Create a *second* func, to avoid collision in c.commandsDict. 

1136 

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

1138 func() 

1139 

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

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

1142 registerAllCommandsCallback.__doc__ = func.__doc__ 

1143 # Make sure we never redefine an existing commandName. 

1144 if commandName2 in c.commandsDict: 

1145 # A warning here would be annoying. 

1146 if trace: 

1147 g.trace('Already in commandsDict: %r' % commandName2) 

1148 else: 

1149 k.registerCommand( 

1150 commandName=commandName2, 

1151 func=registerAllCommandsCallback, 

1152 pane=pane, 

1153 shortcut=None 

1154 ) 

1155 #@+node:ekr.20150402021505.1: *4* sc.setButtonColor 

1156 def setButtonColor(self, b, bg): 

1157 """Set the background color of Qt button b to bg.""" 

1158 if not bg: 

1159 return 

1160 if not bg.startswith('#'): 

1161 bg0 = bg 

1162 d = leoColor.leo_color_database 

1163 bg = d.get(bg.lower()) 

1164 if not bg: 

1165 g.trace('bad color? %s' % bg0) 

1166 return 

1167 try: 

1168 b.button.setStyleSheet("QPushButton{background-color: %s}" % (bg)) 

1169 except Exception: 

1170 # g.es_exception() 

1171 pass # Might not be a valid color. 

1172 #@+node:ekr.20061015125212: *4* sc.truncateButtonText 

1173 def truncateButtonText(self, s): 

1174 # 2011/10/16: Remove @button here only. 

1175 i = 0 

1176 while g.match(s, i, '@'): 

1177 i += 1 

1178 if g.match_word(s, i, 'button'): 

1179 i += 6 

1180 s = s[i:] 

1181 if self.maxButtonSize > 10: 

1182 s = s[:self.maxButtonSize] 

1183 if s.endswith('-'): 

1184 s = s[:-1] 

1185 s = s.strip('-') 

1186 return s.strip() 

1187 #@-others 

1188 

1189scriptingController = ScriptingController 

1190#@+node:ekr.20180328085038.1: ** class EvalController 

1191class EvalController: 

1192 """A class defining all eval-* commands.""" 

1193 #@+others 

1194 #@+node:ekr.20180328130835.1: *3* eval.Birth 

1195 def __init__(self, c): 

1196 """Ctor for EvalController class.""" 

1197 self.answers = [] 

1198 self.c = c 

1199 self.d: Dict[str, Any] = {} 

1200 self.globals_d: Dict[str, Any] = {'c': c, 'g': g, 'p': c.p} 

1201 self.locals_d: Dict[str, Any] = {} 

1202 self.legacy = c.config.getBool('legacy-eval', default=True) 

1203 if g.app.ipk: 

1204 # Use the IPython namespace. 

1205 self.c.vs = g.app.ipk.namespace 

1206 elif self.legacy: 

1207 self.c.vs = self.d 

1208 else: 

1209 self.c.vs = self.globals_d 

1210 # allow the auto-completer to complete in this namespace 

1211 self.c.keyHandler.autoCompleter.namespaces.append(self.c.vs) 

1212 # Updated by do_exec. 

1213 self.last_result = None 

1214 self.old_stderr = None 

1215 self.old_stdout = None 

1216 #@+node:ekr.20180328092221.1: *3* eval.Commands 

1217 #@+node:ekr.20180328085426.2: *4* eval 

1218 @eval_cmd("eval") 

1219 def eval_command(self, event): 

1220 #@+<< eval docstring >> 

1221 #@+node:ekr.20180328100519.1: *5* << eval docstring >> 

1222 """ 

1223 Execute the selected text, if any, or the line containing the cursor. 

1224 

1225 Select next line of text. 

1226 

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

1228 selected text:: 

1229 

1230 import datetime 

1231 today = datetime.date.today() 

1232 

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

1234 statement, not an expression. 

1235 

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

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

1238 

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

1240 allowing execution of indented code. 

1241 

1242 ``g``, ``c``, and ``p`` are available to executing code, assignments 

1243 are made in the ``c.vs`` namespace and persist for the life of ``c``. 

1244 """ 

1245 #@-<< eval docstring >> 

1246 c = self.c 

1247 if c == event.get('c'): 

1248 s = self.get_selected_lines() 

1249 if self.legacy and s is None: 

1250 return 

1251 self.eval_text(s) 

1252 # Updates self.last_answer if there is exactly one answer. 

1253 #@+node:ekr.20180328085426.3: *4* eval-block 

1254 @eval_cmd("eval-block") 

1255 def eval_block(self, event): 

1256 #@+<< eval-block docstring >> 

1257 #@+node:ekr.20180328100415.1: *5* << eval-block docstring >> 

1258 """ 

1259 In the body, "# >>>" marks the end of a code block, and "# <<<" marks 

1260 the end of an output block. E.g.:: 

1261 

1262 a = 2 

1263 # >>> 

1264 4 

1265 # <<< 

1266 b = 2.0*a 

1267 # >>> 

1268 4.0 

1269 # <<< 

1270 

1271 ``eval-block`` evaluates the current code block, either the code block 

1272 the cursor's in, or the code block preceding the output block the cursor's 

1273 in. Subsequent output blocks are marked "# >>> *" to show they may need 

1274 re-evaluation. 

1275 

1276 Note: you don't really need to type the "# >>>" and "# <<<" markers 

1277 because ``eval-block`` will add them as needed. So just type the 

1278 first code block and run ``eval-block``. 

1279 

1280 """ 

1281 #@-<< eval-block docstring >> 

1282 c = self.c 

1283 if c != event.get('c'): 

1284 return 

1285 pos = 0 

1286 lines = [] 

1287 current_seen = False 

1288 for current, source, output in self.get_blocks(): 

1289 lines.append(source) 

1290 lines.append("# >>>" + (" *" if current_seen else "")) 

1291 if current: 

1292 old_log = c.frame.log.logCtrl.getAllText() 

1293 self.eval_text(source) 

1294 new_log = c.frame.log.logCtrl.getAllText()[len(old_log) :] 

1295 lines.append(new_log.strip()) 

1296 if not self.legacy: 

1297 if self.last_result: 

1298 lines.append(self.last_result) 

1299 pos = len('\n'.join(lines)) + 7 

1300 current_seen = True 

1301 else: 

1302 lines.append(output) 

1303 lines.append("# <<<") 

1304 c.p.b = '\n'.join(lines) + '\n' 

1305 c.frame.body.wrapper.setInsertPoint(pos) 

1306 c.redraw() 

1307 c.bodyWantsFocusNow() 

1308 #@+node:ekr.20180328085426.5: *4* eval-last 

1309 @eval_cmd("eval-last") 

1310 def eval_last(self, event, text=None): 

1311 """ 

1312 Insert the last result from ``eval``. 

1313 

1314 Inserted as a string, so ``"1\n2\n3\n4"`` will cover four lines and 

1315 insert no quotes, for ``repr()`` style insertion use ``last-pretty``. 

1316 """ 

1317 c = self.c 

1318 if c != event.get('c'): 

1319 return 

1320 if self.legacy: 

1321 text = str(c.vs.get('_last')) 

1322 else: 

1323 if not text and not self.last_result: 

1324 return 

1325 if not text: 

1326 text = str(self.last_result) 

1327 w = c.frame.body.wrapper 

1328 i = w.getInsertPoint() 

1329 w.insert(i, text + '\n') 

1330 w.setInsertPoint(i + len(text) + 1) 

1331 c.setChanged() 

1332 #@+node:ekr.20180328085426.6: *4* eval-last-pretty 

1333 @eval_cmd("eval-last-pretty") 

1334 def vs_last_pretty(self, event): 

1335 """ 

1336 Insert the last result from ``eval``. 

1337 

1338 Formatted by ``pprint.pformat()``, so ``"1\n2\n3\n4"`` will appear as 

1339 '``"1\n2\n3\n4"``', see all ``last``. 

1340 """ 

1341 c = self.c 

1342 if c != event.get('c'): 

1343 return 

1344 if self.legacy: 

1345 text = str(c.vs.get('_last')) 

1346 else: 

1347 text = self.last_result 

1348 if text: 

1349 text = pprint.pformat(text) 

1350 self.eval_last(event, text=text) 

1351 #@+node:ekr.20180328085426.4: *4* eval-replace 

1352 @eval_cmd("eval-replace") 

1353 def eval_replace(self, event): 

1354 """ 

1355 Execute the selected text, if any. 

1356 Undoably replace it with the result. 

1357 """ 

1358 c = self.c 

1359 if c != event.get('c'): 

1360 return 

1361 w = c.frame.body.wrapper 

1362 s = w.getSelectedText() 

1363 if not s.strip(): 

1364 g.es_print('no selected text') 

1365 return 

1366 self.eval_text(s) 

1367 if self.legacy: 

1368 last = c.vs.get('_last') 

1369 else: 

1370 last = self.last_result 

1371 if not last: 

1372 return 

1373 s = pprint.pformat(last) 

1374 i, j = w.getSelectionRange() 

1375 new_text = c.p.b[:i] + s + c.p.b[j:] 

1376 bunch = c.undoer.beforeChangeNodeContents(c.p) 

1377 w.setAllText(new_text) 

1378 c.p.b = new_text 

1379 w.setInsertPoint(i + len(s)) 

1380 c.undoer.afterChangeNodeContents(c.p, 'Insert result', bunch) 

1381 c.setChanged() 

1382 #@+node:ekr.20180328151652.1: *3* eval.Helpers 

1383 #@+node:ekr.20180328090830.1: *4* eval.eval_text & helpers 

1384 def eval_text(self, s): 

1385 """Evaluate string s.""" 

1386 s = textwrap.dedent(s) 

1387 if not s.strip(): 

1388 return None 

1389 self.redirect() 

1390 if self.legacy: 

1391 blocks = re.split('\n(?=[^\\s])', s) 

1392 ans = self.old_exec(blocks, s) 

1393 self.show_legacy_answer(ans, blocks) 

1394 return ans # needed by mod_http 

1395 self.new_exec(s) 

1396 self.show_answers() 

1397 self.unredirect() 

1398 return None 

1399 #@+node:ekr.20180329130626.1: *5* eval.new_exec 

1400 def new_exec(self, s): 

1401 try: 

1402 self.answers = [] 

1403 self.locals_d = {} 

1404 exec(s, self.globals_d, self.locals_d) 

1405 for key in self.locals_d: 

1406 val = self.locals_d.get(key) 

1407 self.globals_d[key] = val 

1408 self.answers.append((key, val),) 

1409 if len(self.answers) == 1: 

1410 key, val = self.answers[0] 

1411 self.last_result = val 

1412 else: 

1413 self.last_result = None 

1414 except Exception: 

1415 g.es_exception() 

1416 #@+node:ekr.20180329130623.1: *5* eval.old_exec 

1417 def old_exec(self, blocks, txt): 

1418 

1419 # pylint: disable=eval-used 

1420 c = self.c 

1421 leo_globals = {'c': c, 'g': g, 'p': c.p} 

1422 all_done, ans = False, None 

1423 try: 

1424 # Execute all but the last 'block' 

1425 exec('\n'.join(blocks[:-1]), leo_globals, c.vs) # Compatible with Python 3.x. 

1426 all_done = False 

1427 except SyntaxError: 

1428 # Splitting the last block caused syntax error 

1429 try: 

1430 # Is the whole thing a single expression? 

1431 ans = eval(txt, leo_globals, c.vs) 

1432 except SyntaxError: 

1433 try: 

1434 exec(txt, leo_globals, c.vs) 

1435 except Exception: 

1436 g.es_exception() 

1437 all_done = True # Either way, the last block will be used. 

1438 if not all_done: # last block still needs using 

1439 try: 

1440 ans = eval(blocks[-1], leo_globals, c.vs) 

1441 except SyntaxError: 

1442 try: 

1443 exec(txt, leo_globals, c.vs) 

1444 except Exception: 

1445 g.es_exception() 

1446 return ans 

1447 #@+node:ekr.20180328130526.1: *5* eval.redirect & unredirect 

1448 def redirect(self): 

1449 c = self.c 

1450 if c.config.getBool('eval-redirect'): 

1451 self.old_stderr = g.stdErrIsRedirected() 

1452 self.old_stdout = g.stdOutIsRedirected() 

1453 if not self.old_stderr: 

1454 g.redirectStderr() 

1455 if not self.old_stdout: 

1456 g.redirectStdout() 

1457 

1458 def unredirect(self): 

1459 c = self.c 

1460 if c.config.getBool('eval-redirect'): 

1461 if not self.old_stderr: 

1462 g.restoreStderr() 

1463 if not self.old_stdout: 

1464 g.restoreStdout() 

1465 #@+node:ekr.20180328132748.1: *5* eval.show_answers 

1466 def show_answers(self): 

1467 """ Show all new values computed by do_exec.""" 

1468 if len(self.answers) > 1: 

1469 g.es('') 

1470 for answer in self.answers: 

1471 key, val = answer 

1472 g.es('%s = %s' % (key, val)) 

1473 #@+node:ekr.20180329154232.1: *5* eval.show_legacy_answer 

1474 def show_legacy_answer(self, ans, blocks): 

1475 

1476 cvs = self.c.vs 

1477 if ans is None: # see if last block was a simple "var =" assignment 

1478 key = blocks[-1].split('=', 1)[0].strip() 

1479 if key in cvs: 

1480 ans = cvs[key] 

1481 if ans is None: # see if whole text was a simple /multi-line/ "var =" assignment 

1482 key = blocks[0].split('=', 1)[0].strip() 

1483 if key in cvs: 

1484 ans = cvs[key] 

1485 cvs['_last'] = ans 

1486 if ans is not None: 

1487 # annoying to echo 'None' to the log during line by line execution 

1488 txt = str(ans) 

1489 lines = txt.split('\n') 

1490 if len(lines) > 10: 

1491 txt = '\n'.join(lines[:5] + ['<snip>'] + lines[-5:]) 

1492 if len(txt) > 500: 

1493 txt = txt[:500] + ' <truncated>' 

1494 g.es(txt) 

1495 return ans 

1496 #@+node:ekr.20180329125626.1: *4* eval.exec_then_eval (not used yet) 

1497 def exec_then_eval(self, code, ns): 

1498 # From Milan Melena. 

1499 import ast 

1500 block = ast.parse(code, mode='exec') 

1501 if block.body and isinstance(block.body[-1], ast.Expr): 

1502 last = ast.Expression(block.body.pop().value) 

1503 exec(compile(block, '<string>', mode='exec'), ns) 

1504 # pylint: disable=eval-used 

1505 return eval(compile(last, '<string>', mode='eval'), ns) 

1506 exec(compile(block, '<string>', mode='exec'), ns) 

1507 return "" 

1508 #@+node:tbrown.20170516194332.1: *4* eval.get_blocks 

1509 def get_blocks(self): 

1510 """get_blocks - iterate code blocks 

1511 

1512 :return: (current, source, output) 

1513 :rtype: (bool, str, str) 

1514 """ 

1515 c = self.c 

1516 pos = c.frame.body.wrapper.getInsertPoint() 

1517 chrs = 0 

1518 lines = c.p.b.split('\n') 

1519 block: Dict[str, List] = {'source': [], 'output': []} 

1520 reading = 'source' 

1521 seeking_current = True 

1522 # if the last non-blank line isn't the end of a possibly empty 

1523 # output block, make it one 

1524 if [i for i in lines if i.strip()][-1] != "# <<<": 

1525 lines.append("# <<<") 

1526 while lines: 

1527 line = lines.pop(0) 

1528 chrs += len(line) + 1 

1529 if line.startswith("# >>>"): 

1530 reading = 'output' 

1531 continue 

1532 if line.startswith("# <<<"): 

1533 current = seeking_current and (chrs >= pos + 1) 

1534 if current: 

1535 seeking_current = False 

1536 yield current, '\n'.join(block['source']), '\n'.join(block['output']) 

1537 block = {'source': [], 'output': []} 

1538 reading = 'source' 

1539 continue 

1540 block[reading].append(line) 

1541 #@+node:ekr.20180328145035.1: *4* eval.get_selected_lines 

1542 def get_selected_lines(self): 

1543 

1544 c, p = self.c, self.c.p 

1545 w = c.frame.body.wrapper 

1546 body = w.getAllText() 

1547 i = w.getInsertPoint() 

1548 if w.hasSelection(): 

1549 if self.legacy: 

1550 i1, i2 = w.getSelectionRange() 

1551 else: 

1552 j, k = w.getSelectionRange() 

1553 i1, junk = g.getLine(body, j) 

1554 junk, i2 = g.getLine(body, k) 

1555 s = body[i1:i2] 

1556 else: 

1557 if self.legacy: 

1558 k = w.getInsertPoint() 

1559 junk, i2 = g.getLine(body, k) 

1560 w.setSelectionRange(k, i2) 

1561 return None 

1562 i1, i2 = g.getLine(body, i) 

1563 s = body[i1:i2].strip() 

1564 # Select next line for next eval. 

1565 if self.legacy: 

1566 i = j = i2 

1567 j += 1 

1568 while j < len(body) and body[j] != '\n': 

1569 j += 1 

1570 w.setSelectionRange(i, j) 

1571 else: 

1572 if not body.endswith('\n'): 

1573 if i >= len(p.b): 

1574 i2 += 1 

1575 p.b = p.b + '\n' 

1576 ins = min(len(p.b), i2) 

1577 w.setSelectionRange(i1, ins, insert=ins, s=p.b) 

1578 return s 

1579 #@-others 

1580#@-others 

1581#@@language python 

1582#@@tabwidth -4 

1583#@-leo