Coverage for C:\Repos\leo-editor\leo\commands\editCommands.py: 57%

2748 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20150514035813.1: * @file ../commands/editCommands.py 

4#@@first 

5"""Leo's general editing commands.""" 

6#@+<< imports >> 

7#@+node:ekr.20150514050149.1: ** << imports >> (editCommands.py) 

8import os 

9import re 

10from typing import Any, List 

11from leo.core import leoGlobals as g 

12from leo.commands.baseCommands import BaseEditCommandsClass 

13#@-<< imports >> 

14 

15def cmd(name): 

16 """Command decorator for the EditCommandsClass class.""" 

17 return g.new_cmd_decorator(name, ['c', 'editCommands',]) 

18 

19#@+others 

20#@+node:ekr.20180504180844.1: ** Top-level helper functions 

21#@+node:ekr.20180504180247.2: *3* function: find_next_trace 

22# Will not find in comments, which is fine. 

23if_pat = re.compile(r'\n[ \t]*(if|elif)\s*trace\b.*:') 

24 

25skip_pat = re.compile(r'=.*in g.app.debug') 

26 

27def find_next_trace(ins, p): 

28 while p: 

29 ins = max(0, ins - 1) # Back up over newline. 

30 s = p.b[ins:] 

31 m = re.search(skip_pat, s) 

32 if m: 

33 # Skip this node. 

34 g.es_print('Skipping', p.h) 

35 else: 

36 m = re.search(if_pat, s) 

37 if m: 

38 i = m.start() + 1 

39 j = m.end() 

40 k = find_trace_block(i, j, s) 

41 i += ins 

42 k += ins 

43 return i, k, p 

44 p.moveToThreadNext() 

45 ins = 0 

46 return None, None, p 

47#@+node:ekr.20180504180247.3: *3* function: find_trace_block 

48def find_trace_block(i, j, s): 

49 """Find the statement or block starting at i.""" 

50 assert s[i] != '\n' 

51 s = s[i:] 

52 lws = len(s) - len(s.lstrip()) 

53 n = 1 # Number of lines to skip. 

54 lines = g.splitLines(s) 

55 for line in lines[1:]: 

56 lws2 = len(line) - len(line.lstrip()) 

57 if lws2 <= lws: 

58 break 

59 n += 1 

60 assert n >= 1 

61 result_lines = lines[:n] 

62 return i + len(''.join(result_lines)) 

63#@+node:ekr.20190926103141.1: *3* function: lineScrollHelper 

64# by Brian Theado. 

65 

66def lineScrollHelper(c, prefix1, prefix2, suffix): 

67 w = c.frame.body.wrapper 

68 ins = w.getInsertPoint() 

69 c.inCommand = False 

70 c.k.simulateCommand(prefix1 + 'line' + suffix) 

71 ins2 = w.getInsertPoint() 

72 # If the cursor didn't change, then go to beginning/end of line 

73 if ins == ins2: 

74 c.k.simulateCommand(prefix2 + 'of-line' + suffix) 

75#@+node:ekr.20201129164455.1: ** Top-level commands 

76#@+node:ekr.20180504180134.1: *3* @g.command('delete-trace-statements') 

77@g.command('delete-trace-statements') 

78def delete_trace_statements(event=None): 

79 """ 

80 Delete all trace statements/blocks from c.p to the end of the outline. 

81 

82 **Warning**: Use this command at your own risk. 

83 

84 It can cause "if" and "else" clauses to become empty, resulting in 

85 syntax errors. Having said that, pyflakes & pylint will usually catch 

86 the problems. 

87 """ 

88 c = event.get('c') 

89 if not c: 

90 return 

91 p = c.p 

92 ins = 0 

93 seen = [] 

94 while True: 

95 i, k, p = find_next_trace(ins, p) 

96 if not p: 

97 g.es_print('done') 

98 return 

99 s = p.b 

100 if p.h not in seen: 

101 seen.append(p.h) 

102 g.es_print('Changed:', p.h) 

103 ins = 0 # Rescanning is essential. 

104 p.b = s[:i] + s[k:] 

105#@+node:ekr.20180210160930.1: *3* @g.command('mark-first-parents') 

106@g.command('mark-first-parents') 

107def mark_first_parents(event): 

108 """Mark the node and all its parents.""" 

109 c = event.get('c') 

110 changed: List[Any] = [] 

111 if not c: 

112 return changed 

113 for parent in c.p.self_and_parents(): 

114 if not parent.isMarked(): 

115 parent.setMarked() 

116 parent.setDirty() 

117 changed.append(parent.copy()) 

118 if changed: 

119 # g.es("marked: " + ', '.join([z.h for z in changed])) 

120 c.setChanged() 

121 c.redraw() 

122 return changed 

123#@+node:ekr.20220515193048.1: *3* @g.command('merge-node-with-next-node') 

124@g.command('merge-node-with-next-node') 

125def merge_node_with_next_node(event=None): 

126 """ 

127 Merge p.b into p.next().b and delete p, *provided* that p has no children. 

128 Undo works, but redo doesn't: probably a bug in the u.before/AfterChangeGroup. 

129 """ 

130 c = event.get('c') 

131 if not c: 

132 return 

133 command, p, u, w = 'merge-node-with-next-node', c.p, c.undoer, c.frame.body.wrapper 

134 if not p or not p.b.strip() or p.hasChildren(): 

135 return 

136 next = p.next() 

137 if not next: 

138 return 

139 # Outer undo. 

140 u.beforeChangeGroup(p, command) 

141 # Inner undo 1: change next.b. 

142 bunch1 = u.beforeChangeBody(next) 

143 next.b = p.b.rstrip() + '\n\n' + next.b 

144 w.setAllText(next.b) 

145 u.afterChangeBody(next, command, bunch1) 

146 # Inner undo 2: delete p. 

147 bunch2 = u.beforeDeleteNode(p) 

148 p.doDelete(next) # This adjusts next._childIndex. 

149 c.selectPosition(next) 

150 u.afterDeleteNode(next, command, bunch2) 

151 # End outer undo: 

152 u.afterChangeGroup(next, command) 

153 c.redraw(next) 

154#@+node:ekr.20220515193124.1: *3* @g.command('merge-node-with-prev-node') 

155@g.command('merge-node-with-prev-node') 

156def merge_node_with_prev_node(event=None): 

157 """ 

158 Merge p.b into p.back().b and delete p, *provided* that p has no children. 

159 Undo works, but redo doesn't: probably a bug in the u.before/AfterChangeGroup. 

160 """ 

161 c = event.get('c') 

162 if not c: 

163 return 

164 command, p, u, w = 'merge-node-with-prev-node', c.p, c.undoer, c.frame.body.wrapper 

165 if not p or not p.b.strip() or p.hasChildren(): 

166 return 

167 prev = p.back() 

168 if not prev: 

169 return 

170 # Outer undo. 

171 u.beforeChangeGroup(p, command) 

172 # Inner undo 1: change prev.b. 

173 bunch1 = u.beforeChangeBody(prev) 

174 prev.b = prev.b.rstrip() + '\n\n' + p.b 

175 w.setAllText(prev.b) 

176 u.afterChangeBody(prev, command, bunch1) 

177 # Inner undo 2: delete p, select prev. 

178 bunch2 = u.beforeDeleteNode(p) 

179 p.doDelete() # No need to adjust prev._childIndex. 

180 c.selectPosition(prev) 

181 u.afterDeleteNode(prev, command, bunch2) 

182 # End outer undo. 

183 u.afterChangeGroup(prev, command) 

184 c.redraw(prev) 

185#@+node:ekr.20190926103245.1: *3* @g.command('next-or-end-of-line') 

186# by Brian Theado. 

187 

188@g.command('next-or-end-of-line') 

189def nextOrEndOfLine(event): 

190 lineScrollHelper(event['c'], 'next-', 'end-', '') 

191#@+node:ekr.20190926103246.2: *3* @g.command('next-or-end-of-line-extend-selection') 

192# by Brian Theado. 

193 

194@g.command('next-or-end-of-line-extend-selection') 

195def nextOrEndOfLineExtendSelection(event): 

196 lineScrollHelper(event['c'], 'next-', 'end-', '-extend-selection') 

197#@+node:ekr.20190926103246.1: *3* @g.command('previous-or-beginning-of-line') 

198# by Brian Theado. 

199 

200@g.command('previous-or-beginning-of-line') 

201def previousOrBeginningOfLine(event): 

202 lineScrollHelper(event['c'], 'previous-', 'beginning-', '') 

203#@+node:ekr.20190926103246.3: *3* @g.command('previous-or-beginning-of-line-extend-selection') 

204# by Brian Theado. 

205 

206@g.command('previous-or-beginning-of-line-extend-selection') 

207def previousOrBeginningOfLineExtendSelection(event): 

208 lineScrollHelper(event['c'], 'previous-', 'beginning-', '-extend-selection') 

209#@+node:ekr.20190323084957.1: *3* @g.command('promote-bodies') 

210@g.command('promote-bodies') 

211def promoteBodies(event): 

212 """Copy the body text of all descendants to the parent's body text.""" 

213 c = event.get('c') 

214 if not c: 

215 return 

216 p = c.p 

217 result = [p.b.rstrip() + '\n'] if p.b.strip() else [] 

218 b = c.undoer.beforeChangeNodeContents(p) 

219 for child in p.subtree(): 

220 h = child.h.strip() 

221 if child.b: 

222 body = '\n'.join([f" {z}" for z in g.splitLines(child.b)]) 

223 s = f"- {h}\n{body}" 

224 else: 

225 s = f"- {h}" 

226 if s.strip(): 

227 result.append(s.strip()) 

228 if result: 

229 result.append('') 

230 p.b = '\n'.join(result) 

231 c.undoer.afterChangeNodeContents(p, 'promote-bodies', b) 

232#@+node:ekr.20190323085410.1: *3* @g.command('promote-headlines') 

233@g.command('promote-headlines') 

234def promoteHeadlines(event): 

235 """Copy the headlines of all descendants to the parent's body text.""" 

236 c = event.get('c') 

237 if not c: 

238 return 

239 p = c.p 

240 b = c.undoer.beforeChangeNodeContents(p) 

241 result = '\n'.join([p.h.rstrip() for p in p.subtree()]) 

242 if result: 

243 p.b = p.b.lstrip() + '\n' + result 

244 c.undoer.afterChangeNodeContents(p, 'promote-headlines', b) 

245#@+node:ekr.20180504180647.1: *3* @g.command('select-next-trace-statement') 

246@g.command('select-next-trace-statement') 

247def select_next_trace_statement(event=None): 

248 """Select the next statement/block enabled by `if trace...:`""" 

249 c = event.get('c') 

250 if not c: 

251 return 

252 w = c.frame.body.wrapper 

253 ins = w.getInsertPoint() 

254 i, k, p = find_next_trace(ins, c.p) 

255 if p: 

256 c.selectPosition(p) 

257 c.redraw() 

258 w.setSelectionRange(i, k, insert=k) 

259 else: 

260 g.es_print('done') 

261 c.bodyWantsFocus() 

262#@+node:ekr.20191010112910.1: *3* @g.command('show-clone-ancestors') 

263@g.command('show-clone-ancestors') 

264def show_clone_ancestors(event=None): 

265 """Display links to all ancestor nodes of the node c.p.""" 

266 c = event.get('c') 

267 if not c: 

268 return 

269 p = c.p 

270 g.es(f"Ancestors of {p.h}...") 

271 for clone in c.all_positions(): 

272 if clone.v == p.v: 

273 unl = message = clone.get_UNL() 

274 # Drop the file part. 

275 i = unl.find('#') 

276 if i > 0: 

277 message = unl[i + 1 :] 

278 # Drop the target node from the message. 

279 parts = message.split('-->') 

280 if len(parts) > 1: 

281 message = '-->'.join(parts[:-1]) 

282 c.frame.log.put(message + '\n', nodeLink=f"{unl}::1") 

283#@+node:ekr.20191007034723.1: *3* @g.command('show-clone-parents') 

284@g.command('show-clone-parents') 

285def show_clones(event=None): 

286 """Display links to all parent nodes of the node c.p.""" 

287 c = event.get('c') 

288 if not c: 

289 return 

290 seen = [] 

291 for clone in c.vnode2allPositions(c.p.v): 

292 parent = clone.parent() 

293 if parent and parent not in seen: 

294 seen.append(parent) 

295 unl = message = parent.get_UNL() 

296 # Drop the file part. 

297 i = unl.find('#') 

298 if i > 0: 

299 message = unl[i + 1 :] 

300 c.frame.log.put(message + '\n', nodeLink=f"{unl}::1") 

301 

302#@+node:ekr.20180210161001.1: *3* @g.command('unmark-first-parents') 

303@g.command('unmark-first-parents') 

304def unmark_first_parents(event=None): 

305 """Unmark the node and all its parents.""" 

306 c = event.get('c') 

307 changed: List[Any] = [] 

308 if not c: 

309 return changed 

310 for parent in c.p.self_and_parents(): 

311 if parent.isMarked(): 

312 parent.clearMarked() 

313 parent.setDirty() 

314 changed.append(parent.copy()) 

315 if changed: 

316 # g.es("unmarked: " + ', '.join([z.h for z in changed])) 

317 c.setChanged() 

318 c.redraw() 

319 return changed 

320#@+node:ekr.20160514100029.1: ** class EditCommandsClass 

321class EditCommandsClass(BaseEditCommandsClass): 

322 """Editing commands with little or no state.""" 

323 # pylint: disable=eval-used 

324 #@+others 

325 #@+node:ekr.20150514063305.116: *3* ec.__init__ 

326 def __init__(self, c): 

327 """Ctor for EditCommandsClass class.""" 

328 # pylint: disable=super-init-not-called 

329 self.c = c 

330 self.ccolumn = 0 # For comment column functions. 

331 self.cursorStack = [] # Values are tuples, (i, j, ins) 

332 self.extendMode = False # True: all cursor move commands extend the selection. 

333 self.fillPrefix = '' # For fill prefix functions. 

334 # Set by the set-fill-column command. 

335 self.fillColumn = 0 # For line centering. If zero, use @pagewidth value. 

336 self.moveSpotNode = None # A VNode. 

337 self.moveSpot = None # For retaining preferred column when moving up or down. 

338 self.moveCol = None # For retaining preferred column when moving up or down. 

339 self.sampleWidget = None # Created later. 

340 self.swapSpots = [] 

341 self._useRegex = False # For replace-string 

342 self.w = None # For use by state handlers. 

343 # Settings... 

344 cf = c.config 

345 self.autocompleteBrackets = cf.getBool('autocomplete-brackets') 

346 if cf.getBool('auto-justify-on-at-start'): 

347 self.autojustify = abs(cf.getInt('auto-justify') or 0) 

348 else: 

349 self.autojustify = 0 

350 self.bracketsFlashBg = cf.getColor('flash-brackets-background-color') 

351 self.bracketsFlashCount = cf.getInt('flash-brackets-count') 

352 self.bracketsFlashDelay = cf.getInt('flash-brackets-delay') 

353 self.bracketsFlashFg = cf.getColor('flash-brackets-foreground-color') 

354 self.flashMatchingBrackets = cf.getBool('flash-matching-brackets') 

355 self.smartAutoIndent = cf.getBool('smart-auto-indent') 

356 self.openBracketsList = cf.getString('open-flash-brackets') or '([{' 

357 self.closeBracketsList = cf.getString('close-flash-brackets') or ')]}' 

358 self.initBracketMatcher(c) 

359 #@+node:ekr.20150514063305.190: *3* ec.cache 

360 @cmd('clear-all-caches') 

361 @cmd('clear-cache') 

362 def clearAllCaches(self, event=None): # pragma: no cover 

363 """Clear all of Leo's file caches.""" 

364 g.app.global_cacher.clear() 

365 g.app.commander_cacher.clear() 

366 

367 @cmd('dump-caches') 

368 def dumpCaches(self, event=None): # pragma: no cover 

369 """Dump, all of Leo's file caches.""" 

370 g.app.global_cacher.dump() 

371 g.app.commander_cacher.dump() 

372 #@+node:ekr.20150514063305.118: *3* ec.doNothing 

373 @cmd('do-nothing') 

374 def doNothing(self, event): 

375 """A placeholder command, useful for testing bindings.""" 

376 pass 

377 #@+node:ekr.20150514063305.278: *3* ec.insertFileName 

378 @cmd('insert-file-name') 

379 def insertFileName(self, event=None): 

380 """ 

381 Prompt for a file name, then insert it at the cursor position. 

382 This operation is undoable if done in the body pane. 

383 

384 The initial path is made by concatenating path_for_p() and the selected 

385 text, if there is any, or any path like text immediately preceding the 

386 cursor. 

387 """ 

388 c, u, w = self.c, self.c.undoer, self.editWidget(event) 

389 if not w: 

390 return 

391 

392 def callback(arg, w=w): 

393 i = w.getSelectionRange()[0] 

394 p = c.p 

395 w.deleteTextSelection() 

396 w.insert(i, arg) 

397 newText = w.getAllText() 

398 if g.app.gui.widget_name(w) == 'body' and p.b != newText: 

399 bunch = u.beforeChangeBody(p) 

400 p.v.b = newText # p.b would cause a redraw. 

401 u.afterChangeBody(p, 'insert-file-name', bunch) 

402 

403 # see if the widget already contains the start of a path 

404 

405 start_text = w.getSelectedText() 

406 if not start_text: # look at text preceeding insert point 

407 start_text = w.getAllText()[: w.getInsertPoint()] 

408 if start_text: 

409 # make non-path characters whitespace 

410 start_text = ''.join(i if i not in '\'"`()[]{}<>!|*,@#$&' else ' ' 

411 for i in start_text) 

412 if start_text[-1].isspace(): # use node path if nothing typed 

413 start_text = '' 

414 else: 

415 start_text = start_text.rsplit(None, 1)[-1] 

416 # set selection range so w.deleteTextSelection() works in the callback 

417 w.setSelectionRange( 

418 w.getInsertPoint() - len(start_text), w.getInsertPoint()) 

419 

420 c.k.functionTail = g.os_path_finalize_join( 

421 self.path_for_p(c, c.p), start_text or '') 

422 c.k.getFileName(event, callback=callback) 

423 #@+node:ekr.20150514063305.279: *3* ec.insertHeadlineTime 

424 @cmd('insert-headline-time') 

425 def insertHeadlineTime(self, event=None): 

426 """Insert a date/time stamp in the headline of the selected node.""" 

427 frame = self 

428 c, p = frame.c, self.c.p 

429 if g.app.batchMode: 

430 c.notValidInBatchMode("Insert Headline Time") 

431 return 

432 # #131: Do not get w from self.editWidget()! 

433 w = c.frame.tree.edit_widget(p) 

434 if w: 

435 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1185933 

436 # insert-headline-time should insert at cursor. 

437 # Note: The command must be bound to a key for this to work. 

438 ins = w.getInsertPoint() 

439 s = c.getTime(body=False) 

440 w.insert(ins, s) 

441 else: 

442 c.endEditing() 

443 time = c.getTime(body=False) 

444 s = p.h.rstrip() 

445 if s: 

446 p.h = ' '.join([s, time]) 

447 else: 

448 p.h = time 

449 c.redrawAndEdit(p, selectAll=True) 

450 #@+node:tom.20210922140250.1: *3* ec.capitalizeHeadline 

451 @cmd('capitalize-headline') 

452 def capitalizeHeadline(self, event=None): 

453 """Capitalize all words in the headline of the selected node.""" 

454 frame = self 

455 c, p, u = frame.c, self.c.p, self.c.undoer 

456 

457 if g.app.batchMode: 

458 c.notValidInBatchMode("Capitalize Headline") 

459 return 

460 

461 h = p.h 

462 undoType = 'capitalize-headline' 

463 undoData = u.beforeChangeNodeContents(p) 

464 

465 words = [w.capitalize() for w in h.split()] 

466 capitalized = ' '.join(words) 

467 changed = capitalized != h 

468 if changed: 

469 p.h = capitalized 

470 c.setChanged() 

471 p.setDirty() 

472 u.afterChangeNodeContents(p, undoType, undoData) 

473 c.redraw() 

474 

475 #@+node:tbrown.20151118134307.1: *3* ec.path_for_p 

476 def path_for_p(self, c, p): 

477 """path_for_p - return the filesystem path (directory) containing 

478 node `p`. 

479 

480 FIXME: this general purpose code should be somewhere else, and there 

481 may already be functions that do some of the work, although perhaps 

482 without handling so many corner cases (@auto-my-custom-type etc.) 

483 

484 :param outline c: outline containing p 

485 :param position p: position to locate 

486 :return: path 

487 :rtype: str 

488 """ 

489 

490 def atfile(p): 

491 """return True if p is an @<file> node *of any kind*""" 

492 word0 = p.h.split()[0] 

493 return ( 

494 word0 in g.app.atFileNames | set(['@auto']) or 

495 word0.startswith('@auto-') 

496 ) 

497 

498 aList = g.get_directives_dict_list(p) 

499 path = c.scanAtPathDirectives(aList) 

500 while c.positionExists(p): 

501 if atfile(p): # see if it's a @<file> node of some sort 

502 nodepath = p.h.split(None, 1)[-1] 

503 nodepath = g.os_path_join(path, nodepath) 

504 if not g.os_path_isdir(nodepath): # remove filename 

505 nodepath = g.os_path_dirname(nodepath) 

506 if g.os_path_isdir(nodepath): # append if it's a directory 

507 path = nodepath 

508 break 

509 p.moveToParent() 

510 

511 return path 

512 #@+node:ekr.20150514063305.347: *3* ec.tabify & untabify 

513 @cmd('tabify') 

514 def tabify(self, event): 

515 """Convert 4 spaces to tabs in the selected text.""" 

516 self.tabifyHelper(event, which='tabify') 

517 

518 @cmd('untabify') 

