Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- 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.20190926103245.1: *3* @g.command('next-or-end-of-line') 

124# by Brian Theado. 

125 

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

127def nextOrEndOfLine(event): 

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

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

130# by Brian Theado. 

131 

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

133def nextOrEndOfLineExtendSelection(event): 

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

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

136# by Brian Theado. 

137 

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

139def previousOrBeginningOfLine(event): 

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

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

142# by Brian Theado. 

143 

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

145def previousOrBeginningOfLineExtendSelection(event): 

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

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

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

149def promoteBodies(event): 

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

151 c = event.get('c') 

152 if not c: 

153 return 

154 p = c.p 

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

156 b = c.undoer.beforeChangeNodeContents(p) 

157 for child in p.subtree(): 

158 h = child.h.strip() 

159 if child.b: 

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

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

162 else: 

163 s = f"- {h}" 

164 if s.strip(): 

165 result.append(s.strip()) 

166 if result: 

167 result.append('') 

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

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

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

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

172def promoteHeadlines(event): 

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

174 c = event.get('c') 

175 if not c: 

176 return 

177 p = c.p 

178 b = c.undoer.beforeChangeNodeContents(p) 

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

180 if result: 

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

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

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

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

185def select_next_trace_statement(event=None): 

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

187 c = event.get('c') 

188 if not c: 

189 return 

190 w = c.frame.body.wrapper 

191 ins = w.getInsertPoint() 

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

193 if p: 

194 c.selectPosition(p) 

195 c.redraw() 

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

197 else: 

198 g.es_print('done') 

199 c.bodyWantsFocus() 

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

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

202def show_clone_ancestors(event=None): 

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

204 c = event.get('c') 

205 if not c: 

206 return 

207 p = c.p 

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

209 for clone in c.all_positions(): 

210 if clone.v == p.v: 

211 unl = message = clone.get_UNL() 

212 # Drop the file part. 

213 i = unl.find('#') 

214 if i > 0: 

215 message = unl[i + 1:] 

216 # Drop the target node from the message. 

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

218 if len(parts) > 1: 

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

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

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

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

223def show_clones(event=None): 

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

225 c = event.get('c') 

226 if not c: 

227 return 

228 seen = [] 

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

230 parent = clone.parent() 

231 if parent and parent not in seen: 

232 seen.append(parent) 

233 unl = message = parent.get_UNL() 

234 # Drop the file part. 

235 i = unl.find('#') 

236 if i > 0: 

237 message = unl[i + 1:] 

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

239 

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

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

242def unmark_first_parents(event=None): 

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

244 c = event.get('c') 

245 changed: List[Any] = [] 

246 if not c: 

247 return changed 

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

249 if parent.isMarked(): 

250 parent.clearMarked() 

251 parent.setDirty() 

252 changed.append(parent.copy()) 

253 if changed: 

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

255 c.setChanged() 

256 c.redraw() 

257 return changed 

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

259class EditCommandsClass(BaseEditCommandsClass): 

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

261 # pylint: disable=eval-used 

262 #@+others 

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

264 def __init__(self, c): 

265 """Ctor for EditCommandsClass class.""" 

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

267 self.c = c 

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

269 self.cursorStack = [] 

270 # Values are tuples, (i, j, ins) 

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

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

273 self.fillColumn = 0 # For line centering. 

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

275 # If zero, @pagewidth value is used. 

276 self.moveSpotNode = None # A VNode. 

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

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

279 self.sampleWidget = None # Created later. 

280 self.swapSpots = [] 

281 self._useRegex = False # For replace-string 

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

283 # Settings... 

284 cf = c.config 

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

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

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

288 else: 

289 self.autojustify = 0 

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

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

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

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

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

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

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

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

298 self.initBracketMatcher(c) 

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

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

301 @cmd('clear-cache') 

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

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

304 g.app.global_cacher.clear() 

305 g.app.commander_cacher.clear() 

306 

307 @cmd('dump-caches') 

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

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

310 g.app.global_cacher.dump() 

311 g.app.commander_cacher.dump() 

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

313 @cmd('do-nothing') 

314 def doNothing(self, event): 

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

316 pass 

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

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

319 def insertFileName(self, event=None): 

320 """ 

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

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

323 

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

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

326 cursor. 

327 """ 

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

329 if not w: 

330 return 

331 

332 def callback(arg, w=w): 

333 i = w.getSelectionRange()[0] 

334 p = c.p 

335 w.deleteTextSelection() 

336 w.insert(i, arg) 

337 newText = w.getAllText() 

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

339 bunch = u.beforeChangeBody(p) 

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

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

342 

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

344 

345 start_text = w.getSelectedText() 

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

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

348 if start_text: 

349 # make non-path characters whitespace 

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

351 for i in start_text) 

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

353 start_text = '' 

354 else: 

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

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

