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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

2704 statements  

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 # Set by the set-fill-column command. 

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

275 self.moveSpotNode = None # A VNode. 

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

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

278 self.sampleWidget = None # Created later. 

279 self.swapSpots = [] 

280 self._useRegex = False # For replace-string 

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

282 # Settings... 

283 cf = c.config 

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

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

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

287 else: 

288 self.autojustify = 0 

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

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

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

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

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

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

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

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

297 self.initBracketMatcher(c) 

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

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

300 @cmd('clear-cache') 

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

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

303 g.app.global_cacher.clear() 

304 g.app.commander_cacher.clear() 

305 

306 @cmd('dump-caches') 

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

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

309 g.app.global_cacher.dump() 

310 g.app.commander_cacher.dump() 

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

312 @cmd('do-nothing') 

313 def doNothing(self, event): 

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

315 pass 

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

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

318 def insertFileName(self, event=None): 

319 """ 

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

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

322 

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

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

325 cursor. 

326 """ 

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

328 if not w: 

329 return 

330 

331 def callback(arg, w=w): 

332 i = w.getSelectionRange()[0] 

333 p = c.p 

334 w.deleteTextSelection() 

335 w.insert(i, arg) 

336 newText = w.getAllText() 

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

338 bunch = u.beforeChangeBody(p) 

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

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

341 

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

343 

344 start_text = w.getSelectedText() 

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

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

347 if start_text: 

348 # make non-path characters whitespace 

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

350 for i in start_text) 

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

352 start_text = '' 

353 else: 

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

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