519 def untabify(self, event): 

520 """Convert tabs to 4 spaces in the selected text.""" 

521 self.tabifyHelper(event, which='untabify') 

522 

523 def tabifyHelper(self, event, which): 

524 w = self.editWidget(event) 

525 if not w or not w.hasSelection(): 

526 return 

527 self.beginCommand(w, undoType=which) 

528 i, end = w.getSelectionRange() 

529 txt = w.getSelectedText() 

530 if which == 'tabify': 

531 pattern = re.compile(r' {4,4}') # Huh? 

532 ntxt = pattern.sub('\t', txt) 

533 else: 

534 pattern = re.compile(r'\t') 

535 ntxt = pattern.sub(' ', txt) 

536 w.delete(i, end) 

537 w.insert(i, ntxt) 

538 n = i + len(ntxt) 

539 w.setSelectionRange(n, n, insert=n) 

540 self.endCommand(changed=True, setLabel=True) 

541 #@+node:ekr.20150514063305.191: *3* ec: capitalization & case 

542 #@+node:ekr.20150514063305.192: *4* ec.capitalizeWord & up/downCaseWord 

543 @cmd('capitalize-word') 

544 def capitalizeWord(self, event): 

545 """Capitalize the word at the cursor.""" 

546 self.capitalizeHelper(event, 'cap', 'capitalize-word') 

547 

548 @cmd('downcase-word') 

549 def downCaseWord(self, event): 

550 """Convert all characters of the word at the cursor to lower case.""" 

551 self.capitalizeHelper(event, 'low', 'downcase-word') 

552 

553 @cmd('upcase-word') 

554 def upCaseWord(self, event): 

555 """Convert all characters of the word at the cursor to UPPER CASE.""" 

556 self.capitalizeHelper(event, 'up', 'upcase-word') 

557 #@+node:ekr.20150514063305.194: *4* ec.capitalizeHelper 

558 def capitalizeHelper(self, event, which, undoType): 

559 w = self.editWidget(event) 

560 if not w: 

561 return # pragma: no cover (defensive) 

562 s = w.getAllText() 

563 ins = w.getInsertPoint() 

564 i, j = g.getWord(s, ins) 

565 word = s[i:j] 

566 if not word.strip(): 

567 return # pragma: no cover (defensive) 

568 self.beginCommand(w, undoType=undoType) 

569 if which == 'cap': 

570 word2 = word.capitalize() 

571 elif which == 'low': 

572 word2 = word.lower() 

573 elif which == 'up': 

574 word2 = word.upper() 

575 else: 

576 g.trace(f"can not happen: which = {s(which)}") 

577 changed = word != word2 

578 if changed: 

579 w.delete(i, j) 

580 w.insert(i, word2) 

581 w.setSelectionRange(ins, ins, insert=ins) 

582 self.endCommand(changed=changed, setLabel=True) 

583 #@+node:tom.20210922171731.1: *4* ec.capitalizeWords & selection 

584 @cmd('capitalize-words-or-selection') 

585 def capitalizeWords(self, event=None): 

586 """Capitalize Entire Body Or Selection.""" 

587 frame = self 

588 c, p, u = frame.c, self.c.p, self.c.undoer 

589 w = frame.editWidget(event) 

590 s = w.getAllText() 

591 if not s: 

592 return 

593 

594 undoType = 'capitalize-body-words' 

595 undoData = u.beforeChangeNodeContents(p) 

596 

597 i, j = w.getSelectionRange() 

598 if i == j: 

599 sel = '' 

600 else: 

601 sel = s[i:j] 

602 text = sel or s 

603 if sel: 

604 prefix = s[:i] 

605 suffix = s[j:] 

606 

607 # Thanks to 

608 # https://thispointer.com/python-capitalize-the-first-letter-of-each-word-in-a-string/ 

609 def convert_to_uppercase(m): 

610 """Convert the second group to uppercase and join both group 1 & group 2""" 

611 return m.group(1) + m.group(2).upper() 

612 

613 capitalized = re.sub(r"(^|\s)(\S)", convert_to_uppercase, text) 

614 

615 if capitalized != text: 

616 p.b = prefix + capitalized + suffix if sel else capitalized 

617 c.setChanged() 

618 p.setDirty() 

619 u.afterChangeNodeContents(p, undoType, undoData) 

620 c.redraw() 

621 #@+node:ekr.20150514063305.195: *3* ec: clicks and focus 

622 #@+node:ekr.20150514063305.196: *4* ec.activate-x-menu & activateMenu 

623 @cmd('activate-cmds-menu') 

624 def activateCmdsMenu(self, event=None): # pragma: no cover 

625 """Activate Leo's Cmnds menu.""" 

626 self.activateMenu('Cmds') 

627 

628 @cmd('activate-edit-menu') 

629 def activateEditMenu(self, event=None): # pragma: no cover 

630 """Activate Leo's Edit menu.""" 

631 self.activateMenu('Edit') 

632 

633 @cmd('activate-file-menu') 

634 def activateFileMenu(self, event=None): # pragma: no cover 

635 """Activate Leo's File menu.""" 

636 self.activateMenu('File') 

637 

638 @cmd('activate-help-menu') 

639 def activateHelpMenu(self, event=None): # pragma: no cover 

640 """Activate Leo's Help menu.""" 

641 self.activateMenu('Help') 

642 

643 @cmd('activate-outline-menu') 

644 def activateOutlineMenu(self, event=None): # pragma: no cover 

645 """Activate Leo's Outline menu.""" 

646 self.activateMenu('Outline') 

647 

648 @cmd('activate-plugins-menu') 

649 def activatePluginsMenu(self, event=None): # pragma: no cover 

650 """Activate Leo's Plugins menu.""" 

651 self.activateMenu('Plugins') 

652 

653 @cmd('activate-window-menu') 

654 def activateWindowMenu(self, event=None): # pragma: no cover 

655 """Activate Leo's Window menu.""" 

656 self.activateMenu('Window') 

657 

658 def activateMenu(self, menuName): # pragma: no cover 

659 c = self.c 

660 c.frame.menu.activateMenu(menuName) 

661 #@+node:ekr.20150514063305.199: *4* ec.focusTo... 

662 @cmd('focus-to-body') 

663 def focusToBody(self, event=None): # pragma: no cover 

664 """Put the keyboard focus in Leo's body pane.""" 

665 c, k = self.c, self.c.k 

666 c.bodyWantsFocus() 

667 if k: 

668 k.setDefaultInputState() 

669 k.showStateAndMode() 

670 

671 @cmd('focus-to-log') 

672 def focusToLog(self, event=None): # pragma: no cover 

673 """Put the keyboard focus in Leo's log pane.""" 

674 self.c.logWantsFocus() 

675 

676 @cmd('focus-to-minibuffer') 

677 def focusToMinibuffer(self, event=None): # pragma: no cover 

678 """Put the keyboard focus in Leo's minibuffer.""" 

679 self.c.minibufferWantsFocus() 

680 

681 @cmd('focus-to-tree') 

682 def focusToTree(self, event=None): # pragma: no cover 

683 """Put the keyboard focus in Leo's outline pane.""" 

684 self.c.treeWantsFocus() 

685 #@+node:ekr.20150514063305.201: *4* ec.clicks in the icon box 

686 # These call the actual event handlers so as to trigger hooks. 

687 

688 @cmd('ctrl-click-icon') 

689 def ctrlClickIconBox(self, event=None): # pragma: no cover 

690 """Simulate a ctrl-click in the icon box of the presently selected node.""" 

691 c = self.c 

692 c.frame.tree.OnIconCtrlClick(c.p) # Calls the base LeoTree method. 

693 

694 @cmd('click-icon-box') 

695 def clickIconBox(self, event=None): # pragma: no cover 

696 """Simulate a click in the icon box of the presently selected node.""" 

697 c = self.c 

698 c.frame.tree.onIconBoxClick(event, p=c.p) 

699 

700 @cmd('double-click-icon-box') 

701 def doubleClickIconBox(self, event=None): # pragma: no cover 

702 """Simulate a double-click in the icon box of the presently selected node.""" 

703 c = self.c 

704 c.frame.tree.onIconBoxDoubleClick(event, p=c.p) 

705 

706 @cmd('right-click-icon') 

707 def rightClickIconBox(self, event=None): # pragma: no cover 

708 """Simulate a right click in the icon box of the presently selected node.""" 

709 c = self.c 

710 c.frame.tree.onIconBoxRightClick(event, p=c.p) 

711 #@+node:ekr.20150514063305.202: *4* ec.clickClickBox 

712 @cmd('click-click-box') 

713 def clickClickBox(self, event=None): # pragma: no cover 

714 """ 

715 Simulate a click in the click box (+- box) of the presently selected node. 

716 

717 Call the actual event handlers so as to trigger hooks. 

718 """ 

719 c = self.c 

720 c.frame.tree.onClickBoxClick(event, p=c.p) 

721 #@+node:ekr.20150514063305.207: *3* ec: comment column 

722 #@+node:ekr.20150514063305.208: *4* ec.setCommentColumn 

723 @cmd('set-comment-column') 

724 def setCommentColumn(self, event): 

725 """Set the comment column for the indent-to-comment-column command.""" 

726 w = self.editWidget(event) 

727 if not w: 

728 return # pragma: no cover (defensive) 

729 s = w.getAllText() 

730 ins = w.getInsertPoint() 

731 row, col = g.convertPythonIndexToRowCol(s, ins) 

732 self.ccolumn = col 

733 #@+node:ekr.20150514063305.209: *4* ec.indentToCommentColumn 

734 @cmd('indent-to-comment-column') 

735 def indentToCommentColumn(self, event): 

736 """ 

737 Insert whitespace to indent the line containing the insert point to the 

738 comment column. 

739 """ 

740 w = self.editWidget(event) 

741 if not w: 

742 return # pragma: no cover (defensive) 

743 self.beginCommand(w, undoType='indent-to-comment-column') 

744 s = w.getAllText() 

745 ins = w.getInsertPoint() 

746 i, j = g.getLine(s, ins) 

747 line = s[i:j] 

748 c1 = self.ccolumn # 2021/07/28: already an int. 

749 line2 = ' ' * c1 + line.lstrip() 

750 if line2 != line: 

751 w.delete(i, j) 

752 w.insert(i, line2) 

753 w.setInsertPoint(i + c1) 

754 self.endCommand(changed=True, setLabel=True) 

755 #@+node:ekr.20150514063305.214: *3* ec: fill column and centering 

756 #@@language rest 

757 #@+at 

758 # These methods are currently just used in tandem to center the line or 

759 # region within the fill column. for example, dependent upon the fill column, this text: 

760 # 

761 # cats 

762 # raaaaaaaaaaaats 

763 # mats 

764 # zaaaaaaaaap 

765 # 

766 # may look like: 

767 # 

768 # cats 

769 # raaaaaaaaaaaats 

770 # mats 

771 # zaaaaaaaaap 

772 # 

773 # after an center-region command via Alt-x. 

774 #@@language python 

775 #@+node:ekr.20150514063305.215: *4* ec.centerLine 

776 @cmd('center-line') 

777 def centerLine(self, event): 

778 """Centers line within current fill column""" 

779 c, k, w = self.c, self.c.k, self.editWidget(event) 

780 if not w: 

781 return # pragma: no cover (defensive) 

782 if self.fillColumn > 0: 

783 fillColumn = self.fillColumn 

784 else: 

785 d = c.scanAllDirectives(c.p) 

786 fillColumn = d.get("pagewidth") 

787 s = w.getAllText() 

788 i, j = g.getLine(s, w.getInsertPoint()) 

789 line = s[i:j].strip() 

790 if not line or len(line) >= fillColumn: 

791 return 

792 self.beginCommand(w, undoType='center-line') 

793 n = (fillColumn - len(line)) / 2 

794 ws = ' ' * int(n) # mypy. 

795 k = g.skip_ws(s, i) 

796 if k > i: 

797 w.delete(i, k - i) 

798 w.insert(i, ws) 

799 self.endCommand(changed=True, setLabel=True) 

800 #@+node:ekr.20150514063305.216: *4* ec.setFillColumn 

801 @cmd('set-fill-column') 

802 def setFillColumn(self, event): 

803 """Set the fill column used by the center-line and center-region commands.""" 

804 k = self.c.k 

805 self.w = self.editWidget(event) 

806 if not self.w: 

807 return # pragma: no cover (defensive) 

808 k.setLabelBlue('Set Fill Column: ') 

809 k.get1Arg(event, handler=self.setFillColumn1) 

810 

811 def setFillColumn1(self, event): 

812 c, k, w = self.c, self.c.k, self.w 

813 k.clearState() 

814 try: 

815 # Bug fix: 2011/05/23: set the fillColumn ivar! 

816 self.fillColumn = n = int(k.arg) 

817 k.setLabelGrey(f"fill column is: {n:d}") 

818 except ValueError: 

819 k.resetLabel() # pragma: no cover (defensive) 

820 c.widgetWantsFocus(w) 

821 #@+node:ekr.20150514063305.217: *4* ec.centerRegion 

822 @cmd('center-region') 

823 def centerRegion(self, event): 

824 """Centers the selected text within the fill column""" 

825 c, k, w = self.c, self.c.k, self.editWidget(event) 

826 if not w: 

827 return # pragma: no cover (defensive) 

828 s = w.getAllText() 

829 sel_1, sel_2 = w.getSelectionRange() 

830 ind, junk = g.getLine(s, sel_1) 

831 junk, end = g.getLine(s, sel_2) 

832 if self.fillColumn > 0: 

833 fillColumn = self.fillColumn 

834 else: 

835 d = c.scanAllDirectives(c.p) 

836 fillColumn = d.get("pagewidth") 

837 self.beginCommand(w, undoType='center-region') 

838 inserted = 0 

839 while ind < end: 

840 s = w.getAllText() 

841 i, j = g.getLine(s, ind) 

842 line = s[i:j].strip() 

843 if len(line) >= fillColumn: 

844 ind = j 

845 else: 

846 n = int((fillColumn - len(line)) / 2) 

847 inserted += n 

848 k = g.skip_ws(s, i) 

849 if k > i: 

850 w.delete(i, k - i) 

851 w.insert(i, ' ' * n) 

852 ind = j + n - (k - i) 

853 w.setSelectionRange(sel_1, sel_2 + inserted) 

854 self.endCommand(changed=True, setLabel=True) 

855 #@+node:ekr.20150514063305.218: *4* ec.setFillPrefix 

856 @cmd('set-fill-prefix') 

857 def setFillPrefix(self, event): 

858 """Make the selected text the fill prefix.""" 

859 w = self.editWidget(event) 

860 if not w: 

861 return # pragma: no cover (defensive) 

862 s = w.getAllText() 

863 i, j = w.getSelectionRange() 

864 self.fillPrefix = s[i:j] 

865 #@+node:ekr.20150514063305.219: *4* ec._addPrefix 

866 def _addPrefix(self, ntxt): 

867 ntxt = ntxt.split('.') 

868 ntxt = map(lambda a: self.fillPrefix + a, ntxt) 

869 ntxt = '.'.join(ntxt) 

870 return ntxt 

871 #@+node:ekr.20150514063305.220: *3* ec: find quick support 

872 #@+node:ekr.20150514063305.221: *4* ec.backward/findCharacter & helper 

873 @cmd('backward-find-character') 

874 def backwardFindCharacter(self, event): 

875 """Search backwards for a character.""" 

876 return self.findCharacterHelper(event, backward=True, extend=False) 

877 

878 @cmd('backward-find-character-extend-selection') 

879 def backwardFindCharacterExtendSelection(self, event): 

880 """Search backward for a character, extending the selection.""" 

881 return self.findCharacterHelper(event, backward=True, extend=True) 

882 

883 @cmd('find-character') 

884 def findCharacter(self, event): 

885 """Search for a character.""" 

886 return self.findCharacterHelper(event, backward=False, extend=False) 

887 

888 @cmd('find-character-extend-selection') 

889 def findCharacterExtendSelection(self, event): 

890 """Search for a character, extending the selection.""" 

891 return self.findCharacterHelper(event, backward=False, extend=True) 

892 #@+node:ekr.20150514063305.222: *5* ec.findCharacterHelper 

893 def findCharacterHelper(self, event, backward, extend): 

894 """Put the cursor at the next occurance of a character on a line.""" 

895 k = self.c.k 

896 self.w = self.editWidget(event) 

897 if not self.w: 

898 return 

899 self.event = event 

900 self.backward = backward 

901 self.extend = extend or self.extendMode # Bug fix: 2010/01/19 

902 self.insert = self.w.getInsertPoint() 

903 s = ( 

904 f"{'Backward find' if backward else 'Find'} " 

905 f"character{' & extend' if extend else ''}: ") 

906 k.setLabelBlue(s) 

907 # Get the arg without touching the focus. 

908 k.getArg( 

909 event, handler=self.findCharacter1, oneCharacter=True, useMinibuffer=False) 

910 

911 def findCharacter1(self, event): 

912 k = self.c.k 

913 event, w = self.event, self.w 

914 backward = self.backward 

915 extend = self.extend or self.extendMode 

916 ch = k.arg 

917 s = w.getAllText() 

918 ins = w.toPythonIndex(self.insert) 

919 i = ins + -1 if backward else +1 # skip the present character. 

920 if backward: 

921 start = 0 

922 j = s.rfind(ch, start, max(start, i)) # Skip the character at the cursor. 

923 if j > -1: 

924 self.moveToHelper(event, j, extend) 

925 else: 

926 end = len(s) 

927 j = s.find(ch, min(i, end), end) # Skip the character at the cursor. 

928 if j > -1: 

929 self.moveToHelper(event, j, extend) 

930 k.resetLabel() 

931 k.clearState() 

932 #@+node:ekr.20150514063305.223: *4* ec.findWord and FindWordOnLine & helper 

933 @cmd('find-word') 

934 def findWord(self, event): 

935 """Put the cursor at the next word that starts with a character.""" 

936 return self.findWordHelper(event, oneLine=False) 

937 

938 @cmd('find-word-in-line') 

939 def findWordInLine(self, event): 

940 """Put the cursor at the next word (on a line) that starts with a character.""" 

941 return self.findWordHelper(event, oneLine=True) 

942 #@+node:ekr.20150514063305.224: *5* ec.findWordHelper 

943 def findWordHelper(self, event, oneLine): 

944 k = self.c.k 

945 self.w = self.editWidget(event) 

946 if self.w: 

947 self.oneLineFlag = oneLine 

948 k.setLabelBlue( 

949 f"Find word {'in line ' if oneLine else ''}starting with: ") 

950 k.get1Arg(event, handler=self.findWord1, oneCharacter=True) 

951 

952 def findWord1(self, event): 

953 c, k = self.c, self.c.k 

954 ch = k.arg 

955 if ch: 

956 w = self.w 

957 i = w.getInsertPoint() 

958 s = w.getAllText() 

959 end = len(s) 

960 if self.oneLineFlag: 

961 end = s.find('\n', i) # Limit searches to this line. 

962 if end == -1: 

963 end = len(s) 

964 while i < end: 

965 i = s.find(ch, i + 1, end) # Ensure progress and i > 0. 

966 if i == -1: 

967 break 

968 elif not g.isWordChar(s[i - 1]): 

969 w.setSelectionRange(i, i, insert=i) 

970 break 

971 k.resetLabel() 

972 k.clearState() 

973 c.widgetWantsFocus(w) 

974 #@+node:ekr.20150514063305.225: *3* ec: goto node 

975 #@+node:ekr.20170411065920.1: *4* ec.gotoAnyClone 

976 @cmd('goto-any-clone') 

977 def gotoAnyClone(self, event=None): 

978 """Select then next cloned node, regardless of whether c.p is a clone.""" 

979 c = self.c 

980 p = c.p.threadNext() 

981 while p: 

982 if p.isCloned(): 

983 c.selectPosition(p) 

984 return 

985 p.moveToThreadNext() 

986 g.es('no clones found after', c.p.h) 

987 #@+node:ekr.20150514063305.226: *4* ec.gotoCharacter 

988 @cmd('goto-char') 

989 def gotoCharacter(self, event): 

990 """Put the cursor at the n'th character of the buffer.""" 

991 k = self.c.k 

992 self.w = self.editWidget(event) 

993 if self.w: 

994 k.setLabelBlue("Goto n'th character: ") 

995 k.get1Arg(event, handler=self.gotoCharacter1) 

996 

997 def gotoCharacter1(self, event): 

998 c, k = self.c, self.c.k 

999 n = k.arg 

1000 w = self.w 

1001 ok = False 

1002 if n.isdigit(): 

1003 n = int(n) 

1004 if n >= 0: 

1005 w.setInsertPoint(n) 

1006 w.seeInsertPoint() 

1007 ok = True 

1008 if not ok: 

1009 g.warning('goto-char takes non-negative integer argument') 

1010 k.resetLabel() 

1011 k.clearState() 

1012 c.widgetWantsFocus(w) 

1013 #@+node:ekr.20150514063305.227: *4* ec.gotoGlobalLine 

1014 @cmd('goto-global-line') 

1015 def gotoGlobalLine(self, event): 

1016 """ 

1017 Put the cursor at the line in the *outline* corresponding to the line 

1018 with the given line number *in the external file*. 

1019 

1020 For external files containing sentinels, there may be *several* lines 

1021 in the file that correspond to the same line in the outline. 

1022 

1023 An Easter Egg: <Alt-x>number invokes this code. 

1024 """ 

1025 # Improved docstring for #253: Goto Global line (Alt-G) is inconsistent. 

1026 # https://github.com/leo-editor/leo-editor/issues/253 

1027 k = self.c.k 

1028 self.w = self.editWidget(event) 

1029 if self.w: 

1030 k.setLabelBlue('Goto global line: ') 

1031 k.get1Arg(event, handler=self.gotoGlobalLine1) 

1032 

1033 def gotoGlobalLine1(self, event): 