357 w.setSelectionRange( 

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

359 

360 c.k.functionTail = g.os_path_finalize_join( 

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

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

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

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

365 def insertHeadlineTime(self, event=None): 

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

367 frame = self 

368 c, p = frame.c, self.c.p 

369 if g.app.batchMode: 

370 c.notValidInBatchMode("Insert Headline Time") 

371 return 

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

373 # 2015/06/09: Fix bug 131: Insert time in headline now inserts time in body 

374 # Get the wrapper from the tree itself. 

375 # Do *not* set w = self.editWidget! 

376 if w: 

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

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

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

380 ins = w.getInsertPoint() 

381 s = c.getTime(body=False) 

382 w.insert(ins, s) 

383 else: 

384 c.endEditing() 

385 time = c.getTime(body=False) 

386 s = p.h.rstrip() 

387 if s: 

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

389 else: 

390 p.h = time 

391 c.redrawAndEdit(p, selectAll=True) 

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

393 @cmd('capitalize-headline') 

394 def capitalizeHeadline(self, event=None): 

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

396 frame = self 

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

398 

399 if g.app.batchMode: 

400 c.notValidInBatchMode("Capitalize Headline") 

401 return 

402 

403 h = p.h 

404 undoType = 'capitalize-headline' 

405 undoData = u.beforeChangeNodeContents(p) 

406 

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

408 capitalized = ' '.join(words) 

409 changed = capitalized != h 

410 if changed: 

411 p.h = capitalized 

412 c.setChanged() 

413 p.setDirty() 

414 u.afterChangeNodeContents(p, undoType, undoData) 

415 c.redraw() 

416 

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

418 def path_for_p(self, c, p): 

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

420 node `p`. 

421 

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

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

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

425 

426 :param outline c: outline containing p 

427 :param position p: position to locate 

428 :return: path 

429 :rtype: str 

430 """ 

431 

432 def atfile(p): 

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

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

435 return ( 

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

437 word0.startswith('@auto-') 

438 ) 

439 

440 aList = g.get_directives_dict_list(p) 

441 path = c.scanAtPathDirectives(aList) 

442 while c.positionExists(p): 

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

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

445 nodepath = g.os_path_join(path, nodepath) 

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

447 nodepath = g.os_path_dirname(nodepath) 

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

449 path = nodepath 

450 break 

451 p.moveToParent() 

452 

453 return path 

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

455 @cmd('tabify') 

456 def tabify(self, event): 

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

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

459 

460 @cmd('untabify') 

461 def untabify(self, event): 

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

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

464 

465 def tabifyHelper(self, event, which): 

466 w = self.editWidget(event) 

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

468 return 

469 self.beginCommand(w, undoType=which) 

470 i, end = w.getSelectionRange() 

471 txt = w.getSelectedText() 

472 if which == 'tabify': 

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

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

475 else: 

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

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

478 w.delete(i, end) 

479 w.insert(i, ntxt) 

480 n = i + len(ntxt) 

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

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

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

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

485 @cmd('capitalize-word') 

486 def capitalizeWord(self, event): 

487 """Capitalize the word at the cursor.""" 

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

489 

490 @cmd('downcase-word') 

491 def downCaseWord(self, event): 

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

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

494 

495 @cmd('upcase-word') 

496 def upCaseWord(self, event): 

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

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

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

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

501 w = self.editWidget(event) 

502 if not w: 

503 return # pragma: no cover (defensive) 

504 s = w.getAllText() 

505 ins = w.getInsertPoint() 

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

507 word = s[i:j] 

508 if not word.strip(): 

509 return # pragma: no cover (defensive) 

510 self.beginCommand(w, undoType=undoType) 

511 if which == 'cap': 

512 word2 = word.capitalize() 

513 elif which == 'low': 

514 word2 = word.lower() 

515 elif which == 'up': 

516 word2 = word.upper() 

517 else: 

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

519 changed = word != word2 

520 if changed: 

521 w.delete(i, j) 

522 w.insert(i, word2) 

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

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

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

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

527 def capitalizeWords(self, event=None): 

528 """Capitalize Entire Body Or Selection.""" 

529 frame = self 

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

531 w = frame.editWidget(event) 

532 s = w.getAllText() 

533 if not s: 

534 return 

535 

536 undoType = 'capitalize-body-words' 

537 undoData = u.beforeChangeNodeContents(p) 

538 

539 i, j = w.getSelectionRange() 

540 if i == j: 

541 sel = '' 

542 else: 

543 sel = s[i:j] 

544 text = sel or s 

545 if sel: 

546 prefix = s[:i] 

547 suffix = s[j:] 

548 

549 # Thanks to 

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

551 def convert_to_uppercase(m): 

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

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

554 

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

556 

557 if capitalized != text: 

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

559 c.setChanged() 

560 p.setDirty() 

561 u.afterChangeNodeContents(p, undoType, undoData) 

562 c.redraw() 

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

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

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

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

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

568 self.activateMenu('Cmds') 

569 

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

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

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

573 self.activateMenu('Edit') 

574 

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

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

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

578 self.activateMenu('File') 

579 

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

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

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

583 self.activateMenu('Help') 

584 

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

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

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

588 self.activateMenu('Outline') 

589 

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

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

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

593 self.activateMenu('Plugins') 

594 

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

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

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

598 self.activateMenu('Window') 

599 

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

601 c = self.c 

602 c.frame.menu.activateMenu(menuName) 

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

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

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

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

607 c, k = self.c, self.c.k 

608 c.bodyWantsFocus() 

609 if k: 

610 k.setDefaultInputState() 

611 k.showStateAndMode() 

612 

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

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

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

616 self.c.logWantsFocus() 

617 

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

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

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

621 self.c.minibufferWantsFocus() 

622 

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

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

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

626 self.c.treeWantsFocus() 

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

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

629 

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

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

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

633 c = self.c 

634 c.frame.tree.OnIconCtrlClick(c.p) 

635 # Calls the base LeoTree method. 

636 

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

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

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

640 c = self.c 

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

642 

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

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

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

646 c = self.c 

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

648 

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

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

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

652 c = self.c 

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

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

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

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

657 """ 

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

659 

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

661 """ 

662 c = self.c 

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

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

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

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

667 def setCommentColumn(self, event): 

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

669 w = self.editWidget(event) 

670 if not w: 

671 return # pragma: no cover (defensive) 

672 s = w.getAllText() 

673 ins = w.getInsertPoint() 

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

675 self.ccolumn = col 

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

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

678 def indentToCommentColumn(self, event): 

679 """ 

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

681 comment column. 

682 """ 

683 w = self.editWidget(event) 

684 if not w: 

685 return # pragma: no cover (defensive) 

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

687 s = w.getAllText() 

688 ins = w.getInsertPoint() 

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

690 line = s[i:j] 

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

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

693 if line2 != line: 

694 w.delete(i, j) 

695 w.insert(i, line2) 

696 w.setInsertPoint(i + c1) 

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

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

699 #@@language rest 

700 #@+at 

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

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

703 # 

704 # cats 

705 # raaaaaaaaaaaats 

706 # mats 

707 # zaaaaaaaaap 

708 # 

709 # may look like: 

710 # 

711 # cats 

712 # raaaaaaaaaaaats 

713 # mats 

714 # zaaaaaaaaap 

715 # 

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

717 #@@language python 

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

719 @cmd('center-line') 

720 def centerLine(self, event): 

721 """Centers line within current fill column""" 

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

723 if not w: 

724 return # pragma: no cover (defensive) 

725 if self.fillColumn > 0: 

726 fillColumn = self.fillColumn 

727 else: 

728 d = c.scanAllDirectives(c.p) 

729 fillColumn = d.get("pagewidth") 

730 s = w.getAllText() 

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

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

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

734 return 

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

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

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

738 k = g.skip_ws(s, i) 

739 if k > i: 

740 w.delete(i, k - i) 

741 w.insert(i, ws) 

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

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

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

745 def setFillColumn(self, event): 

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

747 k = self.c.k 

748 self.w = self.editWidget(event) 

749 if not self.w: 

750 return # pragma: no cover (defensive) 

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

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

753 

754 def setFillColumn1(self, event): 

755 c, k, w = self.c, self.c.k, self.w 

756 k.clearState() 

757 try: 

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

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

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

761 except ValueError: 

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

763 c.widgetWantsFocus(w) 

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

765 @cmd('center-region') 

766 def centerRegion(self, event): 

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

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

769 if not w: 

770 return # pragma: no cover (defensive) 

771 s = w.getAllText() 

772 sel_1, sel_2 = w.getSelectionRange() 

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

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

775 if self.fillColumn > 0: 

776 fillColumn = self.fillColumn 

777 else: 

778 d = c.scanAllDirectives(c.p) 

779 fillColumn = d.get("pagewidth") 

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

781 inserted = 0 

782 while ind < end: 

783 s = w.getAllText() 

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

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

786 if len(line) >= fillColumn: 

787 ind = j 

788 else: 

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

790 inserted += n 

791 k = g.skip_ws(s, i) 

792 if k > i: 

793 w.delete(i, k - i) 

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

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

796 w.setSelectionRange(sel_1, sel_2 + inserted) 

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

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

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

800 def setFillPrefix(self, event): 

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

802 w = self.editWidget(event) 

803 if not w: 

804 return # pragma: no cover (defensive) 

805 s = w.getAllText() 

806 i, j = w.getSelectionRange() 

807 self.fillPrefix = s[i:j] 

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

809 def _addPrefix(self, ntxt): 

810 ntxt = ntxt.split('.') 

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

812 ntxt = '.'.join(ntxt) 

813 return ntxt 

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

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

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

817 def backwardFindCharacter(self, event): 

818 """Search backwards for a character.""" 

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

820 

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

822 def backwardFindCharacterExtendSelection(self, event): 

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

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

825 

826 @cmd('find-character') 

827 def findCharacter(self, event): 

828 """Search for a character.""" 

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

830 

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

832 def findCharacterExtendSelection(self, event): 

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

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

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

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

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

838 k = self.c.k 

839 self.w = self.editWidget(event) 

840 if not self.w: 

841 return 

842 self.event = event 

843 self.backward = backward 

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

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

846 s = ( 

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

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

849 k.setLabelBlue(s) 

850 # Get the arg without touching the focus. 

851 k.getArg( 

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

853 

854 def findCharacter1(self, event): 

855 k = self.c.k 

856 event, w = self.event, self.w 

857 backward = self.backward 

858 extend = self.extend or self.extendMode 

859 ch = k.arg 

860 s = w.getAllText() 

861 ins = w.toPythonIndex(self.insert) 

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

863 if backward: 

864 start = 0 

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

866 if j > -1: 

867 self.moveToHelper(event, j, extend) 

868 else: 

869 end = len(s) 

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

871 if j > -1: 

872 self.moveToHelper(event, j, extend) 

873 k.resetLabel() 

874 k.clearState() 

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

876 @cmd('find-word') 

877 def findWord(self, event): 

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

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

880 

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

882 def findWordInLine(self, event): 

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

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

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

886 def findWordHelper(self, event, oneLine): 

887 k = self.c.k 

888 self.w = self.editWidget(event) 

889 if self.w: 

890 self.oneLineFlag = oneLine 

891 k.setLabelBlue( 

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

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

894 

895 def findWord1(self, event): 

896 c, k = self.c, self.c.k 

897 ch = k.arg 

898 if ch: 

899 w = self.w 

900 i = w.getInsertPoint() 

901 s = w.getAllText() 

902 end = len(s) 

903 if self.oneLineFlag: 

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

905 if end == -1: 

906 end = len(s) 

907 while i < end: 

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

909 if i == -1: 

910 break 

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

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

913 break 

914 k.resetLabel() 

915 k.clearState() 

916 c.widgetWantsFocus(w) 

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

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

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

920 def gotoAnyClone(self, event=None): 

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

922 c = self.c 

923 p = c.p.threadNext() 

924 while p: 

925 if p.isCloned(): 

926 c.selectPosition(p) 

927 return 

928 p.moveToThreadNext() 

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

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

931 @cmd('goto-char') 

932 def gotoCharacter(self, event): 

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

934 k = self.c.k 

935 self.w = self.editWidget(event) 

936 if self.w: 

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

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

939 

940 def gotoCharacter1(self, event): 

941 c, k = self.c, self.c.k 

942 n = k.arg 

943 w = self.w 

944 ok = False 

945 if n.isdigit(): 

946 n = int(n) 

947 if n >= 0: 

948 w.setInsertPoint(n) 

949 w.seeInsertPoint() 

950 ok = True 

951 if not ok: 

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

953 k.resetLabel() 

954 k.clearState() 

955 c.widgetWantsFocus(w) 

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

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

958 def gotoGlobalLine(self, event): 

959 """ 

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

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

962 

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

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

965 

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

967 """ 

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

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

970 k = self.c.k 

971 self.w = self.editWidget(event) 

972 if self.w: 

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

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

975 

976 def gotoGlobalLine1(self, event): 

977 c, k = self.c, self.c.k 

978 n = k.arg 

979 k.resetLabel() 

980 k.clearState() 

981 if n.isdigit(): 

982 # Very important: n is one-based. 

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

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

985 @cmd('goto-line') 

986 def gotoLine(self, event): 

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

988 k = self.c.k 

989 self.w = self.editWidget(event) 

990 if self.w: 

991 k.setLabelBlue('Goto line: ') 

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

993 

994 def gotoLine1(self, event): 

995 c, k = self.c, self.c.k 

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

997 if n.isdigit(): 

998 n = int(n) 

999 s = w.getAllText() 

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

1001 w.setInsertPoint(i) 

1002 w.seeInsertPoint() 

1003 k.resetLabel() 

1004 k.clearState() 

1005 c.widgetWantsFocus(w) 

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

1007 #@+at 

1008 # To do: 

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

1010 # - Tree control recomputes height of each line. 

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

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

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

1014 c = self.c 

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

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

1017 # pylint: disable=unpacking-non-sequence 

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

1019 if not image: 

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

1021 return xoffset 

1022 if image_height is None: 

1023 yoffset = 0 

1024 else: 

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

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

1027 newEntry = { 

1028 'type': 'file', 

1029 'file': path, 

1030 'relPath': relPath, 

1031 'where': 'beforeHeadline', 

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

1033 'on': 'VNode', 

1034 } 

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

1036 aList.append(newEntry) 

1037 xoffset += 2 

1038 return xoffset 

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

1040 def dHash(self, d): 

1041 """Hash a dictionary""" 

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

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

1044 def getIconList(self, p): 

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

1046 fromVnode = [] 

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

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

1049 for i in fromVnode: 

1050 i['on'] = 'VNode' 

1051 return fromVnode 

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

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

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

1055 current = self.getIconList(p) 

1056 if not l and not current: 

1057 return # nothing to do 

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

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

1060 if lHash == cHash: 

1061 # no difference between original and current list of dictionaries 

1062 return 

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

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

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

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

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

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

1069 

1070 p - postion 

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

1072 uaLoc - the v or t node 

1073 """ 

1074 if subl: # Update the uA. 

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

1076 uaLoc.unknownAttributes = {} 

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

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

1079 uaLoc._p_changed = True 

1080 if setDirty: 

1081 p.setDirty() 

1082 else: # delete the uA. 

1083 if hasattr(uaLoc, 'unknownAttributes'): 

1084 if 'icons' in uaLoc.unknownAttributes: 

1085 del uaLoc.unknownAttributes['icons'] 

1086 uaLoc._p_changed = True 

1087 if setDirty: 

1088 p.setDirty() 

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

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

1091 def deleteFirstIcon(self, event=None): 

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

1093 c = self.c 

1094 aList = self.getIconList(c.p) 

1095 if aList: 

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

1097 c.setChanged() 

1098 c.redraw_after_icons_changed() 

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

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

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

1102 c, p = self.c, self.c.p 

1103 aList = self.getIconList(p) 

1104 if not aList: 

1105 return 

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

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

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

1109 newList = [] 

1110 for d in aList: 

1111 name2 = d.get('file') 

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

1113 name2rel = d.get('relPath') 

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

1115 newList.append(d) 

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

1117 self.setIconList(p, newList) 

1118 c.setChanged() 

1119 c.redraw_after_icons_changed() 

1120 else: 

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

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

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

1124 def deleteLastIcon(self, event=None): 

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

1126 c = self.c 

1127 aList = self.getIconList(c.p) 

1128 if aList: 

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

1130 c.setChanged() 

1131 c.redraw_after_icons_changed() 

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

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

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

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

1136 c = self.c 

1137 p = p or c.p 

1138 if p.u: 

1139 p.v._p_changed = True 

1140 self.setIconList(p, []) 

1141 p.setDirty() 

1142 c.setChanged() 

1143 c.redraw_after_icons_changed() 

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

1145 @cmd('insert-icon') 

1146 def insertIcon(self, event=None): 

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

1148 c, p = self.c, self.c.p 

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

1150 os.chdir(iconDir) 

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

1152 title='Get Icons', 

1153 filetypes=[ 

1154 ('All files', '*'), 

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

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

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

1158 ], 

1159 defaultextension=None, multiple=True) 

1160 if not paths: 

1161 return 

1162 aList: List[Any] = [] 

1163 xoffset = 2 

1164 for path in paths: 

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

1166 aList2 = self.getIconList(p) 

1167 aList2.extend(aList) 

1168 self.setIconList(p, aList2) 

1169 c.setChanged() 

1170 c.redraw_after_icons_changed() 

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

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

1173 c = self.c 

1174 if not p: 

1175 p = c.p 

1176 aList: List[Any] = [] 

1177 xoffset = 2 

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

1179 aList2 = self.getIconList(p) 

1180 if pos is None: 

1181 pos = len(aList2) 

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

1183 self.setIconList(p, aList2) 

1184 c.setChanged() 

1185 c.redraw_after_icons_changed() 

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

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

1188 @cmd('delete-indentation') 

1189 def deleteIndentation(self, event): 

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

1191 w = self.editWidget(event) 

1192 if not w: 

1193 return # pragma: no cover (defensive) 

1194 s = w.getAllText() 

1195 ins = w.getInsertPoint() 

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

1197 line = s[i:j] 

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

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

1200 if delta: 

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

1202 w.delete(i, j) 

1203 w.insert(i, line2) 

1204 ins -= delta 

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

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

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

1208 @cmd('indent-relative') 

1209 def indentRelative(self, event): 

1210 """ 

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

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

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

1214 previous line. 

1215 

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

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

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

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

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

1221 """ 

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

1223 undoType = 'indent-relative' 

1224 w = self.editWidget(event) 

1225 if not w: 

1226 return # pragma: no cover (defensive) 

1227 s = w.getAllText() 

1228 ins = w.getInsertPoint() 

1229 # Find the previous non-blank line 

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

1231 while 1: 

1232 if i <= 0: 

1233 return 

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

1235 line = s[i:j] 

1236 if line.strip(): 

1237 break 

1238 self.beginCommand(w, undoType=undoType) 

1239 try: 

1240 bunch = u.beforeChangeBody(p) 

1241 k = g.skip_ws(s, i) 

1242 ws = s[i:k] 

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

1244 k = g.skip_ws(s, i2) 

1245 line = ws + s[k:j2] 

1246 w.delete(i2, j2) 

1247 w.insert(i2, line) 

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

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

1250 u.afterChangeBody(p, undoType, bunch) 

1251 finally: 

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

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

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

1255 @cmd('copy-gnx') 

1256 def copyGnx(self, event): 

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

1258 c = self.c 

1259 if not c: 

1260 return 

1261 gnx = c.p and c.p.gnx 

1262 if not gnx: 

1263 return 

1264 g.app.gui.replaceClipboardWith(gnx) 

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

1266 if status_line: 

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

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

1269 @cmd('line-number') 

1270 def lineNumber(self, event): 

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

1272 k = self.c.k 

1273 w = self.editWidget(event) 

1274 if not w: 

1275 return # pragma: no cover (defensive) 

1276 s = w.getAllText() 

1277 i = w.getInsertPoint() 

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

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

1280 k.setLabelGrey( 

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

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

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

1284 @cmd('view-lossage') 

1285 def viewLossage(self, event): 

1286 """Print recent keystrokes.""" 

1287 print('Recent keystrokes...') 

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

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

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

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

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

1293 def viewRecentCommands(self, event): 

1294 """Print recently-executed commands.""" 

1295 c = self.c 

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

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

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

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

1300 @cmd('what-line') 

1301 def whatLine(self, event): 

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

1303 k = self.c.k 

1304 w = self.editWidget(event) 

1305 if w: 

1306 s = w.getAllText() 

1307 i = w.getInsertPoint() 

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

1309 k.keyboardQuit() 

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

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

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

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

1314 def addSpaceToLines(self, event): 

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

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

1317 

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

1319 def addTabToLines(self, event): 

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

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

1322 

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

1324 def removeSpaceFromLines(self, event): 

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

1326 self.addRemoveHelper( 

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

1328 

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

1330 def removeTabFromLines(self, event): 

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

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

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

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

1335 c = self.c 

1336 w = self.editWidget(event) 

1337 if not w: 

1338 return 

1339 if w.hasSelection(): 

1340 s = w.getSelectedText() 

1341 else: 

1342 s = w.getAllText() 

1343 if not s: 

1344 return 

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

1346 d = c.scanAllDirectives(c.p) 

1347 width = d.get('tabwidth') 

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

1349 ch = ' ' * abs(width) 

1350 self.beginCommand(w, undoType=undoType) 

1351 lines = g.splitLines(s) 

1352 if add: 

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

1354 else: 

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

1356 result = ''.join(result_list) 

1357 if w.hasSelection(): 

1358 i, j = w.getSelectionRange() 

1359 w.delete(i, j) 

1360 w.insert(i, result) 

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

1362 else: 

1363 w.setAllText(result) 

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

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

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

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

1368 def backwardDeleteCharacter(self, event=None): 

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

1370 c = self.c 

1371 w = self.editWidget(event) 

1372 if not w: 

1373 return # pragma: no cover (defensive) 

1374 wname = c.widget_name(w) 

1375 ins = w.getInsertPoint() 

1376 i, j = w.getSelectionRange() 

1377 if wname.startswith('body'): 

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

1379 try: 

1380 tab_width = c.getTabWidth(c.p) 

1381 changed = True 

1382 if i != j: 

1383 w.delete(i, j) 

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

1385 elif i == 0: 

1386 changed = False 

1387 elif tab_width > 0: 

1388 w.delete(ins - 1) 

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

1390 else: 

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

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

1393 s = prev = w.getAllText() 

1394 ins = w.getInsertPoint() 

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

1396 s = prev = s[i:ins] 

1397 n = len(prev) 

1398 abs_width = abs(tab_width) 

1399 # Delete up to this many spaces. 

1400 n2 = (n % abs_width) or abs_width 

1401 n2 = min(n, n2) 

1402 count = 0 

1403 while n2 > 0: 

1404 n2 -= 1 

1405 ch = prev[n - count - 1] 

1406 if ch != ' ': 

1407 break 

1408 else: count += 1 

1409 # Make sure we actually delete something. 

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

1411 w.delete(i, ins) 

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

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

1414 finally: 

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

1416 # Necessary to make text changes stick. 

1417 else: 

1418 # No undo in this widget. 

1419 s = w.getAllText() 

1420 # Delete something if we can. 

1421 if i != j: 

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

1423 w.delete(i, j) 

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

1425 elif ins != 0: 

1426 # Do nothing at the start of the headline. 

1427 w.delete(ins - 1) 

1428 ins = ins - 1 

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

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

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

1432 def cleanAllLines(self, event): 

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

1434 c = self.c 

1435 u = c.undoer 

1436 w = c.frame.body.wrapper 

1437 if not w: 

1438 return 

1439 tag = 'clean-all-lines' 

1440 u.beforeChangeGroup(c.p, tag) 

1441 n = 0 

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

1443 lines = [] 

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

1445 if line.rstrip(): 

1446 lines.append(line.rstrip()) 

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

1448 lines.append('\n') 

1449 s2 = ''.join(lines) 

1450 if s2 != p.b: 

1451 print(p.h) 

1452 bunch = u.beforeChangeNodeContents(p) 

1453 p.b = s2 

1454 p.setDirty() 

1455 n += 1 

1456 u.afterChangeNodeContents(p, tag, bunch) 

1457 u.afterChangeGroup(c.p, tag) 

1458 c.redraw_after_icons_changed() 

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

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

1461 @cmd('clean-lines') 

1462 def cleanLines(self, event): 

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

1464 """ 

1465 w = self.editWidget(event) 

1466 if not w: 

1467 return # pragma: no cover (defensive) 

1468 if w.hasSelection(): 

1469 s = w.getSelectedText() 

1470 else: 

1471 s = w.getAllText() 

1472 lines = [] 

1473 for line in g.splitlines(s): 

1474 if line.rstrip(): 

1475 lines.append(line.rstrip()) 

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

1477 lines.append('\n') 

1478 result = ''.join(lines) 

1479 if s != result: 

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

1481 if w.hasSelection(): 

1482 i, j = w.getSelectionRange() 

1483 w.delete(i, j) 

1484 w.insert(i, result) 

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

1486 else: 

1487 i = w.getInsertPoint() 

1488 w.delete(0, 'end') 

1489 w.insert(0, result) 

1490 w.setInsertPoint(i) 

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

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

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

1494 def clearSelectedText(self, event): 

1495 """Delete the selected text.""" 

1496 w = self.editWidget(event) 

1497 if not w: 

1498 return 

1499 i, j = w.getSelectionRange() 

1500 if i == j: 

1501 return 

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

1503 w.delete(i, j) 

1504 w.setInsertPoint(i) 

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

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

1507 @cmd('delete-word') 

1508 def deleteWord(self, event=None): 

1509 """Delete the word at the cursor.""" 

1510 self.deleteWordHelper(event, forward=True) 

1511 

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

1513 def backwardDeleteWord(self, event=None): 

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

1515 self.deleteWordHelper(event, forward=False) 

1516 

1517 # Patch by NH2. 

1518 

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

1520 def deleteWordSmart(self, event=None): 

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

1522 and symbols smartly.""" 

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

1524 

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

1526 def backwardDeleteWordSmart(self, event=None): 

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

1528 and symbols smartly.""" 

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

1530 

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

1532 # c = self.c 

1533 w = self.editWidget(event) 

1534 if not w: 

1535 return 

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

1537 if w.hasSelection(): 

1538 from_pos, to_pos = w.getSelectionRange() 

1539 else: 

1540 from_pos = w.getInsertPoint() 

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

1542 to_pos = w.getInsertPoint() 

1543 # For Tk GUI, make sure to_pos > from_pos 

1544 if from_pos > to_pos: 

1545 from_pos, to_pos = to_pos, from_pos 

1546 w.delete(from_pos, to_pos) 

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

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

1549 @cmd('delete-char') 

1550 def deleteNextChar(self, event): 

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

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

1553 if not w: 

1554 return 

1555 wname = c.widget_name(w) 

1556 if wname.startswith('body'): 

1557 s = w.getAllText() 

1558 i, j = w.getSelectionRange() 

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

1560 changed = True 

1561 if i != j: 

1562 w.delete(i, j) 

1563 w.setInsertPoint(i) 

1564 elif j < len(s): 

1565 w.delete(i) 

1566 w.setInsertPoint(i) 

1567 else: 

1568 changed = False 

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

1570 else: 

1571 # No undo in this widget. 

1572 s = w.getAllText() 

1573 i, j = w.getSelectionRange() 

1574 # Delete something if we can. 

1575 if i != j: 

1576 w.delete(i, j) 

1577 w.setInsertPoint(i) 

1578 elif j < len(s): 

1579 w.delete(i) 

1580 w.setInsertPoint(i) 

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

1582 @cmd('delete-spaces') 

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

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

1585 w = self.editWidget(event) 

1586 if not w: 

1587 return # pragma: no cover (defensive) 

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

1589 s = w.getAllText() 

1590 ins = w.getInsertPoint() 

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

1592 w1 = ins - 1 

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

1594 w1 -= 1 

1595 w1 += 1 

1596 w2 = ins 

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

1598 w2 += 1 

1599 spaces = s[w1:w2] 

1600 if spaces: 

1601 self.beginCommand(w, undoType=undoType) 

1602 if insertspace: 

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

1604 else: 

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

1606 w.setAllText(s) 

1607 w.setInsertPoint(w1) 

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

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

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

1611 def insertHardTab(self, event): 

1612 """Insert one hard tab.""" 

1613 c = self.c 

1614 w = self.editWidget(event) 

1615 if not w: 

1616 return 

1617 if not g.isTextWrapper(w): 

1618 return 

1619 name = c.widget_name(w) 

1620 if name.startswith('head'): 

1621 return 

1622 ins = w.getInsertPoint() 

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

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

1625 ins += 1 

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

1627 self.endCommand() 

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

1629 @cmd('insert-newline') 

1630 def insertNewLine(self, event): 

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

1632 self.insertNewlineBase(event) 

1633 

1634 insertNewline = insertNewLine 

1635 

1636 def insertNewlineBase(self, event): 

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

1638 # Note: insertNewlineHelper already exists. 

1639 c, k = self.c, self.c.k 

1640 w = self.editWidget(event) 

1641 if not w: 

1642 return # pragma: no cover (defensive) 

1643 if not g.isTextWrapper(w): 

1644 return # pragma: no cover (defensive) 

1645 name = c.widget_name(w) 

1646 if name.startswith('head'): 

1647 return 

1648 oldSel = w.getSelectionRange() 

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

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

1651 k.setInputState('insert') 

1652 k.showStateAndMode() 

1653 self.endCommand() 

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

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

1656 def insertNewLineAndTab(self, event): 

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

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

1659 c, k = self.c, self.c.k 

1660 p = c.p 

1661 w = self.editWidget(event) 

1662 if not w: 

1663 return 

1664 if not g.isTextWrapper(w): 

1665 return 

1666 name = c.widget_name(w) 

1667 if name.startswith('head'): 

1668 return 

1669 if trace: 

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

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

1672 oldSel = w.getSelectionRange() 

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

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

1675 k.setInputState('insert') 

1676 k.showStateAndMode() 

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

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

1679 @cmd('insert-parentheses') 

1680 def insertParentheses(self, event): 

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

1682 w = self.editWidget(event) 

1683 if w: 

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

1685 i = w.getInsertPoint() 

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

1687 w.setInsertPoint(i + 1) 

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

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

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

1691 def insertSoftTab(self, event): 

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

1693 c = self.c 

1694 w = self.editWidget(event) 

1695 if not w: 

1696 return 

1697 if not g.isTextWrapper(w): 

1698 return 

1699 name = c.widget_name(w) 

1700 if name.startswith('head'): 

1701 return 

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

1703 ins = w.getInsertPoint() 

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

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

1706 ins += tab_width 

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

1708 self.endCommand() 

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

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

1711 def removeBlankLines(self, event): 

1712 """ 

1713 Remove lines containing nothing but whitespace. 

1714 

1715 Select all lines if there is no existing selection. 

1716 """ 

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

1718 # 

1719 # "Before" snapshot. 

1720 bunch = u.beforeChangeBody(p) 

1721 # 

1722 # Initial data. 

1723 oldYview = w.getYScrollPosition() 

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

1725 # 

1726 # Calculate the result. 

1727 result_list = [] 

1728 changed = False 

1729 for line in lines: 

1730 if line.strip(): 

1731 result_list.append(line) 

1732 else: 

1733 changed = True 

1734 if not changed: 

1735 return # pragma: no cover (defensive) 

1736 # 

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

1738 result = ''.join(result_list) 

1739 p.b = result 

1740 w.setAllText(result) 

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

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

1743 w.setYScrollPosition(oldYview) 

1744 # 

1745 # "after" snapshot. 

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

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

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

1749 def replaceCurrentCharacter(self, event): 

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

1751 k = self.c.k 

1752 self.w = self.editWidget(event) 

1753 if self.w: 

1754 k.setLabelBlue('Replace Character: ') 

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

1756 

1757 def replaceCurrentCharacter1(self, event): 

1758 c, k, w = self.c, self.c.k, self.w 

1759 ch = k.arg 

1760 if ch: 

1761 i, j = w.getSelectionRange() 

1762 if i > j: 

1763 i, j = j, i 

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

1765 if i == j: 

1766 i = max(0, i - 1) 

1767 w.delete(i) 

1768 else: 

1769 w.delete(i, j) 

1770 w.insert(i, ch) 

1771 w.setInsertPoint(i + 1) 

1772 k.clearState() 

1773 k.resetLabel() 

1774 k.showStateAndMode() 

1775 c.widgetWantsFocus(w) 

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

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

1778 

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

1780 """ 

1781 Insert a character in the body pane. 

1782 

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

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

1785 """ 

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

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

1788 undoType = 'Typing' 

1789 if not w: 

1790 return # pragma: no cover (defensive) 

1791 #@+<< set local vars >> 

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

1793 stroke = event.stroke if event else None 

1794 ch = event.char if event else '' 

1795 if ch == 'Return': 

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

1797 if ch == 'Tab': 

1798 ch = '\t' 

1799 name = c.widget_name(w) 

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

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

1802 oldYview = w.getYScrollPosition() 

1803 brackets = self.openBracketsList + self.closeBracketsList 

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

1805 #@-<< set local vars >> 

1806 if not ch: 

1807 return 

1808 if trace: 

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

1810 assert g.isStrokeOrNone(stroke) 

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

1812 return 

1813 if ch == '\t': 

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

1815 elif ch == '\b': 

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

1817 self.backwardDeleteCharacter(event) 

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

1819 ch = '\n' 

1820 self.insertNewlineHelper(w, oldSel, undoType) 

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

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

1823 elif inBrackets and self.autocompleteBrackets: 

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

1825 elif ch: 

1826 # Null chars must not delete the selection. 

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

1828 # 

1829 # Common processing. 

1830 # Set the column for up and down keys. 

1831 spot = w.getInsertPoint() 

1832 c.editCommands.setMoveCol(w, spot) 

1833 # 

1834 # Update the text and handle undo. 

1835 newText = w.getAllText() 

1836 if newText != oldText: 

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

1838 newSel = w.getSelectionRange() 

1839 newInsert = w.getInsertPoint() 

1840 newSel = w.getSelectionRange() 

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

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

1843 oldSel=oldSel, oldYview=oldYview, newInsert=newInsert, newSel=newSel) 

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

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

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

1847 c, p = self.c, self.c.p 

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

1849 i, j = oldSel 

1850 if i > j: 

1851 i, j = j, i 

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

1853 if i != j: 

1854 w.delete(i, j) 

1855 elif action == 'overwrite': 

1856 w.delete(i) 

1857 if isPlain: 

1858 ins = w.getInsertPoint() 

1859 if self.autojustify > 0 and not inBrackets: 

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

1861 s = w.getAllText() 

1862 i = g.skip_to_start_of_line(s, ins) 

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

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

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

1866 # Find the start of the word. 

1867 n = 0 

1868 ins -= 1 

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

1870 n += 1 

1871 ins -= 1 

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

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

1874 sins -= 1 

1875 oldSel = (sins, ins) 

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

1877 ins = w.getInsertPoint() 

1878 ins += (n + 1) 

1879 w.insert(ins, ch) 

1880 w.setInsertPoint(ins + 1) 

1881 else: 

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

1883 if inBrackets and self.flashMatchingBrackets: 

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

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

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

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

1888 i, j = oldSel 

1889 if i > j: 

1890 i, j = j, i 

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

1892 if i != j: 

1893 w.delete(i, j) 

1894 elif action == 'overwrite': 

1895 w.delete(i) 

1896 ins = w.getInsertPoint() 

1897 # Pick the correct curly quote. 

1898 s = w.getAllText() or "" 

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

1900 open_curly = ins == i2 or ins > i2 and s[ins - 1] in ' \t' 

1901 # not s[ins-1].isalnum() 

1902 if open_curly: 

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

1904 else: 

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

1906 w.insert(ins, ch) 

1907 w.setInsertPoint(ins + 1) 

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

1909 def flashCharacter(self, w, i): 

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

1911 bg = self.bracketsFlashBg or 'DodgerBlue1' 

1912 fg = self.bracketsFlashFg or 'white' 

1913 flashes = self.bracketsFlashCount or 3 

1914 delay = self.bracketsFlashDelay or 75 

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

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

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

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

1919 d = {} 

1920 # pylint: disable=consider-using-enumerate 

1921 if ch in self.openBracketsList: 

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

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

1924 # reverse = False # Search forward 

1925 else: 

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

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

1928 # reverse = True # Search backward 

1929 s = w.getAllText() 

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

1931 language = g.getLanguageAtPosition(c, p) 

1932 if language == 'perl': 

1933 return 

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

1935 if j is not None: 

1936 self.flashCharacter(w, j) 

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

1938 def initBracketMatcher(self, c): 

1939 """Init the bracket matching code.""" 

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

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

1942 self.openBracketsList = '([{' 

1943 self.closeBracketsList = ')]}' 

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

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

1946 

1947 c, p = self.c, self.c.p 

1948 i, j = oldSel 

1949 ch = '\n' 

1950 if i != j: 

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

1952 w.delete(i, j) 

1953 w.insert(i, ch) 

1954 w.setInsertPoint(i + 1) 

1955 else: 

1956 w.insert(i, ch) 

1957 w.setInsertPoint(i + 1) 

1958 if (c.autoindent_in_nocolor or 

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

1960 undoType != "Change") 

1961 ): 

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

1963 self.updateAutoIndent(p, w) 

1964 w.seeInsertPoint() 

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

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

1967 

1968 def updateAutoIndent(self, p, w): 

1969 """Handle auto indentation.""" 

1970 c = self.c 

1971 tab_width = c.getTabWidth(p) 

1972 # Get the previous line. 

1973 s = w.getAllText() 

1974 ins = w.getInsertPoint() 

1975 i = g.skip_to_start_of_line(s, ins) 

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

1977 s = s[i : j - 1] 

1978 # Add the leading whitespace to the present line. 

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

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

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

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

1983 width += abs(tab_width) 

1984 if self.smartAutoIndent: 

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

1986 bracketWidths = [width] 

1987 tabex = 0 

1988 for i, ch in enumerate(s): 

1989 if ch == '\t': 

1990 tabex += tab_width - 1 

1991 if ch in '([{': 

1992 bracketWidths.append(i + tabex + 1) 

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

1994 bracketWidths.pop() 

1995 width = bracketWidths.pop() 

1996 ws = g.computeLeadingWhitespace(width, tab_width) 

1997 if ws: 

1998 i = w.getInsertPoint() 

1999 w.insert(i, ws) 

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

2001 w.seeInsertPoint() 

2002 # 2011/10/02: Fix cursor-movement bug. 

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

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

2005 

2006 c = self.c 

2007 d = c.scanAllDirectives(p) 

2008 i, j = oldSel 

2009 language = d.get('language') 

2010 s = w.getAllText() 

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

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

2013 if automatch: 

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

2015 if i != j: 

2016 w.delete(i, j) 

2017 w.insert(i, ch) 

2018 if automatch: 

2019 ins = w.getInsertPoint() 

2020 w.setInsertPoint(ins - 1) 

2021 else: 

2022 ins = w.getInsertPoint() 

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

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

2025 ins = w.getInsertPoint() 

2026 w.setInsertPoint(ins + 1) 

2027 else: 

2028 if i != j: 

2029 w.delete(i, j) 

2030 w.insert(i, ch) 

2031 w.setInsertPoint(i + 1) 

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

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

2034 """ 

2035 A helper for selfInsertCommand. 

2036 

2037 Add spaces equivalent to a tab. 

2038 """ 

2039 c = self.c 

2040 i, j = w.getSelectionRange() 

2041 # Returns insert point if no selection, with i <= j. 

2042 if i != j: 

2043 c.indentBody(event) 

2044 return 

2045 tab_width = c.getTabWidth(p) 

2046 # Get the preceeding characters. 

2047 s = w.getAllText() 

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

2049 after = s[i:end] 

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

2051 after = after[:-1] 

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

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

2054 # Truly at the start of the line. 

2055 # and not after # Nothing *at all* after the cursor. 

2056 if doSmartTab: 

2057 self.updateAutoIndent(p, w) 

2058 # Add a tab if otherwise nothing would happen. 

2059 if s == w.getAllText(): 

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

2061 else: 

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

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

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

2065 """ 

2066 A helper for selfInsertCommand, called from updateTab. 

2067 

2068 Insert spaces equivalent to one tab. 

2069 """ 

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

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

2072 s2 = s[start:i] 

2073 width = g.computeWidth(s2, tab_width) 

2074 if trace: 

2075 g.trace('width', width) 

2076 if tab_width > 0: 

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

2078 ins = i + 1 

2079 else: 

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

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

2082 ins = i + n 

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

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

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

2086 @cmd('flush-lines') 

2087 def flushLines(self, event): 

2088 """ 

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

2090 text after point. 

2091 

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

2093 on the region instead. 

2094 """ 

2095 k = self.c.k 

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

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

2098 

2099 def flushLines1(self, event): 

2100 k = self.c.k 

2101 k.clearState() 

2102 k.resetLabel() 

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

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

2105 @cmd('keep-lines') 

2106 def keepLines(self, event): 

2107 """ 

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

2109 the text after point. 

2110 

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

2112 on the region instead. 

2113 """ 

2114 k = self.c.k 

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

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

2117 

2118 def keepLines1(self, event): 

2119 k = self.c.k 

2120 k.clearState() 

2121 k.resetLabel() 

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

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

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

2125 w = self.editWidget(event) 

2126 if not w: 

2127 return # pragma: no cover (defensive) 

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

2129 if w.hasSelection(): 

2130 i, end = w.getSelectionRange() 

2131 else: 

2132 i = w.getInsertPoint() 

2133 end = 'end' 

2134 txt = w.get(i, end) 

2135 tlines = txt.splitlines(True) 

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

2137 try: 

2138 regex = re.compile(pattern) 

2139 for n, z in enumerate(tlines): 

2140 f = regex.findall(z) 

2141 if which == 'flush' and f: 

2142 keeplines[n] = None 

2143 elif f: 

2144 keeplines.append(z) 

2145 except Exception: 

2146 return 

2147 if which == 'flush': 

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

2149 w.delete(i, end) 

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

2151 w.setInsertPoint(i) 

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

2153 #@+node:ekr.20200619082429.1: *4* ec.moveLinesToNextNode (new) 

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

2155 def moveLineToNextNode(self, event): 

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

2157 c = self.c 

2158 if not c.p.threadNext(): 

2159 return 

2160 w = self.editWidget(event) 

2161 if not w: 

2162 return 

2163 s = w.getAllText() 

2164 sel_1, sel_2 = w.getSelectionRange() 

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

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

2167 lines = s[i:j] 

2168 if not lines.strip(): 

2169 return 

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

2171 try: 

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

2173 w.delete(i, next_j) 

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

2175 c.selectPosition(c.p.threadNext()) 

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

2177 c.recolor() 

2178 finally: 

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

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

2181 @cmd('split-line') 

2182 def splitLine(self, event): 

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

2184 w = self.editWidget(event) 

2185 if w: 

2186 self.beginCommand(w, undoType='split-line') 

2187 s = w.getAllText() 

2188 ins = w.getInsertPoint() 

2189 w.setAllText(s[:ins] + '\n' + s[ins:]) 

2190 w.setInsertPoint(ins + 1) 

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

2192 #@+node:ekr.20150514063305.285: *3* ec: move cursor 

2193 #@+node:ekr.20150514063305.286: *4* ec. helpers 

2194 #@+node:ekr.20150514063305.287: *5* ec.extendHelper 

2195 def extendHelper(self, w, extend, spot, upOrDown=False): 

2196 """ 

2197 Handle the details of extending the selection. 

2198 This method is called for all cursor moves. 

2199 

2200 extend: Clear the selection unless this is True. 

2201 spot: The *new* insert point. 

2202 """ 

2203 c, p = self.c, self.c.p 

2204 extend = extend or self.extendMode 

2205 ins = w.getInsertPoint() 

2206 i, j = w.getSelectionRange() 

2207 # Reset the move spot if needed. 

2208 if self.moveSpot is None or p.v != self.moveSpotNode: 

2209 self.setMoveCol(w, ins if extend else spot) # sets self.moveSpot. 

2210 elif extend: 

2211 # 2011/05/20: Fix bug 622819 

2212 # Ctrl-Shift movement is incorrect when there is an unexpected selection. 

2213 if i == j: 

2214 self.setMoveCol(w, ins) # sets self.moveSpot. 

2215 elif self.moveSpot in (i, j) and self.moveSpot != ins: 

2216 # The bug fix, part 1. 

2217 pass 

2218 else: 

2219 # The bug fix, part 2. 

2220 # Set the moveCol to the *not* insert point. 

2221 if ins == i: 

2222 k = j 

2223 elif ins == j: 

2224 k = i 

2225 else: 

2226 k = ins 

2227 self.setMoveCol(w, k) # sets self.moveSpot. 

2228 else: 

2229 if upOrDown: 

2230 s = w.getAllText() 

2231 i2, j2 = g.getLine(s, spot) 

2232 line = s[i2:j2] 

2233 row, col = g.convertPythonIndexToRowCol(s, spot) 

2234 if True: # was j2 < len(s)-1: 

2235 n = min(self.moveCol, max(0, len(line) - 1)) 

2236 else: 

2237 n = min(self.moveCol, max(0, len(line))) # A tricky boundary. 

2238 spot = g.convertRowColToPythonIndex(s, row, n) 

2239 else: # Plain move forward or back. 

2240 self.setMoveCol(w, spot) # sets self.moveSpot. 

2241 if extend: 

2242 if spot < self.moveSpot: 

2243 w.setSelectionRange(spot, self.moveSpot, insert=spot) 

2244 else: 

2245 w.setSelectionRange(self.moveSpot, spot, insert=spot) 

2246 else: 

2247 w.setSelectionRange(spot, spot, insert=spot) 

2248 w.seeInsertPoint() 

2249 c.frame.updateStatusLine() 

2250 #@+node:ekr.20150514063305.288: *5* ec.moveToHelper 

2251 def moveToHelper(self, event, spot, extend): 

2252 """ 

2253 Common helper method for commands the move the cursor 

2254 in a way that can be described by a Tk Text expression. 

2255 """ 

2256 c, k = self.c, self.c.k 

2257 w = self.editWidget(event) 

2258 if not w: 

2259 return 

2260 c.widgetWantsFocusNow(w) 

2261 # Put the request in the proper range. 

2262 if c.widget_name(w).startswith('mini'): 

2263 i, j = k.getEditableTextRange() 

2264 if spot < i: 

2265 spot = i 

2266 elif spot > j: 

2267 spot = j 

2268 self.extendHelper(w, extend, spot, upOrDown=False) 

2269 #@+node:ekr.20150514063305.305: *5* ec.moveWithinLineHelper 

2270 def moveWithinLineHelper(self, event, spot, extend): 

2271 w = self.editWidget(event) 

2272 if not w: 

2273 return 

2274 # Bug fix: 2012/02/28: don't use the Qt end-line logic: 

2275 # it apparently does not work for wrapped lines. 

2276 spots = ('end-line', 'finish-line', 'start-line') 

2277 if hasattr(w, 'leoMoveCursorHelper') and spot not in spots: 

2278 extend = extend or self.extendMode 

2279 w.leoMoveCursorHelper(kind=spot, extend=extend) 

2280 else: 

2281 s = w.getAllText() 

2282 ins = w.getInsertPoint() 

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

2284 line = s[i:j] 

2285 if spot == 'begin-line': # was 'start-line' 

2286 self.moveToHelper(event, i, extend=extend) 

2287 elif spot == 'end-line': 

2288 # Bug fix: 2011/11/13: Significant in external tests. 

2289 if g.match(s, j - 1, '\n') and i != j: 

2290 j -= 1 

2291 self.moveToHelper(event, j, extend=extend) 

2292 elif spot == 'finish-line': 

2293 if not line.isspace(): 

2294 if g.match(s, j - 1, '\n'): 

2295 j -= 1 

2296 while j >= 0 and s[j].isspace(): 

2297 j -= 1 

2298 self.moveToHelper(event, j, extend=extend) 

2299 elif spot == 'start-line': # new 

2300 if not line.isspace(): 

2301 while i < j and s[i].isspace(): 

2302 i += 1 

2303 self.moveToHelper(event, i, extend=extend) 

2304 else: 

2305 g.trace(f"can not happen: bad spot: {spot}") 

2306 #@+node:ekr.20150514063305.317: *5* ec.moveWordHelper 

2307 def moveWordHelper(self, event, extend, forward, end=False, smart=False): 

2308 """ 

2309 Move the cursor to the next/previous word. 

2310 The cursor is placed at the start of the word unless end=True 

2311 """ 

2312 c = self.c 

2313 w = self.editWidget(event) 

2314 if not w: 

2315 return # pragma: no cover (defensive) 

2316 c.widgetWantsFocusNow(w) 

2317 s = w.getAllText() 

2318 n = len(s) 

2319 i = w.getInsertPoint() 

2320 alphanumeric_re = re.compile(r"\w") 

2321 whitespace_re = re.compile(r"\s") 

2322 simple_whitespace_re = re.compile(r"[ \t]") 

2323 #@+others 

2324 #@+node:ekr.20150514063305.318: *6* ec.moveWordHelper functions 

2325 def is_alphanumeric(c): 

2326 return alphanumeric_re.match(c) is not None 

2327 

2328 def is_whitespace(c): 

2329 return whitespace_re.match(c) is not None 

2330 

2331 def is_simple_whitespace(c): 

2332 return simple_whitespace_re.match(c) is not None 

2333 

2334 def is_line_break(c): 

2335 return is_whitespace(c) and not is_simple_whitespace(c) 

2336 

2337 def is_special(c): 

2338 return not is_alphanumeric(c) and not is_whitespace(c) 

2339 

2340 def seek_until_changed(i, match_function, step): 

2341 while 0 <= i < n and match_function(s[i]): 

2342 i += step 

2343 return i 

2344 

2345 def seek_word_end(i): 

2346 return seek_until_changed(i, is_alphanumeric, 1) 

2347 

2348 def seek_word_start(i): 

2349 return seek_until_changed(i, is_alphanumeric, -1) 

2350 

2351 def seek_simple_whitespace_end(i): 

2352 return seek_until_changed(i, is_simple_whitespace, 1) 

2353 

2354 def seek_simple_whitespace_start(i): 

2355 return seek_until_changed(i, is_simple_whitespace, -1) 

2356 

2357 def seek_special_end(i): 

2358 return seek_until_changed(i, is_special, 1) 

2359 

2360 def seek_special_start(i): 

2361 return seek_until_changed(i, is_special, -1) 

2362 #@-others 

2363 if smart: 

2364 if forward: 

2365 if 0 <= i < n: 

2366 if is_alphanumeric(s[i]): 

2367 i = seek_word_end(i) 

2368 i = seek_simple_whitespace_end(i) 

2369 elif is_simple_whitespace(s[i]): 

2370 i = seek_simple_whitespace_end(i) 

2371 elif is_special(s[i]): 

2372 i = seek_special_end(i) 

2373 i = seek_simple_whitespace_end(i) 

2374 else: 

2375 i += 1 # e.g. for newlines 

2376 else: 

2377 i -= 1 # Shift cursor temporarily by -1 to get easy read access to the prev. char 

2378 if 0 <= i < n: 

2379 if is_alphanumeric(s[i]): 

2380 i = seek_word_start(i) 

2381 # Do not seek further whitespace here 

2382 elif is_simple_whitespace(s[i]): 

2383 i = seek_simple_whitespace_start(i) 

2384 elif is_special(s[i]): 

2385 i = seek_special_start(i) 

2386 # Do not seek further whitespace here 

2387 else: 

2388 i -= 1 # e.g. for newlines 

2389 i += 1 

2390 else: 

2391 if forward: 

2392 # Unlike backward-word moves, there are two options... 

2393 if end: 

2394 while 0 <= i < n and not g.isWordChar(s[i]): 

2395 i += 1 

2396 while 0 <= i < n and g.isWordChar(s[i]): 

2397 i += 1 

2398 else: 

2399 #1653. Scan for non-words *first*. 

2400 while 0 <= i < n and not g.isWordChar(s[i]): 

2401 i += 1 

2402 while 0 <= i < n and g.isWordChar(s[i]): 

2403 i += 1 

2404 else: 

2405 i -= 1 

2406 while 0 <= i < n and not g.isWordChar(s[i]): 

2407 i -= 1 

2408 while 0 <= i < n and g.isWordChar(s[i]): 

2409 i -= 1 

2410 i += 1 # 2015/04/30 

2411 self.moveToHelper(event, i, extend) 

2412 #@+node:ekr.20150514063305.289: *5* ec.setMoveCol 

2413 def setMoveCol(self, w, spot): 

2414 """Set the column to which an up or down arrow will attempt to move.""" 

2415 p = self.c.p 

2416 i, row, col = w.toPythonIndexRowCol(spot) 

2417 self.moveSpot = i 

2418 self.moveCol = col 

2419 self.moveSpotNode = p.v 

2420 #@+node:ekr.20150514063305.290: *4* ec.backToHome/ExtendSelection 

2421 @cmd('back-to-home') 

2422 def backToHome(self, event, extend=False): 

2423 """ 

2424 Smart home: 

2425 Position the point at the first non-blank character on the line, 

2426 or the start of the line if already there. 

2427 """ 

2428 w = self.editWidget(event) 

2429 if not w: 

2430 return 

2431 s = w.getAllText() 

2432 ins = w.getInsertPoint() 

2433 if s: 

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

2435 i1 = i 

2436 while i < j and s[i] in ' \t': 

2437 i += 1 

2438 if i == ins: 

2439 i = i1 

2440 self.moveToHelper(event, i, extend=extend) 

2441 

2442 @cmd('back-to-home-extend-selection') 

2443 def backToHomeExtendSelection(self, event): 

2444 self.backToHome(event, extend=True) 

2445 #@+node:ekr.20150514063305.291: *4* ec.backToIndentation 

2446 @cmd('back-to-indentation') 

2447 def backToIndentation(self, event): 

2448 """Position the point at the first non-blank character on the line.""" 

2449 w = self.editWidget(event) 

2450 if not w: 

2451 return # pragma: no cover (defensive) 

2452 s = w.getAllText() 

2453 ins = w.getInsertPoint() 

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

2455 while i < j and s[i] in ' \t': 

2456 i += 1 

2457 self.moveToHelper(event, i, extend=False) 

2458 #@+node:ekr.20150514063305.316: *4* ec.backward*/ExtendSelection 

2459 @cmd('back-word') 

2460 def backwardWord(self, event): 

2461 """Move the cursor to the previous word.""" 

2462 self.moveWordHelper(event, extend=False, forward=False) 

2463 

2464 @cmd('back-word-extend-selection') 

2465 def backwardWordExtendSelection(self, event): 

2466 """Extend the selection by moving the cursor to the previous word.""" 

2467 self.moveWordHelper(event, extend=True, forward=False) 

2468 

2469 @cmd('back-word-smart') 

2470 def backwardWordSmart(self, event): 

2471 """Move the cursor to the beginning of the current or the end of the previous word.""" 

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

2473 

2474 @cmd('back-word-smart-extend-selection') 

2475 def backwardWordSmartExtendSelection(self, event): 

2476 """Extend the selection by moving the cursor to the beginning of the current 

2477 or the end of the previous word.""" 

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

2479 #@+node:ekr.20170707072347.1: *4* ec.beginningOfLine/ExtendSelection 

2480 @cmd('beginning-of-line') 

2481 def beginningOfLine(self, event): 

2482 """Move the cursor to the first character of the line.""" 

2483 self.moveWithinLineHelper(event, 'begin-line', extend=False) 

2484 

2485 @cmd('beginning-of-line-extend-selection') 

2486 def beginningOfLineExtendSelection(self, event): 

2487 """ 

2488 Extend the selection by moving the cursor to the first character of the 

2489 line. 

2490 """ 

2491 self.moveWithinLineHelper(event, 'begin-line', extend=True) 

2492 #@+node:ekr.20150514063305.292: *4* ec.between lines & helper 

2493 @cmd('next-line') 

2494 def nextLine(self, event): 

2495 """Move the cursor down, extending the selection if in extend mode.""" 

2496 self.moveUpOrDownHelper(event, 'down', extend=False) 

2497 

2498 @cmd('next-line-extend-selection') 

2499 def nextLineExtendSelection(self, event): 

2500 """Extend the selection by moving the cursor down.""" 

2501 self.moveUpOrDownHelper(event, 'down', extend=True) 

2502 

2503 @cmd('previous-line') 

2504 def prevLine(self, event): 

2505 """Move the cursor up, extending the selection if in extend mode.""" 

2506 self.moveUpOrDownHelper(event, 'up', extend=False) 

2507 

2508 @cmd('previous-line-extend-selection') 

2509 def prevLineExtendSelection(self, event): 

2510 """Extend the selection by moving the cursor up.""" 

2511 self.moveUpOrDownHelper(event, 'up', extend=True) 

2512 #@+node:ekr.20150514063305.293: *5* ec.moveUpOrDownHelper 

2513 def moveUpOrDownHelper(self, event, direction, extend): 

2514 

2515 w = self.editWidget(event) 

2516 if not w: 

2517 return # pragma: no cover (defensive) 

2518 ins = w.getInsertPoint() 

2519 s = w.getAllText() 

2520 w.seeInsertPoint() 

2521 if hasattr(w, 'leoMoveCursorHelper'): 

2522 extend = extend or self.extendMode 

2523 w.leoMoveCursorHelper(kind=direction, extend=extend) 

2524 else: 

2525 # Find the start of the next/prev line. 

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

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

2528 if direction == 'down': 

2529 i2, j2 = g.getLine(s, j) 

2530 else: 

2531 i2, j2 = g.getLine(s, i - 1) 

2532 # The spot is the start of the line plus the column index. 

2533 n = max(0, j2 - i2 - 1) # The length of the new line. 

2534 col2 = min(col, n) 

2535 spot = i2 + col2 

2536 self.extendHelper(w, extend, spot, upOrDown=True) 

2537 #@+node:ekr.20150514063305.294: *4* ec.buffers & helper 

2538 @cmd('beginning-of-buffer') 

2539 def beginningOfBuffer(self, event): 

2540 """Move the cursor to the start of the body text.""" 

2541 self.moveToBufferHelper(event, 'home', extend=False) 

2542 

2543 @cmd('beginning-of-buffer-extend-selection') 

2544 def beginningOfBufferExtendSelection(self, event): 

2545 """Extend the text selection by moving the cursor to the start of the body text.""" 

2546 self.moveToBufferHelper(event, 'home', extend=True) 

2547 

2548 @cmd('end-of-buffer') 

2549 def endOfBuffer(self, event): 

2550 """Move the cursor to the end of the body text.""" 

2551 self.moveToBufferHelper(event, 'end', extend=False) 

2552 

2553 @cmd('end-of-buffer-extend-selection') 

2554 def endOfBufferExtendSelection(self, event): 

2555 """Extend the text selection by moving the cursor to the end of the body text.""" 

2556 self.moveToBufferHelper(event, 'end', extend=True) 

2557 #@+node:ekr.20150514063305.295: *5* ec.moveToBufferHelper 

2558 def moveToBufferHelper(self, event, spot, extend): 

2559 w = self.editWidget(event) 

2560 if not w: 

2561 return # pragma: no cover (defensive) 

2562 if hasattr(w, 'leoMoveCursorHelper'): 

2563 extend = extend or self.extendMode 

2564 w.leoMoveCursorHelper(kind=spot, extend=extend) 

2565 else: 

2566 if spot == 'home': 

2567 self.moveToHelper(event, 0, extend=extend) 

2568 elif spot == 'end': 

2569 s = w.getAllText() 

2570 self.moveToHelper(event, len(s), extend=extend) 

2571 else: 

2572 g.trace('can not happen: bad spot', spot) # pragma: no cover (defensive) 

2573 #@+node:ekr.20150514063305.296: *4* ec.characters & helper 

2574 @cmd('back-char') 

2575 def backCharacter(self, event): 

2576 """Move the cursor back one character, extending the selection if in extend mode.""" 

2577 self.moveToCharacterHelper(event, 'left', extend=False) 

2578 

2579 @cmd('back-char-extend-selection') 

2580 def backCharacterExtendSelection(self, event): 

2581 """Extend the selection by moving the cursor back one character.""" 

2582 self.moveToCharacterHelper(event, 'left', extend=True) 

2583 

2584 @cmd('forward-char') 

2585 def forwardCharacter(self, event): 

2586 """Move the cursor forward one character, extending the selection if in extend mode.""" 

2587 self.moveToCharacterHelper(event, 'right', extend=False) 

2588 

2589 @cmd('forward-char-extend-selection') 

2590 def forwardCharacterExtendSelection(self, event): 

2591 """Extend the selection by moving the cursor forward one character.""" 

2592 self.moveToCharacterHelper(event, 'right', extend=True) 

2593 #@+node:ekr.20150514063305.297: *5* ec.moveToCharacterHelper 

2594 def moveToCharacterHelper(self, event, spot, extend): 

2595 w = self.editWidget(event) 

2596 if not w: 

2597 return 

2598 if hasattr(w, 'leoMoveCursorHelper'): 

2599 extend = extend or self.extendMode 

2600 w.leoMoveCursorHelper(kind=spot, extend=extend) 

2601 else: 

2602 i = w.getInsertPoint() 

2603 if spot == 'left': 

2604 i = max(0, i - 1) 

2605 self.moveToHelper(event, i, extend=extend) 

2606 elif spot == 'right': 

2607 i = min(i + 1, len(w.getAllText())) 

2608 self.moveToHelper(event, i, extend=extend) 

2609 else: 

2610 g.trace(f"can not happen: bad spot: {spot}") 

2611 #@+node:ekr.20150514063305.298: *4* ec.clear/set/ToggleExtendMode 

2612 @cmd('clear-extend-mode') 

2613 def clearExtendMode(self, event): 

2614 """Turn off extend mode: cursor movement commands do not extend the selection.""" 

2615 self.extendModeHelper(event, False) 

2616 

2617 @cmd('set-extend-mode') 

2618 def setExtendMode(self, event): 

2619 """Turn on extend mode: cursor movement commands do extend the selection.""" 

2620 self.extendModeHelper(event, True) 

2621 

2622 @cmd('toggle-extend-mode') 

2623 def toggleExtendMode(self, event): 

2624 """Toggle extend mode, i.e., toggle whether cursor movement commands extend the selections.""" 

2625 self.extendModeHelper(event, not self.extendMode) 

2626 

2627 def extendModeHelper(self, event, val): 

2628 c = self.c 

2629 w = self.editWidget(event) 

2630 if w: 

2631 self.extendMode = val 

2632 if not g.unitTesting: 

2633 # g.red('extend mode','on' if val else 'off')) 

2634 c.k.showStateAndMode() 

2635 c.widgetWantsFocusNow(w) 

2636 #@+node:ekr.20170707072524.1: *4* ec.endOfLine/ExtendSelection 

2637 @cmd('end-of-line') 

2638 def endOfLine(self, event): 

2639 """Move the cursor to the last character of the line.""" 

2640 self.moveWithinLineHelper(event, 'end-line', extend=False) 

2641 

2642 @cmd('end-of-line-extend-selection') 

2643 def endOfLineExtendSelection(self, event): 

2644 """Extend the selection by moving the cursor to the last character of the line.""" 

2645 self.moveWithinLineHelper(event, 'end-line', extend=True) 

2646 #@+node:ekr.20150514063305.299: *4* ec.exchangePointMark 

2647 @cmd('exchange-point-mark') 

2648 def exchangePointMark(self, event): 

2649 """ 

2650 Exchange the point (insert point) with the mark (the other end of the 

2651 selected text). 

2652 """ 

2653 c = self.c 

2654 w = self.editWidget(event) 

2655 if not w: 

2656 return 

2657 if hasattr(w, 'leoMoveCursorHelper'): 

2658 w.leoMoveCursorHelper(kind='exchange', extend=False) 

2659 else: 

2660 c.widgetWantsFocusNow(w) 

2661 i, j = w.getSelectionRange(sort=False) 

2662 if i == j: 

2663 return 

2664 ins = w.getInsertPoint() 

2665 ins = j if ins == i else i 

2666 w.setInsertPoint(ins) 

2667 w.setSelectionRange(i, j, insert=None) 

2668 #@+node:ekr.20150514063305.300: *4* ec.extend-to-line 

2669 @cmd('extend-to-line') 

2670 def extendToLine(self, event): 

2671 """Select the line at the cursor.""" 

2672 w = self.editWidget(event) 

2673 if not w: 

2674 return 

2675 s = w.getAllText() 

2676 n = len(s) 

2677 i = w.getInsertPoint() 

2678 while 0 <= i < n and not s[i] == '\n': 

2679 i -= 1 

2680 i += 1 

2681 i1 = i 

2682 while 0 <= i < n and not s[i] == '\n': 

2683 i += 1 

2684 w.setSelectionRange(i1, i) 

2685 #@+node:ekr.20150514063305.301: *4* ec.extend-to-sentence 

2686 @cmd('extend-to-sentence') 

2687 def extendToSentence(self, event): 

2688 """Select the line at the cursor.""" 

2689 w = self.editWidget(event) 

2690 if not w: 

2691 return # pragma: no cover (defensive) 

2692 s = w.getAllText() 

2693 n = len(s) 

2694 i = w.getInsertPoint() 

2695 i2 = 1 + s.find('.', i) 

2696 if i2 == -1: 

2697 i2 = n 

2698 i1 = 1 + s.rfind('.', 0, i2 - 1) 

2699 w.setSelectionRange(i1, i2) 

2700 #@+node:ekr.20150514063305.302: *4* ec.extend-to-word 

2701 @cmd('extend-to-word') 

2702 def extendToWord(self, event, select=True, w=None): 

2703 """Compute the word at the cursor. Select it if select arg is True.""" 

2704 if not w: 

2705 w = self.editWidget(event) 

2706 if not w: 

2707 return 0, 0 # pragma: no cover (defensive) 

2708 s = w.getAllText() 

2709 n = len(s) 

2710 i = i1 = w.getInsertPoint() 

2711 # Find a word char on the present line if one isn't at the cursor. 

2712 if not (0 <= i < n and g.isWordChar(s[i])): 

2713 # First, look forward 

2714 while i < n and not g.isWordChar(s[i]) and s[i] != '\n': 

2715 i += 1 

2716 # Next, look backward. 

2717 if not (0 <= i < n and g.isWordChar(s[i])): 

2718 i = i1 - 1 if (i >= n or s[i] == '\n') else i1 

2719 while i >= 0 and not g.isWordChar(s[i]) and s[i] != '\n': 

2720 i -= 1 

2721 # Make sure s[i] is a word char. 

2722 if 0 <= i < n and g.isWordChar(s[i]): 

2723 # Find the start of the word. 

2724 while 0 <= i < n and g.isWordChar(s[i]): 

2725 i -= 1 

2726 i += 1 

2727 i1 = i 

2728 # Find the end of the word. 

2729 while 0 <= i < n and g.isWordChar(s[i]): 

2730 i += 1 

2731 if select: 

2732 w.setSelectionRange(i1, i) 

2733 return i1, i 

2734 return 0, 0 

2735 #@+node:ekr.20170707072837.1: *4* ec.finishOfLine/ExtendSelection 

2736 @cmd('finish-of-line') 

2737 def finishOfLine(self, event): 

2738 """Move the cursor to the last character of the line.""" 

2739 self.moveWithinLineHelper(event, 'finish-line', extend=False) 

2740 

2741 @cmd('finish-of-line-extend-selection') 

2742 def finishOfLineExtendSelection(self, event): 

2743 """Extend the selection by moving the cursor to the last character of the line.""" 

2744 self.moveWithinLineHelper(event, 'finish-line', extend=True) 

2745 #@+node:ekr.20170707160947.1: *4* ec.forward*/ExtendSelection 

2746 @cmd('forward-end-word') 

2747 def forwardEndWord(self, event): # New in Leo 4.4.2 

2748 """Move the cursor to the next word.""" 

2749 self.moveWordHelper(event, extend=False, forward=True, end=True) 

2750 

2751 @cmd('forward-end-word-extend-selection') 

2752 def forwardEndWordExtendSelection(self, event): # New in Leo 4.4.2 

2753 """Extend the selection by moving the cursor to the next word.""" 

2754 self.moveWordHelper(event, extend=True, forward=True, end=True) 

2755 

2756 @cmd('forward-word') 

2757 def forwardWord(self, event): 

2758 """Move the cursor to the next word.""" 

2759 self.moveWordHelper(event, extend=False, forward=True) 

2760 

2761 @cmd('forward-word-extend-selection') 

2762 def forwardWordExtendSelection(self, event): 

2763 """Extend the selection by moving the cursor to the end of the next word.""" 

2764 self.moveWordHelper(event, extend=True, forward=True) 

2765 

2766 @cmd('forward-word-smart') 

2767 def forwardWordSmart(self, event): 

2768 """Move the cursor to the end of the current or the beginning of the next word.""" 

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

2770 

2771 @cmd('forward-word-smart-extend-selection') 

2772 def forwardWordSmartExtendSelection(self, event): 

2773 """Extend the selection by moving the cursor to the end of the current 

2774 or the beginning of the next word.""" 

2775 self.moveWordHelper(event, extend=True, forward=True, smart=True) 

2776 #@+node:ekr.20150514063305.303: *4* ec.movePastClose & helper 

2777 @cmd('move-past-close') 

2778 def movePastClose(self, event): 

2779 """Move the cursor past the closing parenthesis.""" 

2780 self.movePastCloseHelper(event, extend=False) 

2781 

2782 @cmd('move-past-close-extend-selection') 

2783 def movePastCloseExtendSelection(self, event): 

2784 """Extend the selection by moving the cursor past the closing parenthesis.""" 

2785 self.movePastCloseHelper(event, extend=True) 

2786 #@+node:ekr.20150514063305.304: *5* ec.movePastCloseHelper 

2787 def movePastCloseHelper(self, event, extend): 

2788 c = self.c 

2789 w = self.editWidget(event) 

2790 if not w: 

2791 return 

2792 c.widgetWantsFocusNow(w) 

2793 s = w.getAllText() 

2794 ins = w.getInsertPoint() 

2795 # Scan backwards for i,j. 

2796 i = ins 

2797 while len(s) > i >= 0 and s[i] != '\n': 

2798 if s[i] == '(': 

2799 break 

2800 i -= 1 

2801 else: 

2802 return 

2803 j = ins 

2804 while len(s) > j >= 0 and s[j] != '\n': 

2805 if s[j] == '(': 

2806 break 

2807 j -= 1 

2808 if i < j: 

2809 return 

2810 # Scan forward for i2,j2. 

2811 i2 = ins 

2812 while i2 < len(s) and s[i2] != '\n': 

2813 if s[i2] == ')': 

2814 break 

2815 i2 += 1 

2816 else: 

2817 return 

2818 j2 = ins 

2819 while j2 < len(s) and s[j2] != '\n': 

2820 if s[j2] == ')': 

2821 break 

2822 j2 += 1 

2823 if i2 > j2: 

2824 return 

2825 self.moveToHelper(event, i2 + 1, extend) 

2826 #@+node:ekr.20150514063305.306: *4* ec.pages & helper 

2827 @cmd('back-page') 

2828 def backPage(self, event): 

2829 """Move the cursor back one page, 

2830 extending the selection if in extend mode.""" 

2831 self.movePageHelper(event, kind='back', extend=False) 

2832 

2833 @cmd('back-page-extend-selection') 

2834 def backPageExtendSelection(self, event): 

2835 """Extend the selection by moving the cursor back one page.""" 

2836 self.movePageHelper(event, kind='back', extend=True) 

2837 

2838 @cmd('forward-page') 

2839 def forwardPage(self, event): 

2840 """Move the cursor forward one page, 

2841 extending the selection if in extend mode.""" 

2842 self.movePageHelper(event, kind='forward', extend=False) 

2843 

2844 @cmd('forward-page-extend-selection') 

2845 def forwardPageExtendSelection(self, event): 

2846 """Extend the selection by moving the cursor forward one page.""" 

2847 self.movePageHelper(event, kind='forward', extend=True) 

2848 #@+node:ekr.20150514063305.307: *5* ec.movePageHelper 

2849 def movePageHelper(self, event, kind, extend): # kind in back/forward. 

2850 """Move the cursor up/down one page, possibly extending the selection.""" 

2851 w = self.editWidget(event) 

2852 if not w: 

2853 return 

2854 linesPerPage = 15 # To do. 

2855 if hasattr(w, 'leoMoveCursorHelper'): 

2856 extend = extend or self.extendMode 

2857 w.leoMoveCursorHelper( 

2858 kind='page-down' if kind == 'forward' else 'page-up', 

2859 extend=extend, linesPerPage=linesPerPage) 

2860 # w.seeInsertPoint() 

2861 # c.frame.updateStatusLine() 

2862 # w.rememberSelectionAndScroll() 

2863 else: 

2864 ins = w.getInsertPoint() 

2865 s = w.getAllText() 

2866 lines = g.splitLines(s) 

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

2868 if kind == 'back': 

2869 row2 = max(0, row - linesPerPage) 

2870 else: 

2871 row2 = min(row + linesPerPage, len(lines) - 1) 

2872 if row == row2: 

2873 return 

2874 spot = g.convertRowColToPythonIndex(s, row2, col, lines=lines) 

2875 self.extendHelper(w, extend, spot, upOrDown=True) 

2876 #@+node:ekr.20150514063305.308: *4* ec.paragraphs & helpers 

2877 @cmd('back-paragraph') 

2878 def backwardParagraph(self, event): 

2879 """Move the cursor to the previous paragraph.""" 

2880 self.backwardParagraphHelper(event, extend=False) 

2881 

2882 @cmd('back-paragraph-extend-selection') 

2883 def backwardParagraphExtendSelection(self, event): 

2884 """Extend the selection by moving the cursor to the previous paragraph.""" 

2885 self.backwardParagraphHelper(event, extend=True) 

2886 

2887 @cmd('forward-paragraph') 

2888 def forwardParagraph(self, event): 

2889 """Move the cursor to the next paragraph.""" 

2890 self.forwardParagraphHelper(event, extend=False) 

2891 

2892 @cmd('forward-paragraph-extend-selection') 

2893 def forwardParagraphExtendSelection(self, event): 

2894 """Extend the selection by moving the cursor to the next paragraph.""" 

2895 self.forwardParagraphHelper(event, extend=True) 

2896 #@+node:ekr.20150514063305.309: *5* ec.backwardParagraphHelper 

2897 def backwardParagraphHelper(self, event, extend): 

2898 w = self.editWidget(event) 

2899 if not w: 

2900 return # pragma: no cover (defensive) 

2901 s = w.getAllText() 

2902 i, j = w.getSelectionRange() 

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

2904 line = s[i:j] 

2905 if line.strip(): 

2906 # Find the start of the present paragraph. 

2907 while i > 0: 

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

2909 line = s[i:j] 

2910 if not line.strip(): 

2911 break 

2912 # Find the end of the previous paragraph. 

2913 while i > 0: 

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

2915 line = s[i:j] 

2916 if line.strip(): 

2917 i = j - 1 

2918 break 

2919 self.moveToHelper(event, i, extend) 

2920 #@+node:ekr.20150514063305.310: *5* ec.forwardParagraphHelper 

2921 def forwardParagraphHelper(self, event, extend): 

2922 w = self.editWidget(event) 

2923 if not w: 

2924 return 

2925 s = w.getAllText() 

2926 ins = w.getInsertPoint() 

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

2928 line = s[i:j] 

2929 if line.strip(): # Skip past the present paragraph. 

2930 self.selectParagraphHelper(w, i) 

2931 i, j = w.getSelectionRange() 

2932 j += 1 

2933 # Skip to the next non-blank line. 

2934 i = j 

2935 while j < len(s): 

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

2937 line = s[i:j] 

2938 if line.strip(): 

2939 break 

2940 w.setInsertPoint(ins) # Restore the original insert point. 

2941 self.moveToHelper(event, i, extend) 

2942 #@+node:ekr.20170707093335.1: *4* ec.pushCursor and popCursor 

2943 @cmd('pop-cursor') 

2944 def popCursor(self, event=None): 

2945 """Restore the node, selection range and insert point from the stack.""" 

2946 c = self.c 

2947 w = self.editWidget(event) 

2948 if w and self.cursorStack: 

2949 p, i, j, ins = self.cursorStack.pop() 

2950 if c.positionExists(p): 

2951 c.selectPosition(p) 

2952 c.redraw() 

2953 w.setSelectionRange(i, j, insert=ins) 

2954 c.bodyWantsFocus() 

2955 else: 

2956 g.es('invalid position', c.p.h) 

2957 elif not w: 

2958 g.es('no stacked cursor', color='blue') 

2959 

2960 @cmd('push-cursor') 

2961 def pushCursor(self, event=None): 

2962 """Push the selection range and insert point on the stack.""" 

2963 c = self.c 

2964 w = self.editWidget(event) 

2965 if w: 

2966 p = c.p.copy() 

2967 i, j = w.getSelectionRange() 

2968 ins = w.getInsertPoint() 

2969 self.cursorStack.append((p, i, j, ins),) 

2970 else: 

2971 g.es('cursor not pushed', color='blue') 

2972 #@+node:ekr.20150514063305.311: *4* ec.selectAllText 

2973 @cmd('select-all') 

2974 def selectAllText(self, event): 

2975 """Select all text.""" 

2976 k = self.c.k 

2977 w = self.editWidget(event) 

2978 if not w: 

2979 return 

2980 # Bug fix 2013/12/13: Special case the minibuffer. 

2981 if w == k.w: 

2982 k.selectAll() 

2983 elif w and g.isTextWrapper(w): 

2984 w.selectAllText() 

2985 #@+node:ekr.20150514063305.312: *4* ec.sentences & helpers 

2986 @cmd('back-sentence') 

2987 def backSentence(self, event): 

2988 """Move the cursor to the previous sentence.""" 

2989 self.backSentenceHelper(event, extend=False) 

2990 

2991 @cmd('back-sentence-extend-selection') 

2992 def backSentenceExtendSelection(self, event): 

2993 """Extend the selection by moving the cursor to the previous sentence.""" 

2994 self.backSentenceHelper(event, extend=True) 

2995 

2996 @cmd('forward-sentence') 

2997 def forwardSentence(self, event): 

2998 """Move the cursor to the next sentence.""" 

2999 self.forwardSentenceHelper(event, extend=False) 

3000 

3001 @cmd('forward-sentence-extend-selection') 

3002 def forwardSentenceExtendSelection(self, event): 

3003 """Extend the selection by moving the cursor to the next sentence.""" 

3004 self.forwardSentenceHelper(event, extend=True) 

3005 #@+node:ekr.20150514063305.313: *5* ec.backSentenceHelper 

3006 def backSentenceHelper(self, event, extend): 

3007 c = self.c 

3008 w = self.editWidget(event) 

3009 if not w: 

3010 return # pragma: no cover (defensive) 

3011 c.widgetWantsFocusNow(w) 

3012 s = w.getAllText() 

3013 ins = w.getInsertPoint() 

3014 # Find the starting point of the scan. 

3015 i = ins 

3016 i -= 1 # Ensure some progress. 

3017 if i < 0 or i >= len(s): 

3018 return 

3019 # Tricky. 

3020 if s[i] == '.': 

3021 i -= 1 

3022 while i >= 0 and s[i] in ' \n': 

3023 i -= 1 

3024 if i >= ins: 

3025 i -= 1 

3026 if i >= len(s): 

3027 i -= 1 

3028 if i <= 0: 

3029 return 

3030 if s[i] == '.': 

3031 i -= 1 

3032 # Scan backwards to the end of the paragraph. 

3033 # Stop at empty lines. 

3034 # Skip periods within words. 

3035 # Stop at sentences ending in non-periods. 

3036 end = False 

3037 while not end and i >= 0: 

3038 progress = i 

3039 if s[i] == '.': 

3040 # Skip periods surrounded by letters/numbers 

3041 if i > 0 and s[i - 1].isalnum() and s[i + 1].isalnum(): 

3042 i -= 1 

3043 else: 

3044 i += 1 

3045 while i < len(s) and s[i] in ' \n': 

3046 i += 1 

3047 i -= 1 

3048 break 

3049 elif s[i] == '\n': 

3050 j = i - 1 

3051 while j >= 0: 

3052 if s[j] == '\n': 

3053 # Don't include first newline. 

3054 end = True 

3055 break # found blank line. 

3056 elif s[j] == ' ': 

3057 j -= 1 

3058 else: 

3059 i -= 1 

3060 break # no blank line found. 

3061 else: 

3062 # No blank line found. 

3063 i -= 1 

3064 else: 

3065 i -= 1 

3066 assert end or progress > i 

3067 i += 1 

3068 if i < ins: 

3069 self.moveToHelper(event, i, extend) 

3070 #@+node:ekr.20150514063305.314: *5* ec.forwardSentenceHelper 

3071 def forwardSentenceHelper(self, event, extend): 

3072 c = self.c 

3073 w = self.editWidget(event) 

3074 if not w: 

3075 return 

3076 c.widgetWantsFocusNow(w) 

3077 s = w.getAllText() 

3078 ins = w.getInsertPoint() 

3079 if ins >= len(s): 

3080 return 

3081 # Find the starting point of the scan. 

3082 i = ins 

3083 if i + 1 < len(s) and s[i + 1] == '.': 

3084 i += 1 

3085 if s[i] == '.': 

3086 i += 1 

3087 else: 

3088 while i < len(s) and s[i] in ' \n': 

3089 i += 1 

3090 i -= 1 

3091 if i <= ins: 

3092 i += 1 

3093 if i >= len(s): 

3094 return 

3095 # Scan forward to the end of the paragraph. 

3096 # Stop at empty lines. 

3097 # Skip periods within words. 

3098 # Stop at sentences ending in non-periods. 

3099 end = False 

3100 while not end and i < len(s): 

3101 progress = i 

3102 if s[i] == '.': 

3103 # Skip periods surrounded by letters/numbers 

3104 if 0 < i < len(s) and s[i - 1].isalnum() and s[i + 1].isalnum(): 

3105 i += 1 

3106 else: 

3107 i += 1 

3108 break # Include the paragraph. 

3109 elif s[i] == '\n': 

3110 j = i + 1 

3111 while j < len(s): 

3112 if s[j] == '\n': 

3113 # Don't include first newline. 

3114 end = True 

3115 break # found blank line. 

3116 elif s[j] == ' ': 

3117 j += 1 

3118 else: 

3119 i += 1 

3120 break # no blank line found. 

3121 else: 

3122 # No blank line found. 

3123 i += 1 

3124 else: 

3125 i += 1 

3126 assert end or progress < i 

3127 i = min(i, len(s)) 

3128 if i > ins: 

3129 self.moveToHelper(event, i, extend) 

3130 #@+node:ekr.20170707072644.1: *4* ec.startOfLine/ExtendSelection 

3131 @cmd('start-of-line') 

3132 def startOfLine(self, event): 

3133 """Move the cursor to first non-blank character of the line.""" 

3134 self.moveWithinLineHelper(event, 'start-line', extend=False) 

3135 

3136 @cmd('start-of-line-extend-selection') 

3137 def startOfLineExtendSelection(self, event): 

3138 """ 

3139 Extend the selection by moving the cursor to first non-blank character 

3140 of the line. 

3141 """ 

3142 self.moveWithinLineHelper(event, 'start-line', extend=True) 

3143 #@+node:ekr.20150514063305.319: *3* ec: paragraph 

3144 #@+node:ekr.20150514063305.320: *4* ec.backwardKillParagraph 

3145 @cmd('backward-kill-paragraph') 

3146 def backwardKillParagraph(self, event): 

3147 """Kill the previous paragraph.""" 

3148 c = self.c 

3149 w = self.editWidget(event) 

3150 if not w: 

3151 return # pragma: no cover (defensive) 

3152 self.beginCommand(w, undoType='backward-kill-paragraph') 

3153 try: 

3154 self.backwardParagraphHelper(event, extend=True) 

3155 i, j = w.getSelectionRange() 

3156 if i > 0: 

3157 i = min(i + 1, j) 

3158 c.killBufferCommands.killParagraphHelper(event, i, j) 

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

3160 finally: 

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

3162 #@+node:ekr.20150514063305.321: *4* ec.fillRegion 

3163 @cmd('fill-region') 

3164 def fillRegion(self, event): 

3165 """Fill all paragraphs in the selected text.""" 

3166 c, p = self.c, self.c.p 

3167 undoType = 'fill-region' 

3168 w = self.editWidget(event) 

3169 i, j = w.getSelectionRange() 

3170 c.undoer.beforeChangeGroup(p, undoType) 

3171 while 1: 

3172 progress = w.getInsertPoint() 

3173 c.reformatParagraph(event, undoType='reformat-paragraph') 

3174 ins = w.getInsertPoint() 

3175 s = w.getAllText() 

3176 w.setInsertPoint(ins) 

3177 if progress >= ins or ins >= j or ins >= len(s): 

3178 break 

3179 c.undoer.afterChangeGroup(p, undoType) 

3180 #@+node:ekr.20150514063305.322: *4* ec.fillRegionAsParagraph 

3181 @cmd('fill-region-as-paragraph') 

3182 def fillRegionAsParagraph(self, event): 

3183 """Fill the selected text.""" 

3184 w = self.editWidget(event) 

3185 if not w or not self._chckSel(event): 

3186 return # pragma: no cover (defensive) 

3187 self.beginCommand(w, undoType='fill-region-as-paragraph') 

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

3189 #@+node:ekr.20150514063305.323: *4* ec.fillParagraph 

3190 @cmd('fill-paragraph') 

3191 def fillParagraph(self, event): 

3192 """Fill the selected paragraph""" 

3193 w = self.editWidget(event) 

3194 if not w: 

3195 return # pragma: no cover (defensive) 

3196 # Clear the selection range. 

3197 i, j = w.getSelectionRange() 

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

3199 self.c.reformatParagraph(event) 

3200 #@+node:ekr.20150514063305.324: *4* ec.killParagraph 

3201 @cmd('kill-paragraph') 

3202 def killParagraph(self, event): 

3203 """Kill the present paragraph.""" 

3204 c = self.c 

3205 w = self.editWidget(event) 

3206 if not w: 

3207 return 

3208 self.beginCommand(w, undoType='kill-paragraph') 

3209 try: 

3210 self.extendToParagraph(event) 

3211 i, j = w.getSelectionRange() 

3212 c.killBufferCommands.killParagraphHelper(event, i, j) 

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

3214 finally: 

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

3216 #@+node:ekr.20150514063305.325: *4* ec.extend-to-paragraph & helper 

3217 @cmd('extend-to-paragraph') 

3218 def extendToParagraph(self, event): 

3219 """Select the paragraph surrounding the cursor.""" 

3220 w = self.editWidget(event) 

3221 if not w: 

3222 return 

3223 s = w.getAllText() 

3224 ins = w.getInsertPoint() 

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

3226 line = s[i:j] 

3227 # Find the start of the paragraph. 

3228 if line.strip(): # Search backward. 

3229 while i > 0: 

3230 i2, j2 = g.getLine(s, i - 1) 

3231 line = s[i2:j2] 

3232 if line.strip(): 

3233 i = i2 

3234 else: 

3235 break # Use the previous line. 

3236 else: # Search forward. 

3237 while j < len(s): 

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

3239 line = s[i:j] 

3240 if line.strip(): 

3241 break 

3242 else: return 

3243 # Select from i to the end of the paragraph. 

3244 self.selectParagraphHelper(w, i) 

3245 #@+node:ekr.20150514063305.326: *5* ec.selectParagraphHelper 

3246 def selectParagraphHelper(self, w, start): 

3247 """Select from start to the end of the paragraph.""" 

3248 s = w.getAllText() 

3249 i1, j = g.getLine(s, start) 

3250 while j < len(s): 

3251 i, j2 = g.getLine(s, j) 

3252 line = s[i:j2] 

3253 if line.strip(): 

3254 j = j2 

3255 else: 

3256 break 

3257 j = max(start, j - 1) 

3258 w.setSelectionRange(i1, j, insert=j) 

3259 #@+node:ekr.20150514063305.327: *3* ec: region 

3260 #@+node:ekr.20150514063305.328: *4* ec.tabIndentRegion (indent-rigidly) 

3261 @cmd('indent-rigidly') 

3262 def tabIndentRegion(self, event): 

3263 """Insert a hard tab at the start of each line of the selected text.""" 

3264 w = self.editWidget(event) 

3265 if not w or not self._chckSel(event): 

3266 return # pragma: no cover (defensive) 

3267 self.beginCommand(w, undoType='indent-rigidly') 

3268 s = w.getAllText() 

3269 i1, j1 = w.getSelectionRange() 

3270 i, junk = g.getLine(s, i1) 

3271 junk, j = g.getLine(s, j1) 

3272 lines = g.splitlines(s[i:j]) 

3273 n = len(lines) 

3274 lines_s = ''.join('\t' + line for line in lines) 

3275 s = s[:i] + lines_s + s[j:] 

3276 w.setAllText(s) 

3277 # Retain original row/col selection. 

3278 w.setSelectionRange(i1, j1 + n, insert=j1 + n) 

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

3280 #@+node:ekr.20150514063305.329: *4* ec.countRegion 

3281 @cmd('count-region') 

3282 def countRegion(self, event): 

3283 """Print the number of lines and characters in the selected text.""" 

3284 k = self.c.k 

3285 w = self.editWidget(event) 

3286 if not w: 

3287 return # pragma: no cover (defensive) 

3288 txt = w.getSelectedText() 

3289 lines = 1 

3290 chars = 0 

3291 for z in txt: 

3292 if z == '\n': 

3293 lines += 1 

3294 else: chars += 1 

3295 k.setLabelGrey( 

3296 f"Region has {lines} lines, " 

3297 f"{chars} character{g.plural(chars)}") 

3298 #@+node:ekr.20150514063305.330: *4* ec.moveLinesDown 

3299 @cmd('move-lines-down') 

3300 def moveLinesDown(self, event): 

3301 """ 

3302 Move all lines containing any selected text down one line, 

3303 moving to the next node if the lines are the last lines of the body. 

3304 """ 

3305 c = self.c 

3306 w = self.editWidget(event) 

3307 if not w: 

3308 return 

3309 s = w.getAllText() 

3310 sel_1, sel_2 = w.getSelectionRange() 

3311 insert_pt = w.getInsertPoint() 

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

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

3314 lines = s[i:j] 

3315 # Select from start of the first line to the *start* of the last line. 

3316 # This prevents selection creep. 

3317 self.beginCommand(w, undoType='move-lines-down') 

3318 try: 

3319 next_i, next_j = g.getLine(s, j) # 2011/04/01: was j+1 

3320 next_line = s[next_i:next_j] 

3321 n2 = next_j - next_i 

3322 if j < len(s): 

3323 w.delete(i, next_j) 

3324 if next_line.endswith('\n'): 

3325 # Simply swap positions with next line 

3326 new_lines = next_line + lines 

3327 else: 

3328 # Last line of the body to be moved up doesn't end in a newline 

3329 # while we have to remove the newline from the line above moving down. 

3330 new_lines = next_line + '\n' + lines[:-1] 

3331 n2 += 1 

3332 w.insert(i, new_lines) 

3333 w.setSelectionRange(sel_1 + n2, sel_2 + n2, insert=insert_pt + n2) 

3334 else: 

3335 # Leo 5.6: insert a blank line before the selected lines. 

3336 w.insert(i, '\n') 

3337 w.setSelectionRange(sel_1 + 1, sel_2 + 1, insert=insert_pt + 1) 

3338 # Fix bug 799695: colorizer bug after move-lines-up into a docstring 

3339 c.recolor() 

3340 finally: 

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

3342 #@+node:ekr.20150514063305.331: *4* ec.moveLinesUp 

3343 @cmd('move-lines-up') 

3344 def moveLinesUp(self, event): 

3345 """ 

3346 Move all lines containing any selected text up one line, 

3347 moving to the previous node as needed. 

3348 """ 

3349 c = self.c 

3350 w = self.editWidget(event) 

3351 if not w: 

3352 return # pragma: no cover (defensive) 

3353 s = w.getAllText() 

3354 sel_1, sel_2 = w.getSelectionRange() 

3355 insert_pt = w.getInsertPoint() # 2011/04/01 

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

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

3358 lines = s[i:j] 

3359 self.beginCommand(w, undoType='move-lines-up') 

3360 try: 

3361 prev_i, prev_j = g.getLine(s, i - 1) 

3362 prev_line = s[prev_i:prev_j] 

3363 n2 = prev_j - prev_i 

3364 if i > 0: 

3365 w.delete(prev_i, j) 

3366 if lines.endswith('\n'): 

3367 # Simply swap positions with next line 

3368 new_lines = lines + prev_line 

3369 else: 

3370 # Lines to be moved up don't end in a newline while the 

3371 # previous line going down needs its newline taken off. 

3372 new_lines = lines + '\n' + prev_line[:-1] 

3373 w.insert(prev_i, new_lines) 

3374 w.setSelectionRange(sel_1 - n2, sel_2 - n2, insert=insert_pt - n2) 

3375 else: 

3376 # Leo 5.6: Insert a blank line after the line. 

3377 w.insert(j, '\n') 

3378 w.setSelectionRange(sel_1, sel_2, insert=sel_1) 

3379 # Fix bug 799695: colorizer bug after move-lines-up into a docstring 

3380 c.recolor() 

3381 finally: 

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

3383 #@+node:ekr.20150514063305.332: *4* ec.reverseRegion 

3384 @cmd('reverse-region') 

3385 def reverseRegion(self, event): 

3386 """Reverse the order of lines in the selected text.""" 

3387 w = self.editWidget(event) 

3388 if not w or not self._chckSel(event): 

3389 return # pragma: no cover (defensive) 

3390 self.beginCommand(w, undoType='reverse-region') 

3391 s = w.getAllText() 

3392 i1, j1 = w.getSelectionRange() 

3393 i, junk = g.getLine(s, i1) 

3394 junk, j = g.getLine(s, j1) 

3395 txt = s[i:j] 

3396 aList = txt.split('\n') 

3397 aList.reverse() 

3398 txt = '\n'.join(aList) + '\n' 

3399 w.setAllText(s[:i1] + txt + s[j1:]) 

3400 ins = i1 + len(txt) - 1 

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

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

3403 #@+node:ekr.20150514063305.333: *4* ec.up/downCaseRegion & helper 

3404 @cmd('downcase-region') 

3405 def downCaseRegion(self, event): 

3406 """Convert all characters in the selected text to lower case.""" 

3407 self.caseHelper(event, 'low', 'downcase-region') 

3408 

3409 @cmd('toggle-case-region') 

3410 def toggleCaseRegion(self, event): 

3411 """Toggle the case of all characters in the selected text.""" 

3412 self.caseHelper(event, 'toggle', 'toggle-case-region') 

3413 

3414 @cmd('upcase-region') 

3415 def upCaseRegion(self, event): 

3416 """Convert all characters in the selected text to UPPER CASE.""" 

3417 self.caseHelper(event, 'up', 'upcase-region') 

3418 

3419 def caseHelper(self, event, way, undoType): 

3420 w = self.editWidget(event) 

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

3422 return # pragma: no cover (defensive) 

3423 self.beginCommand(w, undoType=undoType) 

3424 s = w.getAllText() 

3425 i, j = w.getSelectionRange() 

3426 ins = w.getInsertPoint() 

3427 s2 = s[i:j] 

3428 if way == 'low': 

3429 sel = s2.lower() 

3430 elif way == 'up': 

3431 sel = s2.upper() 

3432 else: 

3433 assert way == 'toggle' 

3434 sel = s2.swapcase() 

3435 s2 = s[:i] + sel + s[j:] 

3436 changed = s2 != s 

3437 if changed: 

3438 w.setAllText(s2) 

3439 w.setSelectionRange(i, j, insert=ins) 

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

3441 #@+node:ekr.20150514063305.334: *3* ec: scrolling 

3442 #@+node:ekr.20150514063305.335: *4* ec.scrollUp/Down & helper 

3443 @cmd('scroll-down-half-page') 

3444 def scrollDownHalfPage(self, event): 

3445 """Scroll the presently selected pane down one line.""" 

3446 self.scrollHelper(event, 'down', 'half-page') 

3447 

3448 @cmd('scroll-down-line') 

3449 def scrollDownLine(self, event): 

3450 """Scroll the presently selected pane down one line.""" 

3451 self.scrollHelper(event, 'down', 'line') 

3452 

3453 @cmd('scroll-down-page') 

3454 def scrollDownPage(self, event): 

3455 """Scroll the presently selected pane down one page.""" 

3456 self.scrollHelper(event, 'down', 'page') 

3457 

3458 @cmd('scroll-up-half-page') 

3459 def scrollUpHalfPage(self, event): 

3460 """Scroll the presently selected pane down one line.""" 

3461 self.scrollHelper(event, 'up', 'half-page') 

3462 

3463 @cmd('scroll-up-line') 

3464 def scrollUpLine(self, event): 

3465 """Scroll the presently selected pane up one page.""" 

3466 self.scrollHelper(event, 'up', 'line') 

3467 

3468 @cmd('scroll-up-page') 

3469 def scrollUpPage(self, event): 

3470 """Scroll the presently selected pane up one page.""" 

3471 self.scrollHelper(event, 'up', 'page') 

3472 #@+node:ekr.20150514063305.336: *5* ec.scrollHelper 

3473 def scrollHelper(self, event, direction, distance): 

3474 """ 

3475 Scroll the present pane up or down one page 

3476 kind is in ('up/down-half-page/line/page) 

3477 """ 

3478 w = event and event.w 

3479 if w and hasattr(w, 'scrollDelegate'): 

3480 kind = direction + '-' + distance 

3481 w.scrollDelegate(kind) 

3482 #@+node:ekr.20150514063305.337: *4* ec.scrollOutlineUp/Down/Line/Page 

3483 @cmd('scroll-outline-down-line') 

3484 def scrollOutlineDownLine(self, event=None): 

3485 """Scroll the outline pane down one line.""" 

3486 tree = self.c.frame.tree 

3487 if hasattr(tree, 'scrollDelegate'): 

3488 tree.scrollDelegate('down-line') 

3489 elif hasattr(tree.canvas, 'leo_treeBar'): 

3490 a, b = tree.canvas.leo_treeBar.get() 

3491 if b < 1.0: 

3492 tree.canvas.yview_scroll(1, "unit") 

3493 

3494 @cmd('scroll-outline-down-page') 

3495 def scrollOutlineDownPage(self, event=None): 

3496 """Scroll the outline pane down one page.""" 

3497 tree = self.c.frame.tree 

3498 if hasattr(tree, 'scrollDelegate'): 

3499 tree.scrollDelegate('down-page') 

3500 elif hasattr(tree.canvas, 'leo_treeBar'): 

3501 a, b = tree.canvas.leo_treeBar.get() 

3502 if b < 1.0: 

3503 tree.canvas.yview_scroll(1, "page") 

3504 

3505 @cmd('scroll-outline-up-line') 

3506 def scrollOutlineUpLine(self, event=None): 

3507 """Scroll the outline pane up one line.""" 

3508 tree = self.c.frame.tree 

3509 if hasattr(tree, 'scrollDelegate'): 

3510 tree.scrollDelegate('up-line') 

3511 elif hasattr(tree.canvas, 'leo_treeBar'): 

3512 a, b = tree.canvas.leo_treeBar.get() 

3513 if a > 0.0: 

3514 tree.canvas.yview_scroll(-1, "unit") 

3515 

3516 @cmd('scroll-outline-up-page') 

3517 def scrollOutlineUpPage(self, event=None): 

3518 """Scroll the outline pane up one page.""" 

3519 tree = self.c.frame.tree 

3520 if hasattr(tree, 'scrollDelegate'): 

3521 tree.scrollDelegate('up-page') 

3522 elif hasattr(tree.canvas, 'leo_treeBar'): 

3523 a, b = tree.canvas.leo_treeBar.get() 

3524 if a > 0.0: 

3525 tree.canvas.yview_scroll(-1, "page") 

3526 #@+node:ekr.20150514063305.338: *4* ec.scrollOutlineLeftRight 

3527 @cmd('scroll-outline-left') 

3528 def scrollOutlineLeft(self, event=None): 

3529 """Scroll the outline left.""" 

3530 tree = self.c.frame.tree 

3531 if hasattr(tree, 'scrollDelegate'): 

3532 tree.scrollDelegate('left') 

3533 elif hasattr(tree.canvas, 'xview_scroll'): 

3534 tree.canvas.xview_scroll(1, "unit") 

3535 

3536 @cmd('scroll-outline-right') 

3537 def scrollOutlineRight(self, event=None): 

3538 """Scroll the outline left.""" 

3539 tree = self.c.frame.tree 

3540 if hasattr(tree, 'scrollDelegate'): 

3541 tree.scrollDelegate('right') 

3542 elif hasattr(tree.canvas, 'xview_scroll'): 

3543 tree.canvas.xview_scroll(-1, "unit") 

3544 #@+node:ekr.20150514063305.339: *3* ec: sort 

3545 #@@language rest 

3546 #@+at 

3547 # XEmacs provides several commands for sorting text in a buffer. All 

3548 # operate on the contents of the region (the text between point and the 

3549 # mark). They divide the text of the region into many "sort records", 

3550 # identify a "sort key" for each record, and then reorder the records 

3551 # using the order determined by the sort keys. The records are ordered so 

3552 # that their keys are in alphabetical order, or, for numerical sorting, in 

3553 # numerical order. In alphabetical sorting, all upper-case letters `A' 

3554 # through `Z' come before lower-case `a', in accordance with the ASCII 

3555 # character sequence. 

3556 # 

3557 # The sort commands differ in how they divide the text into sort 

3558 # records and in which part of each record they use as the sort key. 

3559 # Most of the commands make each line a separate sort record, but some 

3560 # commands use paragraphs or pages as sort records. Most of the sort 

3561 # commands use each entire sort record as its own sort key, but some use 

3562 # only a portion of the record as the sort key. 

3563 # 

3564 # `M-x sort-lines' 

3565 # Divide the region into lines and sort by comparing the entire text 

3566 # of a line. A prefix argument means sort in descending order. 

3567 # 

3568 # `M-x sort-paragraphs' 

3569 # Divide the region into paragraphs and sort by comparing the entire 

3570 # text of a paragraph (except for leading blank lines). A prefix 

3571 # argument means sort in descending order. 

3572 # 

3573 # `M-x sort-pages' 

3574 # Divide the region into pages and sort by comparing the entire text 

3575 # of a page (except for leading blank lines). A prefix argument 

3576 # means sort in descending order. 

3577 # 

3578 # `M-x sort-fields' 

3579 # Divide the region into lines and sort by comparing the contents of 

3580 # one field in each line. Fields are defined as separated by 

3581 # whitespace, so the first run of consecutive non-whitespace 

3582 # characters in a line constitutes field 1, the second such run 

3583 # constitutes field 2, etc. 

3584 # 

3585 # You specify which field to sort by with a numeric argument: 1 to 

3586 # sort by field 1, etc. A negative argument means sort in descending 

3587 # order. Thus, minus 2 means sort by field 2 in reverse-alphabetical 

3588 # order. 

3589 # 

3590 # `M-x sort-numeric-fields' 

3591 # Like `M-x sort-fields', except the specified field is converted to 

3592 # a number for each line and the numbers are compared. `10' comes 

3593 # before `2' when considered as text, but after it when considered 

3594 # as a number. 

3595 # 

3596 # `M-x sort-columns' 

3597 # Like `M-x sort-fields', except that the text within each line used 

3598 # for comparison comes from a fixed range of columns. An explanation 

3599 # is given below. 

3600 # 

3601 # For example, if the buffer contains: 

3602 # 

3603 # On systems where clash detection (locking of files being edited) is 

3604 # implemented, XEmacs also checks the first time you modify a buffer 

3605 # whether the file has changed on disk since it was last visited or 

3606 # saved. If it has, you are asked to confirm that you want to change 

3607 # the buffer. 

3608 # 

3609 # then if you apply `M-x sort-lines' to the entire buffer you get: 

3610 # 

3611 # On systems where clash detection (locking of files being edited) is 

3612 # implemented, XEmacs also checks the first time you modify a buffer 

3613 # saved. If it has, you are asked to confirm that you want to change 

3614 # the buffer. 

3615 # whether the file has changed on disk since it was last visited or 

3616 # 

3617 # where the upper case `O' comes before all lower case letters. If you 

3618 # apply instead `C-u 2 M-x sort-fields' you get: 

3619 # 

3620 # saved. If it has, you are asked to confirm that you want to change 

3621 # implemented, XEmacs also checks the first time you modify a buffer 

3622 # the buffer. 

3623 # On systems where clash detection (locking of files being edited) is 

3624 # whether the file has changed on disk since it was last visited or 

3625 # 

3626 # where the sort keys were `If', `XEmacs', `buffer', `systems', and `the'. 

3627 # 

3628 # `M-x sort-columns' requires more explanation. You specify the 

3629 # columns by putting point at one of the columns and the mark at the other 

3630 # column. Because this means you cannot put point or the mark at the 

3631 # beginning of the first line to sort, this command uses an unusual 

3632 # definition of `region': all of the line point is in is considered part 

3633 # of the region, and so is all of the line the mark is in. 

3634 # 

3635 # For example, to sort a table by information found in columns 10 to 

3636 # 15, you could put the mark on column 10 in the first line of the table, 

3637 # and point on column 15 in the last line of the table, and then use this 

3638 # command. Or you could put the mark on column 15 in the first line and 

3639 # point on column 10 in the last line. 

3640 # 

3641 # This can be thought of as sorting the rectangle specified by point 

3642 # and the mark, except that the text on each line to the left or right of 

3643 # the rectangle moves along with the text inside the rectangle. *Note 

3644 # Rectangles::. 

3645 #@@language python 

3646 #@+node:ekr.20150514063305.340: *4* ec.sortLines commands 

3647 @cmd('reverse-sort-lines-ignoring-case') 

3648 def reverseSortLinesIgnoringCase(self, event): 

3649 """Sort the selected lines in reverse order, ignoring case.""" 

3650 return self.sortLines(event, ignoreCase=True, reverse=True) 

3651 

3652 @cmd('reverse-sort-lines') 

3653 def reverseSortLines(self, event): 

3654 """Sort the selected lines in reverse order.""" 

3655 return self.sortLines(event, reverse=True) 

3656 

3657 @cmd('sort-lines-ignoring-case') 

3658 def sortLinesIgnoringCase(self, event): 

3659 """Sort the selected lines, ignoring case.""" 

3660 return self.sortLines(event, ignoreCase=True) 

3661 

3662 @cmd('sort-lines') 

3663 def sortLines(self, event, ignoreCase=False, reverse=False): 

3664 """Sort the selected lines.""" 

3665 w = self.editWidget(event) 

3666 if not self._chckSel(event): 

3667 return 

3668 undoType = 'reverse-sort-lines' if reverse else 'sort-lines' 

3669 self.beginCommand(w, undoType=undoType) 

3670 try: 

3671 s = w.getAllText() 

3672 sel1, sel2 = w.getSelectionRange() 

3673 ins = w.getInsertPoint() 

3674 i, junk = g.getLine(s, sel1) 

3675 junk, j = g.getLine(s, sel2) 

3676 s2 = s[i:j] 

3677 if not s2.endswith('\n'): 

3678 s2 = s2 + '\n' 

3679 aList = g.splitLines(s2) 

3680 

3681 def lower(s): 

3682 return s.lower() if ignoreCase else s 

3683 

3684 aList.sort(key=lower) 

3685 # key is a function that extracts args. 

3686 if reverse: 

3687 aList.reverse() 

3688 s = ''.join(aList) 

3689 w.delete(i, j) 

3690 w.insert(i, s) 

3691 w.setSelectionRange(sel1, sel2, insert=ins) 

3692 finally: 

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

3694 #@+node:ekr.20150514063305.341: *4* ec.sortColumns 

3695 @cmd('sort-columns') 

3696 def sortColumns(self, event): 

3697 """ 

3698 Sort lines of selected text using only lines in the given columns to do 

3699 the comparison. 

3700 """ 

3701 w = self.editWidget(event) 

3702 if not self._chckSel(event): 

3703 return # pragma: no cover (defensive) 

3704 self.beginCommand(w, undoType='sort-columns') 

3705 try: 

3706 s = w.getAllText() 

3707 sel_1, sel_2 = w.getSelectionRange() 

3708 sint1, sint2 = g.convertPythonIndexToRowCol(s, sel_1) 

3709 sint3, sint4 = g.convertPythonIndexToRowCol(s, sel_2) 

3710 sint1 += 1 

3711 sint3 += 1 

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

3713 junk, j = g.getLine(s, sel_2) 

3714 txt = s[i:j] 

3715 columns = [w.get(f"{z}.{sint2}", f"{z}.{sint4}") 

3716 for z in range(sint1, sint3 + 1)] 

3717 aList = g.splitLines(txt) 

3718 zlist = list(zip(columns, aList)) 

3719 zlist.sort() 

3720 s = ''.join([z[1] for z in zlist]) 

3721 w.delete(i, j) 

3722 w.insert(i, s) 

3723 w.setSelectionRange(sel_1, sel_1 + len(s), insert=sel_1 + len(s)) 

3724 finally: 

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

3726 #@+node:ekr.20150514063305.342: *4* ec.sortFields 

3727 @cmd('sort-fields') 

3728 def sortFields(self, event, which=None): 

3729 """ 

3730 Divide the selected text into lines and sort by comparing the contents 

3731 of one field in each line. Fields are defined as separated by 

3732 whitespace, so the first run of consecutive non-whitespace characters 

3733 in a line constitutes field 1, the second such run constitutes field 2, 

3734 etc. 

3735 

3736 You specify which field to sort by with a numeric argument: 1 to sort 

3737 by field 1, etc. A negative argument means sort in descending order. 

3738 Thus, minus 2 means sort by field 2 in reverse-alphabetical order. 

3739 """ 

3740 w = self.editWidget(event) 

3741 if not w or not self._chckSel(event): 

3742 return 

3743 self.beginCommand(w, undoType='sort-fields') 

3744 s = w.getAllText() 

3745 ins = w.getInsertPoint() 

3746 r1, r2, r3, r4 = self.getRectanglePoints(w) 

3747 i, junk = g.getLine(s, r1) 

3748 junk, j = g.getLine(s, r4) 

3749 txt = s[i:j] # bug reported by pychecker. 

3750 txt = txt.split('\n') 

3751 fields = [] 

3752 fn = r'\w+' 

3753 frx = re.compile(fn) 

3754 for line in txt: 

3755 f = frx.findall(line) 

3756 if not which: 

3757 fields.append(f[0]) 

3758 else: 

3759 i = int(which) 

3760 if len(f) < i: 

3761 return 

3762 i = i - 1 

3763 fields.append(f[i]) 

3764 nz = sorted(zip(fields, txt)) 

3765 w.delete(i, j) 

3766 int1 = i 

3767 for z in nz: 

3768 w.insert(f"{int1}.0", f"{z[1]}\n") 

3769 int1 = int1 + 1 

3770 w.setInsertPoint(ins) 

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

3772 #@+node:ekr.20150514063305.343: *3* ec: swap/transpose 

3773 #@+node:ekr.20150514063305.344: *4* ec.transposeLines 

3774 @cmd('transpose-lines') 

3775 def transposeLines(self, event): 

3776 """Transpose the line containing the cursor with the preceding line.""" 

3777 w = self.editWidget(event) 

3778 if not w: 

3779 return # pragma: no cover (defensive) 

3780 ins = w.getInsertPoint() 

3781 s = w.getAllText() 

3782 if not s.strip(): 

3783 return # pragma: no cover (defensive) 

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

3785 line1 = s[i:j] 

3786 self.beginCommand(w, undoType='transpose-lines') 

3787 if i == 0: # Transpose the next line. 

3788 i2, j2 = g.getLine(s, j + 1) 

3789 line2 = s[i2:j2] 

3790 w.delete(0, j2) 

3791 w.insert(0, line2 + line1) 

3792 w.setInsertPoint(j2 - 1) 

3793 else: # Transpose the previous line. 

3794 i2, j2 = g.getLine(s, i - 1) 

3795 line2 = s[i2:j2] 

3796 w.delete(i2, j) 

3797 w.insert(i2, line1 + line2) 

3798 w.setInsertPoint(j - 1) 

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

3800 #@+node:ekr.20150514063305.345: *4* ec.transposeWords 

3801 @cmd('transpose-words') 

3802 def transposeWords(self, event): 

3803 """ 

3804 Transpose the word before the cursor with the word after the cursor 

3805 Punctuation between words does not move. For example, ‘FOO, BAR’ 

3806 transposes into ‘BAR, FOO’. 

3807 """ 

3808 w = self.editWidget(event) 

3809 if not w: 

3810 return 

3811 self.beginCommand(w, undoType='transpose-words') 

3812 s = w.getAllText() 

3813 i1, j1 = self.extendToWord(event, select=False) 

3814 s1 = s[i1:j1] 

3815 if i1 > j1: 

3816 i1, j1 = j1, i1 

3817 # Search for the next word. 

3818 k = j1 + 1 

3819 while k < len(s) and s[k] != '\n' and not g.isWordChar1(s[k]): 

3820 k += 1 

3821 changed = k < len(s) 

3822 if changed: 

3823 ws = s[j1:k] 

3824 w.setInsertPoint(k + 1) 

3825 i2, j2 = self.extendToWord(event, select=False) 

3826 s2 = s[i2:j2] 

3827 s3 = s[:i1] + s2 + ws + s1 + s[j2:] 

3828 w.setAllText(s3) 

3829 w.setSelectionRange(j1, j1, insert=j1) 

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

3831 #@+node:ekr.20150514063305.346: *4* ec.swapCharacters & transeposeCharacters 

3832 @cmd('transpose-chars') 

3833 def transposeCharacters(self, event): 

3834 """Swap the characters at the cursor.""" 

3835 w = self.editWidget(event) 

3836 if not w: 

3837 return # pragma: no cover (defensive) 

3838 self.beginCommand(w, undoType='swap-characters') 

3839 s = w.getAllText() 

3840 i = w.getInsertPoint() 

3841 if 0 < i < len(s): 

3842 w.setAllText(s[: i - 1] + s[i] + s[i - 1] + s[i + 1 :]) 

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

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

3845 

3846 swapCharacters = transposeCharacters 

3847 #@+node:ekr.20150514063305.348: *3* ec: uA's 

3848 #@+node:ekr.20150514063305.349: *4* ec.clearNodeUas & clearAllUas 

3849 @cmd('clear-node-uas') 

3850 def clearNodeUas(self, event=None): 

3851 """Clear the uA's in the selected VNode.""" 

3852 c = self.c 

3853 p = c and c.p 

3854 if p and p.v.u: 

3855 p.v.u = {} 

3856 # #1276. 

3857 p.setDirty() 

3858 c.setChanged() 

3859 c.redraw() 

3860 

3861 @cmd('clear-all-uas') 

3862 def clearAllUas(self, event=None): 

3863 """Clear all uAs in the entire outline.""" 

3864 c = self.c 

3865 # #1276. 

3866 changed = False 

3867 for p in self.c.all_unique_positions(): 

3868 if p.v.u: 

3869 p.v.u = {} 

3870 p.setDirty() 

3871 changed = True 

3872 if changed: 

3873 c.setChanged() 

3874 c.redraw() 

3875 #@+node:ekr.20150514063305.350: *4* ec.showUas & showAllUas 

3876 @cmd('show-all-uas') 

3877 def showAllUas(self, event=None): 

3878 """Print all uA's in the outline.""" 

3879 g.es_print('Dump of uAs...') 

3880 for v in self.c.all_unique_nodes(): 

3881 if v.u: 

3882 self.showNodeUas(v=v) 

3883 

3884 @cmd('show-node-uas') 

3885 def showNodeUas(self, event=None, v=None): 

3886 """Print the uA's in the selected node.""" 

3887 c = self.c 

3888 if v: 

3889 d, h = v.u, v.h 

3890 else: 

3891 d, h = c.p.v.u, c.p.h 

3892 g.es_print(h) 

3893 g.es_print(g.objToString(d)) 

3894 #@+node:ekr.20150514063305.351: *4* ec.setUa 

3895 @cmd('set-ua') 

3896 def setUa(self, event): 

3897 """Prompt for the name and value of a uA, then set the uA in the present node.""" 

3898 k = self.c.k 

3899 self.w = self.editWidget(event) 

3900 if self.w: 

3901 k.setLabelBlue('Set uA: ') 

3902 k.get1Arg(event, handler=self.setUa1) 

3903 

3904 def setUa1(self, event): 

3905 k = self.c.k 

3906 self.uaName = k.arg 

3907 s = f"Set uA: {self.uaName} To: " 

3908 k.setLabelBlue(s) 

3909 k.getNextArg(self.setUa2) 

3910 

3911 def setUa2(self, event): 

3912 c, k = self.c, self.c.k 

3913 val = k.arg 

3914 d = c.p.v.u 

3915 d[self.uaName] = val 

3916 self.showNodeUas() 

3917 k.clearState() 

3918 k.resetLabel() 

3919 k.showStateAndMode() 

3920 #@-others 

3921#@-others 

3922#@-leo