356 w.setSelectionRange( 

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

358 

359 c.k.functionTail = g.os_path_finalize_join( 

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

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

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

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

364 def insertHeadlineTime(self, event=None): 

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

366 frame = self 

367 c, p = frame.c, self.c.p 

368 if g.app.batchMode: 

369 c.notValidInBatchMode("Insert Headline Time") 

370 return 

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

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

373 if w: 

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

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

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

377 ins = w.getInsertPoint() 

378 s = c.getTime(body=False) 

379 w.insert(ins, s) 

380 else: 

381 c.endEditing() 

382 time = c.getTime(body=False) 

383 s = p.h.rstrip() 

384 if s: 

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

386 else: 

387 p.h = time 

388 c.redrawAndEdit(p, selectAll=True) 

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

390 @cmd('capitalize-headline') 

391 def capitalizeHeadline(self, event=None): 

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

393 frame = self 

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

395 

396 if g.app.batchMode: 

397 c.notValidInBatchMode("Capitalize Headline") 

398 return 

399 

400 h = p.h 

401 undoType = 'capitalize-headline' 

402 undoData = u.beforeChangeNodeContents(p) 

403 

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

405 capitalized = ' '.join(words) 

406 changed = capitalized != h 

407 if changed: 

408 p.h = capitalized 

409 c.setChanged() 

410 p.setDirty() 

411 u.afterChangeNodeContents(p, undoType, undoData) 

412 c.redraw() 

413 

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

415 def path_for_p(self, c, p): 

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

417 node `p`. 

418 

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

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

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

422 

423 :param outline c: outline containing p 

424 :param position p: position to locate 

425 :return: path 

426 :rtype: str 

427 """ 

428 

429 def atfile(p): 

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

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

432 return ( 

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

434 word0.startswith('@auto-') 

435 ) 

436 

437 aList = g.get_directives_dict_list(p) 

438 path = c.scanAtPathDirectives(aList) 

439 while c.positionExists(p): 

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

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

442 nodepath = g.os_path_join(path, nodepath) 

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

444 nodepath = g.os_path_dirname(nodepath) 

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

446 path = nodepath 

447 break 

448 p.moveToParent() 

449 

450 return path 

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

452 @cmd('tabify') 

453 def tabify(self, event): 

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

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

456 

457 @cmd('untabify') 

458 def untabify(self, event): 

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

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

461 

462 def tabifyHelper(self, event, which): 

463 w = self.editWidget(event) 

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

465 return 

466 self.beginCommand(w, undoType=which) 

467 i, end = w.getSelectionRange() 

468 txt = w.getSelectedText() 

469 if which == 'tabify': 

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

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

472 else: 

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

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

475 w.delete(i, end) 

476 w.insert(i, ntxt) 

477 n = i + len(ntxt) 

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

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

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

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

482 @cmd('capitalize-word') 

483 def capitalizeWord(self, event): 

484 """Capitalize the word at the cursor.""" 

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

486 

487 @cmd('downcase-word') 

488 def downCaseWord(self, event): 

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

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

491 

492 @cmd('upcase-word') 

493 def upCaseWord(self, event): 

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

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

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

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

498 w = self.editWidget(event) 

499 if not w: 

500 return # pragma: no cover (defensive) 

501 s = w.getAllText() 

502 ins = w.getInsertPoint() 

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

504 word = s[i:j] 

505 if not word.strip(): 

506 return # pragma: no cover (defensive) 

507 self.beginCommand(w, undoType=undoType) 

508 if which == 'cap': 

509 word2 = word.capitalize() 

510 elif which == 'low': 

511 word2 = word.lower() 

512 elif which == 'up': 

513 word2 = word.upper() 

514 else: 

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

516 changed = word != word2 

517 if changed: 

518 w.delete(i, j) 

519 w.insert(i, word2) 

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

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

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

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

524 def capitalizeWords(self, event=None): 

525 """Capitalize Entire Body Or Selection.""" 

526 frame = self 

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

528 w = frame.editWidget(event) 

529 s = w.getAllText() 

530 if not s: 

531 return 

532 

533 undoType = 'capitalize-body-words' 

534 undoData = u.beforeChangeNodeContents(p) 

535 

536 i, j = w.getSelectionRange() 

537 if i == j: 

538 sel = '' 

539 else: 

540 sel = s[i:j] 

541 text = sel or s 

542 if sel: 

543 prefix = s[:i] 

544 suffix = s[j:] 

545 

546 # Thanks to 

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

548 def convert_to_uppercase(m): 

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

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

551 

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

553 

554 if capitalized != text: 

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

556 c.setChanged() 

557 p.setDirty() 

558 u.afterChangeNodeContents(p, undoType, undoData) 

559 c.redraw() 

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

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

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

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

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

565 self.activateMenu('Cmds') 

566 

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

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

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

570 self.activateMenu('Edit') 

571 

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

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

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

575 self.activateMenu('File') 

576 

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

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

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

580 self.activateMenu('Help') 

581 

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

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

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

585 self.activateMenu('Outline') 

586 

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

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

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

590 self.activateMenu('Plugins') 

591 

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

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

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

595 self.activateMenu('Window') 

596 

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

598 c = self.c 

599 c.frame.menu.activateMenu(menuName) 

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

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

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

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

604 c, k = self.c, self.c.k 

605 c.bodyWantsFocus() 

606 if k: 

607 k.setDefaultInputState() 

608 k.showStateAndMode() 

609 

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

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

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

613 self.c.logWantsFocus() 

614 

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

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

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

618 self.c.minibufferWantsFocus() 

619 

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

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

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

623 self.c.treeWantsFocus() 

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

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

626 

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

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

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

630 c = self.c 

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

632 # Calls the base LeoTree method. 

633 

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

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

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

637 c = self.c 

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

639 

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

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

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

643 c = self.c 

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

645 

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

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

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

649 c = self.c 

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

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

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

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

654 """ 

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

656 

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

658 """ 

659 c = self.c 

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

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

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

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

664 def setCommentColumn(self, event): 

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

666 w = self.editWidget(event) 

667 if not w: 

668 return # pragma: no cover (defensive) 

669 s = w.getAllText() 

670 ins = w.getInsertPoint() 

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

672 self.ccolumn = col 

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

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

675 def indentToCommentColumn(self, event): 

676 """ 

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

678 comment column. 

679 """ 

680 w = self.editWidget(event) 

681 if not w: 

682 return # pragma: no cover (defensive) 

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

684 s = w.getAllText() 

685 ins = w.getInsertPoint() 

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

687 line = s[i:j] 

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

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

690 if line2 != line: 

691 w.delete(i, j) 

692 w.insert(i, line2) 

693 w.setInsertPoint(i + c1) 

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

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

696 #@@language rest 

697 #@+at 

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

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

700 # 

701 # cats 

702 # raaaaaaaaaaaats 

703 # mats 

704 # zaaaaaaaaap 

705 # 

706 # may look like: 

707 # 

708 # cats 

709 # raaaaaaaaaaaats 

710 # mats 

711 # zaaaaaaaaap 

712 # 

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

714 #@@language python 

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

716 @cmd('center-line') 

717 def centerLine(self, event): 

718 """Centers line within current fill column""" 

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

720 if not w: 

721 return # pragma: no cover (defensive) 

722 if self.fillColumn > 0: 

723 fillColumn = self.fillColumn 

724 else: 

725 d = c.scanAllDirectives(c.p) 

726 fillColumn = d.get("pagewidth") 

727 s = w.getAllText() 

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

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

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

731 return 

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

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

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

735 k = g.skip_ws(s, i) 

736 if k > i: 

737 w.delete(i, k - i) 

738 w.insert(i, ws) 

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

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

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

742 def setFillColumn(self, event): 

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

744 k = self.c.k 

745 self.w = self.editWidget(event) 

746 if not self.w: 

747 return # pragma: no cover (defensive) 

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

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

750 

751 def setFillColumn1(self, event): 

752 c, k, w = self.c, self.c.k, self.w 

753 k.clearState() 

754 try: 

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

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

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

758 except ValueError: 

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

760 c.widgetWantsFocus(w) 

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

762 @cmd('center-region') 

763 def centerRegion(self, event): 

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

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

766 if not w: 

767 return # pragma: no cover (defensive) 

768 s = w.getAllText() 

769 sel_1, sel_2 = w.getSelectionRange() 

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

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

772 if self.fillColumn > 0: 

773 fillColumn = self.fillColumn 

774 else: 

775 d = c.scanAllDirectives(c.p) 

776 fillColumn = d.get("pagewidth") 

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

778 inserted = 0 

779 while ind < end: 

780 s = w.getAllText() 

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

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

783 if len(line) >= fillColumn: 

784 ind = j 

785 else: 

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

787 inserted += n 

788 k = g.skip_ws(s, i) 

789 if k > i: 

790 w.delete(i, k - i) 

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

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

793 w.setSelectionRange(sel_1, sel_2 + inserted) 

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

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

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

797 def setFillPrefix(self, event): 

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

799 w = self.editWidget(event) 

800 if not w: 

801 return # pragma: no cover (defensive) 

802 s = w.getAllText() 

803 i, j = w.getSelectionRange() 

804 self.fillPrefix = s[i:j] 

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

806 def _addPrefix(self, ntxt): 

807 ntxt = ntxt.split('.') 

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

809 ntxt = '.'.join(ntxt) 

810 return ntxt 

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

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

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

814 def backwardFindCharacter(self, event): 

815 """Search backwards for a character.""" 

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

817 

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

819 def backwardFindCharacterExtendSelection(self, event): 

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

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

822 

823 @cmd('find-character') 

824 def findCharacter(self, event): 

825 """Search for a character.""" 

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

827 

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

829 def findCharacterExtendSelection(self, event): 

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

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

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

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

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

835 k = self.c.k 

836 self.w = self.editWidget(event) 

837 if not self.w: 

838 return 

839 self.event = event 

840 self.backward = backward 

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

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

843 s = ( 

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

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

846 k.setLabelBlue(s) 

847 # Get the arg without touching the focus. 

848 k.getArg( 

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

850 

851 def findCharacter1(self, event): 

852 k = self.c.k 

853 event, w = self.event, self.w 

854 backward = self.backward 

855 extend = self.extend or self.extendMode 

856 ch = k.arg 

857 s = w.getAllText() 

858 ins = w.toPythonIndex(self.insert) 

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

860 if backward: 

861 start = 0 

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

863 if j > -1: 

864 self.moveToHelper(event, j, extend) 

865 else: 

866 end = len(s) 

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

868 if j > -1: 

869 self.moveToHelper(event, j, extend) 

870 k.resetLabel() 

871 k.clearState() 

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

873 @cmd('find-word') 

874 def findWord(self, event): 

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

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

877 

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

879 def findWordInLine(self, event): 

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

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

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

883 def findWordHelper(self, event, oneLine): 

884 k = self.c.k 

885 self.w = self.editWidget(event) 

886 if self.w: 

887 self.oneLineFlag = oneLine 

888 k.setLabelBlue( 

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

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

891 

892 def findWord1(self, event): 

893 c, k = self.c, self.c.k 

894 ch = k.arg 

895 if ch: 

896 w = self.w 

897 i = w.getInsertPoint() 

898 s = w.getAllText() 

899 end = len(s) 

900 if self.oneLineFlag: 

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

902 if end == -1: 

903 end = len(s) 

904 while i < end: 

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

906 if i == -1: 

907 break 

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

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

910 break 

911 k.resetLabel() 

912 k.clearState() 

913 c.widgetWantsFocus(w) 

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

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

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

917 def gotoAnyClone(self, event=None): 

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

919 c = self.c 

920 p = c.p.threadNext() 

921 while p: 

922 if p.isCloned(): 

923 c.selectPosition(p) 

924 return 

925 p.moveToThreadNext() 

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

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

928 @cmd('goto-char') 

929 def gotoCharacter(self, event): 

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

931 k = self.c.k 

932 self.w = self.editWidget(event) 

933 if self.w: 

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

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

936 

937 def gotoCharacter1(self, event): 

938 c, k = self.c, self.c.k 

939 n = k.arg 

940 w = self.w 

941 ok = False 

942 if n.isdigit(): 

943 n = int(n) 

944 if n >= 0: 

945 w.setInsertPoint(n) 

946 w.seeInsertPoint() 

947 ok = True 

948 if not ok: 

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

950 k.resetLabel() 

951 k.clearState() 

952 c.widgetWantsFocus(w) 

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

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

955 def gotoGlobalLine(self, event): 

956 """ 

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

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

959 

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

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

962 

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

964 """ 

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

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

967 k = self.c.k 

968 self.w = self.editWidget(event) 

969 if self.w: 

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

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

972 

973 def gotoGlobalLine1(self, event): 

974 c, k = self.c, self.c.k 

975 n = k.arg 

976 k.resetLabel() 

977 k.clearState() 

978 if n.isdigit(): 

979 # Very important: n is one-based. 

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

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

982 @cmd('goto-line') 

983 def gotoLine(self, event): 

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

985 k = self.c.k 

986 self.w = self.editWidget(event) 

987 if self.w: 

988 k.setLabelBlue('Goto line: ') 

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

990 

991 def gotoLine1(self, event): 

992 c, k = self.c, self.c.k 

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

994 if n.isdigit(): 

995 n = int(n) 

996 s = w.getAllText() 

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

998 w.setInsertPoint(i) 

999 w.seeInsertPoint() 

1000 k.resetLabel() 

1001 k.clearState() 

1002 c.widgetWantsFocus(w) 

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

1004 #@+at 

1005 # To do: 

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

1007 # - Tree control recomputes height of each line. 

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

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

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

1011 c = self.c 

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

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

1014 # pylint: disable=unpacking-non-sequence 

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

1016 if not image: 

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

1018 return xoffset 

1019 if image_height is None: 

1020 yoffset = 0 

1021 else: 

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

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

1024 newEntry = { 

1025 'type': 'file', 

1026 'file': path, 

1027 'relPath': relPath, 

1028 'where': 'beforeHeadline', 

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

1030 'on': 'VNode', 

1031 } 

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

1033 aList.append(newEntry) 

1034 xoffset += 2 

1035 return xoffset 

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

1037 def dHash(self, d): 

1038 """Hash a dictionary""" 

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

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

1041 def getIconList(self, p): 

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

1043 fromVnode = [] 

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

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

1046 for i in fromVnode: 

1047 i['on'] = 'VNode' 

1048 return fromVnode 

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

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

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

1052 current = self.getIconList(p) 

1053 if not l and not current: 

1054 return # nothing to do 

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

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

1057 if lHash == cHash: 

1058 # no difference between original and current list of dictionaries 

1059 return 

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

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

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

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

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

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

1066 

1067 p - postion 

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

1069 uaLoc - the v or t node 

1070 """ 

1071 if subl: # Update the uA. 

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

1073 uaLoc.unknownAttributes = {} 

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

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

1076 uaLoc._p_changed = True 

1077 if setDirty: 

1078 p.setDirty() 

1079 else: # delete the uA. 

1080 if hasattr(uaLoc, 'unknownAttributes'): 

1081 if 'icons' in uaLoc.unknownAttributes: 

1082 del uaLoc.unknownAttributes['icons'] 

1083 uaLoc._p_changed = True 

1084 if setDirty: 

1085 p.setDirty() 

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

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

1088 def deleteFirstIcon(self, event=None): 

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

1090 c = self.c 

1091 aList = self.getIconList(c.p) 

1092 if aList: 

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

1094 c.setChanged() 

1095 c.redraw_after_icons_changed() 

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

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

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

1099 c, p = self.c, self.c.p 

1100 aList = self.getIconList(p) 

1101 if not aList: 

1102 return 

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

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

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

1106 newList = [] 

1107 for d in aList: 

1108 name2 = d.get('file') 

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

1110 name2rel = d.get('relPath') 

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

1112 newList.append(d) 

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

1114 self.setIconList(p, newList) 

1115 c.setChanged() 

1116 c.redraw_after_icons_changed() 

1117 else: 

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

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

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

1121 def deleteLastIcon(self, event=None): 

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

1123 c = self.c 

1124 aList = self.getIconList(c.p) 

1125 if aList: 

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

1127 c.setChanged() 

1128 c.redraw_after_icons_changed() 

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

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

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

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

1133 c = self.c 

1134 p = p or c.p 

1135 if p.u: 

1136 p.v._p_changed = True 

1137 self.setIconList(p, []) 

1138 p.setDirty() 

1139 c.setChanged() 

1140 c.redraw_after_icons_changed() 

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

1142 @cmd('insert-icon') 

1143 def insertIcon(self, event=None): 

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

1145 c, p = self.c, self.c.p 

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

1147 os.chdir(iconDir) 

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

1149 title='Get Icons', 

1150 filetypes=[ 

1151 ('All files', '*'), 

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

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

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

1155 ], 

1156 defaultextension=None, multiple=True) 

1157 if not paths: 

1158 return 

1159 aList: List[Any] = [] 

1160 xoffset = 2 

1161 for path in paths: 

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

1163 aList2 = self.getIconList(p) 

1164 aList2.extend(aList) 

1165 self.setIconList(p, aList2) 

1166 c.setChanged() 

1167 c.redraw_after_icons_changed() 

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

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

1170 c = self.c 

1171 if not p: 

1172 p = c.p 

1173 aList: List[Any] = [] 

1174 xoffset = 2 

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

1176 aList2 = self.getIconList(p) 

1177 if pos is None: 

1178 pos = len(aList2) 

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

1180 self.setIconList(p, aList2) 

1181 c.setChanged() 

1182 c.redraw_after_icons_changed() 

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

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

1185 @cmd('delete-indentation') 

1186 def deleteIndentation(self, event): 

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

1188 w = self.editWidget(event) 

1189 if not w: 

1190 return # pragma: no cover (defensive) 

1191 s = w.getAllText() 

1192 ins = w.getInsertPoint() 

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

1194 line = s[i:j] 

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

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

1197 if delta: 

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

1199 w.delete(i, j) 

1200 w.insert(i, line2) 

1201 ins -= delta 

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

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

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

1205 @cmd('indent-relative') 

1206 def indentRelative(self, event): 

1207 """ 

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

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

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

1211 previous line. 

1212 

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

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

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

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

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

1218 """ 

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

1220 undoType = 'indent-relative' 

1221 w = self.editWidget(event) 

1222 if not w: 

1223 return # pragma: no cover (defensive) 

1224 s = w.getAllText() 

1225 ins = w.getInsertPoint() 

1226 # Find the previous non-blank line 

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

1228 while 1: 

1229 if i <= 0: 

1230 return 

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

1232 line = s[i:j] 

1233 if line.strip(): 

1234 break 

1235 self.beginCommand(w, undoType=undoType) 

1236 try: 

1237 bunch = u.beforeChangeBody(p) 

1238 k = g.skip_ws(s, i) 

1239 ws = s[i:k] 

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

1241 k = g.skip_ws(s, i2) 

1242 line = ws + s[k:j2] 

1243 w.delete(i2, j2) 

1244 w.insert(i2, line) 

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

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

1247 u.afterChangeBody(p, undoType, bunch) 

1248 finally: 

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

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

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

1252 @cmd('copy-gnx') 

1253 def copyGnx(self, event): 

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

1255 c = self.c 

1256 if not c: 

1257 return 

1258 gnx = c.p and c.p.gnx 

1259 if not gnx: 

1260 return 

1261 g.app.gui.replaceClipboardWith(gnx) 

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

1263 if status_line: 

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

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

1266 @cmd('line-number') 

1267 def lineNumber(self, event): 

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

1269 k = self.c.k 

1270 w = self.editWidget(event) 

1271 if not w: 

1272 return # pragma: no cover (defensive) 

1273 s = w.getAllText() 

1274 i = w.getInsertPoint() 

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

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

1277 k.setLabelGrey( 

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

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

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

1281 @cmd('view-lossage') 

1282 def viewLossage(self, event): 

1283 """Print recent keystrokes.""" 

1284 print('Recent keystrokes...') 

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

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

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

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

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

1290 def viewRecentCommands(self, event): 

1291 """Print recently-executed commands.""" 

1292 c = self.c 

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

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

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

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

1297 @cmd('what-line') 

1298 def whatLine(self, event): 

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

1300 k = self.c.k 

1301 w = self.editWidget(event) 

1302 if w: 

1303 s = w.getAllText() 

1304 i = w.getInsertPoint() 

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

1306 k.keyboardQuit() 

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

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

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

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

1311 def addSpaceToLines(self, event): 

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

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

1314 

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

1316 def addTabToLines(self, event): 

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

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

1319 

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

1321 def removeSpaceFromLines(self, event): 

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

1323 self.addRemoveHelper( 

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

1325 

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

1327 def removeTabFromLines(self, event): 

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

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

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

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

1332 c = self.c 

1333 w = self.editWidget(event) 

1334 if not w: 

1335 return 

1336 if w.hasSelection(): 

1337 s = w.getSelectedText() 

1338 else: 

1339 s = w.getAllText() 

1340 if not s: 

1341 return 

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

1343 d = c.scanAllDirectives(c.p) 

1344 width = d.get('tabwidth') 

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

1346 ch = ' ' * abs(width) 

1347 self.beginCommand(w, undoType=undoType) 

1348 lines = g.splitLines(s) 

1349 if add: 

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

1351 else: 

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

1353 result = ''.join(result_list) 

1354 if w.hasSelection(): 

1355 i, j = w.getSelectionRange() 

1356 w.delete(i, j) 

1357 w.insert(i, result) 

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

1359 else: 

1360 w.setAllText(result) 

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

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

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

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

1365 def backwardDeleteCharacter(self, event=None): 

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

1367 c = self.c 

1368 w = self.editWidget(event) 

1369 if not w: 

1370 return # pragma: no cover (defensive) 

1371 wname = c.widget_name(w) 

1372 ins = w.getInsertPoint() 

1373 i, j = w.getSelectionRange() 

1374 if wname.startswith('body'): 

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

1376 changed = True 

1377 try: 

1378 tab_width = c.getTabWidth(c.p) 

1379 if i != j: 

1380 w.delete(i, j) 

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

1382 elif i == 0: 

1383 changed = False 

1384 elif tab_width > 0: 

1385 w.delete(ins - 1) 

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

1387 else: 

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

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

1390 s = prev = w.getAllText() 

1391 ins = w.getInsertPoint() 

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

1393 s = prev = s[i:ins] 

1394 n = len(prev) 

1395 abs_width = abs(tab_width) 

1396 # Delete up to this many spaces. 

1397 n2 = (n % abs_width) or abs_width 

1398 n2 = min(n, n2) 

1399 count = 0 

1400 while n2 > 0: 

1401 n2 -= 1 

1402 ch = prev[n - count - 1] 

1403 if ch != ' ': 

1404 break 

1405 else: count += 1 

1406 # Make sure we actually delete something. 

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

1408 w.delete(i, ins) 

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

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

1411 finally: 

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

1413 # Necessary to make text changes stick. 

1414 else: 

1415 # No undo in this widget. 

1416 s = w.getAllText() 

1417 # Delete something if we can. 

1418 if i != j: 

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

1420 w.delete(i, j) 

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

1422 elif ins != 0: 

1423 # Do nothing at the start of the headline. 

1424 w.delete(ins - 1) 

1425 ins = ins - 1 

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

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

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

1429 def cleanAllLines(self, event): 

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

1431 c = self.c 

1432 u = c.undoer 

1433 w = c.frame.body.wrapper 

1434 if not w: 

1435 return 

1436 tag = 'clean-all-lines' 

1437 u.beforeChangeGroup(c.p, tag) 

1438 n = 0 

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

1440 lines = [] 

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

1442 if line.rstrip(): 

1443 lines.append(line.rstrip()) 

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

1445 lines.append('\n') 

1446 s2 = ''.join(lines) 

1447 if s2 != p.b: 

1448 print(p.h) 

1449 bunch = u.beforeChangeNodeContents(p) 

1450 p.b = s2 

1451 p.setDirty() 

1452 n += 1 

1453 u.afterChangeNodeContents(p, tag, bunch) 

1454 u.afterChangeGroup(c.p, tag) 

1455 c.redraw_after_icons_changed() 

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

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

1458 @cmd('clean-lines') 

1459 def cleanLines(self, event): 

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

1461 """ 

1462 w = self.editWidget(event) 

1463 if not w: 

1464 return # pragma: no cover (defensive) 

1465 if w.hasSelection(): 

1466 s = w.getSelectedText() 

1467 else: 

1468 s = w.getAllText() 

1469 lines = [] 

1470 for line in g.splitlines(s): 

1471 if line.rstrip(): 

1472 lines.append(line.rstrip()) 

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

1474 lines.append('\n') 

1475 result = ''.join(lines) 

1476 if s != result: 

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

1478 if w.hasSelection(): 

1479 i, j = w.getSelectionRange() 

1480 w.delete(i, j) 

1481 w.insert(i, result) 

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

1483 else: 

1484 i = w.getInsertPoint() 

1485 w.delete(0, 'end') 

1486 w.insert(0, result) 

1487 w.setInsertPoint(i) 

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

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

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

1491 def clearSelectedText(self, event): 

1492 """Delete the selected text.""" 

1493 w = self.editWidget(event) 

1494 if not w: 

1495 return 

1496 i, j = w.getSelectionRange() 

1497 if i == j: 

1498 return 

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

1500 w.delete(i, j) 

1501 w.setInsertPoint(i) 

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

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

1504 @cmd('delete-word') 

1505 def deleteWord(self, event=None): 

1506 """Delete the word at the cursor.""" 

1507 self.deleteWordHelper(event, forward=True) 

1508 

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

1510 def backwardDeleteWord(self, event=None): 

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

1512 self.deleteWordHelper(event, forward=False) 

1513 

1514 # Patch by NH2. 

1515 

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

1517 def deleteWordSmart(self, event=None): 

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

1519 and symbols smartly.""" 

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

1521 

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

1523 def backwardDeleteWordSmart(self, event=None): 

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

1525 and symbols smartly.""" 

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

1527 

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

1529 # c = self.c 

1530 w = self.editWidget(event) 

1531 if not w: 

1532 return 

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

1534 if w.hasSelection(): 

1535 from_pos, to_pos = w.getSelectionRange() 

1536 else: 

1537 from_pos = w.getInsertPoint() 

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

1539 to_pos = w.getInsertPoint() 

1540 # For Tk GUI, make sure to_pos > from_pos 

1541 if from_pos > to_pos: 

1542 from_pos, to_pos = to_pos, from_pos 

1543 w.delete(from_pos, to_pos) 

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

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

1546 @cmd('delete-char') 

1547 def deleteNextChar(self, event): 

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

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

1550 if not w: 

1551 return 

1552 wname = c.widget_name(w) 

1553 if wname.startswith('body'): 

1554 s = w.getAllText() 

1555 i, j = w.getSelectionRange() 

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

1557 changed = True 

1558 if i != j: 

1559 w.delete(i, j) 

1560 w.setInsertPoint(i) 

1561 elif j < len(s): 

1562 w.delete(i) 

1563 w.setInsertPoint(i) 

1564 else: 

1565 changed = False 

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

1567 else: 

1568 # No undo in this widget. 

1569 s = w.getAllText() 

1570 i, j = w.getSelectionRange() 

1571 # Delete something if we can. 

1572 if i != j: 

1573 w.delete(i, j) 

1574 w.setInsertPoint(i) 

1575 elif j < len(s): 

1576 w.delete(i) 

1577 w.setInsertPoint(i) 

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

1579 @cmd('delete-spaces') 

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

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

1582 w = self.editWidget(event) 

1583 if not w: 

1584 return # pragma: no cover (defensive) 

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

1586 s = w.getAllText() 

1587 ins = w.getInsertPoint() 

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

1589 w1 = ins - 1 

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

1591 w1 -= 1 

1592 w1 += 1 

1593 w2 = ins 

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

1595 w2 += 1 

1596 spaces = s[w1:w2] 

1597 if spaces: 

1598 self.beginCommand(w, undoType=undoType) 

1599 if insertspace: 

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

1601 else: 

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

1603 w.setAllText(s) 

1604 w.setInsertPoint(w1) 

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

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

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

1608 def insertHardTab(self, event): 

1609 """Insert one hard tab.""" 

1610 c = self.c 

1611 w = self.editWidget(event) 

1612 if not w: 

1613 return 

1614 if not g.isTextWrapper(w): 

1615 return 

1616 name = c.widget_name(w) 

1617 if name.startswith('head'): 

1618 return 

1619 ins = w.getInsertPoint() 

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

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

1622 ins += 1 

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

1624 self.endCommand() 

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

1626 @cmd('insert-newline') 

1627 def insertNewLine(self, event): 

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

1629 self.insertNewlineBase(event) 

1630 

1631 insertNewline = insertNewLine 

1632 

1633 def insertNewlineBase(self, event): 

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

1635 # Note: insertNewlineHelper already exists. 

1636 c, k = self.c, self.c.k 

1637 w = self.editWidget(event) 

1638 if not w: 

1639 return # pragma: no cover (defensive) 

1640 if not g.isTextWrapper(w): 

1641 return # pragma: no cover (defensive) 

1642 name = c.widget_name(w) 

1643 if name.startswith('head'): 

1644 return 

1645 oldSel = w.getSelectionRange() 

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

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

1648 k.setInputState('insert') 

1649 k.showStateAndMode() 

1650 self.endCommand() 

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

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

1653 def insertNewLineAndTab(self, event): 

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

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

1656 c, k = self.c, self.c.k 

1657 p = c.p 

1658 w = self.editWidget(event) 

1659 if not w: 

1660 return 

1661 if not g.isTextWrapper(w): 

1662 return 

1663 name = c.widget_name(w) 

1664 if name.startswith('head'): 

1665 return 

1666 if trace: 

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

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

1669 oldSel = w.getSelectionRange() 

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

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

1672 k.setInputState('insert') 

1673 k.showStateAndMode() 

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

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

1676 @cmd('insert-parentheses') 

1677 def insertParentheses(self, event): 

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

1679 w = self.editWidget(event) 

1680 if w: 

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

1682 i = w.getInsertPoint() 

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

1684 w.setInsertPoint(i + 1) 

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

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

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

1688 def insertSoftTab(self, event): 

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

1690 c = self.c 

1691 w = self.editWidget(event) 

1692 if not w: 

1693 return 

1694 if not g.isTextWrapper(w): 

1695 return 

1696 name = c.widget_name(w) 

1697 if name.startswith('head'): 

1698 return 

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

1700 ins = w.getInsertPoint() 

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

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

1703 ins += tab_width 

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

1705 self.endCommand() 

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

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

1708 def removeBlankLines(self, event): 

1709 """ 

1710 Remove lines containing nothing but whitespace. 

1711 

1712 Select all lines if there is no existing selection. 

1713 """ 

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

1715 # 

1716 # "Before" snapshot. 

1717 bunch = u.beforeChangeBody(p) 

1718 # 

1719 # Initial data. 

1720 oldYview = w.getYScrollPosition() 

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

1722 # 

1723 # Calculate the result. 

1724 result_list = [] 

1725 changed = False 

1726 for line in lines: 

1727 if line.strip(): 

1728 result_list.append(line) 

1729 else: 

1730 changed = True 

1731 if not changed: 

1732 return # pragma: no cover (defensive) 

1733 # 

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

1735 result = ''.join(result_list) 

1736 p.b = result 

1737 w.setAllText(result) 

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

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

1740 w.setYScrollPosition(oldYview) 

1741 # 

1742 # "after" snapshot. 

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

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

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

1746 def replaceCurrentCharacter(self, event): 

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

1748 k = self.c.k 

1749 self.w = self.editWidget(event) 

1750 if self.w: 

1751 k.setLabelBlue('Replace Character: ') 

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

1753 

1754 def replaceCurrentCharacter1(self, event): 

1755 c, k, w = self.c, self.c.k, self.w 

1756 ch = k.arg 

1757 if ch: 

1758 i, j = w.getSelectionRange() 

1759 if i > j: 

1760 i, j = j, i 

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

1762 if i == j: 

1763 i = max(0, i - 1) 

1764 w.delete(i) 

1765 else: 

1766 w.delete(i, j) 

1767 w.insert(i, ch) 

1768 w.setInsertPoint(i + 1) 

1769 k.clearState() 

1770 k.resetLabel() 

1771 k.showStateAndMode() 

1772 c.widgetWantsFocus(w) 

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

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

1775 

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

1777 """ 

1778 Insert a character in the body pane. 

1779 

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

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

1782 """ 

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

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

1785 undoType = 'Typing' 

1786 if not w: 

1787 return # pragma: no cover (defensive) 

1788 #@+<< set local vars >> 

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

1790 stroke = event.stroke if event else None 

1791 ch = event.char if event else '' 

1792 if ch == 'Return': 

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

1794 if ch == 'Tab': 

1795 ch = '\t' 

1796 name = c.widget_name(w) 

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

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

1799 oldYview = w.getYScrollPosition() 

1800 brackets = self.openBracketsList + self.closeBracketsList 

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

1802 #@-<< set local vars >> 

1803 if not ch: 

1804 return 

1805 if trace: 

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

1807 assert g.isStrokeOrNone(stroke) 

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

1809 return 

1810 if ch == '\t': 

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

1812 elif ch == '\b': 

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

1814 self.backwardDeleteCharacter(event) 

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

1816 ch = '\n' 

1817 self.insertNewlineHelper(w, oldSel, undoType) 

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

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

1820 elif inBrackets and self.autocompleteBrackets: 

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

1822 elif ch: 

1823 # Null chars must not delete the selection. 

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

1825 # 

1826 # Common processing. 

1827 # Set the column for up and down keys. 

1828 spot = w.getInsertPoint() 

1829 c.editCommands.setMoveCol(w, spot) 

1830 # 

1831 # Update the text and handle undo. 

1832 newText = w.getAllText() 

1833 if newText != oldText: 

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

1835 newSel = w.getSelectionRange() 

1836 newInsert = w.getInsertPoint() 

1837 newSel = w.getSelectionRange() 

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

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

1840 oldSel=oldSel, oldYview=oldYview, newInsert=newInsert, newSel=newSel) 

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

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

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

1844 c, p = self.c, self.c.p 

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

1846 i, j = oldSel 

1847 if i > j: 

1848 i, j = j, i 

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

1850 if i != j: 

1851 w.delete(i, j) 

1852 elif action == 'overwrite': 

1853 w.delete(i) 

1854 if isPlain: 

1855 ins = w.getInsertPoint() 

1856 if self.autojustify > 0 and not inBrackets: 

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

1858 s = w.getAllText() 

1859 i = g.skip_to_start_of_line(s, ins) 

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

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

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

1863 # Find the start of the word. 

1864 n = 0 

1865 ins -= 1 

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

1867 n += 1 

1868 ins -= 1 

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

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

1871 sins -= 1 

1872 oldSel = (sins, ins) 

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

1874 ins = w.getInsertPoint() 

1875 ins += (n + 1) 

1876 w.insert(ins, ch) 

1877 w.setInsertPoint(ins + 1) 

1878 else: 

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

1880 if inBrackets and self.flashMatchingBrackets: 

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

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

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

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

1885 i, j = oldSel 

1886 if i > j: 

1887 i, j = j, i 

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

1889 if i != j: 

1890 w.delete(i, j) 

1891 elif action == 'overwrite': 

1892 w.delete(i) 

1893 ins = w.getInsertPoint() 

1894 # Pick the correct curly quote. 

1895 s = w.getAllText() or "" 

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

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

1898 if open_curly: 

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

1900 else: 

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

1902 w.insert(ins, ch) 

1903 w.setInsertPoint(ins + 1) 

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

1905 def flashCharacter(self, w, i): 

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

1907 bg = self.bracketsFlashBg or 'DodgerBlue1' 

1908 fg = self.bracketsFlashFg or 'white' 

1909 flashes = self.bracketsFlashCount or 3 

1910 delay = self.bracketsFlashDelay or 75 

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

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

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

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

1915 d = {} 

1916 # pylint: disable=consider-using-enumerate 

1917 if ch in self.openBracketsList: 

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

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

1920 # reverse = False # Search forward 

1921 else: 

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

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

1924 # reverse = True # Search backward 

1925 s = w.getAllText() 

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

1927 language = g.getLanguageAtPosition(c, p) 

1928 if language == 'perl': 

1929 return 

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

1931 if j is not None: 

1932 self.flashCharacter(w, j) 

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

1934 def initBracketMatcher(self, c): 

1935 """Init the bracket matching code.""" 

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

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

1938 self.openBracketsList = '([{' 

1939 self.closeBracketsList = ')]}' 

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

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

1942 

1943 c, p = self.c, self.c.p 

1944 i, j = oldSel 

1945 ch = '\n' 

1946 if i != j: 

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

1948 w.delete(i, j) 

1949 w.insert(i, ch) 

1950 w.setInsertPoint(i + 1) 

1951 else: 

1952 w.insert(i, ch) 

1953 w.setInsertPoint(i + 1) 

1954 if (c.autoindent_in_nocolor or 

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

1956 undoType != "Change") 

1957 ): 

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

1959 self.updateAutoIndent(p, w) 

1960 w.seeInsertPoint() 

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

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

1963 

1964 def updateAutoIndent(self, p, w): 

1965 """Handle auto indentation.""" 

1966 c = self.c 

1967 tab_width = c.getTabWidth(p) 

1968 # Get the previous line. 

1969 s = w.getAllText() 

1970 ins = w.getInsertPoint() 

1971 i = g.skip_to_start_of_line(s, ins) 

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

1973 s = s[i : j - 1] 

1974 # Add the leading whitespace to the present line. 

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

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

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

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

1979 width += abs(tab_width) 

1980 if self.smartAutoIndent: 

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

1982 bracketWidths = [width] 

1983 tabex = 0 

1984 for i, ch in enumerate(s): 

1985 if ch == '\t': 

1986 tabex += tab_width - 1 

1987 if ch in '([{': 

1988 bracketWidths.append(i + tabex + 1) 

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

1990 bracketWidths.pop() 

1991 width = bracketWidths.pop() 

1992 ws = g.computeLeadingWhitespace(width, tab_width) 

1993 if ws: 

1994 i = w.getInsertPoint() 

1995 w.insert(i, ws) 

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

1997 w.seeInsertPoint() 

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

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

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

2001 

2002 c = self.c 

2003 d = c.scanAllDirectives(p) 

2004 i, j = oldSel 

2005 language = d.get('language') 

2006 s = w.getAllText() 

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

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

2009 if automatch: 

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

2011 if i != j: 

2012 w.delete(i, j) 

2013 w.insert(i, ch) 

2014 if automatch: 

2015 ins = w.getInsertPoint() 

2016 w.setInsertPoint(ins - 1) 

2017 else: 

2018 ins = w.getInsertPoint() 

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

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

2021 ins = w.getInsertPoint() 

2022 w.setInsertPoint(ins + 1) 

2023 else: 

2024 if i != j: 

2025 w.delete(i, j) 

2026 w.insert(i, ch) 

2027 w.setInsertPoint(i + 1) 

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

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

2030 """ 

2031 A helper for selfInsertCommand. 

2032 

2033 Add spaces equivalent to a tab. 

2034 """ 

2035 c = self.c 

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

2037 if i != j: 

2038 c.indentBody(event) 

2039 return 

2040 tab_width = c.getTabWidth(p) 

2041 # Get the preceeding characters. 

2042 s = w.getAllText() 

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

2044 after = s[i:end] 

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

2046 after = after[:-1] 

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

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

2049 if doSmartTab: 

2050 self.updateAutoIndent(p, w) 

2051 # Add a tab if otherwise nothing would happen. 

2052 if s == w.getAllText(): 

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

2054 else: 

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

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

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

2058 """ 

2059 A helper for selfInsertCommand, called from updateTab. 

2060 

2061 Insert spaces equivalent to one tab. 

2062 """ 

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

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

2065 s2 = s[start:i] 

2066 width = g.computeWidth(s2, tab_width) 

2067 if trace: 

2068 g.trace('width', width) 

2069 if tab_width > 0: 

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

2071 ins = i + 1 

2072 else: 

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

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

2075 ins = i + n 

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

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

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

2079 @cmd('flush-lines') 

2080 def flushLines(self, event): 

2081 """ 

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

2083 text after point. 

2084 

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

2086 on the region instead. 

2087 """ 

2088 k = self.c.k 

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

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

2091 

2092 def flushLines1(self, event): 

2093 k = self.c.k 

2094 k.clearState() 

2095 k.resetLabel() 

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

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

2098 @cmd('keep-lines') 

2099 def keepLines(self, event): 

2100 """ 

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

2102 the text after point. 

2103 

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

2105 on the region instead. 

2106 """ 

2107 k = self.c.k 

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

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

2110 

2111 def keepLines1(self, event): 

2112 k = self.c.k 

2113 k.clearState() 

2114 k.resetLabel() 

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

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

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

2118 w = self.editWidget(event) 

2119 if not w: 

2120 return # pragma: no cover (defensive) 

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

2122 if w.hasSelection(): 

2123 i, end = w.getSelectionRange() 

2124 else: 

2125 i = w.getInsertPoint() 

2126 end = 'end' 

2127 txt = w.get(i, end) 

2128 tlines = txt.splitlines(True) 

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

2130 try: 

2131 regex = re.compile(pattern) 

2132 for n, z in enumerate(tlines): 

2133 f = regex.findall(z) 

2134 if which == 'flush' and f: 

2135 keeplines[n] = None 

2136 elif f: 

2137 keeplines.append(z) 

2138 except Exception: 

2139 return 

2140 if which == 'flush': 

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

2142 w.delete(i, end) 

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

2144 w.setInsertPoint(i) 

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

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

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

2148 def moveLineToNextNode(self, event): 

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

2150 c = self.c 

2151 if not c.p.threadNext(): 

2152 return 

2153 w = self.editWidget(event) 

2154 if not w: 

2155 return 

2156 s = w.getAllText() 

2157 sel_1, sel_2 = w.getSelectionRange() 

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

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

2160 lines = s[i:j] 

2161 if not lines.strip(): 

2162 return 

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

2164 try: 

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

2166 w.delete(i, next_j) 

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

2168 c.selectPosition(c.p.threadNext()) 

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

2170 c.recolor() 

2171 finally: 

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

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

2174 @cmd('split-line') 

2175 def splitLine(self, event): 

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

2177 w = self.editWidget(event) 

2178 if w: 

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

2180 s = w.getAllText() 

2181 ins = w.getInsertPoint() 

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

2183 w.setInsertPoint(ins + 1) 

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

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

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

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

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

2189 """ 

2190 Handle the details of extending the selection. 

2191 This method is called for all cursor moves. 

2192 

2193 extend: Clear the selection unless this is True. 

2194 spot: The *new* insert point. 

2195 """ 

2196 c, p = self.c, self.c.p 

2197 extend = extend or self.extendMode 

2198 ins = w.getInsertPoint() 

2199 i, j = w.getSelectionRange() 

2200 # Reset the move spot if needed. 

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

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

2203 elif extend: 

2204 # 2011/05/20: Fix bug 622819 

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

2206 if i == j: 

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

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

2209 # The bug fix, part 1. 

2210 pass 

2211 else: 

2212 # The bug fix, part 2. 

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

2214 if ins == i: 

2215 k = j 

2216 elif ins == j: 

2217 k = i 

2218 else: 

2219 k = ins 

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

2221 else: 

2222 if upOrDown: 

2223 s = w.getAllText() 

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

2225 line = s[i2:j2] 

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

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

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

2229 else: 

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

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

2232 else: # Plain move forward or back. 

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

2234 if extend: 

2235 if spot < self.moveSpot: 

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

2237 else: 

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

2239 else: 

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

2241 w.seeInsertPoint() 

2242 c.frame.updateStatusLine() 

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

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

2245 """ 

2246 Common helper method for commands the move the cursor 

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

2248 """ 

2249 c, k = self.c, self.c.k 

2250 w = self.editWidget(event) 

2251 if not w: 

2252 return 

2253 c.widgetWantsFocusNow(w) 

2254 # Put the request in the proper range. 

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

2256 i, j = k.getEditableTextRange() 

2257 if spot < i: 

2258 spot = i 

2259 elif spot > j: 

2260 spot = j 

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

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

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

2264 w = self.editWidget(event) 

2265 if not w: 

2266 return 

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

2268 # it apparently does not work for wrapped lines. 

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

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

2271 extend = extend or self.extendMode 

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

2273 else: 

2274 s = w.getAllText() 

2275 ins = w.getInsertPoint() 

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

2277 line = s[i:j] 

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

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

2280 elif spot == 'end-line': 

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

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

2283 j -= 1 

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

2285 elif spot == 'finish-line': 

2286 if not line.isspace(): 

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

2288 j -= 1 

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

2290 j -= 1 

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

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

2293 if not line.isspace(): 

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

2295 i += 1 

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

2297 else: 

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

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

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

2301 """ 

2302 Move the cursor to the next/previous word. 

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

2304 """ 

2305 c = self.c 

2306 w = self.editWidget(event) 

2307 if not w: 

2308 return # pragma: no cover (defensive) 

2309 c.widgetWantsFocusNow(w) 

2310 s = w.getAllText() 

2311 n = len(s) 

2312 i = w.getInsertPoint() 

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

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

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

2316 #@+others 

2317 #@+node:ekr.20150514063305.318: *6* ec.moveWordHelper functions 

2318 def is_alphanumeric(c): 

2319 return alphanumeric_re.match(c) is not None 

2320 

2321 def is_whitespace(c): 

2322 return whitespace_re.match(c) is not None 

2323 

2324 def is_simple_whitespace(c): 

2325 return simple_whitespace_re.match(c) is not None 

2326 

2327 def is_line_break(c): 

2328 return is_whitespace(c) and not is_simple_whitespace(c) 

2329 

2330 def is_special(c): 

2331 return not is_alphanumeric(c) and not is_whitespace(c) 

2332 

2333 def seek_until_changed(i, match_function, step): 

2334 while 0 <= i < n and match_function(s[i]): 

2335 i += step 

2336 return i 

2337 

2338 def seek_word_end(i): 

2339 return seek_until_changed(i, is_alphanumeric, 1) 

2340 

2341 def seek_word_start(i): 

2342 return seek_until_changed(i, is_alphanumeric, -1) 

2343 

2344 def seek_simple_whitespace_end(i): 

2345 return seek_until_changed(i, is_simple_whitespace, 1) 

2346 

2347 def seek_simple_whitespace_start(i): 

2348 return seek_until_changed(i, is_simple_whitespace, -1) 

2349 

2350 def seek_special_end(i): 

2351 return seek_until_changed(i, is_special, 1) 

2352 

2353 def seek_special_start(i): 

2354 return seek_until_changed(i, is_special, -1) 

2355 #@-others 

2356 if smart: 

2357 if forward: 

2358 if 0 <= i < n: 

2359 if is_alphanumeric(s[i]): 

2360 i = seek_word_end(i) 

2361 i = seek_simple_whitespace_end(i) 

2362 elif is_simple_whitespace(s[i]): 

2363 i = seek_simple_whitespace_end(i) 

2364 elif is_special(s[i]): 

2365 i = seek_special_end(i) 

2366 i = seek_simple_whitespace_end(i) 

2367 else: 

2368 i += 1 # e.g. for newlines 

2369 else: 

2370 i -= 1 # Shift cursor temporarily by -1 to get easy read access to the prev. char 

2371 if 0 <= i < n: 

2372 if is_alphanumeric(s[i]): 

2373 i = seek_word_start(i) 

2374 # Do not seek further whitespace here 

2375 elif is_simple_whitespace(s[i]): 

2376 i = seek_simple_whitespace_start(i) 

2377 elif is_special(s[i]): 

2378 i = seek_special_start(i) 

2379 # Do not seek further whitespace here 

2380 else: 

2381 i -= 1 # e.g. for newlines 

2382 i += 1 

2383 else: 

2384 if forward: 

2385 # Unlike backward-word moves, there are two options... 

2386 if end: 

2387 while 0 <= i < n and not g.isWordChar(s[i]): 

2388 i += 1 

2389 while 0 <= i < n and g.isWordChar(s[i]): 

2390 i += 1 

2391 else: 

2392 #1653. Scan for non-words *first*. 

2393 while 0 <= i < n and not g.isWordChar(s[i]): 

2394 i += 1 

2395 while 0 <= i < n and g.isWordChar(s[i]): 

2396 i += 1 

2397 else: 

2398 i -= 1 

2399 while 0 <= i < n and not g.isWordChar(s[i]): 

2400 i -= 1 

2401 while 0 <= i < n and g.isWordChar(s[i]): 

2402 i -= 1 

2403 i += 1 # 2015/04/30 

2404 self.moveToHelper(event, i, extend) 

2405 #@+node:ekr.20150514063305.289: *5* ec.setMoveCol 

2406 def setMoveCol(self, w, spot): 

2407 """Set the column to which an up or down arrow will attempt to move.""" 

2408 p = self.c.p 

2409 i, row, col = w.toPythonIndexRowCol(spot) 

2410 self.moveSpot = i 

2411 self.moveCol = col 

2412 self.moveSpotNode = p.v 

2413 #@+node:ekr.20150514063305.290: *4* ec.backToHome/ExtendSelection 

2414 @cmd('back-to-home') 

2415 def backToHome(self, event, extend=False): 

2416 """ 

2417 Smart home: 

2418 Position the point at the first non-blank character on the line, 

2419 or the start of the line if already there. 

2420 """ 

2421 w = self.editWidget(event) 

2422 if not w: 

2423 return 

2424 s = w.getAllText() 

2425 ins = w.getInsertPoint() 

2426 if s: 

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

2428 i1 = i 

2429 while i < j and s[i] in ' \t': 

2430 i += 1 

2431 if i == ins: 

2432 i = i1 

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

2434 

2435 @cmd('back-to-home-extend-selection') 

2436 def backToHomeExtendSelection(self, event): 

2437 self.backToHome(event, extend=True) 

2438 #@+node:ekr.20150514063305.291: *4* ec.backToIndentation 

2439 @cmd('back-to-indentation') 

2440 def backToIndentation(self, event): 

2441 """Position the point at the first non-blank character on the line.""" 

2442 w = self.editWidget(event) 

2443 if not w: 

2444 return # pragma: no cover (defensive) 

2445 s = w.getAllText() 

2446 ins = w.getInsertPoint() 

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

2448 while i < j and s[i] in ' \t': 

2449 i += 1 

2450 self.moveToHelper(event, i, extend=False) 

2451 #@+node:ekr.20150514063305.316: *4* ec.backward*/ExtendSelection 

2452 @cmd('back-word') 

2453 def backwardWord(self, event): 

2454 """Move the cursor to the previous word.""" 

2455 self.moveWordHelper(event, extend=False, forward=False) 

2456 

2457 @cmd('back-word-extend-selection') 

2458 def backwardWordExtendSelection(self, event): 

2459 """Extend the selection by moving the cursor to the previous word.""" 

2460 self.moveWordHelper(event, extend=True, forward=False) 

2461 

2462 @cmd('back-word-smart') 

2463 def backwardWordSmart(self, event): 

2464 """Move the cursor to the beginning of the current or the end of the previous word.""" 

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

2466 

2467 @cmd('back-word-smart-extend-selection') 

2468 def backwardWordSmartExtendSelection(self, event): 

2469 """Extend the selection by moving the cursor to the beginning of the current 

2470 or the end of the previous word.""" 

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

2472 #@+node:ekr.20170707072347.1: *4* ec.beginningOfLine/ExtendSelection 

2473 @cmd('beginning-of-line') 

2474 def beginningOfLine(self, event): 

2475 """Move the cursor to the first character of the line.""" 

2476 self.moveWithinLineHelper(event, 'begin-line', extend=False) 

2477 

2478 @cmd('beginning-of-line-extend-selection') 

2479 def beginningOfLineExtendSelection(self, event): 

2480 """ 

2481 Extend the selection by moving the cursor to the first character of the 

2482 line. 

2483 """ 

2484 self.moveWithinLineHelper(event, 'begin-line', extend=True) 

2485 #@+node:ekr.20150514063305.292: *4* ec.between lines & helper 

2486 @cmd('next-line') 

2487 def nextLine(self, event): 

2488 """Move the cursor down, extending the selection if in extend mode.""" 

2489 self.moveUpOrDownHelper(event, 'down', extend=False) 

2490 

2491 @cmd('next-line-extend-selection') 

2492 def nextLineExtendSelection(self, event): 

2493 """Extend the selection by moving the cursor down.""" 

2494 self.moveUpOrDownHelper(event, 'down', extend=True) 

2495 

2496 @cmd('previous-line') 

2497 def prevLine(self, event): 

2498 """Move the cursor up, extending the selection if in extend mode.""" 

2499 self.moveUpOrDownHelper(event, 'up', extend=False) 

2500 

2501 @cmd('previous-line-extend-selection') 

2502 def prevLineExtendSelection(self, event): 

2503 """Extend the selection by moving the cursor up.""" 

2504 self.moveUpOrDownHelper(event, 'up', extend=True) 

2505 #@+node:ekr.20150514063305.293: *5* ec.moveUpOrDownHelper 

2506 def moveUpOrDownHelper(self, event, direction, extend): 

2507 

2508 w = self.editWidget(event) 

2509 if not w: 

2510 return # pragma: no cover (defensive) 

2511 ins = w.getInsertPoint() 

2512 s = w.getAllText() 

2513 w.seeInsertPoint() 

2514 if hasattr(w, 'leoMoveCursorHelper'): 

2515 extend = extend or self.extendMode 

2516 w.leoMoveCursorHelper(kind=direction, extend=extend) 

2517 else: 

2518 # Find the start of the next/prev line. 

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

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

2521 if direction == 'down': 

2522 i2, j2 = g.getLine(s, j) 

2523 else: 

2524 i2, j2 = g.getLine(s, i - 1) 

2525 # The spot is the start of the line plus the column index. 

2526 n = max(0, j2 - i2 - 1) # The length of the new line. 

2527 col2 = min(col, n) 

2528 spot = i2 + col2 

2529 self.extendHelper(w, extend, spot, upOrDown=True) 

2530 #@+node:ekr.20150514063305.294: *4* ec.buffers & helper 

2531 @cmd('beginning-of-buffer') 

2532 def beginningOfBuffer(self, event): 

2533 """Move the cursor to the start of the body text.""" 

2534 self.moveToBufferHelper(event, 'home', extend=False) 

2535 

2536 @cmd('beginning-of-buffer-extend-selection') 

2537 def beginningOfBufferExtendSelection(self, event): 

2538 """Extend the text selection by moving the cursor to the start of the body text.""" 

2539 self.moveToBufferHelper(event, 'home', extend=True) 

2540 

2541 @cmd('end-of-buffer') 

2542 def endOfBuffer(self, event): 

2543 """Move the cursor to the end of the body text.""" 

2544 self.moveToBufferHelper(event, 'end', extend=False) 

2545 

2546 @cmd('end-of-buffer-extend-selection') 

2547 def endOfBufferExtendSelection(self, event): 

2548 """Extend the text selection by moving the cursor to the end of the body text.""" 

2549 self.moveToBufferHelper(event, 'end', extend=True) 

2550 #@+node:ekr.20150514063305.295: *5* ec.moveToBufferHelper 

2551 def moveToBufferHelper(self, event, spot, extend): 

2552 w = self.editWidget(event) 

2553 if not w: 

2554 return # pragma: no cover (defensive) 

2555 if hasattr(w, 'leoMoveCursorHelper'): 

2556 extend = extend or self.extendMode 

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

2558 else: 

2559 if spot == 'home': 

2560 self.moveToHelper(event, 0, extend=extend) 

2561 elif spot == 'end': 

2562 s = w.getAllText() 

2563 self.moveToHelper(event, len(s), extend=extend) 

2564 else: 

2565 g.trace('can not happen: bad spot', spot) # pragma: no cover (defensive) 

2566 #@+node:ekr.20150514063305.296: *4* ec.characters & helper 

2567 @cmd('back-char') 

2568 def backCharacter(self, event): 

2569 """Move the cursor back one character, extending the selection if in extend mode.""" 

2570 self.moveToCharacterHelper(event, 'left', extend=False) 

2571 

2572 @cmd('back-char-extend-selection') 

2573 def backCharacterExtendSelection(self, event): 

2574 """Extend the selection by moving the cursor back one character.""" 

2575 self.moveToCharacterHelper(event, 'left', extend=True) 

2576 

2577 @cmd('forward-char') 

2578 def forwardCharacter(self, event): 

2579 """Move the cursor forward one character, extending the selection if in extend mode.""" 

2580 self.moveToCharacterHelper(event, 'right', extend=False) 

2581 

2582 @cmd('forward-char-extend-selection') 

2583 def forwardCharacterExtendSelection(self, event): 

2584 """Extend the selection by moving the cursor forward one character.""" 

2585 self.moveToCharacterHelper(event, 'right', extend=True) 

2586 #@+node:ekr.20150514063305.297: *5* ec.moveToCharacterHelper 

2587 def moveToCharacterHelper(self, event, spot, extend): 

2588 w = self.editWidget(event) 

2589 if not w: 

2590 return 

2591 if hasattr(w, 'leoMoveCursorHelper'): 

2592 extend = extend or self.extendMode 

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

2594 else: 

2595 i = w.getInsertPoint() 

2596 if spot == 'left': 

2597 i = max(0, i - 1) 

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

2599 elif spot == 'right': 

2600 i = min(i + 1, len(w.getAllText())) 

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

2602 else: 

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

2604 #@+node:ekr.20150514063305.298: *4* ec.clear/set/ToggleExtendMode 

2605 @cmd('clear-extend-mode') 

2606 def clearExtendMode(self, event): 

2607 """Turn off extend mode: cursor movement commands do not extend the selection.""" 

2608 self.extendModeHelper(event, False) 

2609 

2610 @cmd('set-extend-mode') 

2611 def setExtendMode(self, event): 

2612 """Turn on extend mode: cursor movement commands do extend the selection.""" 

2613 self.extendModeHelper(event, True) 

2614 

2615 @cmd('toggle-extend-mode') 

2616 def toggleExtendMode(self, event): 

2617 """Toggle extend mode, i.e., toggle whether cursor movement commands extend the selections.""" 

2618 self.extendModeHelper(event, not self.extendMode) 

2619 

2620 def extendModeHelper(self, event, val): 

2621 c = self.c 

2622 w = self.editWidget(event) 

2623 if w: 

2624 self.extendMode = val 

2625 if not g.unitTesting: 

2626 # g.red('extend mode','on' if val else 'off')) 

2627 c.k.showStateAndMode() 

2628 c.widgetWantsFocusNow(w) 

2629 #@+node:ekr.20170707072524.1: *4* ec.endOfLine/ExtendSelection 

2630 @cmd('end-of-line') 

2631 def endOfLine(self, event): 

2632 """Move the cursor to the last character of the line.""" 

2633 self.moveWithinLineHelper(event, 'end-line', extend=False) 

2634 

2635 @cmd('end-of-line-extend-selection') 

2636 def endOfLineExtendSelection(self, event): 

2637 """Extend the selection by moving the cursor to the last character of the line.""" 

2638 self.moveWithinLineHelper(event, 'end-line', extend=True) 

2639 #@+node:ekr.20150514063305.299: *4* ec.exchangePointMark 

2640 @cmd('exchange-point-mark') 

2641 def exchangePointMark(self, event): 

2642 """ 

2643 Exchange the point (insert point) with the mark (the other end of the 

2644 selected text). 

2645 """ 

2646 c = self.c 

2647 w = self.editWidget(event) 

2648 if not w: 

2649 return 

2650 if hasattr(w, 'leoMoveCursorHelper'): 

2651 w.leoMoveCursorHelper(kind='exchange', extend=False) 

2652 else: 

2653 c.widgetWantsFocusNow(w) 

2654 i, j = w.getSelectionRange(sort=False) 

2655 if i == j: 

2656 return 

2657 ins = w.getInsertPoint() 

2658 ins = j if ins == i else i 

2659 w.setInsertPoint(ins) 

2660 w.setSelectionRange(i, j, insert=None) 

2661 #@+node:ekr.20150514063305.300: *4* ec.extend-to-line 

2662 @cmd('extend-to-line') 

2663 def extendToLine(self, event): 

2664 """Select the line at the cursor.""" 

2665 w = self.editWidget(event) 

2666 if not w: 

2667 return 

2668 s = w.getAllText() 

2669 n = len(s) 

2670 i = w.getInsertPoint() 

2671 while 0 <= i < n and not s[i] == '\n': 

2672 i -= 1 

2673 i += 1 

2674 i1 = i 

2675 while 0 <= i < n and not s[i] == '\n': 

2676 i += 1 

2677 w.setSelectionRange(i1, i) 

2678 #@+node:ekr.20150514063305.301: *4* ec.extend-to-sentence 

2679 @cmd('extend-to-sentence') 

2680 def extendToSentence(self, event): 

2681 """Select the line at the cursor.""" 

2682 w = self.editWidget(event) 

2683 if not w: 

2684 return # pragma: no cover (defensive) 

2685 s = w.getAllText() 

2686 n = len(s) 

2687 i = w.getInsertPoint() 

2688 i2 = 1 + s.find('.', i) 

2689 if i2 == -1: 

2690 i2 = n 

2691 i1 = 1 + s.rfind('.', 0, i2 - 1) 

2692 w.setSelectionRange(i1, i2) 

2693 #@+node:ekr.20150514063305.302: *4* ec.extend-to-word 

2694 @cmd('extend-to-word') 

2695 def extendToWord(self, event, select=True, w=None): 

2696 """Compute the word at the cursor. Select it if select arg is True.""" 

2697 if not w: 

2698 w = self.editWidget(event) 

2699 if not w: 

2700 return 0, 0 # pragma: no cover (defensive) 

2701 s = w.getAllText() 

2702 n = len(s) 

2703 i = i1 = w.getInsertPoint() 

2704 # Find a word char on the present line if one isn't at the cursor. 

2705 if not (0 <= i < n and g.isWordChar(s[i])): 

2706 # First, look forward 

2707 while i < n and not g.isWordChar(s[i]) and s[i] != '\n': 

2708 i += 1 

2709 # Next, look backward. 

2710 if not (0 <= i < n and g.isWordChar(s[i])): 

2711 i = i1 - 1 if (i >= n or s[i] == '\n') else i1 

2712 while i >= 0 and not g.isWordChar(s[i]) and s[i] != '\n': 

2713 i -= 1 

2714 # Make sure s[i] is a word char. 

2715 if 0 <= i < n and g.isWordChar(s[i]): 

2716 # Find the start of the word. 

2717 while 0 <= i < n and g.isWordChar(s[i]): 

2718 i -= 1 

2719 i += 1 

2720 i1 = i 

2721 # Find the end of the word. 

2722 while 0 <= i < n and g.isWordChar(s[i]): 

2723 i += 1 

2724 if select: 

2725 w.setSelectionRange(i1, i) 

2726 return i1, i 

2727 return 0, 0 

2728 #@+node:ekr.20170707072837.1: *4* ec.finishOfLine/ExtendSelection 

2729 @cmd('finish-of-line') 

2730 def finishOfLine(self, event): 

2731 """Move the cursor to the last character of the line.""" 

2732 self.moveWithinLineHelper(event, 'finish-line', extend=False) 

2733 

2734 @cmd('finish-of-line-extend-selection') 

2735 def finishOfLineExtendSelection(self, event): 

2736 """Extend the selection by moving the cursor to the last character of the line.""" 

2737 self.moveWithinLineHelper(event, 'finish-line', extend=True) 

2738 #@+node:ekr.20170707160947.1: *4* ec.forward*/ExtendSelection 

2739 @cmd('forward-end-word') 

2740 def forwardEndWord(self, event): # New in Leo 4.4.2 

2741 """Move the cursor to the next word.""" 

2742 self.moveWordHelper(event, extend=False, forward=True, end=True) 

2743 

2744 @cmd('forward-end-word-extend-selection') 

2745 def forwardEndWordExtendSelection(self, event): # New in Leo 4.4.2 

2746 """Extend the selection by moving the cursor to the next word.""" 

2747 self.moveWordHelper(event, extend=True, forward=True, end=True) 

2748 

2749 @cmd('forward-word') 

2750 def forwardWord(self, event): 

2751 """Move the cursor to the next word.""" 

2752 self.moveWordHelper(event, extend=False, forward=True) 

2753 

2754 @cmd('forward-word-extend-selection') 

2755 def forwardWordExtendSelection(self, event): 

2756 """Extend the selection by moving the cursor to the end of the next word.""" 

2757 self.moveWordHelper(event, extend=True, forward=True) 

2758 

2759 @cmd('forward-word-smart') 

2760 def forwardWordSmart(self, event): 

2761 """Move the cursor to the end of the current or the beginning of the next word.""" 

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

2763 

2764 @cmd('forward-word-smart-extend-selection') 

2765 def forwardWordSmartExtendSelection(self, event): 

2766 """Extend the selection by moving the cursor to the end of the current 

2767 or the beginning of the next word.""" 

2768 self.moveWordHelper(event, extend=True, forward=True, smart=True) 

2769 #@+node:ekr.20150514063305.303: *4* ec.movePastClose & helper 

2770 @cmd('move-past-close') 

2771 def movePastClose(self, event): 

2772 """Move the cursor past the closing parenthesis.""" 

2773 self.movePastCloseHelper(event, extend=False) 

2774 

2775 @cmd('move-past-close-extend-selection') 

2776 def movePastCloseExtendSelection(self, event): 

2777 """Extend the selection by moving the cursor past the closing parenthesis.""" 

2778 self.movePastCloseHelper(event, extend=True) 

2779 #@+node:ekr.20150514063305.304: *5* ec.movePastCloseHelper 

2780 def movePastCloseHelper(self, event, extend): 

2781 c = self.c 

2782 w = self.editWidget(event) 

2783 if not w: 

2784 return 

2785 c.widgetWantsFocusNow(w) 

2786 s = w.getAllText() 

2787 ins = w.getInsertPoint() 

2788 # Scan backwards for i,j. 

2789 i = ins 

2790 while len(s) > i >= 0 and s[i] != '\n': 

2791 if s[i] == '(': 

2792 break 

2793 i -= 1 

2794 else: 

2795 return 

2796 j = ins 

2797 while len(s) > j >= 0 and s[j] != '\n': 

2798 if s[j] == '(': 

2799 break 

2800 j -= 1 

2801 if i < j: 

2802 return 

2803 # Scan forward for i2,j2. 

2804 i2 = ins 

2805 while i2 < len(s) and s[i2] != '\n': 

2806 if s[i2] == ')': 

2807 break 

2808 i2 += 1 

2809 else: 

2810 return 

2811 j2 = ins 

2812 while j2 < len(s) and s[j2] != '\n': 

2813 if s[j2] == ')': 

2814 break 

2815 j2 += 1 

2816 if i2 > j2: 

2817 return 

2818 self.moveToHelper(event, i2 + 1, extend) 

2819 #@+node:ekr.20150514063305.306: *4* ec.pages & helper 

2820 @cmd('back-page') 

2821 def backPage(self, event): 

2822 """Move the cursor back one page, 

2823 extending the selection if in extend mode.""" 

2824 self.movePageHelper(event, kind='back', extend=False) 

2825 

2826 @cmd('back-page-extend-selection') 

2827 def backPageExtendSelection(self, event): 

2828 """Extend the selection by moving the cursor back one page.""" 

2829 self.movePageHelper(event, kind='back', extend=True) 

2830 

2831 @cmd('forward-page') 

2832 def forwardPage(self, event): 

2833 """Move the cursor forward one page, 

2834 extending the selection if in extend mode.""" 

2835 self.movePageHelper(event, kind='forward', extend=False) 

2836 

2837 @cmd('forward-page-extend-selection') 

2838 def forwardPageExtendSelection(self, event): 

2839 """Extend the selection by moving the cursor forward one page.""" 

2840 self.movePageHelper(event, kind='forward', extend=True) 

2841 #@+node:ekr.20150514063305.307: *5* ec.movePageHelper 

2842 def movePageHelper(self, event, kind, extend): # kind in back/forward. 

2843 """Move the cursor up/down one page, possibly extending the selection.""" 

2844 w = self.editWidget(event) 

2845 if not w: 

2846 return 

2847 linesPerPage = 15 # To do. 

2848 if hasattr(w, 'leoMoveCursorHelper'): 

2849 extend = extend or self.extendMode 

2850 w.leoMoveCursorHelper( 

2851 kind='page-down' if kind == 'forward' else 'page-up', 

2852 extend=extend, linesPerPage=linesPerPage) 

2853 # w.seeInsertPoint() 

2854 # c.frame.updateStatusLine() 

2855 # w.rememberSelectionAndScroll() 

2856 else: 

2857 ins = w.getInsertPoint() 

2858 s = w.getAllText() 

2859 lines = g.splitLines(s) 

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

2861 if kind == 'back': 

2862 row2 = max(0, row - linesPerPage) 

2863 else: 

2864 row2 = min(row + linesPerPage, len(lines) - 1) 

2865 if row == row2: 

2866 return 

2867 spot = g.convertRowColToPythonIndex(s, row2, col, lines=lines) 

2868 self.extendHelper(w, extend, spot, upOrDown=True) 

2869 #@+node:ekr.20150514063305.308: *4* ec.paragraphs & helpers 

2870 @cmd('back-paragraph') 

2871 def backwardParagraph(self, event): 

2872 """Move the cursor to the previous paragraph.""" 

2873 self.backwardParagraphHelper(event, extend=False) 

2874 

2875 @cmd('back-paragraph-extend-selection') 

2876 def backwardParagraphExtendSelection(self, event): 

2877 """Extend the selection by moving the cursor to the previous paragraph.""" 

2878 self.backwardParagraphHelper(event, extend=True) 

2879 

2880 @cmd('forward-paragraph') 

2881 def forwardParagraph(self, event): 

2882 """Move the cursor to the next paragraph.""" 

2883 self.forwardParagraphHelper(event, extend=False) 

2884 

2885 @cmd('forward-paragraph-extend-selection') 

2886 def forwardParagraphExtendSelection(self, event): 

2887 """Extend the selection by moving the cursor to the next paragraph.""" 

2888 self.forwardParagraphHelper(event, extend=True) 

2889 #@+node:ekr.20150514063305.309: *5* ec.backwardParagraphHelper 

2890 def backwardParagraphHelper(self, event, extend): 

2891 w = self.editWidget(event) 

2892 if not w: 

2893 return # pragma: no cover (defensive) 

2894 s = w.getAllText() 

2895 i, j = w.getSelectionRange() 

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

2897 line = s[i:j] 

2898 if line.strip(): 

2899 # Find the start of the present paragraph. 

2900 while i > 0: 

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

2902 line = s[i:j] 

2903 if not line.strip(): 

2904 break 

2905 # Find the end of the previous paragraph. 

2906 while i > 0: 

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

2908 line = s[i:j] 

2909 if line.strip(): 

2910 i = j - 1 

2911 break 

2912 self.moveToHelper(event, i, extend) 

2913 #@+node:ekr.20150514063305.310: *5* ec.forwardParagraphHelper 

2914 def forwardParagraphHelper(self, event, extend): 

2915 w = self.editWidget(event) 

2916 if not w: 

2917 return 

2918 s = w.getAllText() 

2919 ins = w.getInsertPoint() 

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

2921 line = s[i:j] 

2922 if line.strip(): # Skip past the present paragraph. 

2923 self.selectParagraphHelper(w, i) 

2924 i, j = w.getSelectionRange() 

2925 j += 1 

2926 # Skip to the next non-blank line. 

2927 i = j 

2928 while j < len(s): 

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

2930 line = s[i:j] 

2931 if line.strip(): 

2932 break 

2933 w.setInsertPoint(ins) # Restore the original insert point. 

2934 self.moveToHelper(event, i, extend) 

2935 #@+node:ekr.20170707093335.1: *4* ec.pushCursor and popCursor 

2936 @cmd('pop-cursor') 

2937 def popCursor(self, event=None): 

2938 """Restore the node, selection range and insert point from the stack.""" 

2939 c = self.c 

2940 w = self.editWidget(event) 

2941 if w and self.cursorStack: 

2942 p, i, j, ins = self.cursorStack.pop() 

2943 if c.positionExists(p): 

2944 c.selectPosition(p) 

2945 c.redraw() 

2946 w.setSelectionRange(i, j, insert=ins) 

2947 c.bodyWantsFocus() 

2948 else: 

2949 g.es('invalid position', c.p.h) 

2950 elif not w: 

2951 g.es('no stacked cursor', color='blue') 

2952 

2953 @cmd('push-cursor') 

2954 def pushCursor(self, event=None): 

2955 """Push the selection range and insert point on the stack.""" 

2956 c = self.c 

2957 w = self.editWidget(event) 

2958 if w: 

2959 p = c.p.copy() 

2960 i, j = w.getSelectionRange() 

2961 ins = w.getInsertPoint() 

2962 self.cursorStack.append((p, i, j, ins),) 

2963 else: 

2964 g.es('cursor not pushed', color='blue') 

2965 #@+node:ekr.20150514063305.311: *4* ec.selectAllText 

2966 @cmd('select-all') 

2967 def selectAllText(self, event): 

2968 """Select all text.""" 

2969 k = self.c.k 

2970 w = self.editWidget(event) 

2971 if not w: 

2972 return 

2973 # Bug fix 2013/12/13: Special case the minibuffer. 

2974 if w == k.w: 

2975 k.selectAll() 

2976 elif w and g.isTextWrapper(w): 

2977 w.selectAllText() 

2978 #@+node:ekr.20150514063305.312: *4* ec.sentences & helpers 

2979 @cmd('back-sentence') 

2980 def backSentence(self, event): 

2981 """Move the cursor to the previous sentence.""" 

2982 self.backSentenceHelper(event, extend=False) 

2983 

2984 @cmd('back-sentence-extend-selection') 

2985 def backSentenceExtendSelection(self, event): 

2986 """Extend the selection by moving the cursor to the previous sentence.""" 

2987 self.backSentenceHelper(event, extend=True) 

2988 

2989 @cmd('forward-sentence') 

2990 def forwardSentence(self, event): 

2991 """Move the cursor to the next sentence.""" 

2992 self.forwardSentenceHelper(event, extend=False) 

2993 

2994 @cmd('forward-sentence-extend-selection') 

2995 def forwardSentenceExtendSelection(self, event): 

2996 """Extend the selection by moving the cursor to the next sentence.""" 

2997 self.forwardSentenceHelper(event, extend=True) 

2998 #@+node:ekr.20150514063305.313: *5* ec.backSentenceHelper 

2999 def backSentenceHelper(self, event, extend): 

3000 c = self.c 

3001 w = self.editWidget(event) 

3002 if not w: 

3003 return # pragma: no cover (defensive) 

3004 c.widgetWantsFocusNow(w) 

3005 s = w.getAllText() 

3006 ins = w.getInsertPoint() 

3007 # Find the starting point of the scan. 

3008 i = ins 

3009 i -= 1 # Ensure some progress. 

3010 if i < 0 or i >= len(s): 

3011 return 

3012 # Tricky. 

3013 if s[i] == '.': 

3014 i -= 1 

3015 while i >= 0 and s[i] in ' \n': 

3016 i -= 1 

3017 if i >= ins: 

3018 i -= 1 

3019 if i >= len(s): 

3020 i -= 1 

3021 if i <= 0: 

3022 return 

3023 if s[i] == '.': 

3024 i -= 1 

3025 # Scan backwards to the end of the paragraph. 

3026 # Stop at empty lines. 

3027 # Skip periods within words. 

3028 # Stop at sentences ending in non-periods. 

3029 end = False 

3030 while not end and i >= 0: 

3031 progress = i 

3032 if s[i] == '.': 

3033 # Skip periods surrounded by letters/numbers 

3034 if i > 0 and s[i - 1].isalnum() and s[i + 1].isalnum(): 

3035 i -= 1 

3036 else: 

3037 i += 1 

3038 while i < len(s) and s[i] in ' \n': 

3039 i += 1 

3040 i -= 1 

3041 break 

3042 elif s[i] == '\n': 

3043 j = i - 1 

3044 while j >= 0: 

3045 if s[j] == '\n': 

3046 # Don't include first newline. 

3047 end = True 

3048 break # found blank line. 

3049 elif s[j] == ' ': 

3050 j -= 1 

3051 else: 

3052 i -= 1 

3053 break # no blank line found. 

3054 else: 

3055 # No blank line found. 

3056 i -= 1 

3057 else: 

3058 i -= 1 

3059 assert end or progress > i 

3060 i += 1 

3061 if i < ins: 

3062 self.moveToHelper(event, i, extend) 

3063 #@+node:ekr.20150514063305.314: *5* ec.forwardSentenceHelper 

3064 def forwardSentenceHelper(self, event, extend): 

3065 c = self.c 

3066 w = self.editWidget(event) 

3067 if not w: 

3068 return 

3069 c.widgetWantsFocusNow(w) 

3070 s = w.getAllText() 

3071 ins = w.getInsertPoint() 

3072 if ins >= len(s): 

3073 return 

3074 # Find the starting point of the scan. 

3075 i = ins 

3076 if i + 1 < len(s) and s[i + 1] == '.': 

3077 i += 1 

3078 if s[i] == '.': 

3079 i += 1 

3080 else: 

3081 while i < len(s) and s[i] in ' \n': 

3082 i += 1 

3083 i -= 1 

3084 if i <= ins: 

3085 i += 1 

3086 if i >= len(s): 

3087 return 

3088 # Scan forward to the end of the paragraph. 

3089 # Stop at empty lines. 

3090 # Skip periods within words. 

3091 # Stop at sentences ending in non-periods. 

3092 end = False 

3093 while not end and i < len(s): 

3094 progress = i 

3095 if s[i] == '.': 

3096 # Skip periods surrounded by letters/numbers 

3097 if 0 < i < len(s) and s[i - 1].isalnum() and s[i + 1].isalnum(): 

3098 i += 1 

3099 else: 

3100 i += 1 

3101 break # Include the paragraph. 

3102 elif s[i] == '\n': 

3103 j = i + 1 

3104 while j < len(s): 

3105 if s[j] == '\n': 

3106 # Don't include first newline. 

3107 end = True 

3108 break # found blank line. 

3109 elif s[j] == ' ': 

3110 j += 1 

3111 else: 

3112 i += 1 

3113 break # no blank line found. 

3114 else: 

3115 # No blank line found. 

3116 i += 1 

3117 else: 

3118 i += 1 

3119 assert end or progress < i 

3120 i = min(i, len(s)) 

3121 if i > ins: 

3122 self.moveToHelper(event, i, extend) 

3123 #@+node:ekr.20170707072644.1: *4* ec.startOfLine/ExtendSelection 

3124 @cmd('start-of-line') 

3125 def startOfLine(self, event): 

3126 """Move the cursor to first non-blank character of the line.""" 

3127 self.moveWithinLineHelper(event, 'start-line', extend=False) 

3128 

3129 @cmd('start-of-line-extend-selection') 

3130 def startOfLineExtendSelection(self, event): 

3131 """ 

3132 Extend the selection by moving the cursor to first non-blank character 

3133 of the line. 

3134 """ 

3135 self.moveWithinLineHelper(event, 'start-line', extend=True) 

3136 #@+node:ekr.20150514063305.319: *3* ec: paragraph 

3137 #@+node:ekr.20150514063305.320: *4* ec.backwardKillParagraph 

3138 @cmd('backward-kill-paragraph') 

3139 def backwardKillParagraph(self, event): 

3140 """Kill the previous paragraph.""" 

3141 c = self.c 

3142 w = self.editWidget(event) 

3143 if not w: 

3144 return # pragma: no cover (defensive) 

3145 self.beginCommand(w, undoType='backward-kill-paragraph') 

3146 try: 

3147 self.backwardParagraphHelper(event, extend=True) 

3148 i, j = w.getSelectionRange() 

3149 if i > 0: 

3150 i = min(i + 1, j) 

3151 c.killBufferCommands.killParagraphHelper(event, i, j) 

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

3153 finally: 

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

3155 #@+node:ekr.20150514063305.321: *4* ec.fillRegion 

3156 @cmd('fill-region') 

3157 def fillRegion(self, event): 

3158 """Fill all paragraphs in the selected text.""" 

3159 c, p = self.c, self.c.p 

3160 undoType = 'fill-region' 

3161 w = self.editWidget(event) 

3162 i, j = w.getSelectionRange() 

3163 c.undoer.beforeChangeGroup(p, undoType) 

3164 while 1: 

3165 progress = w.getInsertPoint() 

3166 c.reformatParagraph(event, undoType='reformat-paragraph') 

3167 ins = w.getInsertPoint() 

3168 s = w.getAllText() 

3169 w.setInsertPoint(ins) 

3170 if progress >= ins or ins >= j or ins >= len(s): 

3171 break 

3172 c.undoer.afterChangeGroup(p, undoType) 

3173 #@+node:ekr.20150514063305.322: *4* ec.fillRegionAsParagraph 

3174 @cmd('fill-region-as-paragraph') 

3175 def fillRegionAsParagraph(self, event): 

3176 """Fill the selected text.""" 

3177 w = self.editWidget(event) 

3178 if not w or not self._chckSel(event): 

3179 return # pragma: no cover (defensive) 

3180 self.beginCommand(w, undoType='fill-region-as-paragraph') 

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

3182 #@+node:ekr.20150514063305.323: *4* ec.fillParagraph 

3183 @cmd('fill-paragraph') 

3184 def fillParagraph(self, event): 

3185 """Fill the selected paragraph""" 

3186 w = self.editWidget(event) 

3187 if not w: 

3188 return # pragma: no cover (defensive) 

3189 # Clear the selection range. 

3190 i, j = w.getSelectionRange() 

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

3192 self.c.reformatParagraph(event) 

3193 #@+node:ekr.20150514063305.324: *4* ec.killParagraph 

3194 @cmd('kill-paragraph') 

3195 def killParagraph(self, event): 

3196 """Kill the present paragraph.""" 

3197 c = self.c 

3198 w = self.editWidget(event) 

3199 if not w: 

3200 return 

3201 self.beginCommand(w, undoType='kill-paragraph') 

3202 try: 

3203 self.extendToParagraph(event) 

3204 i, j = w.getSelectionRange() 

3205 c.killBufferCommands.killParagraphHelper(event, i, j) 

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

3207 finally: 

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

3209 #@+node:ekr.20150514063305.325: *4* ec.extend-to-paragraph & helper 

3210 @cmd('extend-to-paragraph') 

3211 def extendToParagraph(self, event): 

3212 """Select the paragraph surrounding the cursor.""" 

3213 w = self.editWidget(event) 

3214 if not w: 

3215 return 

3216 s = w.getAllText() 

3217 ins = w.getInsertPoint() 

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

3219 line = s[i:j] 

3220 # Find the start of the paragraph. 

3221 if line.strip(): # Search backward. 

3222 while i > 0: 

3223 i2, j2 = g.getLine(s, i - 1) 

3224 line = s[i2:j2] 

3225 if line.strip(): 

3226 i = i2 

3227 else: 

3228 break # Use the previous line. 

3229 else: # Search forward. 

3230 while j < len(s): 

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

3232 line = s[i:j] 

3233 if line.strip(): 

3234 break 

3235 else: return 

3236 # Select from i to the end of the paragraph. 

3237 self.selectParagraphHelper(w, i) 

3238 #@+node:ekr.20150514063305.326: *5* ec.selectParagraphHelper 

3239 def selectParagraphHelper(self, w, start): 

3240 """Select from start to the end of the paragraph.""" 

3241 s = w.getAllText() 

3242 i1, j = g.getLine(s, start) 

3243 while j < len(s): 

3244 i, j2 = g.getLine(s, j) 

3245 line = s[i:j2] 

3246 if line.strip(): 

3247 j = j2 

3248 else: 

3249 break 

3250 j = max(start, j - 1) 

3251 w.setSelectionRange(i1, j, insert=j) 

3252 #@+node:ekr.20150514063305.327: *3* ec: region 

3253 #@+node:ekr.20150514063305.328: *4* ec.tabIndentRegion (indent-rigidly) 

3254 @cmd('indent-rigidly') 

3255 def tabIndentRegion(self, event): 

3256 """Insert a hard tab at the start of each line of the selected text.""" 

3257 w = self.editWidget(event) 

3258 if not w or not self._chckSel(event): 

3259 return # pragma: no cover (defensive) 

3260 self.beginCommand(w, undoType='indent-rigidly') 

3261 s = w.getAllText() 

3262 i1, j1 = w.getSelectionRange() 

3263 i, junk = g.getLine(s, i1) 

3264 junk, j = g.getLine(s, j1) 

3265 lines = g.splitlines(s[i:j]) 

3266 n = len(lines) 

3267 lines_s = ''.join('\t' + line for line in lines) 

3268 s = s[:i] + lines_s + s[j:] 

3269 w.setAllText(s) 

3270 # Retain original row/col selection. 

3271 w.setSelectionRange(i1, j1 + n, insert=j1 + n) 

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

3273 #@+node:ekr.20150514063305.329: *4* ec.countRegion 

3274 @cmd('count-region') 

3275 def countRegion(self, event): 

3276 """Print the number of lines and characters in the selected text.""" 

3277 k = self.c.k 

3278 w = self.editWidget(event) 

3279 if not w: 

3280 return # pragma: no cover (defensive) 

3281 txt = w.getSelectedText() 

3282 lines = 1 

3283 chars = 0 

3284 for z in txt: 

3285 if z == '\n': 

3286 lines += 1 

3287 else: chars += 1 

3288 k.setLabelGrey( 

3289 f"Region has {lines} lines, " 

3290 f"{chars} character{g.plural(chars)}") 

3291 #@+node:ekr.20150514063305.330: *4* ec.moveLinesDown 

3292 @cmd('move-lines-down') 

3293 def moveLinesDown(self, event): 

3294 """ 

3295 Move all lines containing any selected text down one line, 

3296 moving to the next node if the lines are the last lines of the body. 

3297 """ 

3298 c = self.c 

3299 w = self.editWidget(event) 

3300 if not w: 

3301 return 

3302 s = w.getAllText() 

3303 sel_1, sel_2 = w.getSelectionRange() 

3304 insert_pt = w.getInsertPoint() 

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

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

3307 lines = s[i:j] 

3308 # Select from start of the first line to the *start* of the last line. 

3309 # This prevents selection creep. 

3310 self.beginCommand(w, undoType='move-lines-down') 

3311 try: 

3312 next_i, next_j = g.getLine(s, j) # 2011/04/01: was j+1 

3313 next_line = s[next_i:next_j] 

3314 n2 = next_j - next_i 

3315 if j < len(s): 

3316 w.delete(i, next_j) 

3317 if next_line.endswith('\n'): 

3318 # Simply swap positions with next line 

3319 new_lines = next_line + lines 

3320 else: 

3321 # Last line of the body to be moved up doesn't end in a newline 

3322 # while we have to remove the newline from the line above moving down. 

3323 new_lines = next_line + '\n' + lines[:-1] 

3324 n2 += 1 

3325 w.insert(i, new_lines) 

3326 w.setSelectionRange(sel_1 + n2, sel_2 + n2, insert=insert_pt + n2) 

3327 else: 

3328 # Leo 5.6: insert a blank line before the selected lines. 

3329 w.insert(i, '\n') 

3330 w.setSelectionRange(sel_1 + 1, sel_2 + 1, insert=insert_pt + 1) 

3331 # Fix bug 799695: colorizer bug after move-lines-up into a docstring 

3332 c.recolor() 

3333 finally: 

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

3335 #@+node:ekr.20150514063305.331: *4* ec.moveLinesUp 

3336 @cmd('move-lines-up') 

3337 def moveLinesUp(self, event): 

3338 """ 

3339 Move all lines containing any selected text up one line, 

3340 moving to the previous node as needed. 

3341 """ 

3342 c = self.c 

3343 w = self.editWidget(event) 

3344 if not w: 

3345 return # pragma: no cover (defensive) 

3346 s = w.getAllText() 

3347 sel_1, sel_2 = w.getSelectionRange() 

3348 insert_pt = w.getInsertPoint() # 2011/04/01 

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

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

3351 lines = s[i:j] 

3352 self.beginCommand(w, undoType='move-lines-up') 

3353 try: 

3354 prev_i, prev_j = g.getLine(s, i - 1) 

3355 prev_line = s[prev_i:prev_j] 

3356 n2 = prev_j - prev_i 

3357 if i > 0: 

3358 w.delete(prev_i, j) 

3359 if lines.endswith('\n'): 

3360 # Simply swap positions with next line 

3361 new_lines = lines + prev_line 

3362 else: 

3363 # Lines to be moved up don't end in a newline while the 

3364 # previous line going down needs its newline taken off. 

3365 new_lines = lines + '\n' + prev_line[:-1] 

3366 w.insert(prev_i, new_lines) 

3367 w.setSelectionRange(sel_1 - n2, sel_2 - n2, insert=insert_pt - n2) 

3368 else: 

3369 # Leo 5.6: Insert a blank line after the line. 

3370 w.insert(j, '\n') 

3371 w.setSelectionRange(sel_1, sel_2, insert=sel_1) 

3372 # Fix bug 799695: colorizer bug after move-lines-up into a docstring 

3373 c.recolor() 

3374 finally: 

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

3376 #@+node:ekr.20150514063305.332: *4* ec.reverseRegion 

3377 @cmd('reverse-region') 

3378 def reverseRegion(self, event): 

3379 """Reverse the order of lines in the selected text.""" 

3380 w = self.editWidget(event) 

3381 if not w or not self._chckSel(event): 

3382 return # pragma: no cover (defensive) 

3383 self.beginCommand(w, undoType='reverse-region') 

3384 s = w.getAllText() 

3385 i1, j1 = w.getSelectionRange() 

3386 i, junk = g.getLine(s, i1) 

3387 junk, j = g.getLine(s, j1) 

3388 txt = s[i:j] 

3389 aList = txt.split('\n') 

3390 aList.reverse() 

3391 txt = '\n'.join(aList) + '\n' 

3392 w.setAllText(s[:i1] + txt + s[j1:]) 

3393 ins = i1 + len(txt) - 1 

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

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

3396 #@+node:ekr.20150514063305.333: *4* ec.up/downCaseRegion & helper 

3397 @cmd('downcase-region') 

3398 def downCaseRegion(self, event): 

3399 """Convert all characters in the selected text to lower case.""" 

3400 self.caseHelper(event, 'low', 'downcase-region') 

3401 

3402 @cmd('toggle-case-region') 

3403 def toggleCaseRegion(self, event): 

3404 """Toggle the case of all characters in the selected text.""" 

3405 self.caseHelper(event, 'toggle', 'toggle-case-region') 

3406 

3407 @cmd('upcase-region') 

3408 def upCaseRegion(self, event): 

3409 """Convert all characters in the selected text to UPPER CASE.""" 

3410 self.caseHelper(event, 'up', 'upcase-region') 

3411 

3412 def caseHelper(self, event, way, undoType): 

3413 w = self.editWidget(event) 

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

3415 return # pragma: no cover (defensive) 

3416 self.beginCommand(w, undoType=undoType) 

3417 s = w.getAllText() 

3418 i, j = w.getSelectionRange() 

3419 ins = w.getInsertPoint() 

3420 s2 = s[i:j] 

3421 if way == 'low': 

3422 sel = s2.lower() 

3423 elif way == 'up': 

3424 sel = s2.upper() 

3425 else: 

3426 assert way == 'toggle' 

3427 sel = s2.swapcase() 

3428 s2 = s[:i] + sel + s[j:] 

3429 changed = s2 != s 

3430 if changed: 

3431 w.setAllText(s2) 

3432 w.setSelectionRange(i, j, insert=ins) 

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

3434 #@+node:ekr.20150514063305.334: *3* ec: scrolling 

3435 #@+node:ekr.20150514063305.335: *4* ec.scrollUp/Down & helper 

3436 @cmd('scroll-down-half-page') 

3437 def scrollDownHalfPage(self, event): 

3438 """Scroll the presently selected pane down one line.""" 

3439 self.scrollHelper(event, 'down', 'half-page') 

3440 

3441 @cmd('scroll-down-line') 

3442 def scrollDownLine(self, event): 

3443 """Scroll the presently selected pane down one line.""" 

3444 self.scrollHelper(event, 'down', 'line') 

3445 

3446 @cmd('scroll-down-page') 

3447 def scrollDownPage(self, event): 

3448 """Scroll the presently selected pane down one page.""" 

3449 self.scrollHelper(event, 'down', 'page') 

3450 

3451 @cmd('scroll-up-half-page') 

3452 def scrollUpHalfPage(self, event): 

3453 """Scroll the presently selected pane down one line.""" 

3454 self.scrollHelper(event, 'up', 'half-page') 

3455 

3456 @cmd('scroll-up-line') 

3457 def scrollUpLine(self, event): 

3458 """Scroll the presently selected pane up one page.""" 

3459 self.scrollHelper(event, 'up', 'line') 

3460 

3461 @cmd('scroll-up-page') 

3462 def scrollUpPage(self, event): 

3463 """Scroll the presently selected pane up one page.""" 

3464 self.scrollHelper(event, 'up', 'page') 

3465 #@+node:ekr.20150514063305.336: *5* ec.scrollHelper 

3466 def scrollHelper(self, event, direction, distance): 

3467 """ 

3468 Scroll the present pane up or down one page 

3469 kind is in ('up/down-half-page/line/page) 

3470 """ 

3471 w = event and event.w 

3472 if w and hasattr(w, 'scrollDelegate'): 

3473 kind = direction + '-' + distance 

3474 w.scrollDelegate(kind) 

3475 #@+node:ekr.20150514063305.337: *4* ec.scrollOutlineUp/Down/Line/Page 

3476 @cmd('scroll-outline-down-line') 

3477 def scrollOutlineDownLine(self, event=None): 

3478 """Scroll the outline pane down one line.""" 

3479 tree = self.c.frame.tree 

3480 if hasattr(tree, 'scrollDelegate'): 

3481 tree.scrollDelegate('down-line') 

3482 elif hasattr(tree.canvas, 'leo_treeBar'): 

3483 a, b = tree.canvas.leo_treeBar.get() 

3484 if b < 1.0: 

3485 tree.canvas.yview_scroll(1, "unit") 

3486 

3487 @cmd('scroll-outline-down-page') 

3488 def scrollOutlineDownPage(self, event=None): 

3489 """Scroll the outline pane down one page.""" 

3490 tree = self.c.frame.tree 

3491 if hasattr(tree, 'scrollDelegate'): 

3492 tree.scrollDelegate('down-page') 

3493 elif hasattr(tree.canvas, 'leo_treeBar'): 

3494 a, b = tree.canvas.leo_treeBar.get() 

3495 if b < 1.0: 

3496 tree.canvas.yview_scroll(1, "page") 

3497 

3498 @cmd('scroll-outline-up-line') 

3499 def scrollOutlineUpLine(self, event=None): 

3500 """Scroll the outline pane up one line.""" 

3501 tree = self.c.frame.tree 

3502 if hasattr(tree, 'scrollDelegate'): 

3503 tree.scrollDelegate('up-line') 

3504 elif hasattr(tree.canvas, 'leo_treeBar'): 

3505 a, b = tree.canvas.leo_treeBar.get() 

3506 if a > 0.0: 

3507 tree.canvas.yview_scroll(-1, "unit") 

3508 

3509 @cmd('scroll-outline-up-page') 

3510 def scrollOutlineUpPage(self, event=None): 

3511 """Scroll the outline pane up one page.""" 

3512 tree = self.c.frame.tree 

3513 if hasattr(tree, 'scrollDelegate'): 

3514 tree.scrollDelegate('up-page') 

3515 elif hasattr(tree.canvas, 'leo_treeBar'): 

3516 a, b = tree.canvas.leo_treeBar.get() 

3517 if a > 0.0: 

3518 tree.canvas.yview_scroll(-1, "page") 

3519 #@+node:ekr.20150514063305.338: *4* ec.scrollOutlineLeftRight 

3520 @cmd('scroll-outline-left') 

3521 def scrollOutlineLeft(self, event=None): 

3522 """Scroll the outline left.""" 

3523 tree = self.c.frame.tree 

3524 if hasattr(tree, 'scrollDelegate'): 

3525 tree.scrollDelegate('left') 

3526 elif hasattr(tree.canvas, 'xview_scroll'): 

3527 tree.canvas.xview_scroll(1, "unit") 

3528 

3529 @cmd('scroll-outline-right') 

3530 def scrollOutlineRight(self, event=None): 

3531 """Scroll the outline left.""" 

3532 tree = self.c.frame.tree 

3533 if hasattr(tree, 'scrollDelegate'): 

3534 tree.scrollDelegate('right') 

3535 elif hasattr(tree.canvas, 'xview_scroll'): 

3536 tree.canvas.xview_scroll(-1, "unit") 

3537 #@+node:ekr.20150514063305.339: *3* ec: sort 

3538 #@@language rest 

3539 #@+at 

3540 # XEmacs provides several commands for sorting text in a buffer. All 

3541 # operate on the contents of the region (the text between point and the 

3542 # mark). They divide the text of the region into many "sort records", 

3543 # identify a "sort key" for each record, and then reorder the records 

3544 # using the order determined by the sort keys. The records are ordered so 

3545 # that their keys are in alphabetical order, or, for numerical sorting, in 

3546 # numerical order. In alphabetical sorting, all upper-case letters `A' 

3547 # through `Z' come before lower-case `a', in accordance with the ASCII 

3548 # character sequence. 

3549 # 

3550 # The sort commands differ in how they divide the text into sort 

3551 # records and in which part of each record they use as the sort key. 

3552 # Most of the commands make each line a separate sort record, but some 

3553 # commands use paragraphs or pages as sort records. Most of the sort 

3554 # commands use each entire sort record as its own sort key, but some use 

3555 # only a portion of the record as the sort key. 

3556 # 

3557 # `M-x sort-lines' 

3558 # Divide the region into lines and sort by comparing the entire text 

3559 # of a line. A prefix argument means sort in descending order. 

3560 # 

3561 # `M-x sort-paragraphs' 

3562 # Divide the region into paragraphs and sort by comparing the entire 

3563 # text of a paragraph (except for leading blank lines). A prefix 

3564 # argument means sort in descending order. 

3565 # 

3566 # `M-x sort-pages' 

3567 # Divide the region into pages and sort by comparing the entire text 

3568 # of a page (except for leading blank lines). A prefix argument 

3569 # means sort in descending order. 

3570 # 

3571 # `M-x sort-fields' 

3572 # Divide the region into lines and sort by comparing the contents of 

3573 # one field in each line. Fields are defined as separated by 

3574 # whitespace, so the first run of consecutive non-whitespace 

3575 # characters in a line constitutes field 1, the second such run 

3576 # constitutes field 2, etc. 

3577 # 

3578 # You specify which field to sort by with a numeric argument: 1 to 

3579 # sort by field 1, etc. A negative argument means sort in descending 

3580 # order. Thus, minus 2 means sort by field 2 in reverse-alphabetical 

3581 # order. 

3582 # 

3583 # `M-x sort-numeric-fields' 

3584 # Like `M-x sort-fields', except the specified field is converted to 

3585 # a number for each line and the numbers are compared. `10' comes 

3586 # before `2' when considered as text, but after it when considered 

3587 # as a number. 

3588 # 

3589 # `M-x sort-columns' 

3590 # Like `M-x sort-fields', except that the text within each line used 

3591 # for comparison comes from a fixed range of columns. An explanation 

3592 # is given below. 

3593 # 

3594 # For example, if the buffer contains: 

3595 # 

3596 # On systems where clash detection (locking of files being edited) is 

3597 # implemented, XEmacs also checks the first time you modify a buffer 

3598 # whether the file has changed on disk since it was last visited or 

3599 # saved. If it has, you are asked to confirm that you want to change 

3600 # the buffer. 

3601 # 

3602 # then if you apply `M-x sort-lines' to the entire buffer you get: 

3603 # 

3604 # On systems where clash detection (locking of files being edited) is 

3605 # implemented, XEmacs also checks the first time you modify a buffer 

3606 # saved. If it has, you are asked to confirm that you want to change 

3607 # the buffer. 

3608 # whether the file has changed on disk since it was last visited or 

3609 # 

3610 # where the upper case `O' comes before all lower case letters. If you 

3611 # apply instead `C-u 2 M-x sort-fields' you get: 

3612 # 

3613 # saved. If it has, you are asked to confirm that you want to change 

3614 # implemented, XEmacs also checks the first time you modify a buffer 

3615 # the buffer. 

3616 # On systems where clash detection (locking of files being edited) is 

3617 # whether the file has changed on disk since it was last visited or 

3618 # 

3619 # where the sort keys were `If', `XEmacs', `buffer', `systems', and `the'. 

3620 # 

3621 # `M-x sort-columns' requires more explanation. You specify the 

3622 # columns by putting point at one of the columns and the mark at the other 

3623 # column. Because this means you cannot put point or the mark at the 

3624 # beginning of the first line to sort, this command uses an unusual 

3625 # definition of `region': all of the line point is in is considered part 

3626 # of the region, and so is all of the line the mark is in. 

3627 # 

3628 # For example, to sort a table by information found in columns 10 to 

3629 # 15, you could put the mark on column 10 in the first line of the table, 

3630 # and point on column 15 in the last line of the table, and then use this 

3631 # command. Or you could put the mark on column 15 in the first line and 

3632 # point on column 10 in the last line. 

3633 # 

3634 # This can be thought of as sorting the rectangle specified by point 

3635 # and the mark, except that the text on each line to the left or right of 

3636 # the rectangle moves along with the text inside the rectangle. *Note 

3637 # Rectangles::. 

3638 #@@language python 

3639 #@+node:ekr.20150514063305.340: *4* ec.sortLines commands 

3640 @cmd('reverse-sort-lines-ignoring-case') 

3641 def reverseSortLinesIgnoringCase(self, event): 

3642 """Sort the selected lines in reverse order, ignoring case.""" 

3643 return self.sortLines(event, ignoreCase=True, reverse=True) 

3644 

3645 @cmd('reverse-sort-lines') 

3646 def reverseSortLines(self, event): 

3647 """Sort the selected lines in reverse order.""" 

3648 return self.sortLines(event, reverse=True) 

3649 

3650 @cmd('sort-lines-ignoring-case') 

3651 def sortLinesIgnoringCase(self, event): 

3652 """Sort the selected lines, ignoring case.""" 

3653 return self.sortLines(event, ignoreCase=True) 

3654 

3655 @cmd('sort-lines') 

3656 def sortLines(self, event, ignoreCase=False, reverse=False): 

3657 """Sort the selected lines.""" 

3658 w = self.editWidget(event) 

3659 if not self._chckSel(event): 

3660 return 

3661 undoType = 'reverse-sort-lines' if reverse else 'sort-lines' 

3662 self.beginCommand(w, undoType=undoType) 

3663 try: 

3664 s = w.getAllText() 

3665 sel1, sel2 = w.getSelectionRange() 

3666 ins = w.getInsertPoint() 

3667 i, junk = g.getLine(s, sel1) 

3668 junk, j = g.getLine(s, sel2) 

3669 s2 = s[i:j] 

3670 if not s2.endswith('\n'): 

3671 s2 = s2 + '\n' 

3672 aList = g.splitLines(s2) 

3673 

3674 def lower(s): 

3675 return s.lower() if ignoreCase else s 

3676 

3677 aList.sort(key=lower) 

3678 # key is a function that extracts args. 

3679 if reverse: 

3680 aList.reverse() 

3681 s = ''.join(aList) 

3682 w.delete(i, j) 

3683 w.insert(i, s) 

3684 w.setSelectionRange(sel1, sel2, insert=ins) 

3685 finally: 

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

3687 #@+node:ekr.20150514063305.341: *4* ec.sortColumns 

3688 @cmd('sort-columns') 

3689 def sortColumns(self, event): 

3690 """ 

3691 Sort lines of selected text using only lines in the given columns to do 

3692 the comparison. 

3693 """ 

3694 w = self.editWidget(event) 

3695 if not self._chckSel(event): 

3696 return # pragma: no cover (defensive) 

3697 self.beginCommand(w, undoType='sort-columns') 

3698 try: 

3699 s = w.getAllText() 

3700 sel_1, sel_2 = w.getSelectionRange() 

3701 sint1, sint2 = g.convertPythonIndexToRowCol(s, sel_1) 

3702 sint3, sint4 = g.convertPythonIndexToRowCol(s, sel_2) 

3703 sint1 += 1 

3704 sint3 += 1 

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

3706 junk, j = g.getLine(s, sel_2) 

3707 txt = s[i:j] 

3708 columns = [w.get(f"{z}.{sint2}", f"{z}.{sint4}") 

3709 for z in range(sint1, sint3 + 1)] 

3710 aList = g.splitLines(txt) 

3711 zlist = list(zip(columns, aList)) 

3712 zlist.sort() 

3713 s = ''.join([z[1] for z in zlist]) 

3714 w.delete(i, j) 

3715 w.insert(i, s) 

3716 w.setSelectionRange(sel_1, sel_1 + len(s), insert=sel_1 + len(s)) 

3717 finally: 

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

3719 #@+node:ekr.20150514063305.342: *4* ec.sortFields 

3720 @cmd('sort-fields') 

3721 def sortFields(self, event, which=None): 

3722 """ 

3723 Divide the selected text into lines and sort by comparing the contents 

3724 of one field in each line. Fields are defined as separated by 

3725 whitespace, so the first run of consecutive non-whitespace characters 

3726 in a line constitutes field 1, the second such run constitutes field 2, 

3727 etc. 

3728 

3729 You specify which field to sort by with a numeric argument: 1 to sort 

3730 by field 1, etc. A negative argument means sort in descending order. 

3731 Thus, minus 2 means sort by field 2 in reverse-alphabetical order. 

3732 """ 

3733 w = self.editWidget(event) 

3734 if not w or not self._chckSel(event): 

3735 return 

3736 self.beginCommand(w, undoType='sort-fields') 

3737 s = w.getAllText() 

3738 ins = w.getInsertPoint() 

3739 r1, r2, r3, r4 = self.getRectanglePoints(w) 

3740 i, junk = g.getLine(s, r1) 

3741 junk, j = g.getLine(s, r4) 

3742 txt = s[i:j] # bug reported by pychecker. 

3743 txt = txt.split('\n') 

3744 fields = [] 

3745 fn = r'\w+' 

3746 frx = re.compile(fn) 

3747 for line in txt: 

3748 f = frx.findall(line) 

3749 if not which: 

3750 fields.append(f[0]) 

3751 else: 

3752 i = int(which) 

3753 if len(f) < i: 

3754 return 

3755 i = i - 1 

3756 fields.append(f[i]) 

3757 nz = sorted(zip(fields, txt)) 

3758 w.delete(i, j) 

3759 int1 = i 

3760 for z in nz: 

3761 w.insert(f"{int1}.0", f"{z[1]}\n") 

3762 int1 = int1 + 1 

3763 w.setInsertPoint(ins) 

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

3765 #@+node:ekr.20150514063305.343: *3* ec: swap/transpose 

3766 #@+node:ekr.20150514063305.344: *4* ec.transposeLines 

3767 @cmd('transpose-lines') 

3768 def transposeLines(self, event): 

3769 """Transpose the line containing the cursor with the preceding line.""" 

3770 w = self.editWidget(event) 

3771 if not w: 

3772 return # pragma: no cover (defensive) 

3773 ins = w.getInsertPoint() 

3774 s = w.getAllText() 

3775 if not s.strip(): 

3776 return # pragma: no cover (defensive) 

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

3778 line1 = s[i:j] 

3779 self.beginCommand(w, undoType='transpose-lines') 

3780 if i == 0: # Transpose the next line. 

3781 i2, j2 = g.getLine(s, j + 1) 

3782 line2 = s[i2:j2] 

3783 w.delete(0, j2) 

3784 w.insert(0, line2 + line1) 

3785 w.setInsertPoint(j2 - 1) 

3786 else: # Transpose the previous line. 

3787 i2, j2 = g.getLine(s, i - 1) 

3788 line2 = s[i2:j2] 

3789 w.delete(i2, j) 

3790 w.insert(i2, line1 + line2) 

3791 w.setInsertPoint(j - 1) 

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

3793 #@+node:ekr.20150514063305.345: *4* ec.transposeWords 

3794 @cmd('transpose-words') 

3795 def transposeWords(self, event): 

3796 """ 

3797 Transpose the word before the cursor with the word after the cursor 

3798 Punctuation between words does not move. For example, ‘FOO, BAR’ 

3799 transposes into ‘BAR, FOO’. 

3800 """ 

3801 w = self.editWidget(event) 

3802 if not w: 

3803 return 

3804 self.beginCommand(w, undoType='transpose-words') 

3805 s = w.getAllText() 

3806 i1, j1 = self.extendToWord(event, select=False) 

3807 s1 = s[i1:j1] 

3808 if i1 > j1: 

3809 i1, j1 = j1, i1 

3810 # Search for the next word. 

3811 k = j1 + 1 

3812 while k < len(s) and s[k] != '\n' and not g.isWordChar1(s[k]): 

3813 k += 1 

3814 changed = k < len(s) 

3815 if changed: 

3816 ws = s[j1:k] 

3817 w.setInsertPoint(k + 1) 

3818 i2, j2 = self.extendToWord(event, select=False) 

3819 s2 = s[i2:j2] 

3820 s3 = s[:i1] + s2 + ws + s1 + s[j2:] 

3821 w.setAllText(s3) 

3822 w.setSelectionRange(j1, j1, insert=j1) 

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

3824 #@+node:ekr.20150514063305.346: *4* ec.swapCharacters & transeposeCharacters 

3825 @cmd('transpose-chars') 

3826 def transposeCharacters(self, event): 

3827 """Swap the characters at the cursor.""" 

3828 w = self.editWidget(event) 

3829 if not w: 

3830 return # pragma: no cover (defensive) 

3831 self.beginCommand(w, undoType='swap-characters') 

3832 s = w.getAllText() 

3833 i = w.getInsertPoint() 

3834 if 0 < i < len(s): 

3835 w.setAllText(s[: i - 1] + s[i] + s[i - 1] + s[i + 1 :]) 

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

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

3838 

3839 swapCharacters = transposeCharacters 

3840 #@+node:ekr.20150514063305.348: *3* ec: uA's 

3841 #@+node:ekr.20150514063305.349: *4* ec.clearNodeUas & clearAllUas 

3842 @cmd('clear-node-uas') 

3843 def clearNodeUas(self, event=None): 

3844 """Clear the uA's in the selected VNode.""" 

3845 c = self.c 

3846 p = c and c.p 

3847 if p and p.v.u: 

3848 p.v.u = {} 

3849 # #1276. 

3850 p.setDirty() 

3851 c.setChanged() 

3852 c.redraw() 

3853 

3854 @cmd('clear-all-uas') 

3855 def clearAllUas(self, event=None): 

3856 """Clear all uAs in the entire outline.""" 

3857 c = self.c 

3858 # #1276. 

3859 changed = False 

3860 for p in self.c.all_unique_positions(): 

3861 if p.v.u: 

3862 p.v.u = {} 

3863 p.setDirty() 

3864 changed = True 

3865 if changed: 

3866 c.setChanged() 

3867 c.redraw() 

3868 #@+node:ekr.20150514063305.350: *4* ec.showUas & showAllUas 

3869 @cmd('show-all-uas') 

3870 def showAllUas(self, event=None): 

3871 """Print all uA's in the outline.""" 

3872 g.es_print('Dump of uAs...') 

3873 for v in self.c.all_unique_nodes(): 

3874 if v.u: 

3875 self.showNodeUas(v=v) 

3876 

3877 @cmd('show-node-uas') 

3878 def showNodeUas(self, event=None, v=None): 

3879 """Print the uA's in the selected node.""" 

3880 c = self.c 

3881 if v: 

3882 d, h = v.u, v.h 

3883 else: 

3884 d, h = c.p.v.u, c.p.h 

3885 g.es_print(h) 

3886 g.es_print(g.objToString(d)) 

3887 #@+node:ekr.20150514063305.351: *4* ec.setUa 

3888 @cmd('set-ua') 

3889 def setUa(self, event): 

3890 """Prompt for the name and value of a uA, then set the uA in the present node.""" 

3891 k = self.c.k 

3892 self.w = self.editWidget(event) 

3893 if self.w: 

3894 k.setLabelBlue('Set uA: ') 

3895 k.get1Arg(event, handler=self.setUa1) 

3896 

3897 def setUa1(self, event): 

3898 k = self.c.k 

3899 self.uaName = k.arg 

3900 s = f"Set uA: {self.uaName} To: " 

3901 k.setLabelBlue(s) 

3902 k.getNextArg(self.setUa2) 

3903 

3904 def setUa2(self, event): 

3905 c, k = self.c, self.c.k 

3906 val = k.arg 

3907 d = c.p.v.u 

3908 d[self.uaName] = val 

3909 self.showNodeUas() 

3910 k.clearState() 

3911 k.resetLabel() 

3912 k.showStateAndMode() 

3913 #@-others 

3914#@-others 

3915#@-leo