1034 c, k = self.c, self.c.k 

1035 n = k.arg 

1036 k.resetLabel() 

1037 k.clearState() 

1038 if n.isdigit(): 

1039 # Very important: n is one-based. 

1040 c.gotoCommands.find_file_line(n=int(n)) 

1041 #@+node:ekr.20150514063305.228: *4* ec.gotoLine 

1042 @cmd('goto-line') 

1043 def gotoLine(self, event): 

1044 """Put the cursor at the n'th line of the buffer.""" 

1045 k = self.c.k 

1046 self.w = self.editWidget(event) 

1047 if self.w: 

1048 k.setLabelBlue('Goto line: ') 

1049 k.get1Arg(event, handler=self.gotoLine1) 

1050 

1051 def gotoLine1(self, event): 

1052 c, k = self.c, self.c.k 

1053 n, w = k.arg, self.w 

1054 if n.isdigit(): 

1055 n = int(n) 

1056 s = w.getAllText() 

1057 i = g.convertRowColToPythonIndex(s, n - 1, 0) 

1058 w.setInsertPoint(i) 

1059 w.seeInsertPoint() 

1060 k.resetLabel() 

1061 k.clearState() 

1062 c.widgetWantsFocus(w) 

1063 #@+node:ekr.20150514063305.229: *3* ec: icons 

1064 #@+at 

1065 # To do: 

1066 # - Define standard icons in a subfolder of Icons folder? 

1067 # - Tree control recomputes height of each line. 

1068 #@+node:ekr.20150514063305.230: *4* ec. Helpers 

1069 #@+node:ekr.20150514063305.231: *5* ec.appendImageDictToList 

1070 def appendImageDictToList(self, aList, path, xoffset, **kargs): 

1071 c = self.c 

1072 relPath = path # for finding icon on load in different environment 

1073 path = g.app.gui.getImageFinder(path) 

1074 # pylint: disable=unpacking-non-sequence 

1075 image, image_height = g.app.gui.getTreeImage(c, path) 

1076 if not image: 

1077 g.es('can not load image:', path) 

1078 return xoffset 

1079 if image_height is None: 

1080 yoffset = 0 

1081 else: 

1082 yoffset = 0 # (c.frame.tree.line_height-image_height)/2 

1083 # TNB: I suspect this is being done again in the drawing code 

1084 newEntry = { 

1085 'type': 'file', 

1086 'file': path, 

1087 'relPath': relPath, 

1088 'where': 'beforeHeadline', 

1089 'yoffset': yoffset, 'xoffset': xoffset, 'xpad': 1, # -2, 

1090 'on': 'VNode', 

1091 } 

1092 newEntry.update(kargs) # may switch 'on' to 'VNode' 

1093 aList.append(newEntry) 

1094 xoffset += 2 

1095 return xoffset 

1096 #@+node:ekr.20150514063305.232: *5* ec.dHash 

1097 def dHash(self, d): 

1098 """Hash a dictionary""" 

1099 return ''.join([f"{str(k)}{str(d[k])}" for k in sorted(d)]) 

1100 #@+node:ekr.20150514063305.233: *5* ec.getIconList 

1101 def getIconList(self, p): 

1102 """Return list of icons for position p, call setIconList to apply changes""" 

1103 fromVnode = [] 

1104 if hasattr(p.v, 'unknownAttributes'): 

1105 fromVnode = [dict(i) for i in p.v.u.get('icons', [])] 

1106 for i in fromVnode: 

1107 i['on'] = 'VNode' 

1108 return fromVnode 

1109 #@+node:ekr.20150514063305.234: *5* ec.setIconList & helpers 

1110 def setIconList(self, p, l, setDirty=True): 

1111 """Set list of icons for position p to l""" 

1112 current = self.getIconList(p) 

1113 if not l and not current: 

1114 return # nothing to do 

1115 lHash = ''.join([self.dHash(i) for i in l]) 

1116 cHash = ''.join([self.dHash(i) for i in current]) 

1117 if lHash == cHash: 

1118 # no difference between original and current list of dictionaries 

1119 return 

1120 self._setIconListHelper(p, l, p.v, setDirty) 

1121 if g.app.gui.guiName() == 'qt': 

1122 self.c.frame.tree.updateAllIcons(p) 

1123 #@+node:ekr.20150514063305.235: *6* ec._setIconListHelper 

1124 def _setIconListHelper(self, p, subl, uaLoc, setDirty): 

1125 """icon setting code common between v and t nodes 

1126 

1127 p - postion 

1128 subl - list of icons for the v or t node 

1129 uaLoc - the v or t node 

1130 """ 

1131 if subl: # Update the uA. 

1132 if not hasattr(uaLoc, 'unknownAttributes'): 

1133 uaLoc.unknownAttributes = {} 

1134 uaLoc.unknownAttributes['icons'] = list(subl) 

1135 # g.es((p.h,uaLoc.unknownAttributes['icons'])) 

1136 uaLoc._p_changed = True 

1137 if setDirty: 

1138 p.setDirty() 

1139 else: # delete the uA. 

1140 if hasattr(uaLoc, 'unknownAttributes'): 

1141 if 'icons' in uaLoc.unknownAttributes: 

1142 del uaLoc.unknownAttributes['icons'] 

1143 uaLoc._p_changed = True 

1144 if setDirty: 

1145 p.setDirty() 

1146 #@+node:ekr.20150514063305.236: *4* ec.deleteFirstIcon 

1147 @cmd('delete-first-icon') 

1148 def deleteFirstIcon(self, event=None): 

1149 """Delete the first icon in the selected node's icon list.""" 

1150 c = self.c 

1151 aList = self.getIconList(c.p) 

1152 if aList: 

1153 self.setIconList(c.p, aList[1:]) 

1154 c.setChanged() 

1155 c.redraw_after_icons_changed() 

1156 #@+node:ekr.20150514063305.237: *4* ec.deleteIconByName 

1157 def deleteIconByName(self, t, name, relPath): # t not used. 

1158 """for use by the right-click remove icon callback""" 

1159 c, p = self.c, self.c.p 

1160 aList = self.getIconList(p) 

1161 if not aList: 

1162 return 

1163 basePath = g.os_path_finalize_join(g.app.loadDir, "..", "Icons") # #1341. 

1164 absRelPath = g.os_path_finalize_join(basePath, relPath) # #1341 

1165 name = g.os_path_finalize(name) # #1341 

1166 newList = [] 

1167 for d in aList: 

1168 name2 = d.get('file') 

1169 name2 = g.os_path_finalize(name2) # #1341 

1170 name2rel = d.get('relPath') 

1171 if not (name == name2 or absRelPath == name2 or relPath == name2rel): 

1172 newList.append(d) 

1173 if len(newList) != len(aList): 

1174 self.setIconList(p, newList) 

1175 c.setChanged() 

1176 c.redraw_after_icons_changed() 

1177 else: 

1178 g.trace('not found', name) 

1179 #@+node:ekr.20150514063305.238: *4* ec.deleteLastIcon 

1180 @cmd('delete-last-icon') 

1181 def deleteLastIcon(self, event=None): 

1182 """Delete the first icon in the selected node's icon list.""" 

1183 c = self.c 

1184 aList = self.getIconList(c.p) 

1185 if aList: 

1186 self.setIconList(c.p, aList[:-1]) 

1187 c.setChanged() 

1188 c.redraw_after_icons_changed() 

1189 #@+node:ekr.20150514063305.239: *4* ec.deleteNodeIcons 

1190 @cmd('delete-node-icons') 

1191 def deleteNodeIcons(self, event=None, p=None): 

1192 """Delete all of the selected node's icons.""" 

1193 c = self.c 

1194 p = p or c.p 

1195 if p.u: 

1196 p.v._p_changed = True 

1197 self.setIconList(p, []) 

1198 p.setDirty() 

1199 c.setChanged() 

1200 c.redraw_after_icons_changed() 

1201 #@+node:ekr.20150514063305.240: *4* ec.insertIcon 

1202 @cmd('insert-icon') 

1203 def insertIcon(self, event=None): 

1204 """Prompt for an icon, and insert it into the node's icon list.""" 

1205 c, p = self.c, self.c.p 

1206 iconDir = g.os_path_finalize_join(g.app.loadDir, "..", "Icons") 

1207 os.chdir(iconDir) 

1208 paths = g.app.gui.runOpenFileDialog(c, 

1209 title='Get Icons', 

1210 filetypes=[ 

1211 ('All files', '*'), 

1212 ('Gif', '*.gif'), 

1213 ('Bitmap', '*.bmp'), 

1214 ('Icon', '*.ico'), 

1215 ], 

1216 defaultextension=None, multiple=True) 

1217 if not paths: 

1218 return 

1219 aList: List[Any] = [] 

1220 xoffset = 2 

1221 for path in paths: 

1222 xoffset = self.appendImageDictToList(aList, path, xoffset) 

1223 aList2 = self.getIconList(p) 

1224 aList2.extend(aList) 

1225 self.setIconList(p, aList2) 

1226 c.setChanged() 

1227 c.redraw_after_icons_changed() 

1228 #@+node:ekr.20150514063305.241: *4* ec.insertIconFromFile 

1229 def insertIconFromFile(self, path, p=None, pos=None, **kargs): 

1230 c = self.c 

1231 if not p: 

1232 p = c.p 

1233 aList: List[Any] = [] 

1234 xoffset = 2 

1235 xoffset = self.appendImageDictToList(aList, path, xoffset, **kargs) 

1236 aList2 = self.getIconList(p) 

1237 if pos is None: 

1238 pos = len(aList2) 

1239 aList2.insert(pos, aList[0]) 

1240 self.setIconList(p, aList2) 

1241 c.setChanged() 

1242 c.redraw_after_icons_changed() 

1243 #@+node:ekr.20150514063305.242: *3* ec: indent 

1244 #@+node:ekr.20150514063305.243: *4* ec.deleteIndentation 

1245 @cmd('delete-indentation') 

1246 def deleteIndentation(self, event): 

1247 """Delete indentation in the presently line.""" 

1248 w = self.editWidget(event) 

1249 if not w: 

1250 return # pragma: no cover (defensive) 

1251 s = w.getAllText() 

1252 ins = w.getInsertPoint() 

1253 i, j = g.getLine(s, ins) 

1254 line = s[i:j] 

1255 line2 = s[i:j].lstrip() 

1256 delta = len(line) - len(line2) 

1257 if delta: 

1258 self.beginCommand(w, undoType='delete-indentation') 

1259 w.delete(i, j) 

1260 w.insert(i, line2) 

1261 ins -= delta 

1262 w.setSelectionRange(ins, ins, insert=ins) 

1263 self.endCommand(changed=True, setLabel=True) 

1264 #@+node:ekr.20150514063305.244: *4* ec.indentRelative 

1265 @cmd('indent-relative') 

1266 def indentRelative(self, event): 

1267 """ 

1268 The indent-relative command indents at the point based on the previous 

1269 line (actually, the last non-empty line.) It inserts whitespace at the 

1270 point, moving point, until it is underneath an indentation point in the 

1271 previous line. 

1272 

1273 An indentation point is the end of a sequence of whitespace or the end of 

1274 the line. If the point is farther right than any indentation point in the 

1275 previous line, the whitespace before point is deleted and the first 

1276 indentation point then applicable is used. If no indentation point is 

1277 applicable even then whitespace equivalent to a single tab is inserted. 

1278 """ 

1279 p, u = self.c.p, self.c.undoer 

1280 undoType = 'indent-relative' 

1281 w = self.editWidget(event) 

1282 if not w: 

1283 return # pragma: no cover (defensive) 

1284 s = w.getAllText() 

1285 ins = w.getInsertPoint() 

1286 # Find the previous non-blank line 

1287 i, j = g.getLine(s, ins) 

1288 while 1: 

1289 if i <= 0: 

1290 return 

1291 i, j = g.getLine(s, i - 1) 

1292 line = s[i:j] 

1293 if line.strip(): 

1294 break 

1295 self.beginCommand(w, undoType=undoType) 

1296 try: 

1297 bunch = u.beforeChangeBody(p) 

1298 k = g.skip_ws(s, i) 

1299 ws = s[i:k] 

1300 i2, j2 = g.getLine(s, ins) 

1301 k = g.skip_ws(s, i2) 

1302 line = ws + s[k:j2] 

1303 w.delete(i2, j2) 

1304 w.insert(i2, line) 

1305 w.setInsertPoint(i2 + len(ws)) 

1306 p.v.b = w.getAllText() 

1307 u.afterChangeBody(p, undoType, bunch) 

1308 finally: 

1309 self.endCommand(changed=True, setLabel=True) 

1310 #@+node:ekr.20150514063305.245: *3* ec: info 

1311 #@+node:ekr.20210311154956.1: *4* ec.copyGnx 

1312 @cmd('copy-gnx') 

1313 def copyGnx(self, event): 

1314 """Copy c.p.gnx to the clipboard and display it in the status area.""" 

1315 c = self.c 

1316 if not c: 

1317 return 

1318 gnx = c.p and c.p.gnx 

1319 if not gnx: 

1320 return 

1321 g.app.gui.replaceClipboardWith(gnx) 

1322 status_line = getattr(c.frame, "statusLine", None) 

1323 if status_line: 

1324 status_line.put(f"gnx: {gnx}") 

1325 #@+node:ekr.20150514063305.247: *4* ec.lineNumber 

1326 @cmd('line-number') 

1327 def lineNumber(self, event): 

1328 """Print the line and column number and percentage of insert point.""" 

1329 k = self.c.k 

1330 w = self.editWidget(event) 

1331 if not w: 

1332 return # pragma: no cover (defensive) 

1333 s = w.getAllText() 

1334 i = w.getInsertPoint() 

1335 row, col = g.convertPythonIndexToRowCol(s, i) 

1336 percent = int((i * 100) / len(s)) 

1337 k.setLabelGrey( 

1338 'char: %s row: %d col: %d pos: %d (%d%% of %d)' % ( 

1339 repr(s[i]), row, col, i, percent, len(s))) 

1340 #@+node:ekr.20150514063305.248: *4* ec.viewLossage 

1341 @cmd('view-lossage') 

1342 def viewLossage(self, event): 

1343 """Print recent keystrokes.""" 

1344 print('Recent keystrokes...') 

1345 # #1933: Use repr to show LossageData objects. 

1346 for i, data in enumerate(reversed(g.app.lossage)): 

1347 print(f"{i:>2} {data!r}") 

1348 #@+node:ekr.20211010131039.1: *4* ec.viewRecentCommands 

1349 @cmd('view-recent-commands') 

1350 def viewRecentCommands(self, event): 

1351 """Print recently-executed commands.""" 

1352 c = self.c 

1353 print('Recently-executed commands...') 

1354 for i, command in enumerate(reversed(c.recent_commands_list)): 

1355 print(f"{i:>2} {command}") 

1356 #@+node:ekr.20150514063305.249: *4* ec.whatLine 

1357 @cmd('what-line') 

1358 def whatLine(self, event): 

1359 """Print the line number of the line containing the cursor.""" 

1360 k = self.c.k 

1361 w = self.editWidget(event) 

1362 if w: 

1363 s = w.getAllText() 

1364 i = w.getInsertPoint() 

1365 row, col = g.convertPythonIndexToRowCol(s, i) 

1366 k.keyboardQuit() 

1367 k.setStatusLabel(f"Line {row}") 

1368 #@+node:ekr.20150514063305.250: *3* ec: insert & delete 

1369 #@+node:ekr.20150514063305.251: *4* ec.addSpace/TabToLines & removeSpace/TabFromLines & helper 

1370 @cmd('add-space-to-lines') 

1371 def addSpaceToLines(self, event): 

1372 """Add a space to start of all lines, or all selected lines.""" 

1373 self.addRemoveHelper(event, ch=' ', add=True, undoType='add-space-to-lines') 

1374 

1375 @cmd('add-tab-to-lines') 

1376 def addTabToLines(self, event): 

1377 """Add a tab to start of all lines, or all selected lines.""" 

1378 self.addRemoveHelper(event, ch='\t', add=True, undoType='add-tab-to-lines') 

1379 

1380 @cmd('remove-space-from-lines') 

1381 def removeSpaceFromLines(self, event): 

1382 """Remove a space from start of all lines, or all selected lines.""" 

1383 self.addRemoveHelper( 

1384 event, ch=' ', add=False, undoType='remove-space-from-lines') 

1385 

1386 @cmd('remove-tab-from-lines') 

1387 def removeTabFromLines(self, event): 

1388 """Remove a tab from start of all lines, or all selected lines.""" 

1389 self.addRemoveHelper(event, ch='\t', add=False, undoType='remove-tab-from-lines') 

1390 #@+node:ekr.20150514063305.252: *5* ec.addRemoveHelper 

1391 def addRemoveHelper(self, event, ch, add, undoType): 

1392 c = self.c 

1393 w = self.editWidget(event) 

1394 if not w: 

1395 return 

1396 if w.hasSelection(): 

1397 s = w.getSelectedText() 

1398 else: 

1399 s = w.getAllText() 

1400 if not s: 

1401 return 

1402 # Insert or delete spaces instead of tabs when negative tab width is in effect. 

1403 d = c.scanAllDirectives(c.p) 

1404 width = d.get('tabwidth') 

1405 if ch == '\t' and width < 0: 

1406 ch = ' ' * abs(width) 

1407 self.beginCommand(w, undoType=undoType) 

1408 lines = g.splitLines(s) 

1409 if add: 

1410 result_list = [ch + line for line in lines] 

1411 else: 

1412 result_list = [line[len(ch) :] if line.startswith(ch) else line for line in lines] 

1413 result = ''.join(result_list) 

1414 if w.hasSelection(): 

1415 i, j = w.getSelectionRange() 

1416 w.delete(i, j) 

1417 w.insert(i, result) 

1418 w.setSelectionRange(i, i + len(result)) 

1419 else: 

1420 w.setAllText(result) 

1421 w.setSelectionRange(0, len(s)) 

1422 self.endCommand(changed=True, setLabel=True) 

1423 #@+node:ekr.20150514063305.253: *4* ec.backwardDeleteCharacter 

1424 @cmd('backward-delete-char') 

1425 def backwardDeleteCharacter(self, event=None): 

1426 """Delete the character to the left of the cursor.""" 

1427 c = self.c 

1428 w = self.editWidget(event) 

1429 if not w: 

1430 return # pragma: no cover (defensive) 

1431 wname = c.widget_name(w) 

1432 ins = w.getInsertPoint() 

1433 i, j = w.getSelectionRange() 

1434 if wname.startswith('body'): 

1435 self.beginCommand(w, undoType='Typing') 

1436 changed = True 

1437 try: 

1438 tab_width = c.getTabWidth(c.p) 

1439 if i != j: 

1440 w.delete(i, j) 

1441 w.setSelectionRange(i, i, insert=i) 

1442 elif i == 0: 

1443 changed = False 

1444 elif tab_width > 0: 

1445 w.delete(ins - 1) 

1446 w.setSelectionRange(ins - 1, ins - 1, insert=ins - 1) 

1447 else: 

1448 #@+<< backspace with negative tab_width >> 

1449 #@+node:ekr.20150514063305.254: *5* << backspace with negative tab_width >> 

1450 s = prev = w.getAllText() 

1451 ins = w.getInsertPoint() 

1452 i, j = g.getLine(s, ins) 

1453 s = prev = s[i:ins] 

1454 n = len(prev) 

1455 abs_width = abs(tab_width) 

1456 # Delete up to this many spaces. 

1457 n2 = (n % abs_width) or abs_width 

1458 n2 = min(n, n2) 

1459 count = 0 

1460 while n2 > 0: 

1461 n2 -= 1 

1462 ch = prev[n - count - 1] 

1463 if ch != ' ': 

1464 break 

1465 else: count += 1 

1466 # Make sure we actually delete something. 

1467 i = ins - (max(1, count)) 

1468 w.delete(i, ins) 

1469 w.setSelectionRange(i, i, insert=i) 

1470 #@-<< backspace with negative tab_width >> 

1471 finally: 

1472 # Necessary to make text changes stick. 

1473 self.endCommand(changed=changed, setLabel=False) 

1474 else: 

1475 # No undo in this widget. 

1476 s = w.getAllText() 

1477 # Delete something if we can. 

1478 if i != j: 

1479 j = max(i, min(j, len(s))) 

1480 w.delete(i, j) 

1481 w.setSelectionRange(i, i, insert=i) 

1482 elif ins != 0: 

1483 # Do nothing at the start of the headline. 

1484 w.delete(ins - 1) 

1485 ins = ins - 1 

1486 w.setSelectionRange(ins, ins, insert=ins) 

1487 #@+node:ekr.20150514063305.255: *4* ec.cleanAllLines 

1488 @cmd('clean-all-lines') 

1489 def cleanAllLines(self, event): 

1490 """Clean all lines in the selected tree.""" 

1491 c = self.c 

1492 u = c.undoer 

1493 w = c.frame.body.wrapper 

1494 if not w: 

1495 return 

1496 tag = 'clean-all-lines' 

1497 u.beforeChangeGroup(c.p, tag) 

1498 n = 0 

1499 for p in c.p.self_and_subtree(): 

1500 lines = [] 

1501 for line in g.splitLines(p.b): 

1502 if line.rstrip(): 

1503 lines.append(line.rstrip()) 

1504 if line.endswith('\n'): 

1505 lines.append('\n') 

1506 s2 = ''.join(lines) 

1507 if s2 != p.b: 

1508 print(p.h) 

1509 bunch = u.beforeChangeNodeContents(p) 

1510 p.b = s2 

1511 p.setDirty() 

1512 n += 1 

1513 u.afterChangeNodeContents(p, tag, bunch) 

1514 u.afterChangeGroup(c.p, tag) 

1515 c.redraw_after_icons_changed() 

1516 g.es(f"cleaned {n} nodes") 

1517 #@+node:ekr.20150514063305.256: *4* ec.cleanLines 

1518 @cmd('clean-lines') 

1519 def cleanLines(self, event): 

1520 """Removes trailing whitespace from all lines, preserving newlines. 

1521 """ 

1522 w = self.editWidget(event) 

1523 if not w: 

1524 return # pragma: no cover (defensive) 

1525 if w.hasSelection(): 

1526 s = w.getSelectedText() 

1527 else: 

1528 s = w.getAllText() 

1529 lines = [] 

1530 for line in g.splitlines(s): 

1531 if line.rstrip(): 

1532 lines.append(line.rstrip()) 

1533 if line.endswith('\n'): 

1534 lines.append('\n') 

1535 result = ''.join(lines) 

1536 if s != result: 

1537 self.beginCommand(w, undoType='clean-lines') 

1538 if w.hasSelection(): 

1539 i, j = w.getSelectionRange() 

1540 w.delete(i, j) 

1541 w.insert(i, result) 

1542 w.setSelectionRange(i, j + len(result)) 

1543 else: 

1544 i = w.getInsertPoint() 

1545 w.delete(0, 'end') 

1546 w.insert(0, result) 

1547 w.setInsertPoint(i) 

1548 self.endCommand(changed=True, setLabel=True) 

1549 #@+node:ekr.20150514063305.257: *4* ec.clearSelectedText 

1550 @cmd('clear-selected-text') 

1551 def clearSelectedText(self, event): 

1552 """Delete the selected text.""" 

1553 w = self.editWidget(event) 

1554 if not w: 

1555 return 

1556 i, j = w.getSelectionRange() 

1557 if i == j: 

1558 return 

1559 self.beginCommand(w, undoType='clear-selected-text') 

1560 w.delete(i, j) 

1561 w.setInsertPoint(i) 

1562 self.endCommand(changed=True, setLabel=True) 

1563 #@+node:ekr.20150514063305.258: *4* ec.delete-word & backward-delete-word 

1564 @cmd('delete-word') 

1565 def deleteWord(self, event=None): 

1566 """Delete the word at the cursor.""" 

1567 self.deleteWordHelper(event, forward=True) 

1568 

1569 @cmd('backward-delete-word') 

1570 def backwardDeleteWord(self, event=None): 

1571 """Delete the word in front of the cursor.""" 

1572 self.deleteWordHelper(event, forward=False) 

1573 

1574 # Patch by NH2. 

1575 

1576 @cmd('delete-word-smart') 

1577 def deleteWordSmart(self, event=None): 

1578 """Delete the word at the cursor, treating whitespace 

1579 and symbols smartly.""" 

1580 self.deleteWordHelper(event, forward=True, smart=True) 

1581 

1582 @cmd('backward-delete-word-smart') 

1583 def backwardDeleteWordSmart(self, event=None): 

1584 """Delete the word in front of the cursor, treating whitespace 

1585 and symbols smartly.""" 

1586 self.deleteWordHelper(event, forward=False, smart=True) 

1587 

1588 def deleteWordHelper(self, event, forward, smart=False): 

1589 # c = self.c 

1590 w = self.editWidget(event) 

1591 if not w: 

1592 return 

1593 self.beginCommand(w, undoType="delete-word") 

1594 if w.hasSelection(): 

1595 from_pos, to_pos = w.getSelectionRange() 

1596 else: 

1597 from_pos = w.getInsertPoint() 

1598 self.moveWordHelper(event, extend=False, forward=forward, smart=smart) 

1599 to_pos = w.getInsertPoint() 

1600 # For Tk GUI, make sure to_pos > from_pos 

1601 if from_pos > to_pos: 

1602 from_pos, to_pos = to_pos, from_pos 

1603 w.delete(from_pos, to_pos) 

1604 self.endCommand(changed=True, setLabel=True) 

1605 #@+node:ekr.20150514063305.259: *4* ec.deleteNextChar 

1606 @cmd('delete-char') 

1607 def deleteNextChar(self, event): 

1608 """Delete the character to the right of the cursor.""" 

1609 c, w = self.c, self.editWidget(event) 

1610 if not w: 

1611 return 

1612 wname = c.widget_name(w) 

1613 if wname.startswith('body'): 

1614 s = w.getAllText() 

1615 i, j = w.getSelectionRange() 

1616 self.beginCommand(w, undoType='delete-char') 

1617 changed = True 

1618 if i != j: 

1619 w.delete(i, j) 

1620 w.setInsertPoint(i) 

1621 elif j < len(s): 

1622 w.delete(i) 

1623 w.setInsertPoint(i) 

1624 else: 

1625 changed = False 

1626 self.endCommand(changed=changed, setLabel=False) 

1627 else: 

1628 # No undo in this widget. 

1629 s = w.getAllText() 

1630 i, j = w.getSelectionRange() 

1631 # Delete something if we can. 

1632 if i != j: 

1633 w.delete(i, j) 

1634 w.setInsertPoint(i) 

1635 elif j < len(s): 

1636 w.delete(i) 

1637 w.setInsertPoint(i) 

1638 #@+node:ekr.20150514063305.260: *4* ec.deleteSpaces 

1639 @cmd('delete-spaces') 

1640 def deleteSpaces(self, event, insertspace=False): 

1641 """Delete all whitespace surrounding the cursor.""" 

1642 w = self.editWidget(event) 

1643 if not w: 

1644 return # pragma: no cover (defensive) 

1645 undoType = 'insert-space' if insertspace else 'delete-spaces' 

1646 s = w.getAllText() 

1647 ins = w.getInsertPoint() 

1648 i, j = g.getLine(s, ins) 

1649 w1 = ins - 1 

1650 while w1 >= i and s[w1].isspace(): 

1651 w1 -= 1 

1652 w1 += 1 

1653 w2 = ins 

1654 while w2 <= j and s[w2].isspace(): 

1655 w2 += 1 

1656 spaces = s[w1:w2] 

1657 if spaces: 

1658 self.beginCommand(w, undoType=undoType) 

1659 if insertspace: 

1660 s = s[:w1] + ' ' + s[w2:] 

1661 else: 

1662 s = s[:w1] + s[w2:] 

1663 w.setAllText(s) 

1664 w.setInsertPoint(w1) 

1665 self.endCommand(changed=True, setLabel=True) 

1666 #@+node:ekr.20150514063305.261: *4* ec.insertHardTab 

1667 @cmd('insert-hard-tab') 

1668 def insertHardTab(self, event): 

1669 """Insert one hard tab.""" 

1670 c = self.c 

1671 w = self.editWidget(event) 

1672 if not w: 

1673 return 

1674 if not g.isTextWrapper(w): 

1675 return 

1676 name = c.widget_name(w) 

1677 if name.startswith('head'): 

1678 return 

1679 ins = w.getInsertPoint() 

1680 self.beginCommand(w, undoType='insert-hard-tab') 

1681 w.insert(ins, '\t') 

1682 ins += 1 

1683 w.setSelectionRange(ins, ins, insert=ins) 

1684 self.endCommand() 

1685 #@+node:ekr.20150514063305.262: *4* ec.insertNewLine (insert-newline) 

1686 @cmd('insert-newline') 

1687 def insertNewLine(self, event): 

1688 """Insert a newline at the cursor.""" 

1689 self.insertNewlineBase(event) 

1690 

1691 insertNewline = insertNewLine 

1692 

1693 def insertNewlineBase(self, event): 

1694 """A helper that can be monkey-patched by tables.py plugin.""" 

1695 # Note: insertNewlineHelper already exists. 

1696 c, k = self.c, self.c.k 

1697 w = self.editWidget(event) 

1698 if not w: 

1699 return # pragma: no cover (defensive) 

1700 if not g.isTextWrapper(w): 

1701 return # pragma: no cover (defensive) 

1702 name = c.widget_name(w) 

1703 if name.startswith('head'): 

1704 return 

1705 oldSel = w.getSelectionRange() 

1706 self.beginCommand(w, undoType='newline') 

1707 self.insertNewlineHelper(w=w, oldSel=oldSel, undoType=None) 

1708 k.setInputState('insert') 

1709 k.showStateAndMode() 

1710 self.endCommand() 

1711 #@+node:ekr.20150514063305.263: *4* ec.insertNewLineAndTab (newline-and-indent) 

1712 @cmd('newline-and-indent') 

1713 def insertNewLineAndTab(self, event): 

1714 """Insert a newline and tab at the cursor.""" 

1715 trace = 'keys' in g.app.debug 

1716 c, k = self.c, self.c.k 

1717 p = c.p 

1718 w = self.editWidget(event) 

1719 if not w: 

1720 return 

1721 if not g.isTextWrapper(w): 

1722 return 

1723 name = c.widget_name(w) 

1724 if name.startswith('head'): 

1725 return 

1726 if trace: 

1727 g.trace('(newline-and-indent)') 

1728 self.beginCommand(w, undoType='insert-newline-and-indent') 

1729 oldSel = w.getSelectionRange() 

1730 self.insertNewlineHelper(w=w, oldSel=oldSel, undoType=None) 

1731 self.updateTab(event, p, w, smartTab=False) 

1732 k.setInputState('insert') 

1733 k.showStateAndMode() 

1734 self.endCommand(changed=True, setLabel=False) 

1735 #@+node:ekr.20150514063305.264: *4* ec.insertParentheses 

1736 @cmd('insert-parentheses') 

1737 def insertParentheses(self, event): 

1738 """Insert () at the cursor.""" 

1739 w = self.editWidget(event) 

1740 if w: 

1741 self.beginCommand(w, undoType='insert-parenthesis') 

1742 i = w.getInsertPoint() 

1743 w.insert(i, '()') 

1744 w.setInsertPoint(i + 1) 

1745 self.endCommand(changed=True, setLabel=False) 

1746 #@+node:ekr.20150514063305.265: *4* ec.insertSoftTab 

1747 @cmd('insert-soft-tab') 

1748 def insertSoftTab(self, event): 

1749 """Insert spaces equivalent to one tab.""" 

1750 c = self.c 

1751 w = self.editWidget(event) 

1752 if not w: 

1753 return 

1754 if not g.isTextWrapper(w): 

1755 return 

1756 name = c.widget_name(w) 

1757 if name.startswith('head'): 

1758 return 

1759 tab_width = abs(c.getTabWidth(c.p)) 

1760 ins = w.getInsertPoint() 

1761 self.beginCommand(w, undoType='insert-soft-tab') 

1762 w.insert(ins, ' ' * tab_width) 

1763 ins += tab_width 

1764 w.setSelectionRange(ins, ins, insert=ins) 

1765 self.endCommand() 

1766 #@+node:ekr.20150514063305.266: *4* ec.removeBlankLines (remove-blank-lines) 

1767 @cmd('remove-blank-lines') 

1768 def removeBlankLines(self, event): 

1769 """ 

1770 Remove lines containing nothing but whitespace. 

1771 

1772 Select all lines if there is no existing selection. 

1773 """ 

1774 c, p, u, w = self.c, self.c.p, self.c.undoer, self.editWidget(event) 

1775 # 

1776 # "Before" snapshot. 

1777 bunch = u.beforeChangeBody(p) 

1778 # 

1779 # Initial data. 

1780 oldYview = w.getYScrollPosition() 

1781 lines = g.splitLines(w.getAllText()) 

1782 # 

1783 # Calculate the result. 

1784 result_list = [] 

1785 changed = False 

1786 for line in lines: 

1787 if line.strip(): 

1788 result_list.append(line) 

1789 else: 

1790 changed = True 

1791 if not changed: 

1792 return # pragma: no cover (defensive) 

1793 # 

1794 # Set p.b and w's text first. 

1795 result = ''.join(result_list) 

1796 p.b = result 

1797 w.setAllText(result) 

1798 i, j = 0, max(0, len(result) - 1) 

1799 w.setSelectionRange(i, j, insert=j) 

1800 w.setYScrollPosition(oldYview) 

1801 # 

1802 # "after" snapshot. 

1803 c.undoer.afterChangeBody(p, 'remove-blank-lines', bunch) 

1804 #@+node:ekr.20150514063305.267: *4* ec.replaceCurrentCharacter 

1805 @cmd('replace-current-character') 

1806 def replaceCurrentCharacter(self, event): 

1807 """Replace the current character with the next character typed.""" 

1808 k = self.c.k 

1809 self.w = self.editWidget(event) 

1810 if self.w: 

1811 k.setLabelBlue('Replace Character: ') 

1812 k.get1Arg(event, handler=self.replaceCurrentCharacter1) 

1813 

1814 def replaceCurrentCharacter1(self, event): 

1815 c, k, w = self.c, self.c.k, self.w 

1816 ch = k.arg 

1817 if ch: 

1818 i, j = w.getSelectionRange() 

1819 if i > j: 

1820 i, j = j, i 

1821 # Use raw insert/delete to retain the coloring. 

1822 if i == j: 

1823 i = max(0, i - 1) 

1824 w.delete(i) 

1825 else: 

1826 w.delete(i, j) 

1827 w.insert(i, ch) 

1828 w.setInsertPoint(i + 1) 

1829 k.clearState() 

1830 k.resetLabel() 

1831 k.showStateAndMode() 

1832 c.widgetWantsFocus(w) 

1833 #@+node:ekr.20150514063305.268: *4* ec.selfInsertCommand, helpers 

1834 # @cmd('self-insert-command') 

1835 

1836 def selfInsertCommand(self, event, action='insert'): 

1837 """ 

1838 Insert a character in the body pane. 

1839 

1840 This is the default binding for all keys in the body pane. 

1841 It handles undo, bodykey events, tabs, back-spaces and bracket matching. 

1842 """ 

1843 trace = 'keys' in g.app.debug 

1844 c, p, u, w = self.c, self.c.p, self.c.undoer, self.editWidget(event) 

1845 undoType = 'Typing' 

1846 if not w: 

1847 return # pragma: no cover (defensive) 

1848 #@+<< set local vars >> 

1849 #@+node:ekr.20150514063305.269: *5* << set local vars >> (selfInsertCommand) 

1850 stroke = event.stroke if event else None 

1851 ch = event.char if event else '' 

1852 if ch == 'Return': 

1853 ch = '\n' # This fixes the MacOS return bug. 

1854 if ch == 'Tab': 

1855 ch = '\t' 

1856 name = c.widget_name(w) 

1857 oldSel = w.getSelectionRange() if name.startswith('body') else (None, None) 

1858 oldText = p.b if name.startswith('body') else '' 

1859 oldYview = w.getYScrollPosition() 

1860 brackets = self.openBracketsList + self.closeBracketsList 

1861 inBrackets = ch and g.checkUnicode(ch) in brackets 

1862 #@-<< set local vars >> 

1863 if not ch: 

1864 return 

1865 if trace: 

1866 g.trace('ch', repr(ch)) # and ch in '\n\r\t' 

1867 assert g.isStrokeOrNone(stroke) 

1868 if g.doHook("bodykey1", c=c, p=p, ch=ch, oldSel=oldSel, undoType=undoType): 

1869 return 

1870 if ch == '\t': 

1871 self.updateTab(event, p, w, smartTab=True) 

1872 elif ch == '\b': 

1873 # This is correct: we only come here if there no bindngs for this key. 

1874 self.backwardDeleteCharacter(event) 

1875 elif ch in ('\r', '\n'): 

1876 ch = '\n' 

1877 self.insertNewlineHelper(w, oldSel, undoType) 

1878 elif ch in '\'"' and c.config.getBool('smart-quotes'): 

1879 self.doSmartQuote(action, ch, oldSel, w) 

1880 elif inBrackets and self.autocompleteBrackets: 

1881 self.updateAutomatchBracket(p, w, ch, oldSel) 

1882 elif ch: 

1883 # Null chars must not delete the selection. 

1884 self.doPlainChar(action, ch, event, inBrackets, oldSel, stroke, w) 

1885 # 

1886 # Common processing. 

1887 # Set the column for up and down keys. 

1888 spot = w.getInsertPoint() 

1889 c.editCommands.setMoveCol(w, spot) 

1890 # 

1891 # Update the text and handle undo. 

1892 newText = w.getAllText() 

1893 if newText != oldText: 

1894 # Call u.doTyping to honor the user's undo granularity. 

1895 newSel = w.getSelectionRange() 

1896 newInsert = w.getInsertPoint() 

1897 newSel = w.getSelectionRange() 

1898 newText = w.getAllText() # Converts to unicode. 

1899 u.doTyping(p, 'Typing', oldText, newText, 

1900 oldSel=oldSel, oldYview=oldYview, newInsert=newInsert, newSel=newSel) 

1901 g.doHook("bodykey2", c=c, p=p, ch=ch, oldSel=oldSel, undoType=undoType) 

1902 #@+node:ekr.20160924135613.1: *5* ec.doPlainChar 

1903 def doPlainChar(self, action, ch, event, inBrackets, oldSel, stroke, w): 

1904 c, p = self.c, self.c.p 

1905 isPlain = stroke.find('Alt') == -1 and stroke.find('Ctrl') == -1 

1906 i, j = oldSel 

1907 if i > j: 

1908 i, j = j, i 

1909 # Use raw insert/delete to retain the coloring. 

1910 if i != j: 

1911 w.delete(i, j) 

1912 elif action == 'overwrite': 

1913 w.delete(i) 

1914 if isPlain: 

1915 ins = w.getInsertPoint() 

1916 if self.autojustify > 0 and not inBrackets: 

1917 # Support #14: auto-justify body text. 

1918 s = w.getAllText() 

1919 i = g.skip_to_start_of_line(s, ins) 

1920 i, j = g.getLine(s, i) 

1921 # Only insert a newline at the end of a line. 

1922 if j - i >= self.autojustify and (ins >= len(s) or s[ins] == '\n'): 

1923 # Find the start of the word. 

1924 n = 0 

1925 ins -= 1 

1926 while ins - 1 > 0 and g.isWordChar(s[ins - 1]): 

1927 n += 1 

1928 ins -= 1 

1929 sins = ins # start of insert, to collect trailing whitespace 

1930 while sins > 0 and s[sins - 1] in ' \t': 

1931 sins -= 1 

1932 oldSel = (sins, ins) 

1933 self.insertNewlineHelper(w, oldSel, undoType=None) 

1934 ins = w.getInsertPoint() 

1935 ins += (n + 1) 

1936 w.insert(ins, ch) 

1937 w.setInsertPoint(ins + 1) 

1938 else: 

1939 g.app.gui.insertKeyEvent(event, i) 

1940 if inBrackets and self.flashMatchingBrackets: 

1941 self.flashMatchingBracketsHelper(c, ch, i, p, w) 

1942 #@+node:ekr.20180806045802.1: *5* ec.doSmartQuote 

1943 def doSmartQuote(self, action, ch, oldSel, w): 

1944 """Convert a straight quote to a curly quote, depending on context.""" 

1945 i, j = oldSel 

1946 if i > j: 

1947 i, j = j, i 

1948 # Use raw insert/delete to retain the coloring. 

1949 if i != j: 

1950 w.delete(i, j) 

1951 elif action == 'overwrite': 

1952 w.delete(i) 

1953 ins = w.getInsertPoint() 

1954 # Pick the correct curly quote. 

1955 s = w.getAllText() or "" 

1956 i2 = g.skip_to_start_of_line(s, max(0, ins - 1)) 

1957 open_curly = ins == i2 or ins > i2 and s[ins - 1] in ' \t' # not s[ins-1].isalnum() 

1958 if open_curly: 

1959 ch = '‘' if ch == "'" else "“" 

1960 else: 

1961 ch = '’' if ch == "'" else "”" 

1962 w.insert(ins, ch) 

1963 w.setInsertPoint(ins + 1) 

1964 #@+node:ekr.20150514063305.271: *5* ec.flashCharacter 

1965 def flashCharacter(self, w, i): 

1966 """Flash the character at position i of widget w.""" 

1967 bg = self.bracketsFlashBg or 'DodgerBlue1' 

1968 fg = self.bracketsFlashFg or 'white' 

1969 flashes = self.bracketsFlashCount or 3 

1970 delay = self.bracketsFlashDelay or 75 

1971 w.flashCharacter(i, bg, fg, flashes, delay) 

1972 #@+node:ekr.20150514063305.272: *5* ec.flashMatchingBracketsHelper 

1973 def flashMatchingBracketsHelper(self, c, ch, i, p, w): 

1974 """Flash matching brackets at char ch at position i at widget w.""" 

1975 d = {} 

1976 # pylint: disable=consider-using-enumerate 

1977 if ch in self.openBracketsList: 

1978 for z in range(len(self.openBracketsList)): 

1979 d[self.openBracketsList[z]] = self.closeBracketsList[z] 

1980 # reverse = False # Search forward 

1981 else: 

1982 for z in range(len(self.openBracketsList)): 

1983 d[self.closeBracketsList[z]] = self.openBracketsList[z] 

1984 # reverse = True # Search backward 

1985 s = w.getAllText() 

1986 # A partial fix for bug 127: Bracket matching is buggy. 

1987 language = g.getLanguageAtPosition(c, p) 

1988 if language == 'perl': 

1989 return 

1990 j = g.MatchBrackets(c, p, language).find_matching_bracket(ch, s, i) 

1991 if j is not None: 

1992 self.flashCharacter(w, j) 

1993 #@+node:ekr.20150514063305.273: *5* ec.initBracketMatcher 

1994 def initBracketMatcher(self, c): 

1995 """Init the bracket matching code.""" 

1996 if len(self.openBracketsList) != len(self.closeBracketsList): 

1997 g.es_print('bad open/close_flash_brackets setting: using defaults') 

1998 self.openBracketsList = '([{' 

1999 self.closeBracketsList = ')]}' 

2000 #@+node:ekr.20150514063305.274: *5* ec.insertNewlineHelper 

2001 def insertNewlineHelper(self, w, oldSel, undoType): 

2002 

2003 c, p = self.c, self.c.p 

2004 i, j = oldSel 

2005 ch = '\n' 

2006 if i != j: 

2007 # No auto-indent if there is selected text. 

2008 w.delete(i, j) 

2009 w.insert(i, ch) 

2010 w.setInsertPoint(i + 1) 

2011 else: 

2012 w.insert(i, ch) 

2013 w.setInsertPoint(i + 1) 

2014 if (c.autoindent_in_nocolor or 

2015 (c.frame.body.colorizer.useSyntaxColoring(p) and 

2016 undoType != "Change") 

2017 ): 

2018 # No auto-indent if in @nocolor mode or after a Change command. 

2019 self.updateAutoIndent(p, w) 

2020 w.seeInsertPoint() 

2021 #@+node:ekr.20150514063305.275: *5* ec.updateAutoIndent 

2022 trailing_colon_pat = re.compile(r'^.*:\s*?#.*$') # #2230 

2023 

2024 def updateAutoIndent(self, p, w): 

2025 """Handle auto indentation.""" 

2026 c = self.c 

2027 tab_width = c.getTabWidth(p) 

2028 # Get the previous line. 

2029 s = w.getAllText() 

2030 ins = w.getInsertPoint() 

2031 i = g.skip_to_start_of_line(s, ins) 

2032 i, j = g.getLine(s, i - 1) 

2033 s = s[i : j - 1] 

2034 # Add the leading whitespace to the present line. 

2035 junk, width = g.skip_leading_ws_with_indent(s, 0, tab_width) 

2036 if s.rstrip() and (s.rstrip()[-1] == ':' or self.trailing_colon_pat.match(s)): #2040. 

2037 # For Python: increase auto-indent after colons. 

2038 if g.findLanguageDirectives(c, p) == 'python': 

2039 width += abs(tab_width) 

2040 if self.smartAutoIndent: 

2041 # Determine if prev line has unclosed parens/brackets/braces 

2042 bracketWidths = [width] 

2043 tabex = 0 

2044 for i, ch in enumerate(s): 

2045 if ch == '\t': 

2046 tabex += tab_width - 1 

2047 if ch in '([{': 

2048 bracketWidths.append(i + tabex + 1) 

2049 elif ch in '}])' and len(bracketWidths) > 1: 

2050 bracketWidths.pop() 

2051 width = bracketWidths.pop() 

2052 ws = g.computeLeadingWhitespace(width, tab_width) 

2053 if ws: 

2054 i = w.getInsertPoint() 

2055 w.insert(i, ws) 

2056 w.setInsertPoint(i + len(ws)) 

2057 w.seeInsertPoint() # 2011/10/02: Fix cursor-movement bug. 

2058 #@+node:ekr.20150514063305.276: *5* ec.updateAutomatchBracket 

2059 def updateAutomatchBracket(self, p, w, ch, oldSel): 

2060 

2061 c = self.c 

2062 d = c.scanAllDirectives(p) 

2063 i, j = oldSel 

2064 language = d.get('language') 

2065 s = w.getAllText() 

2066 if ch in ('(', '[', '{',): 

2067 automatch = language not in ('plain',) 

2068 if automatch: 

2069 ch = ch + {'(': ')', '[': ']', '{': '}'}.get(ch) 

2070 if i != j: 

2071 w.delete(i, j) 

2072 w.insert(i, ch) 

2073 if automatch: 

2074 ins = w.getInsertPoint() 

2075 w.setInsertPoint(ins - 1) 

2076 else: 

2077 ins = w.getInsertPoint() 

2078 ch2 = s[ins] if ins < len(s) else '' 

2079 if ch2 in (')', ']', '}'): 

2080 ins = w.getInsertPoint() 

2081 w.setInsertPoint(ins + 1) 

2082 else: 

2083 if i != j: 

2084 w.delete(i, j) 

2085 w.insert(i, ch) 

2086 w.setInsertPoint(i + 1) 

2087 #@+node:ekr.20150514063305.277: *5* ec.updateTab & helper 

2088 def updateTab(self, event, p, w, smartTab=True): 

2089 """ 

2090 A helper for selfInsertCommand. 

2091 

2092 Add spaces equivalent to a tab. 

2093 """ 

2094 c = self.c 

2095 i, j = w.getSelectionRange() # Returns insert point if no selection, with i <= j. 

2096 if i != j: 

2097 c.indentBody(event) 

2098 return 

2099 tab_width = c.getTabWidth(p) 

2100 # Get the preceeding characters. 

2101 s = w.getAllText() 

2102 start, end = g.getLine(s, i) 

2103 after = s[i:end] 

2104 if after.endswith('\n'): 

2105 after = after[:-1] 

2106 # Only do smart tab at the start of a blank line. 

2107 doSmartTab = (smartTab and c.smart_tab and i == start) 

2108 if doSmartTab: 

2109 self.updateAutoIndent(p, w) 

2110 # Add a tab if otherwise nothing would happen. 

2111 if s == w.getAllText(): 

2112 self.doPlainTab(s, i, tab_width, w) 

2113 else: 

2114 self.doPlainTab(s, i, tab_width, w) 

2115 #@+node:ekr.20150514063305.270: *6* ec.doPlainTab 

2116 def doPlainTab(self, s, i, tab_width, w): 

2117 """ 

2118 A helper for selfInsertCommand, called from updateTab. 

2119 

2120 Insert spaces equivalent to one tab. 

2121 """ 

2122 trace = 'keys' in g.app.debug 

2123 start, end = g.getLine(s, i) 

2124 s2 = s[start:i] 

2125 width = g.computeWidth(s2, tab_width) 

2126 if trace: 

2127 g.trace('width', width) 

2128 if tab_width > 0: 

2129 w.insert(i, '\t') 

2130 ins = i + 1 

2131 else: 

2132 n = abs(tab_width) - (width % abs(tab_width)) 

2133 w.insert(i, ' ' * n) 

2134 ins = i + n 

2135 w.setSelectionRange(ins, ins, insert=ins) 

2136 #@+node:ekr.20150514063305.280: *3* ec: lines 

2137 #@+node:ekr.20150514063305.281: *4* ec.flushLines (doesn't work) 

2138 @cmd('flush-lines') 

2139 def flushLines(self, event): 

2140 """ 

2141 Delete each line that contains a match for regexp, operating on the 

2142 text after point. 

2143 

2144 In Transient Mark mode, if the region is active, the command operates 

2145 on the region instead. 

2146 """ 

2147 k = self.c.k 

2148 k.setLabelBlue('Flush lines regexp: ') 

2149 k.get1Arg(event, handler=self.flushLines1) 

2150 

2151 def flushLines1(self, event): 

2152 k = self.c.k 

2153 k.clearState() 

2154 k.resetLabel() 

2155 self.linesHelper(event, k.arg, 'flush') 

2156 #@+node:ekr.20150514063305.282: *4* ec.keepLines (doesn't work) 

2157 @cmd('keep-lines') 

2158 def keepLines(self, event): 

2159 """ 

2160 Delete each line that does not contain a match for regexp, operating on 

2161 the text after point. 

2162 

2163 In Transient Mark mode, if the region is active, the command operates 

2164 on the region instead. 

2165 """ 

2166 k = self.c.k 

2167 k.setLabelBlue('Keep lines regexp: ') 

2168 k.get1Arg(event, handler=self.keepLines1) 

2169 

2170 def keepLines1(self, event): 

2171 k = self.c.k 

2172 k.clearState() 

2173 k.resetLabel() 

2174 self.linesHelper(event, k.arg, 'keep') 

2175 #@+node:ekr.20150514063305.283: *4* ec.linesHelper 

2176 def linesHelper(self, event, pattern, which): 

2177 w = self.editWidget(event) 

2178 if not w: 

2179 return # pragma: no cover (defensive) 

2180 self.beginCommand(w, undoType=which + '-lines') 

2181 if w.hasSelection(): 

2182 i, end = w.getSelectionRange() 

2183 else: 

2184 i = w.getInsertPoint() 

2185 end = 'end' 

2186 txt = w.get(i, end) 

2187 tlines = txt.splitlines(True) 

2188 keeplines = list(tlines) if which == 'flush' else [] 

2189 try: 

2190 regex = re.compile(pattern) 

2191 for n, z in enumerate(tlines): 

2192 f = regex.findall(z) 

2193 if which == 'flush' and f: 

2194 keeplines[n] = None 

2195 elif f: 

2196 keeplines.append(z) 

2197 except Exception: 

2198 return 

2199 if which == 'flush': 

2200 keeplines = [x for x in keeplines if x is not None] 

2201 w.delete(i, end) 

2202 w.insert(i, ''.join(keeplines)) 

2203 w.setInsertPoint(i) 

2204 self.endCommand(changed=True, setLabel=True) 

2205 #@+node:ekr.20200619082429.1: *4* ec.moveLinesToNextNode 

2206 @cmd('move-lines-to-next-node') 

2207 def moveLineToNextNode(self, event): 

2208 """Move one or *trailing* lines to the start of the next node.""" 

2209 c = self.c 

2210 if not c.p.threadNext(): 

2211 return 

2212 w = self.editWidget(event) 

2213 if not w: 

2214 return 

2215 s = w.getAllText() 

2216 sel_1, sel_2 = w.getSelectionRange() 

2217 i, junk = g.getLine(s, sel_1) 

2218 i2, j = g.getLine(s, sel_2) 

2219 lines = s[i:j] 

2220 if not lines.strip(): 

2221 return 

2222 self.beginCommand(w, undoType='move-lines-to-next-node') 

2223 try: 

2224 next_i, next_j = g.getLine(s, j) 

2225 w.delete(i, next_j) 

2226 c.p.b = w.getAllText().rstrip() + '\n' 

2227 c.selectPosition(c.p.threadNext()) 

2228 c.p.b = lines + '\n' + c.p.b 

2229 c.recolor() 

2230 finally: 

2231 self.endCommand(changed=True, setLabel=True) 

2232 #@+node:ekr.20150514063305.284: *4* ec.splitLine 

2233 @cmd('split-line') 

2234 def splitLine(self, event): 

2235 """Split a line at the cursor position.""" 

2236 w = self.editWidget(event) 

2237 if w: 

2238 self.beginCommand(w, undoType='split-line') 

2239 s = w.getAllText() 

2240 ins = w.getInsertPoint() 

2241 w.setAllText(s[:ins] + '\n' + s[ins:]) 

2242 w.setInsertPoint(ins + 1) 

2243 self.endCommand(changed=True, setLabel=True) 

2244 #@+node:ekr.20150514063305.285: *3* ec: move cursor 

2245 #@+node:ekr.20150514063305.286: *4* ec. helpers 

2246 #@+node:ekr.20150514063305.287: *5* ec.extendHelper 

2247 def extendHelper(self, w, extend, spot, upOrDown=False): 

2248 """ 

2249 Handle the details of extending the selection. 

2250 This method is called for all cursor moves. 

2251 

2252 extend: Clear the selection unless this is True. 

2253 spot: The *new* insert point. 

2254 """ 

2255 c, p = self.c, self.c.p 

2256 extend = extend or self.extendMode 

2257 ins = w.getInsertPoint() 

2258 i, j = w.getSelectionRange() 

2259 # Reset the move spot if needed. 

2260 if self.moveSpot is None or p.v != self.moveSpotNode: 

2261 self.setMoveCol(w, ins if extend else spot) # sets self.moveSpot. 

2262 elif extend: 

2263 # 2011/05/20: Fix bug 622819 

2264 # Ctrl-Shift movement is incorrect when there is an unexpected selection. 

2265 if i == j: 

2266 self.setMoveCol(w, ins) # sets self.moveSpot. 

2267 elif self.moveSpot in (i, j) and self.moveSpot != ins: 

2268 # The bug fix, part 1. 

2269 pass 

2270 else: 

2271 # The bug fix, part 2. 

2272 # Set the moveCol to the *not* insert point. 

2273 if ins == i: 

2274 k = j 

2275 elif ins == j: 

2276 k = i 

2277 else: 

2278 k = ins 

2279 self.setMoveCol(w, k) # sets self.moveSpot. 

2280 else: 

2281 if upOrDown: 

2282 s = w.getAllText() 

2283 i2, j2 = g.getLine(s, spot) 

2284 line = s[i2:j2] 

2285 row, col = g.convertPythonIndexToRowCol(s, spot) 

2286 if True: # was j2 < len(s)-1: 

2287 n = min(self.moveCol, max(0, len(line) - 1)) 

2288 else: 

2289 n = min(self.moveCol, max(0, len(line))) # A tricky boundary. 

2290 spot = g.convertRowColToPythonIndex(s, row, n) 

2291 else: # Plain move forward or back. 

2292 self.setMoveCol(w, spot) # sets self.moveSpot. 

2293 if extend: 

2294 if spot < self.moveSpot: 

2295 w.setSelectionRange(spot, self.moveSpot, insert=spot) 

2296 else: 

2297 w.setSelectionRange(self.moveSpot, spot, insert=spot) 

2298 else: 

2299 w.setSelectionRange(spot, spot, insert=spot) 

2300 w.seeInsertPoint() 

2301 c.frame.updateStatusLine() 

2302 #@+node:ekr.20150514063305.288: *5* ec.moveToHelper 

2303 def moveToHelper(self, event, spot, extend): 

2304 """ 

2305 Common helper method for commands the move the cursor 

2306 in a way that can be described by a Tk Text expression. 

2307 """ 

2308 c, k = self.c, self.c.k 

2309 w = self.editWidget(event) 

2310 if not w: 

2311 return 

2312 c.widgetWantsFocusNow(w) 

2313 # Put the request in the proper range. 

2314 if c.widget_name(w).startswith('mini'): 

2315 i, j = k.getEditableTextRange() 

2316 if spot < i: 

2317 spot = i 

2318 elif spot > j: 

2319 spot = j 

2320 self.extendHelper(w, extend, spot, upOrDown=False) 

2321 #@+node:ekr.20150514063305.305: *5* ec.moveWithinLineHelper 

2322 def moveWithinLineHelper(self, event, spot, extend): 

2323 w = self.editWidget(event) 

2324 if not w: 

2325 return 

2326 # Bug fix: 2012/02/28: don't use the Qt end-line logic: 

2327 # it apparently does not work for wrapped lines. 

2328 spots = ('end-line', 'finish-line', 'start-line') 

2329 if hasattr(w, 'leoMoveCursorHelper') and spot not in spots: 

2330 extend = extend or self.extendMode 

2331 w.leoMoveCursorHelper(kind=spot, extend=extend) 

2332 else: 

2333 s = w.getAllText() 

2334 ins = w.getInsertPoint() 

2335 i, j = g.getLine(s, ins) 

2336 line = s[i:j] 

2337 if spot == 'begin-line': # was 'start-line' 

2338 self.moveToHelper(event, i, extend=extend) 

2339 elif spot == 'end-line': 

2340 # Bug fix: 2011/11/13: Significant in external tests. 

2341 if g.match(s, j - 1, '\n') and i != j: 

2342 j -= 1 

2343 self.moveToHelper(event, j, extend=extend) 

2344 elif spot == 'finish-line': 

2345 if not line.isspace(): 

2346 if g.match(s, j - 1, '\n'): 

2347 j -= 1 

2348 while j >= 0 and s[j].isspace(): 

2349 j -= 1 

2350 self.moveToHelper(event, j, extend=extend) 

2351 elif spot == 'start-line': # new 

2352 if not line.isspace(): 

2353 while i < j and s[i].isspace(): 

2354 i += 1 

2355 self.moveToHelper(event, i, extend=extend) 

2356 else: 

2357 g.trace(f"can not happen: bad spot: {spot}") 

2358 #@+node:ekr.20150514063305.317: *5* ec.moveWordHelper 

2359 def moveWordHelper(self, event, extend, forward, end=False, smart=False): 

2360 """ 

2361 Move the cursor to the next/previous word. 

2362 The cursor is placed at the start of the word unless end=True 

2363 """ 

2364 c = self.c 

2365 w = self.editWidget(event) 

2366 if not w: 

2367 return # pragma: no cover (defensive) 

2368 c.widgetWantsFocusNow(w) 

2369 s = w.getAllText() 

2370 n = len(s) 

2371 i = w.getInsertPoint() 

2372 alphanumeric_re = re.compile(r"\w") 

2373 whitespace_re = re.compile(r"\s") 

2374 simple_whitespace_re = re.compile(r"[ \t]") 

2375 #@+others 

2376 #@+node:ekr.20150514063305.318: *6* ec.moveWordHelper functions 

2377 def is_alphanumeric(c): 

2378 return alphanumeric_re.match(c) is not None 

2379 

2380 def is_whitespace(c): 

2381 return whitespace_re.match(c) is not None 

2382 

2383 def is_simple_whitespace(c): 

2384 return simple_whitespace_re.match(c) is not None 

2385 

2386 def is_line_break(c): 

2387 return is_whitespace(c) and not is_simple_whitespace(c) 

2388 

2389 def is_special(c): 

2390 return not is_alphanumeric(c) and not is_whitespace(c) 

2391 

2392 def seek_until_changed(i, match_function, step): 

2393 while 0 <= i < n and match_function(s[i]): 

2394 i += step 

2395 return i 

2396 

2397 def seek_word_end(i): 

2398 return seek_until_changed(i, is_alphanumeric, 1) 

2399 

2400 def seek_word_start(i): 

2401 return seek_until_changed(i, is_alphanumeric, -1) 

2402 

2403 def seek_simple_whitespace_end(i): 

2404 return seek_until_changed(i, is_simple_whitespace, 1) 

2405 

2406 def seek_simple_whitespace_start(i): 

2407 return seek_until_changed(i, is_simple_whitespace, -1) 

2408 

2409 def seek_special_end(i): 

2410 return seek_until_changed(i, is_special, 1) 

2411 

2412 def seek_special_start(i): 

2413 return seek_until_changed(i, is_special, -1) 

2414 #@-others 

2415 if smart: 

2416 if forward: 

2417 if 0 <= i < n: 

2418 if is_alphanumeric(s[i]): 

2419 i = seek_word_end(i) 

2420 i = seek_simple_whitespace_end(i) 

2421 elif is_simple_whitespace(s[i]): 

2422 i = seek_simple_whitespace_end(i) 

2423 elif is_special(s[i]): 

2424 i = seek_special_end(i) 

2425 i = seek_simple_whitespace_end(i) 

2426 else: 

2427 i += 1 # e.g. for newlines 

2428 else: 

2429 i -= 1 # Shift cursor temporarily by -1 to get easy read access to the prev. char 

2430 if 0 <= i < n: 

2431 if is_alphanumeric(s[i]): 

2432 i = seek_word_start(i) 

2433 # Do not seek further whitespace here 

2434 elif is_simple_whitespace(s[i]): 

2435 i = seek_simple_whitespace_start(i) 

2436 elif is_special(s[i]): 

2437 i = seek_special_start(i) 

2438 # Do not seek further whitespace here 

2439 else: 

2440 i -= 1 # e.g. for newlines 

2441 i += 1 

2442 else: 

2443 if forward: 

2444 # Unlike backward-word moves, there are two options... 

2445 if end: 

2446 while 0 <= i < n and not g.isWordChar(s[i]): 

2447 i += 1 

2448 while 0 <= i < n and g.isWordChar(s[i]): 

2449 i += 1 

2450 else: 

2451 #1653. Scan for non-words *first*. 

2452 while 0 <= i < n and not g.isWordChar(s[i]): 

2453 i += 1 

2454 while 0 <= i < n and g.isWordChar(s[i]): 

2455 i += 1 

2456 else: 

2457 i -= 1 

2458 while 0 <= i < n and not g.isWordChar(s[i]): 

2459 i -= 1 

2460 while 0 <= i < n and g.isWordChar(s[i]): 

2461 i -= 1 

2462 i += 1 # 2015/04/30 

2463 self.moveToHelper(event, i, extend) 

2464 #@+node:ekr.20150514063305.289: *5* ec.setMoveCol 

2465 def setMoveCol(self, w, spot): 

2466 """Set the column to which an up or down arrow will attempt to move.""" 

2467 p = self.c.p 

2468 i, row, col = w.toPythonIndexRowCol(spot) 

2469 self.moveSpot = i 

2470 self.moveCol = col 

2471 self.moveSpotNode = p.v 

2472 #@+node:ekr.20150514063305.290: *4* ec.backToHome/ExtendSelection 

2473 @cmd('back-to-home') 

2474 def backToHome(self, event, extend=False): 

2475 """ 

2476 Smart home: 

2477 Position the point at the first non-blank character on the line, 

2478 or the start of the line if already there. 

2479 """ 

2480 w = self.editWidget(event) 

2481 if not w: 

2482 return 

2483 s = w.getAllText() 

2484 ins = w.getInsertPoint() 

2485 if s: 

2486 i, j = g.getLine(s, ins) 

2487 i1 = i 

2488 while i < j and s[i] in ' \t': 

2489 i += 1 

2490 if i == ins: 

2491 i = i1 

2492 self.moveToHelper(event, i, extend=extend) 

2493 

2494 @cmd('back-to-home-extend-selection') 

2495 def backToHomeExtendSelection(self, event): 

2496 self.backToHome(event, extend=True) 

2497 #@+node:ekr.20150514063305.291: *4* ec.backToIndentation 

2498 @cmd('back-to-indentation') 

2499 def backToIndentation(self, event): 

2500 """Position the point at the first non-blank character on the line.""" 

2501 w = self.editWidget(event) 

2502 if not w: 

2503 return # pragma: no cover (defensive) 

2504 s = w.getAllText() 

2505 ins = w.getInsertPoint() 

2506 i, j = g.getLine(s, ins) 

2507 while i < j and s[i] in ' \t': 

2508 i += 1 

2509 self.moveToHelper(event, i, extend=False) 

2510 #@+node:ekr.20150514063305.316: *4* ec.backward*/ExtendSelection 

2511 @cmd('back-word') 

2512 def backwardWord(self, event): 

2513 """Move the cursor to the previous word.""" 

2514 self.moveWordHelper(event, extend=False, forward=False) 

2515 

2516 @cmd('back-word-extend-selection') 

2517 def backwardWordExtendSelection(self, event): 

2518 """Extend the selection by moving the cursor to the previous word.""" 

2519 self.moveWordHelper(event, extend=True, forward=False) 

2520 

2521 @cmd('back-word-smart') 

2522 def backwardWordSmart(self, event): 

2523 """Move the cursor to the beginning of the current or the end of the previous word.""" 

2524 self.moveWordHelper(event, extend=False, forward=False, smart=True) 

2525 

2526 @cmd('back-word-smart-extend-selection') 

2527 def backwardWordSmartExtendSelection(self, event): 

2528 """Extend the selection by moving the cursor to the beginning of the current 

2529 or the end of the previous word.""" 

2530 self.moveWordHelper(event, extend=True, forward=False, smart=True) 

2531 #@+node:ekr.20170707072347.1: *4* ec.beginningOfLine/ExtendSelection 

2532 @cmd('beginning-of-line') 

2533 def beginningOfLine(self, event): 

2534 """Move the cursor to the first character of the line.""" 

2535 self.moveWithinLineHelper(event, 'begin-line', extend=False) 

2536 

2537 @cmd('beginning-of-line-extend-selection') 

2538 def beginningOfLineExtendSelection(self, event): 

2539 """ 

2540 Extend the selection by moving the cursor to the first character of the 

2541 line. 

2542 """ 

2543 self.moveWithinLineHelper(event, 'begin-line', extend=True) 

2544 #@+node:ekr.20150514063305.292: *4* ec.between lines & helper 

2545 @cmd('next-line') 

2546 def nextLine(self, event): 

2547 """Move the cursor down, extending the selection if in extend mode.""" 

2548 self.moveUpOrDownHelper(event, 'down', extend=False) 

2549 

2550 @cmd('next-line-extend-selection') 

2551 def nextLineExtendSelection(self, event): 

2552 """Extend the selection by moving the cursor down.""" 

2553 self.moveUpOrDownHelper(event, 'down', extend=True) 

2554 

2555 @cmd('previous-line') 

2556 def prevLine(self, event): 

2557 """Move the cursor up, extending the selection if in extend mode.""" 

2558 self.moveUpOrDownHelper(event, 'up', extend=False) 

2559 

2560 @cmd('previous-line-extend-selection') 

2561 def prevLineExtendSelection(self, event): 

2562 """Extend the selection by moving the cursor up.""" 

2563 self.moveUpOrDownHelper(event, 'up', extend=True) 

2564 #@+node:ekr.20150514063305.293: *5* ec.moveUpOrDownHelper 

2565 def moveUpOrDownHelper(self, event, direction, extend): 

2566 

2567 w = self.editWidget(event) 

2568 if not w: 

2569 return # pragma: no cover (defensive) 

2570 ins = w.getInsertPoint() 

2571 s = w.getAllText() 

2572 w.seeInsertPoint() 

2573 if hasattr(w, 'leoMoveCursorHelper'): 

2574 extend = extend or self.extendMode 

2575 w.leoMoveCursorHelper(kind=direction, extend=extend) 

2576 else: 

2577 # Find the start of the next/prev line. 

2578 row, col = g.convertPythonIndexToRowCol(s, ins) 

2579 i, j = g.getLine(s, ins) 

2580 if direction == 'down': 

2581 i2, j2 = g.getLine(s, j) 

2582 else: 

2583 i2, j2 = g.getLine(s, i - 1) 

2584 # The spot is the start of the line plus the column index. 

2585 n = max(0, j2 - i2 - 1) # The length of the new line. 

2586 col2 = min(col, n) 

2587 spot = i2 + col2 

2588 self.extendHelper(w, extend, spot, upOrDown=True) 

2589 #@+node:ekr.20150514063305.294: *4* ec.buffers & helper 

2590 @cmd('beginning-of-buffer') 

2591 def beginningOfBuffer(self, event): 

2592 """Move the cursor to the start of the body text.""" 

2593 self.moveToBufferHelper(event, 'home', extend=False) 

2594 

2595 @cmd('beginning-of-buffer-extend-selection') 

2596 def beginningOfBufferExtendSelection(self, event): 

2597 """Extend the text selection by moving the cursor to the start of the body text.""" 

2598 self.moveToBufferHelper(event, 'home', extend=True) 

2599 

2600 @cmd('end-of-buffer') 

2601 def endOfBuffer(self, event): 

2602 """Move the cursor to the end of the body text.""" 

2603 self.moveToBufferHelper(event, 'end', extend=False) 

2604 

2605 @cmd('end-of-buffer-extend-selection') 

2606 def endOfBufferExtendSelection(self, event): 

2607 """Extend the text selection by moving the cursor to the end of the body text.""" 

2608 self.moveToBufferHelper(event, 'end', extend=True) 

2609 #@+node:ekr.20150514063305.295: *5* ec.moveToBufferHelper 

2610 def moveToBufferHelper(self, event, spot, extend): 

2611 w = self.editWidget(event) 

2612 if not w: 

2613 return # pragma: no cover (defensive) 

2614 if hasattr(w, 'leoMoveCursorHelper'): 

2615 extend = extend or self.extendMode 

2616 w.leoMoveCursorHelper(kind=spot, extend=extend) 

2617 else: 

2618 if spot == 'home': 

2619 self.moveToHelper(event, 0, extend=extend) 

2620 elif spot == 'end': 

2621 s = w.getAllText() 

2622 self.moveToHelper(event, len(s), extend=extend) 

2623 else: 

2624 g.trace('can not happen: bad spot', spot) # pragma: no cover (defensive) 

2625 #@+node:ekr.20150514063305.296: *4* ec.characters & helper 

2626 @cmd('back-char') 

2627 def backCharacter(self, event): 

2628 """Move the cursor back one character, extending the selection if in extend mode.""" 

2629 self.moveToCharacterHelper(event, 'left', extend=False) 

2630 

2631 @cmd('back-char-extend-selection') 

2632 def backCharacterExtendSelection(self, event): 

2633 """Extend the selection by moving the cursor back one character.""" 

2634 self.moveToCharacterHelper(event, 'left', extend=True) 

2635 

2636 @cmd('forward-char') 

2637 def forwardCharacter(self, event): 

2638 """Move the cursor forward one character, extending the selection if in extend mode.""" 

2639 self.moveToCharacterHelper(event, 'right', extend=False) 

2640 

2641 @cmd('forward-char-extend-selection') 

2642 def forwardCharacterExtendSelection(self, event): 

2643 """Extend the selection by moving the cursor forward one character.""" 

2644 self.moveToCharacterHelper(event, 'right', extend=True) 

2645 #@+node:ekr.20150514063305.297: *5* ec.moveToCharacterHelper 

2646 def moveToCharacterHelper(self, event, spot, extend): 

2647 w = self.editWidget(event) 

2648 if not w: 

2649 return 

2650 if hasattr(w, 'leoMoveCursorHelper'): 

2651 extend = extend or self.extendMode 

2652 w.leoMoveCursorHelper(kind=spot, extend=extend) 

2653 else: 

2654 i = w.getInsertPoint() 

2655 if spot == 'left': 

2656 i = max(0, i - 1) 

2657 self.moveToHelper(event, i, extend=extend) 

2658 elif spot == 'right': 

2659 i = min(i + 1, len(w.getAllText())) 

2660 self.moveToHelper(event, i, extend=extend) 

2661 else: 

2662 g.trace(f"can not happen: bad spot: {spot}") 

2663 #@+node:ekr.20150514063305.298: *4* ec.clear/set/ToggleExtendMode 

2664 @cmd('clear-extend-mode') 

2665 def clearExtendMode(self, event): 

2666 """Turn off extend mode: cursor movement commands do not extend the selection.""" 

2667 self.extendModeHelper(event, False) 

2668 

2669 @cmd('set-extend-mode') 

2670 def setExtendMode(self, event): 

2671 """Turn on extend mode: cursor movement commands do extend the selection.""" 

2672 self.extendModeHelper(event, True) 

2673 

2674 @cmd('toggle-extend-mode') 

2675 def toggleExtendMode(self, event): 

2676 """Toggle extend mode, i.e., toggle whether cursor movement commands extend the selections.""" 

2677 self.extendModeHelper(event, not self.extendMode) 

2678 

2679 def extendModeHelper(self, event, val): 

2680 c = self.c 

2681 w = self.editWidget(event) 

2682 if w: 

2683 self.extendMode = val 

2684 if not g.unitTesting: 

2685 # g.red('extend mode','on' if val else 'off')) 

2686 c.k.showStateAndMode() 

2687 c.widgetWantsFocusNow(w) 

2688 #@+node:ekr.20170707072524.1: *4* ec.endOfLine/ExtendSelection 

2689 @cmd('end-of-line') 

2690 def endOfLine(self, event): 

2691 """Move the cursor to the last character of the line.""" 

2692 self.moveWithinLineHelper(event, 'end-line', extend=False) 

2693 

2694 @cmd('end-of-line-extend-selection') 

2695 def endOfLineExtendSelection(self, event): 

2696 """Extend the selection by moving the cursor to the last character of the line.""" 

2697 self.moveWithinLineHelper(event, 'end-line', extend=True) 

2698 #@+node:ekr.20150514063305.299: *4* ec.exchangePointMark 

2699 @cmd('exchange-point-mark') 

2700 def exchangePointMark(self, event): 

2701 """ 

2702 Exchange the point (insert point) with the mark (the other end of the 

2703 selected text). 

2704 """ 

2705 c = self.c 

2706 w = self.editWidget(event) 

2707 if not w: 

2708 return 

2709 if hasattr(w, 'leoMoveCursorHelper'): 

2710 w.leoMoveCursorHelper(kind='exchange', extend=False) 

2711 else: 

2712 c.widgetWantsFocusNow(w) 

2713 i, j = w.getSelectionRange(sort=False) 

2714 if i == j: 

2715 return 

2716 ins = w.getInsertPoint() 

2717 ins = j if ins == i else i 

2718 w.setInsertPoint(ins) 

2719 w.setSelectionRange(i, j, insert=None) 

2720 #@+node:ekr.20150514063305.300: *4* ec.extend-to-line 

2721 @cmd('extend-to-line') 

2722 def extendToLine(self, event): 

2723 """Select the line at the cursor.""" 

2724 w = self.editWidget(event) 

2725 if not w: 

2726 return 

2727 s = w.getAllText() 

2728 n = len(s) 

2729 i = w.getInsertPoint() 

2730 while 0 <= i < n and not s[i] == '\n': 

2731 i -= 1 

2732 i += 1 

2733 i1 = i 

2734 while 0 <= i < n and not s[i] == '\n': 

2735 i += 1 

2736 w.setSelectionRange(i1, i) 

2737 #@+node:ekr.20150514063305.301: *4* ec.extend-to-sentence 

2738 @cmd('extend-to-sentence') 

2739 def extendToSentence(self, event): 

2740 """Select the line at the cursor.""" 

2741 w = self.editWidget(event) 

2742 if not w: 

2743 return # pragma: no cover (defensive) 

2744 s = w.getAllText() 

2745 n = len(s) 

2746 i = w.getInsertPoint() 

2747 i2 = 1 + s.find('.', i) 

2748 if i2 == -1: 

2749 i2 = n 

2750 i1 = 1 + s.rfind('.', 0, i2 - 1) 

2751 w.setSelectionRange(i1, i2) 

2752 #@+node:ekr.20150514063305.302: *4* ec.extend-to-word 

2753 @cmd('extend-to-word') 

2754 def extendToWord(self, event, select=True, w=None): 

2755 """Compute the word at the cursor. Select it if select arg is True.""" 

2756 if not w: 

2757 w = self.editWidget(event) 

2758 if not w: 

2759 return 0, 0 # pragma: no cover (defensive) 

2760 s = w.getAllText() 

2761 n = len(s) 

2762 i = i1 = w.getInsertPoint() 

2763 # Find a word char on the present line if one isn't at the cursor. 

2764 if not (0 <= i < n and g.isWordChar(s[i])): 

2765 # First, look forward 

2766 while i < n and not g.isWordChar(s[i]) and s[i] != '\n': 

2767 i += 1 

2768 # Next, look backward. 

2769 if not (0 <= i < n and g.isWordChar(s[i])): 

2770 i = i1 - 1 if (i >= n or s[i] == '\n') else i1 

2771 while i >= 0 and not g.isWordChar(s[i]) and s[i] != '\n': 

2772 i -= 1 

2773 # Make sure s[i] is a word char. 

2774 if 0 <= i < n and g.isWordChar(s[i]): 

2775 # Find the start of the word. 

2776 while 0 <= i < n and g.isWordChar(s[i]): 

2777 i -= 1 

2778 i += 1 

2779 i1 = i 

2780 # Find the end of the word. 

2781 while 0 <= i < n and g.isWordChar(s[i]): 

2782 i += 1 

2783 if select: 

2784 w.setSelectionRange(i1, i) 

2785 return i1, i 

2786 return 0, 0 

2787 #@+node:ekr.20170707072837.1: *4* ec.finishOfLine/ExtendSelection 

2788 @cmd('finish-of-line') 

2789 def finishOfLine(self, event): 

2790 """Move the cursor to the last character of the line.""" 

2791 self.moveWithinLineHelper(event, 'finish-line', extend=False) 

2792 

2793 @cmd('finish-of-line-extend-selection') 

2794 def finishOfLineExtendSelection(self, event): 

2795 """Extend the selection by moving the cursor to the last character of the line.""" 

2796 self.moveWithinLineHelper(event, 'finish-line', extend=True) 

2797 #@+node:ekr.20170707160947.1: *4* ec.forward*/ExtendSelection 

2798 @cmd('forward-end-word') 

2799 def forwardEndWord(self, event): # New in Leo 4.4.2 

2800 """Move the cursor to the next word.""" 

2801 self.moveWordHelper(event, extend=False, forward=True, end=True) 

2802 

2803 @cmd('forward-end-word-extend-selection') 

2804 def forwardEndWordExtendSelection(self, event): # New in Leo 4.4.2 

2805 """Extend the selection by moving the cursor to the next word.""" 

2806 self.moveWordHelper(event, extend=True, forward=True, end=True) 

2807 

2808 @cmd('forward-word') 

2809 def forwardWord(self, event): 

2810 """Move the cursor to the next word.""" 

2811 self.moveWordHelper(event, extend=False, forward=True) 

2812 

2813 @cmd('forward-word-extend-selection') 

2814 def forwardWordExtendSelection(self, event): 

2815 """Extend the selection by moving the cursor to the end of the next word.""" 

2816 self.moveWordHelper(event, extend=True, forward=True) 

2817 

2818 @cmd('forward-word-smart') 

2819 def forwardWordSmart(self, event): 

2820 """Move the cursor to the end of the current or the beginning of the next word.""" 

2821 self.moveWordHelper(event, extend=False, forward=True, smart=True) 

2822 

2823 @cmd('forward-word-smart-extend-selection') 

2824 def forwardWordSmartExtendSelection(self, event): 

2825 """Extend the selection by moving the cursor to the end of the current 

2826 or the beginning of the next word.""" 

2827 self.moveWordHelper(event, extend=True, forward=True, smart=True) 

2828 #@+node:ekr.20150514063305.303: *4* ec.movePastClose & helper 

2829 @cmd('move-past-close') 

2830 def movePastClose(self, event): 

2831 """Move the cursor past the closing parenthesis.""" 

2832 self.movePastCloseHelper(event, extend=False) 

2833 

2834 @cmd('move-past-close-extend-selection') 

2835 def movePastCloseExtendSelection(self, event): 

2836 """Extend the selection by moving the cursor past the closing parenthesis.""" 

2837 self.movePastCloseHelper(event, extend=True) 

2838 #@+node:ekr.20150514063305.304: *5* ec.movePastCloseHelper 

2839 def movePastCloseHelper(self, event, extend): 

2840 c = self.c 

2841 w = self.editWidget(event) 

2842 if not w: 

2843 return 

2844 c.widgetWantsFocusNow(w) 

2845 s = w.getAllText() 

2846 ins = w.getInsertPoint() 

2847 # Scan backwards for i,j. 

2848 i = ins 

2849 while len(s) > i >= 0 and s[i] != '\n': 

2850 if s[i] == '(': 

2851 break 

2852 i -= 1 

2853 else: 

2854 return 

2855 j = ins 

2856 while len(s) > j >= 0 and s[j] != '\n': 

2857 if s[j] == '(': 

2858 break 

2859 j -= 1 

2860 if i < j: 

2861 return 

2862 # Scan forward for i2,j2. 

2863 i2 = ins 

2864 while i2 < len(s) and s[i2] != '\n': 

2865 if s[i2] == ')': 

2866 break 

2867 i2 += 1 

2868 else: 

2869 return 

2870 j2 = ins 

2871 while j2 < len(s) and s[j2] != '\n': 

2872 if s[j2] == ')': 

2873 break 

2874 j2 += 1 

2875 if i2 > j2: 

2876 return 

2877 self.moveToHelper(event, i2 + 1, extend) 

2878 #@+node:ekr.20150514063305.306: *4* ec.pages & helper 

2879 @cmd('back-page') 

2880 def backPage(self, event): 

2881 """Move the cursor back one page, 

2882 extending the selection if in extend mode.""" 

2883 self.movePageHelper(event, kind='back', extend=False) 

2884 

2885 @cmd('back-page-extend-selection') 

2886 def backPageExtendSelection(self, event): 

2887 """Extend the selection by moving the cursor back one page.""" 

2888 self.movePageHelper(event, kind='back', extend=True) 

2889 

2890 @cmd('forward-page') 

2891 def forwardPage(self, event): 

2892 """Move the cursor forward one page, 

2893 extending the selection if in extend mode.""" 

2894 self.movePageHelper(event, kind='forward', extend=False) 

2895 

2896 @cmd('forward-page-extend-selection') 

2897 def forwardPageExtendSelection(self, event): 

2898 """Extend the selection by moving the cursor forward one page.""" 

2899 self.movePageHelper(event, kind='forward', extend=True) 

2900 #@+node:ekr.20150514063305.307: *5* ec.movePageHelper 

2901 def movePageHelper(self, event, kind, extend): # kind in back/forward. 

2902 """Move the cursor up/down one page, possibly extending the selection.""" 

2903 w = self.editWidget(event) 

2904 if not w: 

2905 return 

2906 linesPerPage = 15 # To do. 

2907 if hasattr(w, 'leoMoveCursorHelper'): 

2908 extend = extend or self.extendMode 

2909 w.leoMoveCursorHelper( 

2910 kind='page-down' if kind == 'forward' else 'page-up', 

2911 extend=extend, linesPerPage=linesPerPage) 

2912 # w.seeInsertPoint() 

2913 # c.frame.updateStatusLine() 

2914 # w.rememberSelectionAndScroll() 

2915 else: 

2916 ins = w.getInsertPoint() 

2917 s = w.getAllText() 

2918 lines = g.splitLines(s) 

2919 row, col = g.convertPythonIndexToRowCol(s, ins) 

2920 if kind == 'back': 

2921 row2 = max(0, row - linesPerPage) 

2922 else: 

2923 row2 = min(row + linesPerPage, len(lines) - 1) 

2924 if row == row2: 

2925 return 

2926 spot = g.convertRowColToPythonIndex(s, row2, col, lines=lines) 

2927 self.extendHelper(w, extend, spot, upOrDown=True) 

2928 #@+node:ekr.20150514063305.308: *4* ec.paragraphs & helpers 

2929 @cmd('back-paragraph') 

2930 def backwardParagraph(self, event): 

2931 """Move the cursor to the previous paragraph.""" 

2932 self.backwardParagraphHelper(event, extend=False) 

2933 

2934 @cmd('back-paragraph-extend-selection') 

2935 def backwardParagraphExtendSelection(self, event): 

2936 """Extend the selection by moving the cursor to the previous paragraph.""" 

2937 self.backwardParagraphHelper(event, extend=True) 

2938 

2939 @cmd('forward-paragraph') 

2940 def forwardParagraph(self, event): 

2941 """Move the cursor to the next paragraph.""" 

2942 self.forwardParagraphHelper(event, extend=False) 

2943 

2944 @cmd('forward-paragraph-extend-selection') 

2945 def forwardParagraphExtendSelection(self, event): 

2946 """Extend the selection by moving the cursor to the next paragraph.""" 

2947 self.forwardParagraphHelper(event, extend=True) 

2948 #@+node:ekr.20150514063305.309: *5* ec.backwardParagraphHelper 

2949 def backwardParagraphHelper(self, event, extend): 

2950 w = self.editWidget(event) 

2951 if not w: 

2952 return # pragma: no cover (defensive) 

2953 s = w.getAllText() 

2954 i, j = w.getSelectionRange() 

2955 i, j = g.getLine(s, j) 

2956 line = s[i:j] 

2957 if line.strip(): 

2958 # Find the start of the present paragraph. 

2959 while i > 0: 

2960 i, j = g.getLine(s, i - 1) 

2961 line = s[i:j] 

2962 if not line.strip(): 

2963 break 

2964 # Find the end of the previous paragraph. 

2965 while i > 0: 

2966 i, j = g.getLine(s, i - 1) 

2967 line = s[i:j] 

2968 if line.strip(): 

2969 i = j - 1 

2970 break 

2971 self.moveToHelper(event, i, extend) 

2972 #@+node:ekr.20150514063305.310: *5* ec.forwardParagraphHelper 

2973 def forwardParagraphHelper(self, event, extend): 

2974 w = self.editWidget(event) 

2975 if not w: 

2976 return 

2977 s = w.getAllText() 

2978 ins = w.getInsertPoint() 

2979 i, j = g.getLine(s, ins) 

2980 line = s[i:j] 

2981 if line.strip(): # Skip past the present paragraph. 

2982 self.selectParagraphHelper(w, i) 

2983 i, j = w.getSelectionRange() 

2984 j += 1 

2985 # Skip to the next non-blank line. 

2986 i = j 

2987 while j < len(s): 

2988 i, j = g.getLine(s, j) 

2989 line = s[i:j] 

2990 if line.strip(): 

2991 break 

2992 w.setInsertPoint(ins) # Restore the original insert point. 

2993 self.moveToHelper(event, i, extend) 

2994 #@+node:ekr.20170707093335.1: *4* ec.pushCursor and popCursor 

2995 @cmd('pop-cursor') 

2996 def popCursor(self, event=None): 

2997 """Restore the node, selection range and insert point from the stack.""" 

2998 c = self.c 

2999 w = self.editWidget(event) 

3000 if w and self.cursorStack: 

3001 p, i, j, ins = self.cursorStack.pop() 

3002 if c.positionExists(p): 

3003 c.selectPosition(p) 

3004 c.redraw() 

3005 w.setSelectionRange(i, j, insert=ins) 

3006 c.bodyWantsFocus() 

3007 else: 

3008 g.es('invalid position', c.p.h) 

3009 elif not w: 

3010 g.es('no stacked cursor', color='blue') 

3011 

3012 @cmd('push-cursor') 

3013 def pushCursor(self, event=None): 

3014 """Push the selection range and insert point on the stack.""" 

3015 c = self.c 

3016 w = self.editWidget(event) 

3017 if w: 

3018 p = c.p.copy() 

3019 i, j = w.getSelectionRange() 

3020 ins = w.getInsertPoint() 

3021 self.cursorStack.append((p, i, j, ins),) 

3022 else: 

3023 g.es('cursor not pushed', color='blue') 

3024 #@+node:ekr.20150514063305.311: *4* ec.selectAllText 

3025 @cmd('select-all') 

3026 def selectAllText(self, event): 

3027 """Select all text.""" 

3028 k = self.c.k 

3029 w = self.editWidget(event) 

3030 if not w: 

3031 return 

3032 # Bug fix 2013/12/13: Special case the minibuffer. 

3033 if w == k.w: 

3034 k.selectAll() 

3035 elif w and g.isTextWrapper(w): 

3036 w.selectAllText() 

3037 #@+node:ekr.20150514063305.312: *4* ec.sentences & helpers 

3038 @cmd('back-sentence') 

3039 def backSentence(self, event): 

3040 """Move the cursor to the previous sentence.""" 

3041 self.backSentenceHelper(event, extend=False) 

3042 

3043 @cmd('back-sentence-extend-selection') 

3044 def backSentenceExtendSelection(self, event): 

3045 """Extend the selection by moving the cursor to the previous sentence.""" 

3046 self.backSentenceHelper(event, extend=True) 

3047 

3048 @cmd('forward-sentence') 

3049 def forwardSentence(self, event): 

3050 """Move the cursor to the next sentence.""" 

3051 self.forwardSentenceHelper(event, extend=False) 

3052 

3053 @cmd('forward-sentence-extend-selection') 

3054 def forwardSentenceExtendSelection(self, event): 

3055 """Extend the selection by moving the cursor to the next sentence.""" 

3056 self.forwardSentenceHelper(event, extend=True) 

3057 #@+node:ekr.20150514063305.313: *5* ec.backSentenceHelper 

3058 def backSentenceHelper(self, event, extend): 

3059 c = self.c 

3060 w = self.editWidget(event) 

3061 if not w: 

3062 return # pragma: no cover (defensive) 

3063 c.widgetWantsFocusNow(w) 

3064 s = w.getAllText() 

3065 ins = w.getInsertPoint() 

3066 # Find the starting point of the scan. 

3067 i = ins 

3068 i -= 1 # Ensure some progress. 

3069 if i < 0 or i >= len(s): 

3070 return 

3071 # Tricky. 

3072 if s[i] == '.': 

3073 i -= 1 

3074 while i >= 0 and s[i] in ' \n': 

3075 i -= 1 

3076 if i >= ins: 

3077 i -= 1 

3078 if i >= len(s): 

3079 i -= 1 

3080 if i <= 0: 

3081 return 

3082 if s[i] == '.': 

3083 i -= 1 

3084 # Scan backwards to the end of the paragraph. 

3085 # Stop at empty lines. 

3086 # Skip periods within words. 

3087 # Stop at sentences ending in non-periods. 

3088 end = False 

3089 while not end and i >= 0: 

3090 progress = i 

3091 if s[i] == '.': 

3092 # Skip periods surrounded by letters/numbers 

3093 if i > 0 and s[i - 1].isalnum() and s[i + 1].isalnum(): 

3094 i -= 1 

3095 else: 

3096 i += 1 

3097 while i < len(s) and s[i] in ' \n': 

3098 i += 1 

3099 i -= 1 

3100 break 

3101 elif s[i] == '\n': 

3102 j = i - 1 

3103 while j >= 0: 

3104 if s[j] == '\n': 

3105 # Don't include first newline. 

3106 end = True 

3107 break # found blank line. 

3108 elif s[j] == ' ': 

3109 j -= 1 

3110 else: 

3111 i -= 1 

3112 break # no blank line found. 

3113 else: 

3114 # No blank line found. 

3115 i -= 1 

3116 else: 

3117 i -= 1 

3118 assert end or progress > i 

3119 i += 1 

3120 if i < ins: 

3121 self.moveToHelper(event, i, extend) 

3122 #@+node:ekr.20150514063305.314: *5* ec.forwardSentenceHelper 

3123 def forwardSentenceHelper(self, event, extend): 

3124 c = self.c 

3125 w = self.editWidget(event) 

3126 if not w: 

3127 return 

3128 c.widgetWantsFocusNow(w) 

3129 s = w.getAllText() 

3130 ins = w.getInsertPoint() 

3131 if ins >= len(s): 

3132 return 

3133 # Find the starting point of the scan. 

3134 i = ins 

3135 if i + 1 < len(s) and s[i + 1] == '.': 

3136 i += 1 

3137 if s[i] == '.': 

3138 i += 1 

3139 else: 

3140 while i < len(s) and s[i] in ' \n': 

3141 i += 1 

3142 i -= 1 

3143 if i <= ins: 

3144 i += 1 

3145 if i >= len(s): 

3146 return 

3147 # Scan forward to the end of the paragraph. 

3148 # Stop at empty lines. 

3149 # Skip periods within words. 

3150 # Stop at sentences ending in non-periods. 

3151 end = False 

3152 while not end and i < len(s): 

3153 progress = i 

3154 if s[i] == '.': 

3155 # Skip periods surrounded by letters/numbers 

3156 if 0 < i < len(s) and s[i - 1].isalnum() and s[i + 1].isalnum(): 

3157 i += 1 

3158 else: 

3159 i += 1 

3160 break # Include the paragraph. 

3161 elif s[i] == '\n': 

3162 j = i + 1 

3163 while j < len(s): 

3164 if s[j] == '\n': 

3165 # Don't include first newline. 

3166 end = True 

3167 break # found blank line. 

3168 elif s[j] == ' ': 

3169 j += 1 

3170 else: 

3171 i += 1 

3172 break # no blank line found. 

3173 else: 

3174 # No blank line found. 

3175 i += 1 

3176 else: 

3177 i += 1 

3178 assert end or progress < i 

3179 i = min(i, len(s)) 

3180 if i > ins: 

3181 self.moveToHelper(event, i, extend) 

3182 #@+node:ekr.20170707072644.1: *4* ec.startOfLine/ExtendSelection 

3183 @cmd('start-of-line') 

3184 def startOfLine(self, event): 

3185 """Move the cursor to first non-blank character of the line.""" 

3186 self.moveWithinLineHelper(event, 'start-line', extend=False) 

3187 

3188 @cmd('start-of-line-extend-selection') 

3189 def startOfLineExtendSelection(self, event): 

3190 """ 

3191 Extend the selection by moving the cursor to first non-blank character 

3192 of the line. 

3193 """ 

3194 self.moveWithinLineHelper(event, 'start-line', extend=True) 

3195 #@+node:ekr.20150514063305.319: *3* ec: paragraph 

3196 #@+node:ekr.20150514063305.320: *4* ec.backwardKillParagraph 

3197 @cmd('backward-kill-paragraph') 

3198 def backwardKillParagraph(self, event): 

3199 """Kill the previous paragraph.""" 

3200 c = self.c 

3201 w = self.editWidget(event) 

3202 if not w: 

3203 return # pragma: no cover (defensive) 

3204 self.beginCommand(w, undoType='backward-kill-paragraph') 

3205 try: 

3206 self.backwardParagraphHelper(event, extend=True) 

3207 i, j = w.getSelectionRange() 

3208 if i > 0: 

3209 i = min(i + 1, j) 

3210 c.killBufferCommands.killParagraphHelper(event, i, j) 

3211 w.setSelectionRange(i, i, insert=i) 

3212 finally: 

3213 self.endCommand(changed=True, setLabel=True) 

3214 #@+node:ekr.20150514063305.321: *4* ec.fillRegion 

3215 @cmd('fill-region') 

3216 def fillRegion(self, event): 

3217 """Fill all paragraphs in the selected text.""" 

3218 c, p = self.c, self.c.p 

3219 undoType = 'fill-region' 

3220 w = self.editWidget(event) 

3221 i, j = w.getSelectionRange() 

3222 c.undoer.beforeChangeGroup(p, undoType) 

3223 while 1: 

3224 progress = w.getInsertPoint() 

3225 c.reformatParagraph(event, undoType='reformat-paragraph') 

3226 ins = w.getInsertPoint() 

3227 s = w.getAllText() 

3228 w.setInsertPoint(ins) 

3229 if progress >= ins or ins >= j or ins >= len(s): 

3230 break 

3231 c.undoer.afterChangeGroup(p, undoType) 

3232 #@+node:ekr.20150514063305.322: *4* ec.fillRegionAsParagraph 

3233 @cmd('fill-region-as-paragraph') 

3234 def fillRegionAsParagraph(self, event): 

3235 """Fill the selected text.""" 

3236 w = self.editWidget(event) 

3237 if not w or not self._chckSel(event): 

3238 return # pragma: no cover (defensive) 

3239 self.beginCommand(w, undoType='fill-region-as-paragraph') 

3240 self.endCommand(changed=True, setLabel=True) 

3241 #@+node:ekr.20150514063305.323: *4* ec.fillParagraph 

3242 @cmd('fill-paragraph') 

3243 def fillParagraph(self, event): 

3244 """Fill the selected paragraph""" 

3245 w = self.editWidget(event) 

3246 if not w: 

3247 return # pragma: no cover (defensive) 

3248 # Clear the selection range. 

3249 i, j = w.getSelectionRange() 

3250 w.setSelectionRange(i, i, insert=i) 

3251 self.c.reformatParagraph(event) 

3252 #@+node:ekr.20150514063305.324: *4* ec.killParagraph 

3253 @cmd('kill-paragraph') 

3254 def killParagraph(self, event): 

3255 """Kill the present paragraph.""" 

3256 c = self.c 

3257 w = self.editWidget(event) 

3258 if not w: 

3259 return 

3260 self.beginCommand(w, undoType='kill-paragraph') 

3261 try: 

3262 self.extendToParagraph(event) 

3263 i, j = w.getSelectionRange() 

3264 c.killBufferCommands.killParagraphHelper(event, i, j) 

3265 w.setSelectionRange(i, i, insert=i) 

3266 finally: 

3267 self.endCommand(changed=True, setLabel=True) 

3268 #@+node:ekr.20150514063305.325: *4* ec.extend-to-paragraph & helper 

3269 @cmd('extend-to-paragraph') 

3270 def extendToParagraph(self, event): 

3271 """Select the paragraph surrounding the cursor.""" 

3272 w = self.editWidget(event) 

3273 if not w: 

3274 return 

3275 s = w.getAllText() 

3276 ins = w.getInsertPoint() 

3277 i, j = g.getLine(s, ins) 

3278 line = s[i:j] 

3279 # Find the start of the paragraph. 

3280 if line.strip(): # Search backward. 

3281 while i > 0: 

3282 i2, j2 = g.getLine(s, i - 1) 

3283 line = s[i2:j2] 

3284 if line.strip(): 

3285 i = i2 

3286 else: 

3287 break # Use the previous line. 

3288 else: # Search forward. 

3289 while j < len(s): 

3290 i, j = g.getLine(s, j) 

3291 line = s[i:j] 

3292 if line.strip(): 

3293 break 

3294 else: return 

3295 # Select from i to the end of the paragraph. 

3296 self.selectParagraphHelper(w, i) 

3297 #@+node:ekr.20150514063305.326: *5* ec.selectParagraphHelper 

3298 def selectParagraphHelper(self, w, start): 

3299 """Select from start to the end of the paragraph.""" 

3300 s = w.getAllText() 

3301 i1, j = g.getLine(s, start) 

3302 while j < len(s): 

3303 i, j2 = g.getLine(s, j) 

3304 line = s[i:j2] 

3305 if line.strip(): 

3306 j = j2 

3307 else: 

3308 break 

3309 j = max(start, j - 1) 

3310 w.setSelectionRange(i1, j, insert=j) 

3311 #@+node:ekr.20150514063305.327: *3* ec: region 

3312 #@+node:ekr.20150514063305.328: *4* ec.tabIndentRegion (indent-rigidly) 

3313 @cmd('indent-rigidly') 

3314 def tabIndentRegion(self, event): 

3315 """Insert a hard tab at the start of each line of the selected text.""" 

3316 w = self.editWidget(event) 

3317 if not w or not self._chckSel(event): 

3318 return # pragma: no cover (defensive) 

3319 self.beginCommand(w, undoType='indent-rigidly') 

3320 s = w.getAllText() 

3321 i1, j1 = w.getSelectionRange() 

3322 i, junk = g.getLine(s, i1) 

3323 junk, j = g.getLine(s, j1) 

3324 lines = g.splitlines(s[i:j]) 

3325 n = len(lines) 

3326 lines_s = ''.join('\t' + line for line in lines) 

3327 s = s[:i] + lines_s + s[j:] 

3328 w.setAllText(s) 

3329 # Retain original row/col selection. 

3330 w.setSelectionRange(i1, j1 + n, insert=j1 + n) 

3331 self.endCommand(changed=True, setLabel=True) 

3332 #@+node:ekr.20150514063305.329: *4* ec.countRegion 

3333 @cmd('count-region') 

3334 def countRegion(self, event): 

3335 """Print the number of lines and characters in the selected text.""" 

3336 k = self.c.k 

3337 w = self.editWidget(event) 

3338 if not w: 

3339 return # pragma: no cover (defensive) 

3340 txt = w.getSelectedText() 

3341 lines = 1 

3342 chars = 0 

3343 for z in txt: 

3344 if z == '\n': 

3345 lines += 1 

3346 else: chars += 1 

3347 k.setLabelGrey( 

3348 f"Region has {lines} lines, " 

3349 f"{chars} character{g.plural(chars)}") 

3350 #@+node:ekr.20150514063305.330: *4* ec.moveLinesDown 

3351 @cmd('move-lines-down') 

3352 def moveLinesDown(self, event): 

3353 """ 

3354 Move all lines containing any selected text down one line, 

3355 moving to the next node if the lines are the last lines of the body. 

3356 """ 

3357 c = self.c 

3358 w = self.editWidget(event) 

3359 if not w: 

3360 return 

3361 s = w.getAllText() 

3362 sel_1, sel_2 = w.getSelectionRange() 

3363 insert_pt = w.getInsertPoint() 

3364 i, junk = g.getLine(s, sel_1) 

3365 i2, j = g.getLine(s, sel_2) 

3366 lines = s[i:j] 

3367 # Select from start of the first line to the *start* of the last line. 

3368 # This prevents selection creep. 

3369 self.beginCommand(w, undoType='move-lines-down') 

3370 try: 

3371 next_i, next_j = g.getLine(s, j) # 2011/04/01: was j+1 

3372 next_line = s[next_i:next_j] 

3373 n2 = next_j - next_i 

3374 if j < len(s): 

3375 w.delete(i, next_j) 

3376 if next_line.endswith('\n'): 

3377 # Simply swap positions with next line 

3378 new_lines = next_line + lines 

3379 else: 

3380 # Last line of the body to be moved up doesn't end in a newline 

3381 # while we have to remove the newline from the line above moving down. 

3382 new_lines = next_line + '\n' + lines[:-1] 

3383 n2 += 1 

3384 w.insert(i, new_lines) 

3385 w.setSelectionRange(sel_1 + n2, sel_2 + n2, insert=insert_pt + n2) 

3386 else: 

3387 # Leo 5.6: insert a blank line before the selected lines. 

3388 w.insert(i, '\n') 

3389 w.setSelectionRange(sel_1 + 1, sel_2 + 1, insert=insert_pt + 1) 

3390 # Fix bug 799695: colorizer bug after move-lines-up into a docstring 

3391 c.recolor() 

3392 finally: 

3393 self.endCommand(changed=True, setLabel=True) 

3394 #@+node:ekr.20150514063305.331: *4* ec.moveLinesUp 

3395 @cmd('move-lines-up') 

3396 def moveLinesUp(self, event): 

3397 """ 

3398 Move all lines containing any selected text up one line, 

3399 moving to the previous node as needed. 

3400 """ 

3401 c = self.c 

3402 w = self.editWidget(event) 

3403 if not w: 

3404 return # pragma: no cover (defensive) 

3405 s = w.getAllText() 

3406 sel_1, sel_2 = w.getSelectionRange() 

3407 insert_pt = w.getInsertPoint() # 2011/04/01 

3408 i, junk = g.getLine(s, sel_1) 

3409 i2, j = g.getLine(s, sel_2) 

3410 lines = s[i:j] 

3411 self.beginCommand(w, undoType='move-lines-up') 

3412 try: 

3413 prev_i, prev_j = g.getLine(s, i - 1) 

3414 prev_line = s[prev_i:prev_j] 

3415 n2 = prev_j - prev_i 

3416 if i > 0: 

3417 w.delete(prev_i, j) 

3418 if lines.endswith('\n'): 

3419 # Simply swap positions with next line 

3420 new_lines = lines + prev_line 

3421 else: 

3422 # Lines to be moved up don't end in a newline while the 

3423 # previous line going down needs its newline taken off. 

3424 new_lines = lines + '\n' + prev_line[:-1] 

3425 w.insert(prev_i, new_lines) 

3426 w.setSelectionRange(sel_1 - n2, sel_2 - n2, insert=insert_pt - n2) 

3427 else: 

3428 # Leo 5.6: Insert a blank line after the line. 

3429 w.insert(j, '\n') 

3430 w.setSelectionRange(sel_1, sel_2, insert=sel_1) 

3431 # Fix bug 799695: colorizer bug after move-lines-up into a docstring 

3432 c.recolor() 

3433 finally: 

3434 self.endCommand(changed=True, setLabel=True) 

3435 #@+node:ekr.20150514063305.332: *4* ec.reverseRegion 

3436 @cmd('reverse-region') 

3437 def reverseRegion(self, event): 

3438 """Reverse the order of lines in the selected text.""" 

3439 w = self.editWidget(event) 

3440 if not w or not self._chckSel(event): 

3441 return # pragma: no cover (defensive) 

3442 self.beginCommand(w, undoType='reverse-region') 

3443 s = w.getAllText() 

3444 i1, j1 = w.getSelectionRange() 

3445 i, junk = g.getLine(s, i1) 

3446 junk, j = g.getLine(s, j1) 

3447 txt = s[i:j] 

3448 aList = txt.split('\n') 

3449 aList.reverse() 

3450 txt = '\n'.join(aList) + '\n' 

3451 w.setAllText(s[:i1] + txt + s[j1:]) 

3452 ins = i1 + len(txt) - 1 

3453 w.setSelectionRange(ins, ins, insert=ins) 

3454 self.endCommand(changed=True, setLabel=True) 

3455 #@+node:ekr.20150514063305.333: *4* ec.up/downCaseRegion & helper 

3456 @cmd('downcase-region') 

3457 def downCaseRegion(self, event): 

3458 """Convert all characters in the selected text to lower case.""" 

3459 self.caseHelper(event, 'low', 'downcase-region') 

3460 

3461 @cmd('toggle-case-region') 

3462 def toggleCaseRegion(self, event): 

3463 """Toggle the case of all characters in the selected text.""" 

3464 self.caseHelper(event, 'toggle', 'toggle-case-region') 

3465 

3466 @cmd('upcase-region') 

3467 def upCaseRegion(self, event): 

3468 """Convert all characters in the selected text to UPPER CASE.""" 

3469 self.caseHelper(event, 'up', 'upcase-region') 

3470 

3471 def caseHelper(self, event, way, undoType): 

3472 w = self.editWidget(event) 

3473 if not w or not w.hasSelection(): 

3474 return # pragma: no cover (defensive) 

3475 self.beginCommand(w, undoType=undoType) 

3476 s = w.getAllText() 

3477 i, j = w.getSelectionRange() 

3478 ins = w.getInsertPoint() 

3479 s2 = s[i:j] 

3480 if way == 'low': 

3481 sel = s2.lower() 

3482 elif way == 'up': 

3483 sel = s2.upper() 

3484 else: 

3485 assert way == 'toggle' 

3486 sel = s2.swapcase() 

3487 s2 = s[:i] + sel + s[j:] 

3488 changed = s2 != s 

3489 if changed: 

3490 w.setAllText(s2) 

3491 w.setSelectionRange(i, j, insert=ins) 

3492 self.endCommand(changed=changed, setLabel=True) 

3493 #@+node:ekr.20150514063305.334: *3* ec: scrolling 

3494 #@+node:ekr.20150514063305.335: *4* ec.scrollUp/Down & helper 

3495 @cmd('scroll-down-half-page') 

3496 def scrollDownHalfPage(self, event): 

3497 """Scroll the presently selected pane down one line.""" 

3498 self.scrollHelper(event, 'down', 'half-page') 

3499 

3500 @cmd('scroll-down-line') 

3501 def scrollDownLine(self, event): 

3502 """Scroll the presently selected pane down one line.""" 

3503 self.scrollHelper(event, 'down', 'line') 

3504 

3505 @cmd('scroll-down-page') 

3506 def scrollDownPage(self, event): 

3507 """Scroll the presently selected pane down one page.""" 

3508 self.scrollHelper(event, 'down', 'page') 

3509 

3510 @cmd('scroll-up-half-page') 

3511 def scrollUpHalfPage(self, event): 

3512 """Scroll the presently selected pane down one line.""" 

3513 self.scrollHelper(event, 'up', 'half-page') 

3514 

3515 @cmd('scroll-up-line') 

3516 def scrollUpLine(self, event): 

3517 """Scroll the presently selected pane up one page.""" 

3518 self.scrollHelper(event, 'up', 'line') 

3519 

3520 @cmd('scroll-up-page') 

3521 def scrollUpPage(self, event): 

3522 """Scroll the presently selected pane up one page.""" 

3523 self.scrollHelper(event, 'up', 'page') 

3524 #@+node:ekr.20150514063305.336: *5* ec.scrollHelper 

3525 def scrollHelper(self, event, direction, distance): 

3526 """ 

3527 Scroll the present pane up or down one page 

3528 kind is in ('up/down-half-page/line/page) 

3529 """ 

3530 w = event and event.w 

3531 if w and hasattr(w, 'scrollDelegate'): 

3532 kind = direction + '-' + distance 

3533 w.scrollDelegate(kind) 

3534 #@+node:ekr.20150514063305.337: *4* ec.scrollOutlineUp/Down/Line/Page 

3535 @cmd('scroll-outline-down-line') 

3536 def scrollOutlineDownLine(self, event=None): 

3537 """Scroll the outline pane down one line.""" 

3538 tree = self.c.frame.tree 

3539 if hasattr(tree, 'scrollDelegate'): 

3540 tree.scrollDelegate('down-line') 

3541 elif hasattr(tree.canvas, 'leo_treeBar'): 

3542 a, b = tree.canvas.leo_treeBar.get() 

3543 if b < 1.0: 

3544 tree.canvas.yview_scroll(1, "unit") 

3545 

3546 @cmd('scroll-outline-down-page') 

3547 def scrollOutlineDownPage(self, event=None): 

3548 """Scroll the outline pane down one page.""" 

3549 tree = self.c.frame.tree 

3550 if hasattr(tree, 'scrollDelegate'): 

3551 tree.scrollDelegate('down-page') 

3552 elif hasattr(tree.canvas, 'leo_treeBar'): 

3553 a, b = tree.canvas.leo_treeBar.get() 

3554 if b < 1.0: 

3555 tree.canvas.yview_scroll(1, "page") 

3556 

3557 @cmd('scroll-outline-up-line') 

3558 def scrollOutlineUpLine(self, event=None): 

3559 """Scroll the outline pane up one line.""" 

3560 tree = self.c.frame.tree 

3561 if hasattr(tree, 'scrollDelegate'): 

3562 tree.scrollDelegate('up-line') 

3563 elif hasattr(tree.canvas, 'leo_treeBar'): 

3564 a, b = tree.canvas.leo_treeBar.get() 

3565 if a > 0.0: 

3566 tree.canvas.yview_scroll(-1, "unit") 

3567 

3568 @cmd('scroll-outline-up-page') 

3569 def scrollOutlineUpPage(self, event=None): 

3570 """Scroll the outline pane up one page.""" 

3571 tree = self.c.frame.tree 

3572 if hasattr(tree, 'scrollDelegate'): 

3573 tree.scrollDelegate('up-page') 

3574 elif hasattr(tree.canvas, 'leo_treeBar'): 

3575 a, b = tree.canvas.leo_treeBar.get() 

3576 if a > 0.0: 

3577 tree.canvas.yview_scroll(-1, "page") 

3578 #@+node:ekr.20150514063305.338: *4* ec.scrollOutlineLeftRight 

3579 @cmd('scroll-outline-left') 

3580 def scrollOutlineLeft(self, event=None): 

3581 """Scroll the outline left.""" 

3582 tree = self.c.frame.tree 

3583 if hasattr(tree, 'scrollDelegate'): 

3584 tree.scrollDelegate('left') 

3585 elif hasattr(tree.canvas, 'xview_scroll'): 

3586 tree.canvas.xview_scroll(1, "unit") 

3587 

3588 @cmd('scroll-outline-right') 

3589 def scrollOutlineRight(self, event=None): 

3590 """Scroll the outline left.""" 

3591 tree = self.c.frame.tree 

3592 if hasattr(tree, 'scrollDelegate'): 

3593 tree.scrollDelegate('right') 

3594 elif hasattr(tree.canvas, 'xview_scroll'): 

3595 tree.canvas.xview_scroll(-1, "unit") 

3596 #@+node:ekr.20150514063305.339: *3* ec: sort 

3597 #@@language rest 

3598 #@+at 

3599 # XEmacs provides several commands for sorting text in a buffer. All 

3600 # operate on the contents of the region (the text between point and the 

3601 # mark). They divide the text of the region into many "sort records", 

3602 # identify a "sort key" for each record, and then reorder the records 

3603 # using the order determined by the sort keys. The records are ordered so 

3604 # that their keys are in alphabetical order, or, for numerical sorting, in 

3605 # numerical order. In alphabetical sorting, all upper-case letters `A' 

3606 # through `Z' come before lower-case `a', in accordance with the ASCII 

3607 # character sequence. 

3608 # 

3609 # The sort commands differ in how they divide the text into sort 

3610 # records and in which part of each record they use as the sort key. 

3611 # Most of the commands make each line a separate sort record, but some 

3612 # commands use paragraphs or pages as sort records. Most of the sort 

3613 # commands use each entire sort record as its own sort key, but some use 

3614 # only a portion of the record as the sort key. 

3615 # 

3616 # `M-x sort-lines' 

3617 # Divide the region into lines and sort by comparing the entire text 

3618 # of a line. A prefix argument means sort in descending order. 

3619 # 

3620 # `M-x sort-paragraphs' 

3621 # Divide the region into paragraphs and sort by comparing the entire 

3622 # text of a paragraph (except for leading blank lines). A prefix 

3623 # argument means sort in descending order. 

3624 # 

3625 # `M-x sort-pages' 

3626 # Divide the region into pages and sort by comparing the entire text 

3627 # of a page (except for leading blank lines). A prefix argument 

3628 # means sort in descending order. 

3629 # 

3630 # `M-x sort-fields' 

3631 # Divide the region into lines and sort by comparing the contents of 

3632 # one field in each line. Fields are defined as separated by 

3633 # whitespace, so the first run of consecutive non-whitespace 

3634 # characters in a line constitutes field 1, the second such run 

3635 # constitutes field 2, etc. 

3636 # 

3637 # You specify which field to sort by with a numeric argument: 1 to 

3638 # sort by field 1, etc. A negative argument means sort in descending 

3639 # order. Thus, minus 2 means sort by field 2 in reverse-alphabetical 

3640 # order. 

3641 # 

3642 # `M-x sort-numeric-fields' 

3643 # Like `M-x sort-fields', except the specified field is converted to 

3644 # a number for each line and the numbers are compared. `10' comes 

3645 # before `2' when considered as text, but after it when considered 

3646 # as a number. 

3647 # 

3648 # `M-x sort-columns' 

3649 # Like `M-x sort-fields', except that the text within each line used 

3650 # for comparison comes from a fixed range of columns. An explanation 

3651 # is given below. 

3652 # 

3653 # For example, if the buffer contains: 

3654 # 

3655 # On systems where clash detection (locking of files being edited) is 

3656 # implemented, XEmacs also checks the first time you modify a buffer 

3657 # whether the file has changed on disk since it was last visited or 

3658 # saved. If it has, you are asked to confirm that you want to change 

3659 # the buffer. 

3660 # 

3661 # then if you apply `M-x sort-lines' to the entire buffer you get: 

3662 # 

3663 # On systems where clash detection (locking of files being edited) is 

3664 # implemented, XEmacs also checks the first time you modify a buffer 

3665 # saved. If it has, you are asked to confirm that you want to change 

3666 # the buffer. 

3667 # whether the file has changed on disk since it was last visited or 

3668 # 

3669 # where the upper case `O' comes before all lower case letters. If you 

3670 # apply instead `C-u 2 M-x sort-fields' you get: 

3671 # 

3672 # saved. If it has, you are asked to confirm that you want to change 

3673 # implemented, XEmacs also checks the first time you modify a buffer 

3674 # the buffer. 

3675 # On systems where clash detection (locking of files being edited) is 

3676 # whether the file has changed on disk since it was last visited or 

3677 # 

3678 # where the sort keys were `If', `XEmacs', `buffer', `systems', and `the'. 

3679 # 

3680 # `M-x sort-columns' requires more explanation. You specify the 

3681 # columns by putting point at one of the columns and the mark at the other 

3682 # column. Because this means you cannot put point or the mark at the 

3683 # beginning of the first line to sort, this command uses an unusual 

3684 # definition of `region': all of the line point is in is considered part 

3685 # of the region, and so is all of the line the mark is in. 

3686 # 

3687 # For example, to sort a table by information found in columns 10 to 

3688 # 15, you could put the mark on column 10 in the first line of the table, 

3689 # and point on column 15 in the last line of the table, and then use this 

3690 # command. Or you could put the mark on column 15 in the first line and 

3691 # point on column 10 in the last line. 

3692 # 

3693 # This can be thought of as sorting the rectangle specified by point 

3694 # and the mark, except that the text on each line to the left or right of 

3695 # the rectangle moves along with the text inside the rectangle. *Note 

3696 # Rectangles::. 

3697 #@@language python 

3698 #@+node:ekr.20150514063305.340: *4* ec.sortLines commands 

3699 @cmd('reverse-sort-lines-ignoring-case') 

3700 def reverseSortLinesIgnoringCase(self, event): 

3701 """Sort the selected lines in reverse order, ignoring case.""" 

3702 return self.sortLines(event, ignoreCase=True, reverse=True) 

3703 

3704 @cmd('reverse-sort-lines') 

3705 def reverseSortLines(self, event): 

3706 """Sort the selected lines in reverse order.""" 

3707 return self.sortLines(event, reverse=True) 

3708 

3709 @cmd('sort-lines-ignoring-case') 

3710 def sortLinesIgnoringCase(self, event): 

3711 """Sort the selected lines, ignoring case.""" 

3712 return self.sortLines(event, ignoreCase=True) 

3713 

3714 @cmd('sort-lines') 

3715 def sortLines(self, event, ignoreCase=False, reverse=False): 

3716 """Sort the selected lines.""" 

3717 w = self.editWidget(event) 

3718 if not self._chckSel(event): 

3719 return 

3720 undoType = 'reverse-sort-lines' if reverse else 'sort-lines' 

3721 self.beginCommand(w, undoType=undoType) 

3722 try: 

3723 s = w.getAllText() 

3724 sel1, sel2 = w.getSelectionRange() 

3725 ins = w.getInsertPoint() 

3726 i, junk = g.getLine(s, sel1) 

3727 junk, j = g.getLine(s, sel2) 

3728 s2 = s[i:j] 

3729 if not s2.endswith('\n'): 

3730 s2 = s2 + '\n' 

3731 aList = g.splitLines(s2) 

3732 

3733 def lower(s): 

3734 return s.lower() if ignoreCase else s 

3735 

3736 aList.sort(key=lower) # key is a function that extracts args. 

3737 if reverse: 

3738 aList.reverse() 

3739 s = ''.join(aList) 

3740 w.delete(i, j) 

3741 w.insert(i, s) 

3742 w.setSelectionRange(sel1, sel2, insert=ins) 

3743 finally: 

3744 self.endCommand(changed=True, setLabel=True) 

3745 #@+node:ekr.20150514063305.341: *4* ec.sortColumns 

3746 @cmd('sort-columns') 

3747 def sortColumns(self, event): 

3748 """ 

3749 Sort lines of selected text using only lines in the given columns to do 

3750 the comparison. 

3751 """ 

3752 w = self.editWidget(event) 

3753 if not self._chckSel(event): 

3754 return # pragma: no cover (defensive) 

3755 self.beginCommand(w, undoType='sort-columns') 

3756 try: 

3757 s = w.getAllText() 

3758 sel_1, sel_2 = w.getSelectionRange() 

3759 sint1, sint2 = g.convertPythonIndexToRowCol(s, sel_1) 

3760 sint3, sint4 = g.convertPythonIndexToRowCol(s, sel_2) 

3761 sint1 += 1 

3762 sint3 += 1 

3763 i, junk = g.getLine(s, sel_1) 

3764 junk, j = g.getLine(s, sel_2) 

3765 txt = s[i:j] 

3766 columns = [w.get(f"{z}.{sint2}", f"{z}.{sint4}") 

3767 for z in range(sint1, sint3 + 1)] 

3768 aList = g.splitLines(txt) 

3769 zlist = list(zip(columns, aList)) 

3770 zlist.sort() 

3771 s = ''.join([z[1] for z in zlist]) 

3772 w.delete(i, j) 

3773 w.insert(i, s) 

3774 w.setSelectionRange(sel_1, sel_1 + len(s), insert=sel_1 + len(s)) 

3775 finally: 

3776 self.endCommand(changed=True, setLabel=True) 

3777 #@+node:ekr.20150514063305.342: *4* ec.sortFields 

3778 @cmd('sort-fields') 

3779 def sortFields(self, event, which=None): 

3780 """ 

3781 Divide the selected text into lines and sort by comparing the contents 

3782 of one field in each line. Fields are defined as separated by 

3783 whitespace, so the first run of consecutive non-whitespace characters 

3784 in a line constitutes field 1, the second such run constitutes field 2, 

3785 etc. 

3786 

3787 You specify which field to sort by with a numeric argument: 1 to sort 

3788 by field 1, etc. A negative argument means sort in descending order. 

3789 Thus, minus 2 means sort by field 2 in reverse-alphabetical order. 

3790 """ 

3791 w = self.editWidget(event) 

3792 if not w or not self._chckSel(event): 

3793 return 

3794 self.beginCommand(w, undoType='sort-fields') 

3795 s = w.getAllText() 

3796 ins = w.getInsertPoint() 

3797 r1, r2, r3, r4 = self.getRectanglePoints(w) 

3798 i, junk = g.getLine(s, r1) 

3799 junk, j = g.getLine(s, r4) 

3800 txt = s[i:j] # bug reported by pychecker. 

3801 txt = txt.split('\n') 

3802 fields = [] 

3803 fn = r'\w+' 

3804 frx = re.compile(fn) 

3805 for line in txt: 

3806 f = frx.findall(line) 

3807 if not which: 

3808 fields.append(f[0]) 

3809 else: 

3810 i = int(which) 

3811 if len(f) < i: 

3812 return 

3813 i = i - 1 

3814 fields.append(f[i]) 

3815 nz = sorted(zip(fields, txt)) 

3816 w.delete(i, j) 

3817 int1 = i 

3818 for z in nz: 

3819 w.insert(f"{int1}.0", f"{z[1]}\n") 

3820 int1 = int1 + 1 

3821 w.setInsertPoint(ins) 

3822 self.endCommand(changed=True, setLabel=True) 

3823 #@+node:ekr.20150514063305.343: *3* ec: swap/transpose 

3824 #@+node:ekr.20150514063305.344: *4* ec.transposeLines 

3825 @cmd('transpose-lines') 

3826 def transposeLines(self, event): 

3827 """Transpose the line containing the cursor with the preceding line.""" 

3828 w = self.editWidget(event) 

3829 if not w: 

3830 return # pragma: no cover (defensive) 

3831 ins = w.getInsertPoint() 

3832 s = w.getAllText() 

3833 if not s.strip(): 

3834 return # pragma: no cover (defensive) 

3835 i, j = g.getLine(s, ins) 

3836 line1 = s[i:j] 

3837 self.beginCommand(w, undoType='transpose-lines') 

3838 if i == 0: # Transpose the next line. 

3839 i2, j2 = g.getLine(s, j + 1) 

3840 line2 = s[i2:j2] 

3841 w.delete(0, j2) 

3842 w.insert(0, line2 + line1) 

3843 w.setInsertPoint(j2 - 1) 

3844 else: # Transpose the previous line. 

3845 i2, j2 = g.getLine(s, i - 1) 

3846 line2 = s[i2:j2] 

3847 w.delete(i2, j) 

3848 w.insert(i2, line1 + line2) 

3849 w.setInsertPoint(j - 1) 

3850 self.endCommand(changed=True, setLabel=True) 

3851 #@+node:ekr.20150514063305.345: *4* ec.transposeWords 

3852 @cmd('transpose-words') 

3853 def transposeWords(self, event): 

3854 """ 

3855 Transpose the word before the cursor with the word after the cursor 

3856 Punctuation between words does not move. For example, ‘FOO, BAR’ 

3857 transposes into ‘BAR, FOO’. 

3858 """ 

3859 w = self.editWidget(event) 

3860 if not w: 

3861 return 

3862 self.beginCommand(w, undoType='transpose-words') 

3863 s = w.getAllText() 

3864 i1, j1 = self.extendToWord(event, select=False) 

3865 s1 = s[i1:j1] 

3866 if i1 > j1: 

3867 i1, j1 = j1, i1 

3868 # Search for the next word. 

3869 k = j1 + 1 

3870 while k < len(s) and s[k] != '\n' and not g.isWordChar1(s[k]): 

3871 k += 1 

3872 changed = k < len(s) 

3873 if changed: 

3874 ws = s[j1:k] 

3875 w.setInsertPoint(k + 1) 

3876 i2, j2 = self.extendToWord(event, select=False) 

3877 s2 = s[i2:j2] 

3878 s3 = s[:i1] + s2 + ws + s1 + s[j2:] 

3879 w.setAllText(s3) 

3880 w.setSelectionRange(j1, j1, insert=j1) 

3881 self.endCommand(changed=changed, setLabel=True) 

3882 #@+node:ekr.20150514063305.346: *4* ec.swapCharacters & transeposeCharacters 

3883 @cmd('transpose-chars') 

3884 def transposeCharacters(self, event): 

3885 """Swap the characters at the cursor.""" 

3886 w = self.editWidget(event) 

3887 if not w: 

3888 return # pragma: no cover (defensive) 

3889 self.beginCommand(w, undoType='swap-characters') 

3890 s = w.getAllText() 

3891 i = w.getInsertPoint() 

3892 if 0 < i < len(s): 

3893 w.setAllText(s[: i - 1] + s[i] + s[i - 1] + s[i + 1 :]) 

3894 w.setSelectionRange(i, i, insert=i) 

3895 self.endCommand(changed=True, setLabel=True) 

3896 

3897 swapCharacters = transposeCharacters 

3898 #@+node:ekr.20150514063305.348: *3* ec: uA's 

3899 #@+node:ekr.20150514063305.349: *4* ec.clearNodeUas & clearAllUas 

3900 @cmd('clear-node-uas') 

3901 def clearNodeUas(self, event=None): 

3902 """Clear the uA's in the selected VNode.""" 

3903 c = self.c 

3904 p = c and c.p 

3905 if p and p.v.u: 

3906 p.v.u = {} 

3907 # #1276. 

3908 p.setDirty() 

3909 c.setChanged() 

3910 c.redraw() 

3911 

3912 @cmd('clear-all-uas') 

3913 def clearAllUas(self, event=None): 

3914 """Clear all uAs in the entire outline.""" 

3915 c = self.c 

3916 # #1276. 

3917 changed = False 

3918 for p in self.c.all_unique_positions(): 

3919 if p.v.u: 

3920 p.v.u = {} 

3921 p.setDirty() 

3922 changed = True 

3923 if changed: 

3924 c.setChanged() 

3925 c.redraw() 

3926 #@+node:ekr.20150514063305.350: *4* ec.showUas & showAllUas 

3927 @cmd('show-all-uas') 

3928 def showAllUas(self, event=None): 

3929 """Print all uA's in the outline.""" 

3930 g.es_print('Dump of uAs...') 

3931 for v in self.c.all_unique_nodes(): 

3932 if v.u: 

3933 self.showNodeUas(v=v) 

3934 

3935 @cmd('show-node-uas') 

3936 def showNodeUas(self, event=None, v=None): 

3937 """Print the uA's in the selected node.""" 

3938 c = self.c 

3939 if v: 

3940 d, h = v.u, v.h 

3941 else: 

3942 d, h = c.p.v.u, c.p.h 

3943 g.es_print(h) 

3944 g.es_print(g.objToString(d)) 

3945 #@+node:ekr.20150514063305.351: *4* ec.setUa 

3946 @cmd('set-ua') 

3947 def setUa(self, event): 

3948 """Prompt for the name and value of a uA, then set the uA in the present node.""" 

3949 k = self.c.k 

3950 self.w = self.editWidget(event) 

3951 if self.w: 

3952 k.setLabelBlue('Set uA: ') 

3953 k.get1Arg(event, handler=self.setUa1) 

3954 

3955 def setUa1(self, event): 

3956 k = self.c.k 

3957 self.uaName = k.arg 

3958 s = f"Set uA: {self.uaName} To: " 

3959 k.setLabelBlue(s) 

3960 k.getNextArg(self.setUa2) 

3961 

3962 def setUa2(self, event): 

3963 c, k = self.c, self.c.k 

3964 val = k.arg 

3965 d = c.p.v.u 

3966 d[self.uaName] = val 

3967 self.showNodeUas() 

3968 k.clearState() 

3969 k.resetLabel() 

3970 k.showStateAndMode() 

3971 #@-others 

3972#@-others 

3973#@-leo