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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20150514035813.1: * @file ../commands/editCommands.py
4#@@first
5"""Leo's general editing commands."""
6#@+<< imports >>
7#@+node:ekr.20150514050149.1: ** << imports >> (editCommands.py)
8import os
9import re
10from typing import Any, List
11from leo.core import leoGlobals as g
12from leo.commands.baseCommands import BaseEditCommandsClass
13#@-<< imports >>
15def cmd(name):
16 """Command decorator for the EditCommandsClass class."""
17 return g.new_cmd_decorator(name, ['c', 'editCommands',])
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.*:')
25skip_pat = re.compile(r'=.*in g.app.debug')
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.
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.
82 **Warning**: Use this command at your own risk.
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.
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.
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.
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.
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")
240#@+node:ekr.20180210161001.1: *3* @g.command('unmark-first-parents')
241@g.command('unmark-first-parents')
242def unmark_first_parents(event=None):
243 """Mark the node and all its parents."""
244 c = event.get('c')
245 changed: List[Any] = []
246 if not c:
247 return changed
248 for parent in c.p.self_and_parents():
249 if parent.isMarked():
250 parent.clearMarked()
251 parent.setDirty()
252 changed.append(parent.copy())
253 if changed:
254 # g.es("unmarked: " + ', '.join([z.h for z in changed]))
255 c.setChanged()
256 c.redraw()
257 return changed
258#@+node:ekr.20160514100029.1: ** class EditCommandsClass
259class EditCommandsClass(BaseEditCommandsClass):
260 """Editing commands with little or no state."""
261 # pylint: disable=eval-used
262 #@+others
263 #@+node:ekr.20150514063305.116: *3* ec.__init__
264 def __init__(self, c):
265 """Ctor for EditCommandsClass class."""
266 # pylint: disable=super-init-not-called
267 self.c = c
268 self.ccolumn = 0 # For comment column functions.
269 self.cursorStack = []
270 # Values are tuples, (i, j, ins)
271 self.extendMode = False # True: all cursor move commands extend the selection.
272 self.fillPrefix = '' # For fill prefix functions.
273 self.fillColumn = 0 # For line centering.
274 # Set by the set-fill-column command.
275 # If zero, @pagewidth value is used.
276 self.moveSpotNode = None # A VNode.
277 self.moveSpot = None # For retaining preferred column when moving up or down.
278 self.moveCol = None # For retaining preferred column when moving up or down.
279 self.sampleWidget = None # Created later.
280 self.swapSpots = []
281 self._useRegex = False # For replace-string
282 self.w = None # For use by state handlers.
283 # Settings...
284 cf = c.config
285 self.autocompleteBrackets = cf.getBool('autocomplete-brackets')
286 if cf.getBool('auto-justify-on-at-start'):
287 self.autojustify = abs(cf.getInt('auto-justify') or 0)
288 else:
289 self.autojustify = 0
290 self.bracketsFlashBg = cf.getColor('flash-brackets-background-color')
291 self.bracketsFlashCount = cf.getInt('flash-brackets-count')
292 self.bracketsFlashDelay = cf.getInt('flash-brackets-delay')
293 self.bracketsFlashFg = cf.getColor('flash-brackets-foreground-color')
294 self.flashMatchingBrackets = cf.getBool('flash-matching-brackets')
295 self.smartAutoIndent = cf.getBool('smart-auto-indent')
296 self.openBracketsList = cf.getString('open-flash-brackets') or '([{'
297 self.closeBracketsList = cf.getString('close-flash-brackets') or ')]}'
298 self.initBracketMatcher(c)
299 #@+node:ekr.20150514063305.190: *3* ec.cache
300 @cmd('clear-all-caches')
301 @cmd('clear-cache')
302 def clearAllCaches(self, event=None): # pragma: no cover
303 """Clear all of Leo's file caches."""
304 g.app.global_cacher.clear()
305 g.app.commander_cacher.clear()
307 @cmd('dump-caches')
308 def dumpCaches(self, event=None): # pragma: no cover
309 """Dump, all of Leo's file caches."""
310 g.app.global_cacher.dump()
311 g.app.commander_cacher.dump()
312 #@+node:ekr.20150514063305.118: *3* ec.doNothing
313 @cmd('do-nothing')
314 def doNothing(self, event):
315 """A placeholder command, useful for testing bindings."""
316 pass
317 #@+node:ekr.20150514063305.278: *3* ec.insertFileName
318 @cmd('insert-file-name')
319 def insertFileName(self, event=None):
320 """
321 Prompt for a file name, then insert it at the cursor position.
322 This operation is undoable if done in the body pane.
324 The initial path is made by concatenating path_for_p() and the selected
325 text, if there is any, or any path like text immediately preceding the
326 cursor.
327 """
328 c, u, w = self.c, self.c.undoer, self.editWidget(event)
329 if not w:
330 return
332 def callback(arg, w=w):
333 i = w.getSelectionRange()[0]
334 p = c.p
335 w.deleteTextSelection()
336 w.insert(i, arg)
337 newText = w.getAllText()
338 if g.app.gui.widget_name(w) == 'body' and p.b != newText:
339 bunch = u.beforeChangeBody(p)
340 p.v.b = newText # p.b would cause a redraw.
341 u.afterChangeBody(p, 'insert-file-name', bunch)
343 # see if the widget already contains the start of a path
345 start_text = w.getSelectedText()
346 if not start_text: # look at text preceeding insert point
347 start_text = w.getAllText()[: w.getInsertPoint()]
348 if start_text:
349 # make non-path characters whitespace
350 start_text = ''.join(i if i not in '\'"`()[]{}<>!|*,@#$&' else ' '
351 for i in start_text)
352 if start_text[-1].isspace(): # use node path if nothing typed
353 start_text = ''
354 else:
355 start_text = start_text.rsplit(None, 1)[-1]
356 # set selection range so w.deleteTextSelection() works in the callback
357 w.setSelectionRange(
358 w.getInsertPoint() - len(start_text), w.getInsertPoint())
360 c.k.functionTail = g.os_path_finalize_join(
361 self.path_for_p(c, c.p), start_text or '')
362 c.k.getFileName(event, callback=callback)
363 #@+node:ekr.20150514063305.279: *3* ec.insertHeadlineTime
364 @cmd('insert-headline-time')
365 def insertHeadlineTime(self, event=None):
366 """Insert a date/time stamp in the headline of the selected node."""
367 frame = self
368 c, p = frame.c, self.c.p
369 if g.app.batchMode:
370 c.notValidInBatchMode("Insert Headline Time")
371 return
372 w = c.frame.tree.edit_widget(p)
373 # 2015/06/09: Fix bug 131: Insert time in headline now inserts time in body
374 # Get the wrapper from the tree itself.
375 # Do *not* set w = self.editWidget!
376 if w:
377 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1185933
378 # insert-headline-time should insert at cursor.
379 # Note: The command must be bound to a key for this to work.
380 ins = w.getInsertPoint()
381 s = c.getTime(body=False)
382 w.insert(ins, s)
383 else:
384 c.endEditing()
385 time = c.getTime(body=False)
386 s = p.h.rstrip()
387 if s:
388 p.h = ' '.join([s, time])
389 else:
390 p.h = time
391 c.redrawAndEdit(p, selectAll=True)
392 #@+node:tom.20210922140250.1: *3* ec.capitalizeHeadline
393 @cmd('capitalize-headline')
394 def capitalizeHeadline(self, event=None):
395 """Capitalize all words in the headline of the selected node."""
396 frame = self
397 c, p, u = frame.c, self.c.p, self.c.undoer
399 if g.app.batchMode:
400 c.notValidInBatchMode("Capitalize Headline")
401 return
403 h = p.h
404 undoType = 'capitalize-headline'
405 undoData = u.beforeChangeNodeContents(p)
407 words = [w.capitalize() for w in h.split()]
408 capitalized = ' '.join(words)
409 changed = capitalized != h
410 if changed:
411 p.h = capitalized
412 c.setChanged()
413 p.setDirty()
414 u.afterChangeNodeContents(p, undoType, undoData)
415 c.redraw()
417 #@+node:tbrown.20151118134307.1: *3* ec.path_for_p
418 def path_for_p(self, c, p):
419 """path_for_p - return the filesystem path (directory) containing
420 node `p`.
422 FIXME: this general purpose code should be somewhere else, and there
423 may already be functions that do some of the work, although perhaps
424 without handling so many corner cases (@auto-my-custom-type etc.)
426 :param outline c: outline containing p
427 :param position p: position to locate
428 :return: path
429 :rtype: str
430 """
432 def atfile(p):
433 """return True if p is an @<file> node *of any kind*"""
434 word0 = p.h.split()[0]
435 return (
436 word0 in g.app.atFileNames | set(['@auto']) or
437 word0.startswith('@auto-')
438 )
440 aList = g.get_directives_dict_list(p)
441 path = c.scanAtPathDirectives(aList)
442 while c.positionExists(p):
443 if atfile(p): # see if it's a @<file> node of some sort
444 nodepath = p.h.split(None, 1)[-1]
445 nodepath = g.os_path_join(path, nodepath)
446 if not g.os_path_isdir(nodepath): # remove filename
447 nodepath = g.os_path_dirname(nodepath)
448 if g.os_path_isdir(nodepath): # append if it's a directory
449 path = nodepath
450 break
451 p.moveToParent()
453 return path
454 #@+node:ekr.20150514063305.347: *3* ec.tabify & untabify
455 @cmd('tabify')
456 def tabify(self, event):
457 """Convert 4 spaces to tabs in the selected text."""
458 self.tabifyHelper(event, which='tabify')
460 @cmd('untabify')
461 def untabify(self, event):
462 """Convert tabs to 4 spaces in the selected text."""
463 self.tabifyHelper(event, which='untabify')
465 def tabifyHelper(self, event, which):
466 w = self.editWidget(event)
467 if not w or not w.hasSelection():
468 return
469 self.beginCommand(w, undoType=which)
470 i, end = w.getSelectionRange()
471 txt = w.getSelectedText()
472 if which == 'tabify':
473 pattern = re.compile(r' {4,4}') # Huh?
474 ntxt = pattern.sub('\t', txt)
475 else:
476 pattern = re.compile(r'\t')
477 ntxt = pattern.sub(' ', txt)
478 w.delete(i, end)
479 w.insert(i, ntxt)
480 n = i + len(ntxt)
481 w.setSelectionRange(n, n, insert=n)
482 self.endCommand(changed=True, setLabel=True)
483 #@+node:ekr.20150514063305.191: *3* ec: capitalization & case
484 #@+node:ekr.20150514063305.192: *4* ec.capitalizeWord & up/downCaseWord
485 @cmd('capitalize-word')
486 def capitalizeWord(self, event):
487 """Capitalize the word at the cursor."""
488 self.capitalizeHelper(event, 'cap', 'capitalize-word')
490 @cmd('downcase-word')
491 def downCaseWord(self, event):
492 """Convert all characters of the word at the cursor to lower case."""
493 self.capitalizeHelper(event, 'low', 'downcase-word')
495 @cmd('upcase-word')
496 def upCaseWord(self, event):
497 """Convert all characters of the word at the cursor to UPPER CASE."""
498 self.capitalizeHelper(event, 'up', 'upcase-word')
499 #@+node:ekr.20150514063305.194: *4* ec.capitalizeHelper
500 def capitalizeHelper(self, event, which, undoType):
501 w = self.editWidget(event)
502 if not w:
503 return # pragma: no cover (defensive)
504 s = w.getAllText()
505 ins = w.getInsertPoint()
506 i, j = g.getWord(s, ins)
507 word = s[i:j]
508 if not word.strip():
509 return # pragma: no cover (defensive)
510 self.beginCommand(w, undoType=undoType)
511 if which == 'cap':
512 word2 = word.capitalize()
513 elif which == 'low':
514 word2 = word.lower()
515 elif which == 'up':
516 word2 = word.upper()
517 else:
518 g.trace(f"can not happen: which = {s(which)}")
519 changed = word != word2
520 if changed:
521 w.delete(i, j)
522 w.insert(i, word2)
523 w.setSelectionRange(ins, ins, insert=ins)
524 self.endCommand(changed=changed, setLabel=True)
525 #@+node:tom.20210922171731.1: *4* ec.capitalizeWords & selection
526 @cmd('capitalize-words-or-selection')
527 def capitalizeWords(self, event=None):
528 """Capitalize Entire Body Or Selection."""
529 frame = self
530 c, p, u = frame.c, self.c.p, self.c.undoer
531 w = frame.editWidget(event)
532 s = w.getAllText()
533 if not s:
534 return
536 undoType = 'capitalize-body-words'
537 undoData = u.beforeChangeNodeContents(p)
539 i, j = w.getSelectionRange()
540 if i == j:
541 sel = ''
542 else:
543 sel = s[i:j]
544 text = sel or s
545 if sel:
546 prefix = s[:i]
547 suffix = s[j:]
549 # Thanks to
550 # https://thispointer.com/python-capitalize-the-first-letter-of-each-word-in-a-string/
551 def convert_to_uppercase(m):
552 """Convert the second group to uppercase and join both group 1 & group 2"""
553 return m.group(1) + m.group(2).upper()
555 capitalized = re.sub(r"(^|\s)(\S)", convert_to_uppercase, text)
557 if capitalized != text:
558 p.b = prefix + capitalized + suffix if sel else capitalized
559 c.setChanged()
560 p.setDirty()
561 u.afterChangeNodeContents(p, undoType, undoData)
562 c.redraw()
563 #@+node:ekr.20150514063305.195: *3* ec: clicks and focus
564 #@+node:ekr.20150514063305.196: *4* ec.activate-x-menu & activateMenu
565 @cmd('activate-cmds-menu')
566 def activateCmdsMenu(self, event=None): # pragma: no cover
567 """Activate Leo's Cmnds menu."""
568 self.activateMenu('Cmds')
570 @cmd('activate-edit-menu')
571 def activateEditMenu(self, event=None): # pragma: no cover
572 """Activate Leo's Edit menu."""
573 self.activateMenu('Edit')
575 @cmd('activate-file-menu')
576 def activateFileMenu(self, event=None): # pragma: no cover
577 """Activate Leo's File menu."""
578 self.activateMenu('File')
580 @cmd('activate-help-menu')
581 def activateHelpMenu(self, event=None): # pragma: no cover
582 """Activate Leo's Help menu."""
583 self.activateMenu('Help')
585 @cmd('activate-outline-menu')
586 def activateOutlineMenu(self, event=None): # pragma: no cover
587 """Activate Leo's Outline menu."""
588 self.activateMenu('Outline')
590 @cmd('activate-plugins-menu')
591 def activatePluginsMenu(self, event=None): # pragma: no cover
592 """Activate Leo's Plugins menu."""
593 self.activateMenu('Plugins')
595 @cmd('activate-window-menu')
596 def activateWindowMenu(self, event=None): # pragma: no cover
597 """Activate Leo's Window menu."""
598 self.activateMenu('Window')
600 def activateMenu(self, menuName): # pragma: no cover
601 c = self.c
602 c.frame.menu.activateMenu(menuName)
603 #@+node:ekr.20150514063305.199: *4* ec.focusTo...
604 @cmd('focus-to-body')
605 def focusToBody(self, event=None): # pragma: no cover
606 """Put the keyboard focus in Leo's body pane."""
607 c, k = self.c, self.c.k
608 c.bodyWantsFocus()
609 if k:
610 k.setDefaultInputState()
611 k.showStateAndMode()
613 @cmd('focus-to-log')
614 def focusToLog(self, event=None): # pragma: no cover
615 """Put the keyboard focus in Leo's log pane."""
616 self.c.logWantsFocus()
618 @cmd('focus-to-minibuffer')
619 def focusToMinibuffer(self, event=None): # pragma: no cover
620 """Put the keyboard focus in Leo's minibuffer."""
621 self.c.minibufferWantsFocus()
623 @cmd('focus-to-tree')
624 def focusToTree(self, event=None): # pragma: no cover
625 """Put the keyboard focus in Leo's outline pane."""
626 self.c.treeWantsFocus()
627 #@+node:ekr.20150514063305.201: *4* ec.clicks in the icon box
628 # These call the actual event handlers so as to trigger hooks.
630 @cmd('ctrl-click-icon')
631 def ctrlClickIconBox(self, event=None): # pragma: no cover
632 """Simulate a ctrl-click in the icon box of the presently selected node."""
633 c = self.c
634 c.frame.tree.OnIconCtrlClick(c.p)
635 # Calls the base LeoTree method.
637 @cmd('click-icon-box')
638 def clickIconBox(self, event=None): # pragma: no cover
639 """Simulate a click in the icon box of the presently selected node."""
640 c = self.c
641 c.frame.tree.onIconBoxClick(event, p=c.p)
643 @cmd('double-click-icon-box')
644 def doubleClickIconBox(self, event=None): # pragma: no cover
645 """Simulate a double-click in the icon box of the presently selected node."""
646 c = self.c
647 c.frame.tree.onIconBoxDoubleClick(event, p=c.p)
649 @cmd('right-click-icon')
650 def rightClickIconBox(self, event=None): # pragma: no cover
651 """Simulate a right click in the icon box of the presently selected node."""
652 c = self.c
653 c.frame.tree.onIconBoxRightClick(event, p=c.p)
654 #@+node:ekr.20150514063305.202: *4* ec.clickClickBox
655 @cmd('click-click-box')
656 def clickClickBox(self, event=None): # pragma: no cover
657 """
658 Simulate a click in the click box (+- box) of the presently selected node.
660 Call the actual event handlers so as to trigger hooks.
661 """
662 c = self.c
663 c.frame.tree.onClickBoxClick(event, p=c.p)
664 #@+node:ekr.20150514063305.207: *3* ec: comment column
665 #@+node:ekr.20150514063305.208: *4* ec.setCommentColumn
666 @cmd('set-comment-column')
667 def setCommentColumn(self, event):
668 """Set the comment column for the indent-to-comment-column command."""
669 w = self.editWidget(event)
670 if not w:
671 return # pragma: no cover (defensive)
672 s = w.getAllText()
673 ins = w.getInsertPoint()
674 row, col = g.convertPythonIndexToRowCol(s, ins)
675 self.ccolumn = col
676 #@+node:ekr.20150514063305.209: *4* ec.indentToCommentColumn
677 @cmd('indent-to-comment-column')
678 def indentToCommentColumn(self, event):
679 """
680 Insert whitespace to indent the line containing the insert point to the
681 comment column.
682 """
683 w = self.editWidget(event)
684 if not w:
685 return # pragma: no cover (defensive)
686 self.beginCommand(w, undoType='indent-to-comment-column')
687 s = w.getAllText()
688 ins = w.getInsertPoint()
689 i, j = g.getLine(s, ins)
690 line = s[i:j]
691 c1 = self.ccolumn # 2021/07/28: already an int.
692 line2 = ' ' * c1 + line.lstrip()
693 if line2 != line:
694 w.delete(i, j)
695 w.insert(i, line2)
696 w.setInsertPoint(i + c1)
697 self.endCommand(changed=True, setLabel=True)
698 #@+node:ekr.20150514063305.214: *3* ec: fill column and centering
699 #@@language rest
700 #@+at
701 # These methods are currently just used in tandem to center the line or
702 # region within the fill column. for example, dependent upon the fill column, this text:
703 #
704 # cats
705 # raaaaaaaaaaaats
706 # mats
707 # zaaaaaaaaap
708 #
709 # may look like:
710 #
711 # cats
712 # raaaaaaaaaaaats
713 # mats
714 # zaaaaaaaaap
715 #
716 # after an center-region command via Alt-x.
717 #@@language python
718 #@+node:ekr.20150514063305.215: *4* ec.centerLine
719 @cmd('center-line')
720 def centerLine(self, event):
721 """Centers line within current fill column"""
722 c, k, w = self.c, self.c.k, self.editWidget(event)
723 if not w:
724 return # pragma: no cover (defensive)
725 if self.fillColumn > 0:
726 fillColumn = self.fillColumn
727 else:
728 d = c.scanAllDirectives(c.p)
729 fillColumn = d.get("pagewidth")
730 s = w.getAllText()
731 i, j = g.getLine(s, w.getInsertPoint())
732 line = s[i:j].strip()
733 if not line or len(line) >= fillColumn:
734 return
735 self.beginCommand(w, undoType='center-line')
736 n = (fillColumn - len(line)) / 2
737 ws = ' ' * int(n) # mypy.
738 k = g.skip_ws(s, i)
739 if k > i:
740 w.delete(i, k - i)
741 w.insert(i, ws)
742 self.endCommand(changed=True, setLabel=True)
743 #@+node:ekr.20150514063305.216: *4* ec.setFillColumn
744 @cmd('set-fill-column')
745 def setFillColumn(self, event):
746 """Set the fill column used by the center-line and center-region commands."""
747 k = self.c.k
748 self.w = self.editWidget(event)
749 if not self.w:
750 return # pragma: no cover (defensive)
751 k.setLabelBlue('Set Fill Column: ')
752 k.get1Arg(event, handler=self.setFillColumn1)
754 def setFillColumn1(self, event):
755 c, k, w = self.c, self.c.k, self.w
756 k.clearState()
757 try:
758 # Bug fix: 2011/05/23: set the fillColumn ivar!
759 self.fillColumn = n = int(k.arg)
760 k.setLabelGrey(f"fill column is: {n:d}")
761 except ValueError:
762 k.resetLabel() # pragma: no cover (defensive)
763 c.widgetWantsFocus(w)
764 #@+node:ekr.20150514063305.217: *4* ec.centerRegion
765 @cmd('center-region')
766 def centerRegion(self, event):
767 """Centers the selected text within the fill column"""
768 c, k, w = self.c, self.c.k, self.editWidget(event)
769 if not w:
770 return # pragma: no cover (defensive)
771 s = w.getAllText()
772 sel_1, sel_2 = w.getSelectionRange()
773 ind, junk = g.getLine(s, sel_1)
774 junk, end = g.getLine(s, sel_2)
775 if self.fillColumn > 0:
776 fillColumn = self.fillColumn
777 else:
778 d = c.scanAllDirectives(c.p)
779 fillColumn = d.get("pagewidth")
780 self.beginCommand(w, undoType='center-region')
781 inserted = 0
782 while ind < end:
783 s = w.getAllText()
784 i, j = g.getLine(s, ind)
785 line = s[i:j].strip()
786 if len(line) >= fillColumn:
787 ind = j
788 else:
789 n = int((fillColumn - len(line)) / 2)
790 inserted += n
791 k = g.skip_ws(s, i)
792 if k > i:
793 w.delete(i, k - i)
794 w.insert(i, ' ' * n)
795 ind = j + n - (k - i)
796 w.setSelectionRange(sel_1, sel_2 + inserted)
797 self.endCommand(changed=True, setLabel=True)
798 #@+node:ekr.20150514063305.218: *4* ec.setFillPrefix
799 @cmd('set-fill-prefix')
800 def setFillPrefix(self, event):
801 """Make the selected text the fill prefix."""
802 w = self.editWidget(event)
803 if not w:
804 return # pragma: no cover (defensive)
805 s = w.getAllText()
806 i, j = w.getSelectionRange()
807 self.fillPrefix = s[i:j]
808 #@+node:ekr.20150514063305.219: *4* ec._addPrefix
809 def _addPrefix(self, ntxt):
810 ntxt = ntxt.split('.')
811 ntxt = map(lambda a: self.fillPrefix + a, ntxt)
812 ntxt = '.'.join(ntxt)
813 return ntxt
814 #@+node:ekr.20150514063305.220: *3* ec: find quick support
815 #@+node:ekr.20150514063305.221: *4* ec.backward/findCharacter & helper
816 @cmd('backward-find-character')
817 def backwardFindCharacter(self, event):
818 """Search backwards for a character."""
819 return self.findCharacterHelper(event, backward=True, extend=False)
821 @cmd('backward-find-character-extend-selection')
822 def backwardFindCharacterExtendSelection(self, event):
823 """Search backward for a character, extending the selection."""
824 return self.findCharacterHelper(event, backward=True, extend=True)
826 @cmd('find-character')
827 def findCharacter(self, event):
828 """Search for a character."""
829 return self.findCharacterHelper(event, backward=False, extend=False)
831 @cmd('find-character-extend-selection')
832 def findCharacterExtendSelection(self, event):
833 """Search for a character, extending the selection."""
834 return self.findCharacterHelper(event, backward=False, extend=True)
835 #@+node:ekr.20150514063305.222: *5* ec.findCharacterHelper
836 def findCharacterHelper(self, event, backward, extend):
837 """Put the cursor at the next occurance of a character on a line."""
838 k = self.c.k
839 self.w = self.editWidget(event)
840 if not self.w:
841 return
842 self.event = event
843 self.backward = backward
844 self.extend = extend or self.extendMode # Bug fix: 2010/01/19
845 self.insert = self.w.getInsertPoint()
846 s = (
847 f"{'Backward find' if backward else 'Find'} "
848 f"character{' & extend' if extend else ''}: ")
849 k.setLabelBlue(s)
850 # Get the arg without touching the focus.
851 k.getArg(
852 event, handler=self.findCharacter1, oneCharacter=True, useMinibuffer=False)
854 def findCharacter1(self, event):
855 k = self.c.k
856 event, w = self.event, self.w
857 backward = self.backward
858 extend = self.extend or self.extendMode
859 ch = k.arg
860 s = w.getAllText()
861 ins = w.toPythonIndex(self.insert)
862 i = ins + -1 if backward else +1 # skip the present character.
863 if backward:
864 start = 0
865 j = s.rfind(ch, start, max(start, i)) # Skip the character at the cursor.
866 if j > -1:
867 self.moveToHelper(event, j, extend)
868 else:
869 end = len(s)
870 j = s.find(ch, min(i, end), end) # Skip the character at the cursor.
871 if j > -1:
872 self.moveToHelper(event, j, extend)
873 k.resetLabel()
874 k.clearState()
875 #@+node:ekr.20150514063305.223: *4* ec.findWord and FindWordOnLine & helper
876 @cmd('find-word')
877 def findWord(self, event):
878 """Put the cursor at the next word that starts with a character."""
879 return self.findWordHelper(event, oneLine=False)
881 @cmd('find-word-in-line')
882 def findWordInLine(self, event):
883 """Put the cursor at the next word (on a line) that starts with a character."""
884 return self.findWordHelper(event, oneLine=True)
885 #@+node:ekr.20150514063305.224: *5* ec.findWordHelper
886 def findWordHelper(self, event, oneLine):
887 k = self.c.k
888 self.w = self.editWidget(event)
889 if self.w:
890 self.oneLineFlag = oneLine
891 k.setLabelBlue(
892 f"Find word {'in line ' if oneLine else ''}starting with: ")
893 k.get1Arg(event, handler=self.findWord1, oneCharacter=True)
895 def findWord1(self, event):
896 c, k = self.c, self.c.k
897 ch = k.arg
898 if ch:
899 w = self.w
900 i = w.getInsertPoint()
901 s = w.getAllText()
902 end = len(s)
903 if self.oneLineFlag:
904 end = s.find('\n', i) # Limit searches to this line.
905 if end == -1:
906 end = len(s)
907 while i < end:
908 i = s.find(ch, i + 1, end) # Ensure progress and i > 0.
909 if i == -1:
910 break
911 elif not g.isWordChar(s[i - 1]):
912 w.setSelectionRange(i, i, insert=i)
913 break
914 k.resetLabel()
915 k.clearState()
916 c.widgetWantsFocus(w)
917 #@+node:ekr.20150514063305.225: *3* ec: goto node
918 #@+node:ekr.20170411065920.1: *4* ec.gotoAnyClone
919 @cmd('goto-any-clone')
920 def gotoAnyClone(self, event=None):
921 """Select then next cloned node, regardless of whether c.p is a clone."""
922 c = self.c
923 p = c.p.threadNext()
924 while p:
925 if p.isCloned():
926 c.selectPosition(p)
927 return
928 p.moveToThreadNext()
929 g.es('no clones found after', c.p.h)
930 #@+node:ekr.20150514063305.226: *4* ec.gotoCharacter
931 @cmd('goto-char')
932 def gotoCharacter(self, event):
933 """Put the cursor at the n'th character of the buffer."""
934 k = self.c.k
935 self.w = self.editWidget(event)
936 if self.w:
937 k.setLabelBlue("Goto n'th character: ")
938 k.get1Arg(event, handler=self.gotoCharacter1)
940 def gotoCharacter1(self, event):
941 c, k = self.c, self.c.k
942 n = k.arg
943 w = self.w
944 ok = False
945 if n.isdigit():
946 n = int(n)
947 if n >= 0:
948 w.setInsertPoint(n)
949 w.seeInsertPoint()
950 ok = True
951 if not ok:
952 g.warning('goto-char takes non-negative integer argument')
953 k.resetLabel()
954 k.clearState()
955 c.widgetWantsFocus(w)
956 #@+node:ekr.20150514063305.227: *4* ec.gotoGlobalLine
957 @cmd('goto-global-line')
958 def gotoGlobalLine(self, event):
959 """
960 Put the cursor at the line in the *outline* corresponding to the line
961 with the given line number *in the external file*.
963 For external files containing sentinels, there may be *several* lines
964 in the file that correspond to the same line in the outline.
966 An Easter Egg: <Alt-x>number invokes this code.
967 """
968 # Improved docstring for #253: Goto Global line (Alt-G) is inconsistent.
969 # https://github.com/leo-editor/leo-editor/issues/253
970 k = self.c.k
971 self.w = self.editWidget(event)
972 if self.w:
973 k.setLabelBlue('Goto global line: ')
974 k.get1Arg(event, handler=self.gotoGlobalLine1)
976 def gotoGlobalLine1(self, event):
977 c, k = self.c, self.c.k
978 n = k.arg
979 k.resetLabel()
980 k.clearState()
981 if n.isdigit():
982 # Very important: n is one-based.
983 c.gotoCommands.find_file_line(n=int(n))
984 #@+node:ekr.20150514063305.228: *4* ec.gotoLine
985 @cmd('goto-line')
986 def gotoLine(self, event):
987 """Put the cursor at the n'th line of the buffer."""
988 k = self.c.k
989 self.w = self.editWidget(event)
990 if self.w:
991 k.setLabelBlue('Goto line: ')
992 k.get1Arg(event, handler=self.gotoLine1)
994 def gotoLine1(self, event):
995 c, k = self.c, self.c.k
996 n, w = k.arg, self.w
997 if n.isdigit():
998 n = int(n)
999 s = w.getAllText()
1000 i = g.convertRowColToPythonIndex(s, n - 1, 0)
1001 w.setInsertPoint(i)
1002 w.seeInsertPoint()
1003 k.resetLabel()
1004 k.clearState()
1005 c.widgetWantsFocus(w)
1006 #@+node:ekr.20150514063305.229: *3* ec: icons
1007 #@+at
1008 # To do:
1009 # - Define standard icons in a subfolder of Icons folder?
1010 # - Tree control recomputes height of each line.
1011 #@+node:ekr.20150514063305.230: *4* ec. Helpers
1012 #@+node:ekr.20150514063305.231: *5* ec.appendImageDictToList
1013 def appendImageDictToList(self, aList, path, xoffset, **kargs):
1014 c = self.c
1015 relPath = path # for finding icon on load in different environment
1016 path = g.app.gui.getImageFinder(path)
1017 # pylint: disable=unpacking-non-sequence
1018 image, image_height = g.app.gui.getTreeImage(c, path)
1019 if not image:
1020 g.es('can not load image:', path)
1021 return xoffset
1022 if image_height is None:
1023 yoffset = 0
1024 else:
1025 yoffset = 0 # (c.frame.tree.line_height-image_height)/2
1026 # TNB: I suspect this is being done again in the drawing code
1027 newEntry = {
1028 'type': 'file',
1029 'file': path,
1030 'relPath': relPath,
1031 'where': 'beforeHeadline',
1032 'yoffset': yoffset, 'xoffset': xoffset, 'xpad': 1, # -2,
1033 'on': 'VNode',
1034 }
1035 newEntry.update(kargs) # may switch 'on' to 'VNode'
1036 aList.append(newEntry)
1037 xoffset += 2
1038 return xoffset
1039 #@+node:ekr.20150514063305.232: *5* ec.dHash
1040 def dHash(self, d):
1041 """Hash a dictionary"""
1042 return ''.join([f"{str(k)}{str(d[k])}" for k in sorted(d)])
1043 #@+node:ekr.20150514063305.233: *5* ec.getIconList
1044 def getIconList(self, p):
1045 """Return list of icons for position p, call setIconList to apply changes"""
1046 fromVnode = []
1047 if hasattr(p.v, 'unknownAttributes'):
1048 fromVnode = [dict(i) for i in p.v.u.get('icons', [])]
1049 for i in fromVnode:
1050 i['on'] = 'VNode'
1051 return fromVnode
1052 #@+node:ekr.20150514063305.234: *5* ec.setIconList & helpers
1053 def setIconList(self, p, l, setDirty=True):
1054 """Set list of icons for position p to l"""
1055 current = self.getIconList(p)
1056 if not l and not current:
1057 return # nothing to do
1058 lHash = ''.join([self.dHash(i) for i in l])
1059 cHash = ''.join([self.dHash(i) for i in current])
1060 if lHash == cHash:
1061 # no difference between original and current list of dictionaries
1062 return
1063 self._setIconListHelper(p, l, p.v, setDirty)
1064 if g.app.gui.guiName() == 'qt':
1065 self.c.frame.tree.updateAllIcons(p)
1066 #@+node:ekr.20150514063305.235: *6* ec._setIconListHelper
1067 def _setIconListHelper(self, p, subl, uaLoc, setDirty):
1068 """icon setting code common between v and t nodes
1070 p - postion
1071 subl - list of icons for the v or t node
1072 uaLoc - the v or t node
1073 """
1074 if subl: # Update the uA.
1075 if not hasattr(uaLoc, 'unknownAttributes'):
1076 uaLoc.unknownAttributes = {}
1077 uaLoc.unknownAttributes['icons'] = list(subl)
1078 # g.es((p.h,uaLoc.unknownAttributes['icons']))
1079 uaLoc._p_changed = True
1080 if setDirty:
1081 p.setDirty()
1082 else: # delete the uA.
1083 if hasattr(uaLoc, 'unknownAttributes'):
1084 if 'icons' in uaLoc.unknownAttributes:
1085 del uaLoc.unknownAttributes['icons']
1086 uaLoc._p_changed = True
1087 if setDirty:
1088 p.setDirty()
1089 #@+node:ekr.20150514063305.236: *4* ec.deleteFirstIcon
1090 @cmd('delete-first-icon')
1091 def deleteFirstIcon(self, event=None):
1092 """Delete the first icon in the selected node's icon list."""
1093 c = self.c
1094 aList = self.getIconList(c.p)
1095 if aList:
1096 self.setIconList(c.p, aList[1:])
1097 c.setChanged()
1098 c.redraw_after_icons_changed()
1099 #@+node:ekr.20150514063305.237: *4* ec.deleteIconByName
1100 def deleteIconByName(self, t, name, relPath): # t not used.
1101 """for use by the right-click remove icon callback"""
1102 c, p = self.c, self.c.p
1103 aList = self.getIconList(p)
1104 if not aList:
1105 return
1106 basePath = g.os_path_finalize_join(g.app.loadDir, "..", "Icons") # #1341.
1107 absRelPath = g.os_path_finalize_join(basePath, relPath) # #1341
1108 name = g.os_path_finalize(name) # #1341
1109 newList = []
1110 for d in aList:
1111 name2 = d.get('file')
1112 name2 = g.os_path_finalize(name2) # #1341
1113 name2rel = d.get('relPath')
1114 if not (name == name2 or absRelPath == name2 or relPath == name2rel):
1115 newList.append(d)
1116 if len(newList) != len(aList):
1117 self.setIconList(p, newList)
1118 c.setChanged()
1119 c.redraw_after_icons_changed()
1120 else:
1121 g.trace('not found', name)
1122 #@+node:ekr.20150514063305.238: *4* ec.deleteLastIcon
1123 @cmd('delete-last-icon')
1124 def deleteLastIcon(self, event=None):
1125 """Delete the first icon in the selected node's icon list."""
1126 c = self.c
1127 aList = self.getIconList(c.p)
1128 if aList:
1129 self.setIconList(c.p, aList[:-1])
1130 c.setChanged()
1131 c.redraw_after_icons_changed()
1132 #@+node:ekr.20150514063305.239: *4* ec.deleteNodeIcons
1133 @cmd('delete-node-icons')
1134 def deleteNodeIcons(self, event=None, p=None):
1135 """Delete all of the selected node's icons."""
1136 c = self.c
1137 p = p or c.p
1138 if p.u:
1139 p.v._p_changed = True
1140 self.setIconList(p, [])
1141 p.setDirty()
1142 c.setChanged()
1143 c.redraw_after_icons_changed()
1144 #@+node:ekr.20150514063305.240: *4* ec.insertIcon
1145 @cmd('insert-icon')
1146 def insertIcon(self, event=None):
1147 """Prompt for an icon, and insert it into the node's icon list."""
1148 c, p = self.c, self.c.p
1149 iconDir = g.os_path_finalize_join(g.app.loadDir, "..", "Icons")
1150 os.chdir(iconDir)
1151 paths = g.app.gui.runOpenFileDialog(c,
1152 title='Get Icons',
1153 filetypes=[
1154 ('All files', '*'),
1155 ('Gif', '*.gif'),
1156 ('Bitmap', '*.bmp'),
1157 ('Icon', '*.ico'),
1158 ],
1159 defaultextension=None, multiple=True)
1160 if not paths:
1161 return
1162 aList: List[Any] = []
1163 xoffset = 2
1164 for path in paths:
1165 xoffset = self.appendImageDictToList(aList, path, xoffset)
1166 aList2 = self.getIconList(p)
1167 aList2.extend(aList)
1168 self.setIconList(p, aList2)
1169 c.setChanged()
1170 c.redraw_after_icons_changed()
1171 #@+node:ekr.20150514063305.241: *4* ec.insertIconFromFile
1172 def insertIconFromFile(self, path, p=None, pos=None, **kargs):
1173 c = self.c
1174 if not p:
1175 p = c.p
1176 aList: List[Any] = []
1177 xoffset = 2
1178 xoffset = self.appendImageDictToList(aList, path, xoffset, **kargs)
1179 aList2 = self.getIconList(p)
1180 if pos is None:
1181 pos = len(aList2)
1182 aList2.insert(pos, aList[0])
1183 self.setIconList(p, aList2)
1184 c.setChanged()
1185 c.redraw_after_icons_changed()
1186 #@+node:ekr.20150514063305.242: *3* ec: indent
1187 #@+node:ekr.20150514063305.243: *4* ec.deleteIndentation
1188 @cmd('delete-indentation')
1189 def deleteIndentation(self, event):
1190 """Delete indentation in the presently line."""
1191 w = self.editWidget(event)
1192 if not w:
1193 return # pragma: no cover (defensive)
1194 s = w.getAllText()
1195 ins = w.getInsertPoint()
1196 i, j = g.getLine(s, ins)
1197 line = s[i:j]
1198 line2 = s[i:j].lstrip()
1199 delta = len(line) - len(line2)
1200 if delta:
1201 self.beginCommand(w, undoType='delete-indentation')
1202 w.delete(i, j)
1203 w.insert(i, line2)
1204 ins -= delta
1205 w.setSelectionRange(ins, ins, insert=ins)
1206 self.endCommand(changed=True, setLabel=True)
1207 #@+node:ekr.20150514063305.244: *4* ec.indentRelative
1208 @cmd('indent-relative')
1209 def indentRelative(self, event):
1210 """
1211 The indent-relative command indents at the point based on the previous
1212 line (actually, the last non-empty line.) It inserts whitespace at the
1213 point, moving point, until it is underneath an indentation point in the
1214 previous line.
1216 An indentation point is the end of a sequence of whitespace or the end of
1217 the line. If the point is farther right than any indentation point in the
1218 previous line, the whitespace before point is deleted and the first
1219 indentation point then applicable is used. If no indentation point is
1220 applicable even then whitespace equivalent to a single tab is inserted.
1221 """
1222 p, u = self.c.p, self.c.undoer
1223 undoType = 'indent-relative'
1224 w = self.editWidget(event)
1225 if not w:
1226 return # pragma: no cover (defensive)
1227 s = w.getAllText()
1228 ins = w.getInsertPoint()
1229 # Find the previous non-blank line
1230 i, j = g.getLine(s, ins)
1231 while 1:
1232 if i <= 0:
1233 return
1234 i, j = g.getLine(s, i - 1)
1235 line = s[i:j]
1236 if line.strip():
1237 break
1238 self.beginCommand(w, undoType=undoType)
1239 try:
1240 bunch = u.beforeChangeBody(p)
1241 k = g.skip_ws(s, i)
1242 ws = s[i:k]
1243 i2, j2 = g.getLine(s, ins)
1244 k = g.skip_ws(s, i2)
1245 line = ws + s[k:j2]
1246 w.delete(i2, j2)
1247 w.insert(i2, line)
1248 w.setInsertPoint(i2 + len(ws))
1249 p.v.b = w.getAllText()
1250 u.afterChangeBody(p, undoType, bunch)
1251 finally:
1252 self.endCommand(changed=True, setLabel=True)
1253 #@+node:ekr.20150514063305.245: *3* ec: info
1254 #@+node:ekr.20210311154956.1: *4* ec.copyGnx
1255 @cmd('copy-gnx')
1256 def copyGnx(self, event):
1257 """Copy c.p.gnx to the clipboard and display it in the status area."""
1258 c = self.c
1259 if not c:
1260 return
1261 gnx = c.p and c.p.gnx
1262 if not gnx:
1263 return
1264 g.app.gui.replaceClipboardWith(gnx)
1265 status_line = getattr(c.frame, "statusLine", None)
1266 if status_line:
1267 status_line.put(f"gnx: {gnx}")
1268 #@+node:ekr.20150514063305.247: *4* ec.lineNumber
1269 @cmd('line-number')
1270 def lineNumber(self, event):
1271 """Print the line and column number and percentage of insert point."""
1272 k = self.c.k
1273 w = self.editWidget(event)
1274 if not w:
1275 return # pragma: no cover (defensive)
1276 s = w.getAllText()
1277 i = w.getInsertPoint()
1278 row, col = g.convertPythonIndexToRowCol(s, i)
1279 percent = int((i * 100) / len(s))
1280 k.setLabelGrey(
1281 'char: %s row: %d col: %d pos: %d (%d%% of %d)' % (
1282 repr(s[i]), row, col, i, percent, len(s)))
1283 #@+node:ekr.20150514063305.248: *4* ec.viewLossage
1284 @cmd('view-lossage')
1285 def viewLossage(self, event):
1286 """Print recent keystrokes."""
1287 print('Recent keystrokes...')
1288 # #1933: Use repr to show LossageData objects.
1289 for i, data in enumerate(reversed(g.app.lossage)):
1290 print(f"{i:>2} {data!r}")
1291 #@+node:ekr.20211010131039.1: *4* ec.viewRecentCommands
1292 @cmd('view-recent-commands')
1293 def viewRecentCommands(self, event):
1294 """Print recently-executed commands."""
1295 c = self.c
1296 print('Recently-executed commands...')
1297 for i, command in enumerate(reversed(c.recent_commands_list)):
1298 print(f"{i:>2} {command}")
1299 #@+node:ekr.20150514063305.249: *4* ec.whatLine
1300 @cmd('what-line')
1301 def whatLine(self, event):
1302 """Print the line number of the line containing the cursor."""
1303 k = self.c.k
1304 w = self.editWidget(event)
1305 if w:
1306 s = w.getAllText()
1307 i = w.getInsertPoint()
1308 row, col = g.convertPythonIndexToRowCol(s, i)
1309 k.keyboardQuit()
1310 k.setStatusLabel(f"Line {row}")
1311 #@+node:ekr.20150514063305.250: *3* ec: insert & delete
1312 #@+node:ekr.20150514063305.251: *4* ec.addSpace/TabToLines & removeSpace/TabFromLines & helper
1313 @cmd('add-space-to-lines')
1314 def addSpaceToLines(self, event):
1315 """Add a space to start of all lines, or all selected lines."""
1316 self.addRemoveHelper(event, ch=' ', add=True, undoType='add-space-to-lines')
1318 @cmd('add-tab-to-lines')
1319 def addTabToLines(self, event):
1320 """Add a tab to start of all lines, or all selected lines."""
1321 self.addRemoveHelper(event, ch='\t', add=True, undoType='add-tab-to-lines')
1323 @cmd('remove-space-from-lines')
1324 def removeSpaceFromLines(self, event):
1325 """Remove a space from start of all lines, or all selected lines."""
1326 self.addRemoveHelper(
1327 event, ch=' ', add=False, undoType='remove-space-from-lines')
1329 @cmd('remove-tab-from-lines')
1330 def removeTabFromLines(self, event):
1331 """Remove a tab from start of all lines, or all selected lines."""
1332 self.addRemoveHelper(event, ch='\t', add=False, undoType='remove-tab-from-lines')
1333 #@+node:ekr.20150514063305.252: *5* ec.addRemoveHelper
1334 def addRemoveHelper(self, event, ch, add, undoType):
1335 c = self.c
1336 w = self.editWidget(event)
1337 if not w:
1338 return
1339 if w.hasSelection():
1340 s = w.getSelectedText()
1341 else:
1342 s = w.getAllText()
1343 if not s:
1344 return
1345 # Insert or delete spaces instead of tabs when negative tab width is in effect.
1346 d = c.scanAllDirectives(c.p)
1347 width = d.get('tabwidth')
1348 if ch == '\t' and width < 0:
1349 ch = ' ' * abs(width)
1350 self.beginCommand(w, undoType=undoType)
1351 lines = g.splitLines(s)
1352 if add:
1353 result_list = [ch + line for line in lines]
1354 else:
1355 result_list = [line[len(ch) :] if line.startswith(ch) else line for line in lines]
1356 result = ''.join(result_list)
1357 if w.hasSelection():
1358 i, j = w.getSelectionRange()
1359 w.delete(i, j)
1360 w.insert(i, result)
1361 w.setSelectionRange(i, i + len(result))
1362 else:
1363 w.setAllText(result)
1364 w.setSelectionRange(0, len(s))
1365 self.endCommand(changed=True, setLabel=True)
1366 #@+node:ekr.20150514063305.253: *4* ec.backwardDeleteCharacter
1367 @cmd('backward-delete-char')
1368 def backwardDeleteCharacter(self, event=None):
1369 """Delete the character to the left of the cursor."""
1370 c = self.c
1371 w = self.editWidget(event)
1372 if not w:
1373 return # pragma: no cover (defensive)
1374 wname = c.widget_name(w)
1375 ins = w.getInsertPoint()
1376 i, j = w.getSelectionRange()
1377 if wname.startswith('body'):
1378 self.beginCommand(w, undoType='Typing')
1379 try:
1380 tab_width = c.getTabWidth(c.p)
1381 changed = True
1382 if i != j:
1383 w.delete(i, j)
1384 w.setSelectionRange(i, i, insert=i)
1385 elif i == 0:
1386 changed = False
1387 elif tab_width > 0:
1388 w.delete(ins - 1)
1389 w.setSelectionRange(ins - 1, ins - 1, insert=ins - 1)
1390 else:
1391 #@+<< backspace with negative tab_width >>
1392 #@+node:ekr.20150514063305.254: *5* << backspace with negative tab_width >>
1393 s = prev = w.getAllText()
1394 ins = w.getInsertPoint()
1395 i, j = g.getLine(s, ins)
1396 s = prev = s[i:ins]
1397 n = len(prev)
1398 abs_width = abs(tab_width)
1399 # Delete up to this many spaces.
1400 n2 = (n % abs_width) or abs_width
1401 n2 = min(n, n2)
1402 count = 0
1403 while n2 > 0:
1404 n2 -= 1
1405 ch = prev[n - count - 1]
1406 if ch != ' ':
1407 break
1408 else: count += 1
1409 # Make sure we actually delete something.
1410 i = ins - (max(1, count))
1411 w.delete(i, ins)
1412 w.setSelectionRange(i, i, insert=i)
1413 #@-<< backspace with negative tab_width >>
1414 finally:
1415 self.endCommand(changed=changed, setLabel=False)
1416 # Necessary to make text changes stick.
1417 else:
1418 # No undo in this widget.
1419 s = w.getAllText()
1420 # Delete something if we can.
1421 if i != j:
1422 j = max(i, min(j, len(s)))
1423 w.delete(i, j)
1424 w.setSelectionRange(i, i, insert=i)
1425 elif ins != 0:
1426 # Do nothing at the start of the headline.
1427 w.delete(ins - 1)
1428 ins = ins - 1
1429 w.setSelectionRange(ins, ins, insert=ins)
1430 #@+node:ekr.20150514063305.255: *4* ec.cleanAllLines
1431 @cmd('clean-all-lines')
1432 def cleanAllLines(self, event):
1433 """Clean all lines in the selected tree."""
1434 c = self.c
1435 u = c.undoer
1436 w = c.frame.body.wrapper
1437 if not w:
1438 return
1439 tag = 'clean-all-lines'
1440 u.beforeChangeGroup(c.p, tag)
1441 n = 0
1442 for p in c.p.self_and_subtree():
1443 lines = []
1444 for line in g.splitLines(p.b):
1445 if line.rstrip():
1446 lines.append(line.rstrip())
1447 if line.endswith('\n'):
1448 lines.append('\n')
1449 s2 = ''.join(lines)
1450 if s2 != p.b:
1451 print(p.h)
1452 bunch = u.beforeChangeNodeContents(p)
1453 p.b = s2
1454 p.setDirty()
1455 n += 1
1456 u.afterChangeNodeContents(p, tag, bunch)
1457 u.afterChangeGroup(c.p, tag)
1458 c.redraw_after_icons_changed()
1459 g.es(f"cleaned {n} nodes")
1460 #@+node:ekr.20150514063305.256: *4* ec.cleanLines
1461 @cmd('clean-lines')
1462 def cleanLines(self, event):
1463 """Removes trailing whitespace from all lines, preserving newlines.
1464 """
1465 w = self.editWidget(event)
1466 if not w:
1467 return # pragma: no cover (defensive)
1468 if w.hasSelection():
1469 s = w.getSelectedText()
1470 else:
1471 s = w.getAllText()
1472 lines = []
1473 for line in g.splitlines(s):
1474 if line.rstrip():
1475 lines.append(line.rstrip())
1476 if line.endswith('\n'):
1477 lines.append('\n')
1478 result = ''.join(lines)
1479 if s != result:
1480 self.beginCommand(w, undoType='clean-lines')
1481 if w.hasSelection():
1482 i, j = w.getSelectionRange()
1483 w.delete(i, j)
1484 w.insert(i, result)
1485 w.setSelectionRange(i, j + len(result))
1486 else:
1487 i = w.getInsertPoint()
1488 w.delete(0, 'end')
1489 w.insert(0, result)
1490 w.setInsertPoint(i)
1491 self.endCommand(changed=True, setLabel=True)
1492 #@+node:ekr.20150514063305.257: *4* ec.clearSelectedText
1493 @cmd('clear-selected-text')
1494 def clearSelectedText(self, event):
1495 """Delete the selected text."""
1496 w = self.editWidget(event)
1497 if not w:
1498 return
1499 i, j = w.getSelectionRange()
1500 if i == j:
1501 return
1502 self.beginCommand(w, undoType='clear-selected-text')
1503 w.delete(i, j)
1504 w.setInsertPoint(i)
1505 self.endCommand(changed=True, setLabel=True)
1506 #@+node:ekr.20150514063305.258: *4* ec.delete-word & backward-delete-word
1507 @cmd('delete-word')
1508 def deleteWord(self, event=None):
1509 """Delete the word at the cursor."""
1510 self.deleteWordHelper(event, forward=True)
1512 @cmd('backward-delete-word')
1513 def backwardDeleteWord(self, event=None):
1514 """Delete the word in front of the cursor."""
1515 self.deleteWordHelper(event, forward=False)
1517 # Patch by NH2.
1519 @cmd('delete-word-smart')
1520 def deleteWordSmart(self, event=None):
1521 """Delete the word at the cursor, treating whitespace
1522 and symbols smartly."""
1523 self.deleteWordHelper(event, forward=True, smart=True)
1525 @cmd('backward-delete-word-smart')
1526 def backwardDeleteWordSmart(self, event=None):
1527 """Delete the word in front of the cursor, treating whitespace
1528 and symbols smartly."""
1529 self.deleteWordHelper(event, forward=False, smart=True)
1531 def deleteWordHelper(self, event, forward, smart=False):
1532 # c = self.c
1533 w = self.editWidget(event)
1534 if not w:
1535 return
1536 self.beginCommand(w, undoType="delete-word")
1537 if w.hasSelection():
1538 from_pos, to_pos = w.getSelectionRange()
1539 else:
1540 from_pos = w.getInsertPoint()
1541 self.moveWordHelper(event, extend=False, forward=forward, smart=smart)
1542 to_pos = w.getInsertPoint()
1543 # For Tk GUI, make sure to_pos > from_pos
1544 if from_pos > to_pos:
1545 from_pos, to_pos = to_pos, from_pos
1546 w.delete(from_pos, to_pos)
1547 self.endCommand(changed=True, setLabel=True)
1548 #@+node:ekr.20150514063305.259: *4* ec.deleteNextChar
1549 @cmd('delete-char')
1550 def deleteNextChar(self, event):
1551 """Delete the character to the right of the cursor."""
1552 c, w = self.c, self.editWidget(event)
1553 if not w:
1554 return
1555 wname = c.widget_name(w)
1556 if wname.startswith('body'):
1557 s = w.getAllText()
1558 i, j = w.getSelectionRange()
1559 self.beginCommand(w, undoType='delete-char')
1560 changed = True
1561 if i != j:
1562 w.delete(i, j)
1563 w.setInsertPoint(i)
1564 elif j < len(s):
1565 w.delete(i)
1566 w.setInsertPoint(i)
1567 else:
1568 changed = False
1569 self.endCommand(changed=changed, setLabel=False)
1570 else:
1571 # No undo in this widget.
1572 s = w.getAllText()
1573 i, j = w.getSelectionRange()
1574 # Delete something if we can.
1575 if i != j:
1576 w.delete(i, j)
1577 w.setInsertPoint(i)
1578 elif j < len(s):
1579 w.delete(i)
1580 w.setInsertPoint(i)
1581 #@+node:ekr.20150514063305.260: *4* ec.deleteSpaces
1582 @cmd('delete-spaces')
1583 def deleteSpaces(self, event, insertspace=False):
1584 """Delete all whitespace surrounding the cursor."""
1585 w = self.editWidget(event)
1586 if not w:
1587 return # pragma: no cover (defensive)
1588 undoType = 'insert-space' if insertspace else 'delete-spaces'
1589 s = w.getAllText()
1590 ins = w.getInsertPoint()
1591 i, j = g.getLine(s, ins)
1592 w1 = ins - 1
1593 while w1 >= i and s[w1].isspace():
1594 w1 -= 1
1595 w1 += 1
1596 w2 = ins
1597 while w2 <= j and s[w2].isspace():
1598 w2 += 1
1599 spaces = s[w1:w2]
1600 if spaces:
1601 self.beginCommand(w, undoType=undoType)
1602 if insertspace:
1603 s = s[:w1] + ' ' + s[w2:]
1604 else:
1605 s = s[:w1] + s[w2:]
1606 w.setAllText(s)
1607 w.setInsertPoint(w1)
1608 self.endCommand(changed=True, setLabel=True)
1609 #@+node:ekr.20150514063305.261: *4* ec.insertHardTab
1610 @cmd('insert-hard-tab')
1611 def insertHardTab(self, event):
1612 """Insert one hard tab."""
1613 c = self.c
1614 w = self.editWidget(event)
1615 if not w:
1616 return
1617 if not g.isTextWrapper(w):
1618 return
1619 name = c.widget_name(w)
1620 if name.startswith('head'):
1621 return
1622 ins = w.getInsertPoint()
1623 self.beginCommand(w, undoType='insert-hard-tab')
1624 w.insert(ins, '\t')
1625 ins += 1
1626 w.setSelectionRange(ins, ins, insert=ins)
1627 self.endCommand()
1628 #@+node:ekr.20150514063305.262: *4* ec.insertNewLine (insert-newline)
1629 @cmd('insert-newline')
1630 def insertNewLine(self, event):
1631 """Insert a newline at the cursor."""
1632 self.insertNewlineBase(event)
1634 insertNewline = insertNewLine
1636 def insertNewlineBase(self, event):
1637 """A helper that can be monkey-patched by tables.py plugin."""
1638 # Note: insertNewlineHelper already exists.
1639 c, k = self.c, self.c.k
1640 w = self.editWidget(event)
1641 if not w:
1642 return # pragma: no cover (defensive)
1643 if not g.isTextWrapper(w):
1644 return # pragma: no cover (defensive)
1645 name = c.widget_name(w)
1646 if name.startswith('head'):
1647 return
1648 oldSel = w.getSelectionRange()
1649 self.beginCommand(w, undoType='newline')
1650 self.insertNewlineHelper(w=w, oldSel=oldSel, undoType=None)
1651 k.setInputState('insert')
1652 k.showStateAndMode()
1653 self.endCommand()
1654 #@+node:ekr.20150514063305.263: *4* ec.insertNewLineAndTab (newline-and-indent)
1655 @cmd('newline-and-indent')
1656 def insertNewLineAndTab(self, event):
1657 """Insert a newline and tab at the cursor."""
1658 trace = 'keys' in g.app.debug
1659 c, k = self.c, self.c.k
1660 p = c.p
1661 w = self.editWidget(event)
1662 if not w:
1663 return
1664 if not g.isTextWrapper(w):
1665 return
1666 name = c.widget_name(w)
1667 if name.startswith('head'):
1668 return
1669 if trace:
1670 g.trace('(newline-and-indent)')
1671 self.beginCommand(w, undoType='insert-newline-and-indent')
1672 oldSel = w.getSelectionRange()
1673 self.insertNewlineHelper(w=w, oldSel=oldSel, undoType=None)
1674 self.updateTab(event, p, w, smartTab=False)
1675 k.setInputState('insert')
1676 k.showStateAndMode()
1677 self.endCommand(changed=True, setLabel=False)
1678 #@+node:ekr.20150514063305.264: *4* ec.insertParentheses
1679 @cmd('insert-parentheses')
1680 def insertParentheses(self, event):
1681 """Insert () at the cursor."""
1682 w = self.editWidget(event)
1683 if w:
1684 self.beginCommand(w, undoType='insert-parenthesis')
1685 i = w.getInsertPoint()
1686 w.insert(i, '()')
1687 w.setInsertPoint(i + 1)
1688 self.endCommand(changed=True, setLabel=False)
1689 #@+node:ekr.20150514063305.265: *4* ec.insertSoftTab
1690 @cmd('insert-soft-tab')
1691 def insertSoftTab(self, event):
1692 """Insert spaces equivalent to one tab."""
1693 c = self.c
1694 w = self.editWidget(event)
1695 if not w:
1696 return
1697 if not g.isTextWrapper(w):
1698 return
1699 name = c.widget_name(w)
1700 if name.startswith('head'):
1701 return
1702 tab_width = abs(c.getTabWidth(c.p))
1703 ins = w.getInsertPoint()
1704 self.beginCommand(w, undoType='insert-soft-tab')
1705 w.insert(ins, ' ' * tab_width)
1706 ins += tab_width
1707 w.setSelectionRange(ins, ins, insert=ins)
1708 self.endCommand()
1709 #@+node:ekr.20150514063305.266: *4* ec.removeBlankLines (remove-blank-lines)
1710 @cmd('remove-blank-lines')
1711 def removeBlankLines(self, event):
1712 """
1713 Remove lines containing nothing but whitespace.
1715 Select all lines if there is no existing selection.
1716 """
1717 c, p, u, w = self.c, self.c.p, self.c.undoer, self.editWidget(event)
1718 #
1719 # "Before" snapshot.
1720 bunch = u.beforeChangeBody(p)
1721 #
1722 # Initial data.
1723 oldYview = w.getYScrollPosition()
1724 lines = g.splitLines(w.getAllText())
1725 #
1726 # Calculate the result.
1727 result_list = []
1728 changed = False
1729 for line in lines:
1730 if line.strip():
1731 result_list.append(line)
1732 else:
1733 changed = True
1734 if not changed:
1735 return # pragma: no cover (defensive)
1736 #
1737 # Set p.b and w's text first.
1738 result = ''.join(result_list)
1739 p.b = result
1740 w.setAllText(result)
1741 i, j = 0, max(0, len(result) - 1)
1742 w.setSelectionRange(i, j, insert=j)
1743 w.setYScrollPosition(oldYview)
1744 #
1745 # "after" snapshot.
1746 c.undoer.afterChangeBody(p, 'remove-blank-lines', bunch)
1747 #@+node:ekr.20150514063305.267: *4* ec.replaceCurrentCharacter
1748 @cmd('replace-current-character')
1749 def replaceCurrentCharacter(self, event):
1750 """Replace the current character with the next character typed."""
1751 k = self.c.k
1752 self.w = self.editWidget(event)
1753 if self.w:
1754 k.setLabelBlue('Replace Character: ')
1755 k.get1Arg(event, handler=self.replaceCurrentCharacter1)
1757 def replaceCurrentCharacter1(self, event):
1758 c, k, w = self.c, self.c.k, self.w
1759 ch = k.arg
1760 if ch:
1761 i, j = w.getSelectionRange()
1762 if i > j:
1763 i, j = j, i
1764 # Use raw insert/delete to retain the coloring.
1765 if i == j:
1766 i = max(0, i - 1)
1767 w.delete(i)
1768 else:
1769 w.delete(i, j)
1770 w.insert(i, ch)
1771 w.setInsertPoint(i + 1)
1772 k.clearState()
1773 k.resetLabel()
1774 k.showStateAndMode()
1775 c.widgetWantsFocus(w)
1776 #@+node:ekr.20150514063305.268: *4* ec.selfInsertCommand, helpers
1777 # @cmd('self-insert-command')
1779 def selfInsertCommand(self, event, action='insert'):
1780 """
1781 Insert a character in the body pane.
1783 This is the default binding for all keys in the body pane.
1784 It handles undo, bodykey events, tabs, back-spaces and bracket matching.
1785 """
1786 trace = 'keys' in g.app.debug
1787 c, p, u, w = self.c, self.c.p, self.c.undoer, self.editWidget(event)
1788 undoType = 'Typing'
1789 if not w:
1790 return # pragma: no cover (defensive)
1791 #@+<< set local vars >>
1792 #@+node:ekr.20150514063305.269: *5* << set local vars >> (selfInsertCommand)
1793 stroke = event.stroke if event else None
1794 ch = event.char if event else ''
1795 if ch == 'Return':
1796 ch = '\n' # This fixes the MacOS return bug.
1797 if ch == 'Tab':
1798 ch = '\t'
1799 name = c.widget_name(w)
1800 oldSel = w.getSelectionRange() if name.startswith('body') else (None, None)
1801 oldText = p.b if name.startswith('body') else ''
1802 oldYview = w.getYScrollPosition()
1803 brackets = self.openBracketsList + self.closeBracketsList
1804 inBrackets = ch and g.checkUnicode(ch) in brackets
1805 #@-<< set local vars >>
1806 if not ch:
1807 return
1808 if trace:
1809 g.trace('ch', repr(ch)) # and ch in '\n\r\t'
1810 assert g.isStrokeOrNone(stroke)
1811 if g.doHook("bodykey1", c=c, p=p, ch=ch, oldSel=oldSel, undoType=undoType):
1812 return
1813 if ch == '\t':
1814 self.updateTab(event, p, w, smartTab=True)
1815 elif ch == '\b':
1816 # This is correct: we only come here if there no bindngs for this key.
1817 self.backwardDeleteCharacter(event)
1818 elif ch in ('\r', '\n'):
1819 ch = '\n'
1820 self.insertNewlineHelper(w, oldSel, undoType)
1821 elif ch in '\'"' and c.config.getBool('smart-quotes'):
1822 self.doSmartQuote(action, ch, oldSel, w)
1823 elif inBrackets and self.autocompleteBrackets:
1824 self.updateAutomatchBracket(p, w, ch, oldSel)
1825 elif ch:
1826 # Null chars must not delete the selection.
1827 self.doPlainChar(action, ch, event, inBrackets, oldSel, stroke, w)
1828 #
1829 # Common processing.
1830 # Set the column for up and down keys.
1831 spot = w.getInsertPoint()
1832 c.editCommands.setMoveCol(w, spot)
1833 #
1834 # Update the text and handle undo.
1835 newText = w.getAllText()
1836 if newText != oldText:
1837 # Call u.doTyping to honor the user's undo granularity.
1838 newSel = w.getSelectionRange()
1839 newInsert = w.getInsertPoint()
1840 newSel = w.getSelectionRange()
1841 newText = w.getAllText() # Converts to unicode.
1842 u.doTyping(p, 'Typing', oldText, newText,
1843 oldSel=oldSel, oldYview=oldYview, newInsert=newInsert, newSel=newSel)
1844 g.doHook("bodykey2", c=c, p=p, ch=ch, oldSel=oldSel, undoType=undoType)
1845 #@+node:ekr.20160924135613.1: *5* ec.doPlainChar
1846 def doPlainChar(self, action, ch, event, inBrackets, oldSel, stroke, w):
1847 c, p = self.c, self.c.p
1848 isPlain = stroke.find('Alt') == -1 and stroke.find('Ctrl') == -1
1849 i, j = oldSel
1850 if i > j:
1851 i, j = j, i
1852 # Use raw insert/delete to retain the coloring.
1853 if i != j:
1854 w.delete(i, j)
1855 elif action == 'overwrite':
1856 w.delete(i)
1857 if isPlain:
1858 ins = w.getInsertPoint()
1859 if self.autojustify > 0 and not inBrackets:
1860 # Support #14: auto-justify body text.
1861 s = w.getAllText()
1862 i = g.skip_to_start_of_line(s, ins)
1863 i, j = g.getLine(s, i)
1864 # Only insert a newline at the end of a line.
1865 if j - i >= self.autojustify and (ins >= len(s) or s[ins] == '\n'):
1866 # Find the start of the word.
1867 n = 0
1868 ins -= 1
1869 while ins - 1 > 0 and g.isWordChar(s[ins - 1]):
1870 n += 1
1871 ins -= 1
1872 sins = ins # start of insert, to collect trailing whitespace
1873 while sins > 0 and s[sins - 1] in ' \t':
1874 sins -= 1
1875 oldSel = (sins, ins)
1876 self.insertNewlineHelper(w, oldSel, undoType=None)
1877 ins = w.getInsertPoint()
1878 ins += (n + 1)
1879 w.insert(ins, ch)
1880 w.setInsertPoint(ins + 1)
1881 else:
1882 g.app.gui.insertKeyEvent(event, i)
1883 if inBrackets and self.flashMatchingBrackets:
1884 self.flashMatchingBracketsHelper(c, ch, i, p, w)
1885 #@+node:ekr.20180806045802.1: *5* ec.doSmartQuote
1886 def doSmartQuote(self, action, ch, oldSel, w):
1887 """Convert a straight quote to a curly quote, depending on context."""
1888 i, j = oldSel
1889 if i > j:
1890 i, j = j, i
1891 # Use raw insert/delete to retain the coloring.
1892 if i != j:
1893 w.delete(i, j)
1894 elif action == 'overwrite':
1895 w.delete(i)
1896 ins = w.getInsertPoint()
1897 # Pick the correct curly quote.
1898 s = w.getAllText() or ""
1899 i2 = g.skip_to_start_of_line(s, max(0, ins - 1))
1900 open_curly = ins == i2 or ins > i2 and s[ins - 1] in ' \t'
1901 # not s[ins-1].isalnum()
1902 if open_curly:
1903 ch = '‘' if ch == "'" else "“"
1904 else:
1905 ch = '’' if ch == "'" else "”"
1906 w.insert(ins, ch)
1907 w.setInsertPoint(ins + 1)
1908 #@+node:ekr.20150514063305.271: *5* ec.flashCharacter
1909 def flashCharacter(self, w, i):
1910 """Flash the character at position i of widget w."""
1911 bg = self.bracketsFlashBg or 'DodgerBlue1'
1912 fg = self.bracketsFlashFg or 'white'
1913 flashes = self.bracketsFlashCount or 3
1914 delay = self.bracketsFlashDelay or 75
1915 w.flashCharacter(i, bg, fg, flashes, delay)
1916 #@+node:ekr.20150514063305.272: *5* ec.flashMatchingBracketsHelper
1917 def flashMatchingBracketsHelper(self, c, ch, i, p, w):
1918 """Flash matching brackets at char ch at position i at widget w."""
1919 d = {}
1920 # pylint: disable=consider-using-enumerate
1921 if ch in self.openBracketsList:
1922 for z in range(len(self.openBracketsList)):
1923 d[self.openBracketsList[z]] = self.closeBracketsList[z]
1924 # reverse = False # Search forward
1925 else:
1926 for z in range(len(self.openBracketsList)):
1927 d[self.closeBracketsList[z]] = self.openBracketsList[z]
1928 # reverse = True # Search backward
1929 s = w.getAllText()
1930 # A partial fix for bug 127: Bracket matching is buggy.
1931 language = g.getLanguageAtPosition(c, p)
1932 if language == 'perl':
1933 return
1934 j = g.MatchBrackets(c, p, language).find_matching_bracket(ch, s, i)
1935 if j is not None:
1936 self.flashCharacter(w, j)
1937 #@+node:ekr.20150514063305.273: *5* ec.initBracketMatcher
1938 def initBracketMatcher(self, c):
1939 """Init the bracket matching code."""
1940 if len(self.openBracketsList) != len(self.closeBracketsList):
1941 g.es_print('bad open/close_flash_brackets setting: using defaults')
1942 self.openBracketsList = '([{'
1943 self.closeBracketsList = ')]}'
1944 #@+node:ekr.20150514063305.274: *5* ec.insertNewlineHelper
1945 def insertNewlineHelper(self, w, oldSel, undoType):
1947 c, p = self.c, self.c.p
1948 i, j = oldSel
1949 ch = '\n'
1950 if i != j:
1951 # No auto-indent if there is selected text.
1952 w.delete(i, j)
1953 w.insert(i, ch)
1954 w.setInsertPoint(i + 1)
1955 else:
1956 w.insert(i, ch)
1957 w.setInsertPoint(i + 1)
1958 if (c.autoindent_in_nocolor or
1959 (c.frame.body.colorizer.useSyntaxColoring(p) and
1960 undoType != "Change")
1961 ):
1962 # No auto-indent if in @nocolor mode or after a Change command.
1963 self.updateAutoIndent(p, w)
1964 w.seeInsertPoint()
1965 #@+node:ekr.20150514063305.275: *5* ec.updateAutoIndent
1966 trailing_colon_pat = re.compile(r'^.*:\s*?#.*$') # #2230
1968 def updateAutoIndent(self, p, w):
1969 """Handle auto indentation."""
1970 c = self.c
1971 tab_width = c.getTabWidth(p)
1972 # Get the previous line.
1973 s = w.getAllText()
1974 ins = w.getInsertPoint()
1975 i = g.skip_to_start_of_line(s, ins)
1976 i, j = g.getLine(s, i - 1)
1977 s = s[i : j - 1]
1978 # Add the leading whitespace to the present line.
1979 junk, width = g.skip_leading_ws_with_indent(s, 0, tab_width)
1980 if s.rstrip() and (s.rstrip()[-1] == ':' or self.trailing_colon_pat.match(s)): #2040.
1981 # For Python: increase auto-indent after colons.
1982 if g.findLanguageDirectives(c, p) == 'python':
1983 width += abs(tab_width)
1984 if self.smartAutoIndent:
1985 # Determine if prev line has unclosed parens/brackets/braces
1986 bracketWidths = [width]
1987 tabex = 0
1988 for i, ch in enumerate(s):
1989 if ch == '\t':
1990 tabex += tab_width - 1
1991 if ch in '([{':
1992 bracketWidths.append(i + tabex + 1)
1993 elif ch in '}])' and len(bracketWidths) > 1:
1994 bracketWidths.pop()
1995 width = bracketWidths.pop()
1996 ws = g.computeLeadingWhitespace(width, tab_width)
1997 if ws:
1998 i = w.getInsertPoint()
1999 w.insert(i, ws)
2000 w.setInsertPoint(i + len(ws))
2001 w.seeInsertPoint()
2002 # 2011/10/02: Fix cursor-movement bug.
2003 #@+node:ekr.20150514063305.276: *5* ec.updateAutomatchBracket
2004 def updateAutomatchBracket(self, p, w, ch, oldSel):
2006 c = self.c
2007 d = c.scanAllDirectives(p)
2008 i, j = oldSel
2009 language = d.get('language')
2010 s = w.getAllText()
2011 if ch in ('(', '[', '{',):
2012 automatch = language not in ('plain',)
2013 if automatch:
2014 ch = ch + {'(': ')', '[': ']', '{': '}'}.get(ch)
2015 if i != j:
2016 w.delete(i, j)
2017 w.insert(i, ch)
2018 if automatch:
2019 ins = w.getInsertPoint()
2020 w.setInsertPoint(ins - 1)
2021 else:
2022 ins = w.getInsertPoint()
2023 ch2 = s[ins] if ins < len(s) else ''
2024 if ch2 in (')', ']', '}'):
2025 ins = w.getInsertPoint()
2026 w.setInsertPoint(ins + 1)
2027 else:
2028 if i != j:
2029 w.delete(i, j)
2030 w.insert(i, ch)
2031 w.setInsertPoint(i + 1)
2032 #@+node:ekr.20150514063305.277: *5* ec.updateTab & helper
2033 def updateTab(self, event, p, w, smartTab=True):
2034 """
2035 A helper for selfInsertCommand.
2037 Add spaces equivalent to a tab.
2038 """
2039 c = self.c
2040 i, j = w.getSelectionRange()
2041 # Returns insert point if no selection, with i <= j.
2042 if i != j:
2043 c.indentBody(event)
2044 return
2045 tab_width = c.getTabWidth(p)
2046 # Get the preceeding characters.
2047 s = w.getAllText()
2048 start, end = g.getLine(s, i)
2049 after = s[i:end]
2050 if after.endswith('\n'):
2051 after = after[:-1]
2052 # Only do smart tab at the start of a blank line.
2053 doSmartTab = (smartTab and c.smart_tab and i == start)
2054 # Truly at the start of the line.
2055 # and not after # Nothing *at all* after the cursor.
2056 if doSmartTab:
2057 self.updateAutoIndent(p, w)
2058 # Add a tab if otherwise nothing would happen.
2059 if s == w.getAllText():
2060 self.doPlainTab(s, i, tab_width, w)
2061 else:
2062 self.doPlainTab(s, i, tab_width, w)
2063 #@+node:ekr.20150514063305.270: *6* ec.doPlainTab
2064 def doPlainTab(self, s, i, tab_width, w):
2065 """
2066 A helper for selfInsertCommand, called from updateTab.
2068 Insert spaces equivalent to one tab.
2069 """
2070 trace = 'keys' in g.app.debug
2071 start, end = g.getLine(s, i)
2072 s2 = s[start:i]
2073 width = g.computeWidth(s2, tab_width)
2074 if trace:
2075 g.trace('width', width)
2076 if tab_width > 0:
2077 w.insert(i, '\t')
2078 ins = i + 1
2079 else:
2080 n = abs(tab_width) - (width % abs(tab_width))
2081 w.insert(i, ' ' * n)
2082 ins = i + n
2083 w.setSelectionRange(ins, ins, insert=ins)
2084 #@+node:ekr.20150514063305.280: *3* ec: lines
2085 #@+node:ekr.20150514063305.281: *4* ec.flushLines (doesn't work)
2086 @cmd('flush-lines')
2087 def flushLines(self, event):
2088 """
2089 Delete each line that contains a match for regexp, operating on the
2090 text after point.
2092 In Transient Mark mode, if the region is active, the command operates
2093 on the region instead.
2094 """
2095 k = self.c.k
2096 k.setLabelBlue('Flush lines regexp: ')
2097 k.get1Arg(event, handler=self.flushLines1)
2099 def flushLines1(self, event):
2100 k = self.c.k
2101 k.clearState()
2102 k.resetLabel()
2103 self.linesHelper(event, k.arg, 'flush')
2104 #@+node:ekr.20150514063305.282: *4* ec.keepLines (doesn't work)
2105 @cmd('keep-lines')
2106 def keepLines(self, event):
2107 """
2108 Delete each line that does not contain a match for regexp, operating on
2109 the text after point.
2111 In Transient Mark mode, if the region is active, the command operates
2112 on the region instead.
2113 """
2114 k = self.c.k
2115 k.setLabelBlue('Keep lines regexp: ')
2116 k.get1Arg(event, handler=self.keepLines1)
2118 def keepLines1(self, event):
2119 k = self.c.k
2120 k.clearState()
2121 k.resetLabel()
2122 self.linesHelper(event, k.arg, 'keep')
2123 #@+node:ekr.20150514063305.283: *4* ec.linesHelper
2124 def linesHelper(self, event, pattern, which):
2125 w = self.editWidget(event)
2126 if not w:
2127 return # pragma: no cover (defensive)
2128 self.beginCommand(w, undoType=which + '-lines')
2129 if w.hasSelection():
2130 i, end = w.getSelectionRange()
2131 else:
2132 i = w.getInsertPoint()
2133 end = 'end'
2134 txt = w.get(i, end)
2135 tlines = txt.splitlines(True)
2136 keeplines = list(tlines) if which == 'flush' else []
2137 try:
2138 regex = re.compile(pattern)
2139 for n, z in enumerate(tlines):
2140 f = regex.findall(z)
2141 if which == 'flush' and f:
2142 keeplines[n] = None
2143 elif f:
2144 keeplines.append(z)
2145 except Exception:
2146 return
2147 if which == 'flush':
2148 keeplines = [x for x in keeplines if x is not None]
2149 w.delete(i, end)
2150 w.insert(i, ''.join(keeplines))
2151 w.setInsertPoint(i)
2152 self.endCommand(changed=True, setLabel=True)
2153 #@+node:ekr.20200619082429.1: *4* ec.moveLinesToNextNode (new)
2154 @cmd('move-lines-to-next-node')
2155 def moveLineToNextNode(self, event):
2156 """Move one or *trailing* lines to the start of the next node."""
2157 c = self.c
2158 if not c.p.threadNext():
2159 return
2160 w = self.editWidget(event)
2161 if not w:
2162 return
2163 s = w.getAllText()
2164 sel_1, sel_2 = w.getSelectionRange()
2165 i, junk = g.getLine(s, sel_1)
2166 i2, j = g.getLine(s, sel_2)
2167 lines = s[i:j]
2168 if not lines.strip():
2169 return
2170 self.beginCommand(w, undoType='move-lines-to-next-node')
2171 try:
2172 next_i, next_j = g.getLine(s, j)
2173 w.delete(i, next_j)
2174 c.p.b = w.getAllText().rstrip() + '\n'
2175 c.selectPosition(c.p.threadNext())
2176 c.p.b = lines + '\n' + c.p.b
2177 c.recolor()
2178 finally:
2179 self.endCommand(changed=True, setLabel=True)
2180 #@+node:ekr.20150514063305.284: *4* ec.splitLine
2181 @cmd('split-line')
2182 def splitLine(self, event):
2183 """Split a line at the cursor position."""
2184 w = self.editWidget(event)
2185 if w:
2186 self.beginCommand(w, undoType='split-line')
2187 s = w.getAllText()
2188 ins = w.getInsertPoint()
2189 w.setAllText(s[:ins] + '\n' + s[ins:])
2190 w.setInsertPoint(ins + 1)
2191 self.endCommand(changed=True, setLabel=True)
2192 #@+node:ekr.20150514063305.285: *3* ec: move cursor
2193 #@+node:ekr.20150514063305.286: *4* ec. helpers
2194 #@+node:ekr.20150514063305.287: *5* ec.extendHelper
2195 def extendHelper(self, w, extend, spot, upOrDown=False):
2196 """
2197 Handle the details of extending the selection.
2198 This method is called for all cursor moves.
2200 extend: Clear the selection unless this is True.
2201 spot: The *new* insert point.
2202 """
2203 c, p = self.c, self.c.p
2204 extend = extend or self.extendMode
2205 ins = w.getInsertPoint()
2206 i, j = w.getSelectionRange()
2207 # Reset the move spot if needed.
2208 if self.moveSpot is None or p.v != self.moveSpotNode:
2209 self.setMoveCol(w, ins if extend else spot) # sets self.moveSpot.
2210 elif extend:
2211 # 2011/05/20: Fix bug 622819
2212 # Ctrl-Shift movement is incorrect when there is an unexpected selection.
2213 if i == j:
2214 self.setMoveCol(w, ins) # sets self.moveSpot.
2215 elif self.moveSpot in (i, j) and self.moveSpot != ins:
2216 # The bug fix, part 1.
2217 pass
2218 else:
2219 # The bug fix, part 2.
2220 # Set the moveCol to the *not* insert point.
2221 if ins == i:
2222 k = j
2223 elif ins == j:
2224 k = i
2225 else:
2226 k = ins
2227 self.setMoveCol(w, k) # sets self.moveSpot.
2228 else:
2229 if upOrDown:
2230 s = w.getAllText()
2231 i2, j2 = g.getLine(s, spot)
2232 line = s[i2:j2]
2233 row, col = g.convertPythonIndexToRowCol(s, spot)
2234 if True: # was j2 < len(s)-1:
2235 n = min(self.moveCol, max(0, len(line) - 1))
2236 else:
2237 n = min(self.moveCol, max(0, len(line))) # A tricky boundary.
2238 spot = g.convertRowColToPythonIndex(s, row, n)
2239 else: # Plain move forward or back.
2240 self.setMoveCol(w, spot) # sets self.moveSpot.
2241 if extend:
2242 if spot < self.moveSpot:
2243 w.setSelectionRange(spot, self.moveSpot, insert=spot)
2244 else:
2245 w.setSelectionRange(self.moveSpot, spot, insert=spot)
2246 else:
2247 w.setSelectionRange(spot, spot, insert=spot)
2248 w.seeInsertPoint()
2249 c.frame.updateStatusLine()
2250 #@+node:ekr.20150514063305.288: *5* ec.moveToHelper
2251 def moveToHelper(self, event, spot, extend):
2252 """
2253 Common helper method for commands the move the cursor
2254 in a way that can be described by a Tk Text expression.
2255 """
2256 c, k = self.c, self.c.k
2257 w = self.editWidget(event)
2258 if not w:
2259 return
2260 c.widgetWantsFocusNow(w)
2261 # Put the request in the proper range.
2262 if c.widget_name(w).startswith('mini'):
2263 i, j = k.getEditableTextRange()
2264 if spot < i:
2265 spot = i
2266 elif spot > j:
2267 spot = j
2268 self.extendHelper(w, extend, spot, upOrDown=False)
2269 #@+node:ekr.20150514063305.305: *5* ec.moveWithinLineHelper
2270 def moveWithinLineHelper(self, event, spot, extend):
2271 w = self.editWidget(event)
2272 if not w:
2273 return
2274 # Bug fix: 2012/02/28: don't use the Qt end-line logic:
2275 # it apparently does not work for wrapped lines.
2276 spots = ('end-line', 'finish-line', 'start-line')
2277 if hasattr(w, 'leoMoveCursorHelper') and spot not in spots:
2278 extend = extend or self.extendMode
2279 w.leoMoveCursorHelper(kind=spot, extend=extend)
2280 else:
2281 s = w.getAllText()
2282 ins = w.getInsertPoint()
2283 i, j = g.getLine(s, ins)
2284 line = s[i:j]
2285 if spot == 'begin-line': # was 'start-line'
2286 self.moveToHelper(event, i, extend=extend)
2287 elif spot == 'end-line':
2288 # Bug fix: 2011/11/13: Significant in external tests.
2289 if g.match(s, j - 1, '\n') and i != j:
2290 j -= 1
2291 self.moveToHelper(event, j, extend=extend)
2292 elif spot == 'finish-line':
2293 if not line.isspace():
2294 if g.match(s, j - 1, '\n'):
2295 j -= 1
2296 while j >= 0 and s[j].isspace():
2297 j -= 1
2298 self.moveToHelper(event, j, extend=extend)
2299 elif spot == 'start-line': # new
2300 if not line.isspace():
2301 while i < j and s[i].isspace():
2302 i += 1
2303 self.moveToHelper(event, i, extend=extend)
2304 else:
2305 g.trace(f"can not happen: bad spot: {spot}")
2306 #@+node:ekr.20150514063305.317: *5* ec.moveWordHelper
2307 def moveWordHelper(self, event, extend, forward, end=False, smart=False):
2308 """
2309 Move the cursor to the next/previous word.
2310 The cursor is placed at the start of the word unless end=True
2311 """
2312 c = self.c
2313 w = self.editWidget(event)
2314 if not w:
2315 return # pragma: no cover (defensive)
2316 c.widgetWantsFocusNow(w)
2317 s = w.getAllText()
2318 n = len(s)
2319 i = w.getInsertPoint()
2320 alphanumeric_re = re.compile(r"\w")
2321 whitespace_re = re.compile(r"\s")
2322 simple_whitespace_re = re.compile(r"[ \t]")
2323 #@+others
2324 #@+node:ekr.20150514063305.318: *6* ec.moveWordHelper functions
2325 def is_alphanumeric(c):
2326 return alphanumeric_re.match(c) is not None
2328 def is_whitespace(c):
2329 return whitespace_re.match(c) is not None
2331 def is_simple_whitespace(c):
2332 return simple_whitespace_re.match(c) is not None
2334 def is_line_break(c):
2335 return is_whitespace(c) and not is_simple_whitespace(c)
2337 def is_special(c):
2338 return not is_alphanumeric(c) and not is_whitespace(c)
2340 def seek_until_changed(i, match_function, step):
2341 while 0 <= i < n and match_function(s[i]):
2342 i += step
2343 return i
2345 def seek_word_end(i):
2346 return seek_until_changed(i, is_alphanumeric, 1)
2348 def seek_word_start(i):
2349 return seek_until_changed(i, is_alphanumeric, -1)
2351 def seek_simple_whitespace_end(i):
2352 return seek_until_changed(i, is_simple_whitespace, 1)
2354 def seek_simple_whitespace_start(i):
2355 return seek_until_changed(i, is_simple_whitespace, -1)
2357 def seek_special_end(i):
2358 return seek_until_changed(i, is_special, 1)
2360 def seek_special_start(i):
2361 return seek_until_changed(i, is_special, -1)
2362 #@-others
2363 if smart:
2364 if forward:
2365 if 0 <= i < n:
2366 if is_alphanumeric(s[i]):
2367 i = seek_word_end(i)
2368 i = seek_simple_whitespace_end(i)
2369 elif is_simple_whitespace(s[i]):
2370 i = seek_simple_whitespace_end(i)
2371 elif is_special(s[i]):
2372 i = seek_special_end(i)
2373 i = seek_simple_whitespace_end(i)
2374 else:
2375 i += 1 # e.g. for newlines
2376 else:
2377 i -= 1 # Shift cursor temporarily by -1 to get easy read access to the prev. char
2378 if 0 <= i < n:
2379 if is_alphanumeric(s[i]):
2380 i = seek_word_start(i)
2381 # Do not seek further whitespace here
2382 elif is_simple_whitespace(s[i]):
2383 i = seek_simple_whitespace_start(i)
2384 elif is_special(s[i]):
2385 i = seek_special_start(i)
2386 # Do not seek further whitespace here
2387 else:
2388 i -= 1 # e.g. for newlines
2389 i += 1
2390 else:
2391 if forward:
2392 # Unlike backward-word moves, there are two options...
2393 if end:
2394 while 0 <= i < n and not g.isWordChar(s[i]):
2395 i += 1
2396 while 0 <= i < n and g.isWordChar(s[i]):
2397 i += 1
2398 else:
2399 #1653. Scan for non-words *first*.
2400 while 0 <= i < n and not g.isWordChar(s[i]):
2401 i += 1
2402 while 0 <= i < n and g.isWordChar(s[i]):
2403 i += 1
2404 else:
2405 i -= 1
2406 while 0 <= i < n and not g.isWordChar(s[i]):
2407 i -= 1
2408 while 0 <= i < n and g.isWordChar(s[i]):
2409 i -= 1
2410 i += 1 # 2015/04/30
2411 self.moveToHelper(event, i, extend)
2412 #@+node:ekr.20150514063305.289: *5* ec.setMoveCol
2413 def setMoveCol(self, w, spot):
2414 """Set the column to which an up or down arrow will attempt to move."""
2415 p = self.c.p
2416 i, row, col = w.toPythonIndexRowCol(spot)
2417 self.moveSpot = i
2418 self.moveCol = col
2419 self.moveSpotNode = p.v
2420 #@+node:ekr.20150514063305.290: *4* ec.backToHome/ExtendSelection
2421 @cmd('back-to-home')
2422 def backToHome(self, event, extend=False):
2423 """
2424 Smart home:
2425 Position the point at the first non-blank character on the line,
2426 or the start of the line if already there.
2427 """
2428 w = self.editWidget(event)
2429 if not w:
2430 return
2431 s = w.getAllText()
2432 ins = w.getInsertPoint()
2433 if s:
2434 i, j = g.getLine(s, ins)
2435 i1 = i
2436 while i < j and s[i] in ' \t':
2437 i += 1
2438 if i == ins:
2439 i = i1
2440 self.moveToHelper(event, i, extend=extend)
2442 @cmd('back-to-home-extend-selection')
2443 def backToHomeExtendSelection(self, event):
2444 self.backToHome(event, extend=True)
2445 #@+node:ekr.20150514063305.291: *4* ec.backToIndentation
2446 @cmd('back-to-indentation')
2447 def backToIndentation(self, event):
2448 """Position the point at the first non-blank character on the line."""
2449 w = self.editWidget(event)
2450 if not w:
2451 return # pragma: no cover (defensive)
2452 s = w.getAllText()
2453 ins = w.getInsertPoint()
2454 i, j = g.getLine(s, ins)
2455 while i < j and s[i] in ' \t':
2456 i += 1
2457 self.moveToHelper(event, i, extend=False)
2458 #@+node:ekr.20150514063305.316: *4* ec.backward*/ExtendSelection
2459 @cmd('back-word')
2460 def backwardWord(self, event):
2461 """Move the cursor to the previous word."""
2462 self.moveWordHelper(event, extend=False, forward=False)
2464 @cmd('back-word-extend-selection')
2465 def backwardWordExtendSelection(self, event):
2466 """Extend the selection by moving the cursor to the previous word."""
2467 self.moveWordHelper(event, extend=True, forward=False)
2469 @cmd('back-word-smart')
2470 def backwardWordSmart(self, event):
2471 """Move the cursor to the beginning of the current or the end of the previous word."""
2472 self.moveWordHelper(event, extend=False, forward=False, smart=True)
2474 @cmd('back-word-smart-extend-selection')
2475 def backwardWordSmartExtendSelection(self, event):
2476 """Extend the selection by moving the cursor to the beginning of the current
2477 or the end of the previous word."""
2478 self.moveWordHelper(event, extend=True, forward=False, smart=True)
2479 #@+node:ekr.20170707072347.1: *4* ec.beginningOfLine/ExtendSelection
2480 @cmd('beginning-of-line')
2481 def beginningOfLine(self, event):
2482 """Move the cursor to the first character of the line."""
2483 self.moveWithinLineHelper(event, 'begin-line', extend=False)
2485 @cmd('beginning-of-line-extend-selection')
2486 def beginningOfLineExtendSelection(self, event):
2487 """
2488 Extend the selection by moving the cursor to the first character of the
2489 line.
2490 """
2491 self.moveWithinLineHelper(event, 'begin-line', extend=True)
2492 #@+node:ekr.20150514063305.292: *4* ec.between lines & helper
2493 @cmd('next-line')
2494 def nextLine(self, event):
2495 """Move the cursor down, extending the selection if in extend mode."""
2496 self.moveUpOrDownHelper(event, 'down', extend=False)
2498 @cmd('next-line-extend-selection')
2499 def nextLineExtendSelection(self, event):
2500 """Extend the selection by moving the cursor down."""
2501 self.moveUpOrDownHelper(event, 'down', extend=True)
2503 @cmd('previous-line')
2504 def prevLine(self, event):
2505 """Move the cursor up, extending the selection if in extend mode."""
2506 self.moveUpOrDownHelper(event, 'up', extend=False)
2508 @cmd('previous-line-extend-selection')
2509 def prevLineExtendSelection(self, event):
2510 """Extend the selection by moving the cursor up."""
2511 self.moveUpOrDownHelper(event, 'up', extend=True)
2512 #@+node:ekr.20150514063305.293: *5* ec.moveUpOrDownHelper
2513 def moveUpOrDownHelper(self, event, direction, extend):
2515 w = self.editWidget(event)
2516 if not w:
2517 return # pragma: no cover (defensive)
2518 ins = w.getInsertPoint()
2519 s = w.getAllText()
2520 w.seeInsertPoint()
2521 if hasattr(w, 'leoMoveCursorHelper'):
2522 extend = extend or self.extendMode
2523 w.leoMoveCursorHelper(kind=direction, extend=extend)
2524 else:
2525 # Find the start of the next/prev line.
2526 row, col = g.convertPythonIndexToRowCol(s, ins)
2527 i, j = g.getLine(s, ins)
2528 if direction == 'down':
2529 i2, j2 = g.getLine(s, j)
2530 else:
2531 i2, j2 = g.getLine(s, i - 1)
2532 # The spot is the start of the line plus the column index.
2533 n = max(0, j2 - i2 - 1) # The length of the new line.
2534 col2 = min(col, n)
2535 spot = i2 + col2
2536 self.extendHelper(w, extend, spot, upOrDown=True)
2537 #@+node:ekr.20150514063305.294: *4* ec.buffers & helper
2538 @cmd('beginning-of-buffer')
2539 def beginningOfBuffer(self, event):
2540 """Move the cursor to the start of the body text."""
2541 self.moveToBufferHelper(event, 'home', extend=False)
2543 @cmd('beginning-of-buffer-extend-selection')
2544 def beginningOfBufferExtendSelection(self, event):
2545 """Extend the text selection by moving the cursor to the start of the body text."""
2546 self.moveToBufferHelper(event, 'home', extend=True)
2548 @cmd('end-of-buffer')
2549 def endOfBuffer(self, event):
2550 """Move the cursor to the end of the body text."""
2551 self.moveToBufferHelper(event, 'end', extend=False)
2553 @cmd('end-of-buffer-extend-selection')
2554 def endOfBufferExtendSelection(self, event):
2555 """Extend the text selection by moving the cursor to the end of the body text."""
2556 self.moveToBufferHelper(event, 'end', extend=True)
2557 #@+node:ekr.20150514063305.295: *5* ec.moveToBufferHelper
2558 def moveToBufferHelper(self, event, spot, extend):
2559 w = self.editWidget(event)
2560 if not w:
2561 return # pragma: no cover (defensive)
2562 if hasattr(w, 'leoMoveCursorHelper'):
2563 extend = extend or self.extendMode
2564 w.leoMoveCursorHelper(kind=spot, extend=extend)
2565 else:
2566 if spot == 'home':
2567 self.moveToHelper(event, 0, extend=extend)
2568 elif spot == 'end':
2569 s = w.getAllText()
2570 self.moveToHelper(event, len(s), extend=extend)
2571 else:
2572 g.trace('can not happen: bad spot', spot) # pragma: no cover (defensive)
2573 #@+node:ekr.20150514063305.296: *4* ec.characters & helper
2574 @cmd('back-char')
2575 def backCharacter(self, event):
2576 """Move the cursor back one character, extending the selection if in extend mode."""
2577 self.moveToCharacterHelper(event, 'left', extend=False)
2579 @cmd('back-char-extend-selection')
2580 def backCharacterExtendSelection(self, event):
2581 """Extend the selection by moving the cursor back one character."""
2582 self.moveToCharacterHelper(event, 'left', extend=True)
2584 @cmd('forward-char')
2585 def forwardCharacter(self, event):
2586 """Move the cursor forward one character, extending the selection if in extend mode."""
2587 self.moveToCharacterHelper(event, 'right', extend=False)
2589 @cmd('forward-char-extend-selection')
2590 def forwardCharacterExtendSelection(self, event):
2591 """Extend the selection by moving the cursor forward one character."""
2592 self.moveToCharacterHelper(event, 'right', extend=True)
2593 #@+node:ekr.20150514063305.297: *5* ec.moveToCharacterHelper
2594 def moveToCharacterHelper(self, event, spot, extend):
2595 w = self.editWidget(event)
2596 if not w:
2597 return
2598 if hasattr(w, 'leoMoveCursorHelper'):
2599 extend = extend or self.extendMode
2600 w.leoMoveCursorHelper(kind=spot, extend=extend)
2601 else:
2602 i = w.getInsertPoint()
2603 if spot == 'left':
2604 i = max(0, i - 1)
2605 self.moveToHelper(event, i, extend=extend)
2606 elif spot == 'right':
2607 i = min(i + 1, len(w.getAllText()))
2608 self.moveToHelper(event, i, extend=extend)
2609 else:
2610 g.trace(f"can not happen: bad spot: {spot}")
2611 #@+node:ekr.20150514063305.298: *4* ec.clear/set/ToggleExtendMode
2612 @cmd('clear-extend-mode')
2613 def clearExtendMode(self, event):
2614 """Turn off extend mode: cursor movement commands do not extend the selection."""
2615 self.extendModeHelper(event, False)
2617 @cmd('set-extend-mode')
2618 def setExtendMode(self, event):
2619 """Turn on extend mode: cursor movement commands do extend the selection."""
2620 self.extendModeHelper(event, True)
2622 @cmd('toggle-extend-mode')
2623 def toggleExtendMode(self, event):
2624 """Toggle extend mode, i.e., toggle whether cursor movement commands extend the selections."""
2625 self.extendModeHelper(event, not self.extendMode)
2627 def extendModeHelper(self, event, val):
2628 c = self.c
2629 w = self.editWidget(event)
2630 if w:
2631 self.extendMode = val
2632 if not g.unitTesting:
2633 # g.red('extend mode','on' if val else 'off'))
2634 c.k.showStateAndMode()
2635 c.widgetWantsFocusNow(w)
2636 #@+node:ekr.20170707072524.1: *4* ec.endOfLine/ExtendSelection
2637 @cmd('end-of-line')
2638 def endOfLine(self, event):
2639 """Move the cursor to the last character of the line."""
2640 self.moveWithinLineHelper(event, 'end-line', extend=False)
2642 @cmd('end-of-line-extend-selection')
2643 def endOfLineExtendSelection(self, event):
2644 """Extend the selection by moving the cursor to the last character of the line."""
2645 self.moveWithinLineHelper(event, 'end-line', extend=True)
2646 #@+node:ekr.20150514063305.299: *4* ec.exchangePointMark
2647 @cmd('exchange-point-mark')
2648 def exchangePointMark(self, event):
2649 """
2650 Exchange the point (insert point) with the mark (the other end of the
2651 selected text).
2652 """
2653 c = self.c
2654 w = self.editWidget(event)
2655 if not w:
2656 return
2657 if hasattr(w, 'leoMoveCursorHelper'):
2658 w.leoMoveCursorHelper(kind='exchange', extend=False)
2659 else:
2660 c.widgetWantsFocusNow(w)
2661 i, j = w.getSelectionRange(sort=False)
2662 if i == j:
2663 return
2664 ins = w.getInsertPoint()
2665 ins = j if ins == i else i
2666 w.setInsertPoint(ins)
2667 w.setSelectionRange(i, j, insert=None)
2668 #@+node:ekr.20150514063305.300: *4* ec.extend-to-line
2669 @cmd('extend-to-line')
2670 def extendToLine(self, event):
2671 """Select the line at the cursor."""
2672 w = self.editWidget(event)
2673 if not w:
2674 return
2675 s = w.getAllText()
2676 n = len(s)
2677 i = w.getInsertPoint()
2678 while 0 <= i < n and not s[i] == '\n':
2679 i -= 1
2680 i += 1
2681 i1 = i
2682 while 0 <= i < n and not s[i] == '\n':
2683 i += 1
2684 w.setSelectionRange(i1, i)
2685 #@+node:ekr.20150514063305.301: *4* ec.extend-to-sentence
2686 @cmd('extend-to-sentence')
2687 def extendToSentence(self, event):
2688 """Select the line at the cursor."""
2689 w = self.editWidget(event)
2690 if not w:
2691 return # pragma: no cover (defensive)
2692 s = w.getAllText()
2693 n = len(s)
2694 i = w.getInsertPoint()
2695 i2 = 1 + s.find('.', i)
2696 if i2 == -1:
2697 i2 = n
2698 i1 = 1 + s.rfind('.', 0, i2 - 1)
2699 w.setSelectionRange(i1, i2)
2700 #@+node:ekr.20150514063305.302: *4* ec.extend-to-word
2701 @cmd('extend-to-word')
2702 def extendToWord(self, event, select=True, w=None):
2703 """Compute the word at the cursor. Select it if select arg is True."""
2704 if not w:
2705 w = self.editWidget(event)
2706 if not w:
2707 return 0, 0 # pragma: no cover (defensive)
2708 s = w.getAllText()
2709 n = len(s)
2710 i = i1 = w.getInsertPoint()
2711 # Find a word char on the present line if one isn't at the cursor.
2712 if not (0 <= i < n and g.isWordChar(s[i])):
2713 # First, look forward
2714 while i < n and not g.isWordChar(s[i]) and s[i] != '\n':
2715 i += 1
2716 # Next, look backward.
2717 if not (0 <= i < n and g.isWordChar(s[i])):
2718 i = i1 - 1 if (i >= n or s[i] == '\n') else i1
2719 while i >= 0 and not g.isWordChar(s[i]) and s[i] != '\n':
2720 i -= 1
2721 # Make sure s[i] is a word char.
2722 if 0 <= i < n and g.isWordChar(s[i]):
2723 # Find the start of the word.
2724 while 0 <= i < n and g.isWordChar(s[i]):
2725 i -= 1
2726 i += 1
2727 i1 = i
2728 # Find the end of the word.
2729 while 0 <= i < n and g.isWordChar(s[i]):
2730 i += 1
2731 if select:
2732 w.setSelectionRange(i1, i)
2733 return i1, i
2734 return 0, 0
2735 #@+node:ekr.20170707072837.1: *4* ec.finishOfLine/ExtendSelection
2736 @cmd('finish-of-line')
2737 def finishOfLine(self, event):
2738 """Move the cursor to the last character of the line."""
2739 self.moveWithinLineHelper(event, 'finish-line', extend=False)
2741 @cmd('finish-of-line-extend-selection')
2742 def finishOfLineExtendSelection(self, event):
2743 """Extend the selection by moving the cursor to the last character of the line."""
2744 self.moveWithinLineHelper(event, 'finish-line', extend=True)
2745 #@+node:ekr.20170707160947.1: *4* ec.forward*/ExtendSelection
2746 @cmd('forward-end-word')
2747 def forwardEndWord(self, event): # New in Leo 4.4.2
2748 """Move the cursor to the next word."""
2749 self.moveWordHelper(event, extend=False, forward=True, end=True)
2751 @cmd('forward-end-word-extend-selection')
2752 def forwardEndWordExtendSelection(self, event): # New in Leo 4.4.2
2753 """Extend the selection by moving the cursor to the next word."""
2754 self.moveWordHelper(event, extend=True, forward=True, end=True)
2756 @cmd('forward-word')
2757 def forwardWord(self, event):
2758 """Move the cursor to the next word."""
2759 self.moveWordHelper(event, extend=False, forward=True)
2761 @cmd('forward-word-extend-selection')
2762 def forwardWordExtendSelection(self, event):
2763 """Extend the selection by moving the cursor to the end of the next word."""
2764 self.moveWordHelper(event, extend=True, forward=True)
2766 @cmd('forward-word-smart')
2767 def forwardWordSmart(self, event):
2768 """Move the cursor to the end of the current or the beginning of the next word."""
2769 self.moveWordHelper(event, extend=False, forward=True, smart=True)
2771 @cmd('forward-word-smart-extend-selection')
2772 def forwardWordSmartExtendSelection(self, event):
2773 """Extend the selection by moving the cursor to the end of the current
2774 or the beginning of the next word."""
2775 self.moveWordHelper(event, extend=True, forward=True, smart=True)
2776 #@+node:ekr.20150514063305.303: *4* ec.movePastClose & helper
2777 @cmd('move-past-close')
2778 def movePastClose(self, event):
2779 """Move the cursor past the closing parenthesis."""
2780 self.movePastCloseHelper(event, extend=False)
2782 @cmd('move-past-close-extend-selection')
2783 def movePastCloseExtendSelection(self, event):
2784 """Extend the selection by moving the cursor past the closing parenthesis."""
2785 self.movePastCloseHelper(event, extend=True)
2786 #@+node:ekr.20150514063305.304: *5* ec.movePastCloseHelper
2787 def movePastCloseHelper(self, event, extend):
2788 c = self.c
2789 w = self.editWidget(event)
2790 if not w:
2791 return
2792 c.widgetWantsFocusNow(w)
2793 s = w.getAllText()
2794 ins = w.getInsertPoint()
2795 # Scan backwards for i,j.
2796 i = ins
2797 while len(s) > i >= 0 and s[i] != '\n':
2798 if s[i] == '(':
2799 break
2800 i -= 1
2801 else:
2802 return
2803 j = ins
2804 while len(s) > j >= 0 and s[j] != '\n':
2805 if s[j] == '(':
2806 break
2807 j -= 1
2808 if i < j:
2809 return
2810 # Scan forward for i2,j2.
2811 i2 = ins
2812 while i2 < len(s) and s[i2] != '\n':
2813 if s[i2] == ')':
2814 break
2815 i2 += 1
2816 else:
2817 return
2818 j2 = ins
2819 while j2 < len(s) and s[j2] != '\n':
2820 if s[j2] == ')':
2821 break
2822 j2 += 1
2823 if i2 > j2:
2824 return
2825 self.moveToHelper(event, i2 + 1, extend)
2826 #@+node:ekr.20150514063305.306: *4* ec.pages & helper
2827 @cmd('back-page')
2828 def backPage(self, event):
2829 """Move the cursor back one page,
2830 extending the selection if in extend mode."""
2831 self.movePageHelper(event, kind='back', extend=False)
2833 @cmd('back-page-extend-selection')
2834 def backPageExtendSelection(self, event):
2835 """Extend the selection by moving the cursor back one page."""
2836 self.movePageHelper(event, kind='back', extend=True)
2838 @cmd('forward-page')
2839 def forwardPage(self, event):
2840 """Move the cursor forward one page,
2841 extending the selection if in extend mode."""
2842 self.movePageHelper(event, kind='forward', extend=False)
2844 @cmd('forward-page-extend-selection')
2845 def forwardPageExtendSelection(self, event):
2846 """Extend the selection by moving the cursor forward one page."""
2847 self.movePageHelper(event, kind='forward', extend=True)
2848 #@+node:ekr.20150514063305.307: *5* ec.movePageHelper
2849 def movePageHelper(self, event, kind, extend): # kind in back/forward.
2850 """Move the cursor up/down one page, possibly extending the selection."""
2851 w = self.editWidget(event)
2852 if not w:
2853 return
2854 linesPerPage = 15 # To do.
2855 if hasattr(w, 'leoMoveCursorHelper'):
2856 extend = extend or self.extendMode
2857 w.leoMoveCursorHelper(
2858 kind='page-down' if kind == 'forward' else 'page-up',
2859 extend=extend, linesPerPage=linesPerPage)
2860 # w.seeInsertPoint()
2861 # c.frame.updateStatusLine()
2862 # w.rememberSelectionAndScroll()
2863 else:
2864 ins = w.getInsertPoint()
2865 s = w.getAllText()
2866 lines = g.splitLines(s)
2867 row, col = g.convertPythonIndexToRowCol(s, ins)
2868 if kind == 'back':
2869 row2 = max(0, row - linesPerPage)
2870 else:
2871 row2 = min(row + linesPerPage, len(lines) - 1)
2872 if row == row2:
2873 return
2874 spot = g.convertRowColToPythonIndex(s, row2, col, lines=lines)
2875 self.extendHelper(w, extend, spot, upOrDown=True)
2876 #@+node:ekr.20150514063305.308: *4* ec.paragraphs & helpers
2877 @cmd('back-paragraph')
2878 def backwardParagraph(self, event):
2879 """Move the cursor to the previous paragraph."""
2880 self.backwardParagraphHelper(event, extend=False)
2882 @cmd('back-paragraph-extend-selection')
2883 def backwardParagraphExtendSelection(self, event):
2884 """Extend the selection by moving the cursor to the previous paragraph."""
2885 self.backwardParagraphHelper(event, extend=True)
2887 @cmd('forward-paragraph')
2888 def forwardParagraph(self, event):
2889 """Move the cursor to the next paragraph."""
2890 self.forwardParagraphHelper(event, extend=False)
2892 @cmd('forward-paragraph-extend-selection')
2893 def forwardParagraphExtendSelection(self, event):
2894 """Extend the selection by moving the cursor to the next paragraph."""
2895 self.forwardParagraphHelper(event, extend=True)
2896 #@+node:ekr.20150514063305.309: *5* ec.backwardParagraphHelper
2897 def backwardParagraphHelper(self, event, extend):
2898 w = self.editWidget(event)
2899 if not w:
2900 return # pragma: no cover (defensive)
2901 s = w.getAllText()
2902 i, j = w.getSelectionRange()
2903 i, j = g.getLine(s, j)
2904 line = s[i:j]
2905 if line.strip():
2906 # Find the start of the present paragraph.
2907 while i > 0:
2908 i, j = g.getLine(s, i - 1)
2909 line = s[i:j]
2910 if not line.strip():
2911 break
2912 # Find the end of the previous paragraph.
2913 while i > 0:
2914 i, j = g.getLine(s, i - 1)
2915 line = s[i:j]
2916 if line.strip():
2917 i = j - 1
2918 break
2919 self.moveToHelper(event, i, extend)
2920 #@+node:ekr.20150514063305.310: *5* ec.forwardParagraphHelper
2921 def forwardParagraphHelper(self, event, extend):
2922 w = self.editWidget(event)
2923 if not w:
2924 return
2925 s = w.getAllText()
2926 ins = w.getInsertPoint()
2927 i, j = g.getLine(s, ins)
2928 line = s[i:j]
2929 if line.strip(): # Skip past the present paragraph.
2930 self.selectParagraphHelper(w, i)
2931 i, j = w.getSelectionRange()
2932 j += 1
2933 # Skip to the next non-blank line.
2934 i = j
2935 while j < len(s):
2936 i, j = g.getLine(s, j)
2937 line = s[i:j]
2938 if line.strip():
2939 break
2940 w.setInsertPoint(ins) # Restore the original insert point.
2941 self.moveToHelper(event, i, extend)
2942 #@+node:ekr.20170707093335.1: *4* ec.pushCursor and popCursor
2943 @cmd('pop-cursor')
2944 def popCursor(self, event=None):
2945 """Restore the node, selection range and insert point from the stack."""
2946 c = self.c
2947 w = self.editWidget(event)
2948 if w and self.cursorStack:
2949 p, i, j, ins = self.cursorStack.pop()
2950 if c.positionExists(p):
2951 c.selectPosition(p)
2952 c.redraw()
2953 w.setSelectionRange(i, j, insert=ins)
2954 c.bodyWantsFocus()
2955 else:
2956 g.es('invalid position', c.p.h)
2957 elif not w:
2958 g.es('no stacked cursor', color='blue')
2960 @cmd('push-cursor')
2961 def pushCursor(self, event=None):
2962 """Push the selection range and insert point on the stack."""
2963 c = self.c
2964 w = self.editWidget(event)
2965 if w:
2966 p = c.p.copy()
2967 i, j = w.getSelectionRange()
2968 ins = w.getInsertPoint()
2969 self.cursorStack.append((p, i, j, ins),)
2970 else:
2971 g.es('cursor not pushed', color='blue')
2972 #@+node:ekr.20150514063305.311: *4* ec.selectAllText
2973 @cmd('select-all')
2974 def selectAllText(self, event):
2975 """Select all text."""
2976 k = self.c.k
2977 w = self.editWidget(event)
2978 if not w:
2979 return
2980 # Bug fix 2013/12/13: Special case the minibuffer.
2981 if w == k.w:
2982 k.selectAll()
2983 elif w and g.isTextWrapper(w):
2984 w.selectAllText()
2985 #@+node:ekr.20150514063305.312: *4* ec.sentences & helpers
2986 @cmd('back-sentence')
2987 def backSentence(self, event):
2988 """Move the cursor to the previous sentence."""
2989 self.backSentenceHelper(event, extend=False)
2991 @cmd('back-sentence-extend-selection')
2992 def backSentenceExtendSelection(self, event):
2993 """Extend the selection by moving the cursor to the previous sentence."""
2994 self.backSentenceHelper(event, extend=True)
2996 @cmd('forward-sentence')
2997 def forwardSentence(self, event):
2998 """Move the cursor to the next sentence."""
2999 self.forwardSentenceHelper(event, extend=False)
3001 @cmd('forward-sentence-extend-selection')
3002 def forwardSentenceExtendSelection(self, event):
3003 """Extend the selection by moving the cursor to the next sentence."""
3004 self.forwardSentenceHelper(event, extend=True)
3005 #@+node:ekr.20150514063305.313: *5* ec.backSentenceHelper
3006 def backSentenceHelper(self, event, extend):
3007 c = self.c
3008 w = self.editWidget(event)
3009 if not w:
3010 return # pragma: no cover (defensive)
3011 c.widgetWantsFocusNow(w)
3012 s = w.getAllText()
3013 ins = w.getInsertPoint()
3014 # Find the starting point of the scan.
3015 i = ins
3016 i -= 1 # Ensure some progress.
3017 if i < 0 or i >= len(s):
3018 return
3019 # Tricky.
3020 if s[i] == '.':
3021 i -= 1
3022 while i >= 0 and s[i] in ' \n':
3023 i -= 1
3024 if i >= ins:
3025 i -= 1
3026 if i >= len(s):
3027 i -= 1
3028 if i <= 0:
3029 return
3030 if s[i] == '.':
3031 i -= 1
3032 # Scan backwards to the end of the paragraph.
3033 # Stop at empty lines.
3034 # Skip periods within words.
3035 # Stop at sentences ending in non-periods.
3036 end = False
3037 while not end and i >= 0:
3038 progress = i
3039 if s[i] == '.':
3040 # Skip periods surrounded by letters/numbers
3041 if i > 0 and s[i - 1].isalnum() and s[i + 1].isalnum():
3042 i -= 1
3043 else:
3044 i += 1
3045 while i < len(s) and s[i] in ' \n':
3046 i += 1
3047 i -= 1
3048 break
3049 elif s[i] == '\n':
3050 j = i - 1
3051 while j >= 0:
3052 if s[j] == '\n':
3053 # Don't include first newline.
3054 end = True
3055 break # found blank line.
3056 elif s[j] == ' ':
3057 j -= 1
3058 else:
3059 i -= 1
3060 break # no blank line found.
3061 else:
3062 # No blank line found.
3063 i -= 1
3064 else:
3065 i -= 1
3066 assert end or progress > i
3067 i += 1
3068 if i < ins:
3069 self.moveToHelper(event, i, extend)
3070 #@+node:ekr.20150514063305.314: *5* ec.forwardSentenceHelper
3071 def forwardSentenceHelper(self, event, extend):
3072 c = self.c
3073 w = self.editWidget(event)
3074 if not w:
3075 return
3076 c.widgetWantsFocusNow(w)
3077 s = w.getAllText()
3078 ins = w.getInsertPoint()
3079 if ins >= len(s):
3080 return
3081 # Find the starting point of the scan.
3082 i = ins
3083 if i + 1 < len(s) and s[i + 1] == '.':
3084 i += 1
3085 if s[i] == '.':
3086 i += 1
3087 else:
3088 while i < len(s) and s[i] in ' \n':
3089 i += 1
3090 i -= 1
3091 if i <= ins:
3092 i += 1
3093 if i >= len(s):
3094 return
3095 # Scan forward to the end of the paragraph.
3096 # Stop at empty lines.
3097 # Skip periods within words.
3098 # Stop at sentences ending in non-periods.
3099 end = False
3100 while not end and i < len(s):
3101 progress = i
3102 if s[i] == '.':
3103 # Skip periods surrounded by letters/numbers
3104 if 0 < i < len(s) and s[i - 1].isalnum() and s[i + 1].isalnum():
3105 i += 1
3106 else:
3107 i += 1
3108 break # Include the paragraph.
3109 elif s[i] == '\n':
3110 j = i + 1
3111 while j < len(s):
3112 if s[j] == '\n':
3113 # Don't include first newline.
3114 end = True
3115 break # found blank line.
3116 elif s[j] == ' ':
3117 j += 1
3118 else:
3119 i += 1
3120 break # no blank line found.
3121 else:
3122 # No blank line found.
3123 i += 1
3124 else:
3125 i += 1
3126 assert end or progress < i
3127 i = min(i, len(s))
3128 if i > ins:
3129 self.moveToHelper(event, i, extend)
3130 #@+node:ekr.20170707072644.1: *4* ec.startOfLine/ExtendSelection
3131 @cmd('start-of-line')
3132 def startOfLine(self, event):
3133 """Move the cursor to first non-blank character of the line."""
3134 self.moveWithinLineHelper(event, 'start-line', extend=False)
3136 @cmd('start-of-line-extend-selection')
3137 def startOfLineExtendSelection(self, event):
3138 """
3139 Extend the selection by moving the cursor to first non-blank character
3140 of the line.
3141 """
3142 self.moveWithinLineHelper(event, 'start-line', extend=True)
3143 #@+node:ekr.20150514063305.319: *3* ec: paragraph
3144 #@+node:ekr.20150514063305.320: *4* ec.backwardKillParagraph
3145 @cmd('backward-kill-paragraph')
3146 def backwardKillParagraph(self, event):
3147 """Kill the previous paragraph."""
3148 c = self.c
3149 w = self.editWidget(event)
3150 if not w:
3151 return # pragma: no cover (defensive)
3152 self.beginCommand(w, undoType='backward-kill-paragraph')
3153 try:
3154 self.backwardParagraphHelper(event, extend=True)
3155 i, j = w.getSelectionRange()
3156 if i > 0:
3157 i = min(i + 1, j)
3158 c.killBufferCommands.killParagraphHelper(event, i, j)
3159 w.setSelectionRange(i, i, insert=i)
3160 finally:
3161 self.endCommand(changed=True, setLabel=True)
3162 #@+node:ekr.20150514063305.321: *4* ec.fillRegion
3163 @cmd('fill-region')
3164 def fillRegion(self, event):
3165 """Fill all paragraphs in the selected text."""
3166 c, p = self.c, self.c.p
3167 undoType = 'fill-region'
3168 w = self.editWidget(event)
3169 i, j = w.getSelectionRange()
3170 c.undoer.beforeChangeGroup(p, undoType)
3171 while 1:
3172 progress = w.getInsertPoint()
3173 c.reformatParagraph(event, undoType='reformat-paragraph')
3174 ins = w.getInsertPoint()
3175 s = w.getAllText()
3176 w.setInsertPoint(ins)
3177 if progress >= ins or ins >= j or ins >= len(s):
3178 break
3179 c.undoer.afterChangeGroup(p, undoType)
3180 #@+node:ekr.20150514063305.322: *4* ec.fillRegionAsParagraph
3181 @cmd('fill-region-as-paragraph')
3182 def fillRegionAsParagraph(self, event):
3183 """Fill the selected text."""
3184 w = self.editWidget(event)
3185 if not w or not self._chckSel(event):
3186 return # pragma: no cover (defensive)
3187 self.beginCommand(w, undoType='fill-region-as-paragraph')
3188 self.endCommand(changed=True, setLabel=True)
3189 #@+node:ekr.20150514063305.323: *4* ec.fillParagraph
3190 @cmd('fill-paragraph')
3191 def fillParagraph(self, event):
3192 """Fill the selected paragraph"""
3193 w = self.editWidget(event)
3194 if not w:
3195 return # pragma: no cover (defensive)
3196 # Clear the selection range.
3197 i, j = w.getSelectionRange()
3198 w.setSelectionRange(i, i, insert=i)
3199 self.c.reformatParagraph(event)
3200 #@+node:ekr.20150514063305.324: *4* ec.killParagraph
3201 @cmd('kill-paragraph')
3202 def killParagraph(self, event):
3203 """Kill the present paragraph."""
3204 c = self.c
3205 w = self.editWidget(event)
3206 if not w:
3207 return
3208 self.beginCommand(w, undoType='kill-paragraph')
3209 try:
3210 self.extendToParagraph(event)
3211 i, j = w.getSelectionRange()
3212 c.killBufferCommands.killParagraphHelper(event, i, j)
3213 w.setSelectionRange(i, i, insert=i)
3214 finally:
3215 self.endCommand(changed=True, setLabel=True)
3216 #@+node:ekr.20150514063305.325: *4* ec.extend-to-paragraph & helper
3217 @cmd('extend-to-paragraph')
3218 def extendToParagraph(self, event):
3219 """Select the paragraph surrounding the cursor."""
3220 w = self.editWidget(event)
3221 if not w:
3222 return
3223 s = w.getAllText()
3224 ins = w.getInsertPoint()
3225 i, j = g.getLine(s, ins)
3226 line = s[i:j]
3227 # Find the start of the paragraph.
3228 if line.strip(): # Search backward.
3229 while i > 0:
3230 i2, j2 = g.getLine(s, i - 1)
3231 line = s[i2:j2]
3232 if line.strip():
3233 i = i2
3234 else:
3235 break # Use the previous line.
3236 else: # Search forward.
3237 while j < len(s):
3238 i, j = g.getLine(s, j)
3239 line = s[i:j]
3240 if line.strip():
3241 break
3242 else: return
3243 # Select from i to the end of the paragraph.
3244 self.selectParagraphHelper(w, i)
3245 #@+node:ekr.20150514063305.326: *5* ec.selectParagraphHelper
3246 def selectParagraphHelper(self, w, start):
3247 """Select from start to the end of the paragraph."""
3248 s = w.getAllText()
3249 i1, j = g.getLine(s, start)
3250 while j < len(s):
3251 i, j2 = g.getLine(s, j)
3252 line = s[i:j2]
3253 if line.strip():
3254 j = j2
3255 else:
3256 break
3257 j = max(start, j - 1)
3258 w.setSelectionRange(i1, j, insert=j)
3259 #@+node:ekr.20150514063305.327: *3* ec: region
3260 #@+node:ekr.20150514063305.328: *4* ec.tabIndentRegion (indent-rigidly)
3261 @cmd('indent-rigidly')
3262 def tabIndentRegion(self, event):
3263 """Insert a hard tab at the start of each line of the selected text."""
3264 w = self.editWidget(event)
3265 if not w or not self._chckSel(event):
3266 return # pragma: no cover (defensive)
3267 self.beginCommand(w, undoType='indent-rigidly')
3268 s = w.getAllText()
3269 i1, j1 = w.getSelectionRange()
3270 i, junk = g.getLine(s, i1)
3271 junk, j = g.getLine(s, j1)
3272 lines = g.splitlines(s[i:j])
3273 n = len(lines)
3274 lines_s = ''.join('\t' + line for line in lines)
3275 s = s[:i] + lines_s + s[j:]
3276 w.setAllText(s)
3277 # Retain original row/col selection.
3278 w.setSelectionRange(i1, j1 + n, insert=j1 + n)
3279 self.endCommand(changed=True, setLabel=True)
3280 #@+node:ekr.20150514063305.329: *4* ec.countRegion
3281 @cmd('count-region')
3282 def countRegion(self, event):
3283 """Print the number of lines and characters in the selected text."""
3284 k = self.c.k
3285 w = self.editWidget(event)
3286 if not w:
3287 return # pragma: no cover (defensive)
3288 txt = w.getSelectedText()
3289 lines = 1
3290 chars = 0
3291 for z in txt:
3292 if z == '\n':
3293 lines += 1
3294 else: chars += 1
3295 k.setLabelGrey(
3296 f"Region has {lines} lines, "
3297 f"{chars} character{g.plural(chars)}")
3298 #@+node:ekr.20150514063305.330: *4* ec.moveLinesDown
3299 @cmd('move-lines-down')
3300 def moveLinesDown(self, event):
3301 """
3302 Move all lines containing any selected text down one line,
3303 moving to the next node if the lines are the last lines of the body.
3304 """
3305 c = self.c
3306 w = self.editWidget(event)
3307 if not w:
3308 return
3309 s = w.getAllText()
3310 sel_1, sel_2 = w.getSelectionRange()
3311 insert_pt = w.getInsertPoint()
3312 i, junk = g.getLine(s, sel_1)
3313 i2, j = g.getLine(s, sel_2)
3314 lines = s[i:j]
3315 # Select from start of the first line to the *start* of the last line.
3316 # This prevents selection creep.
3317 self.beginCommand(w, undoType='move-lines-down')
3318 try:
3319 next_i, next_j = g.getLine(s, j) # 2011/04/01: was j+1
3320 next_line = s[next_i:next_j]
3321 n2 = next_j - next_i
3322 if j < len(s):
3323 w.delete(i, next_j)
3324 if next_line.endswith('\n'):
3325 # Simply swap positions with next line
3326 new_lines = next_line + lines
3327 else:
3328 # Last line of the body to be moved up doesn't end in a newline
3329 # while we have to remove the newline from the line above moving down.
3330 new_lines = next_line + '\n' + lines[:-1]
3331 n2 += 1
3332 w.insert(i, new_lines)
3333 w.setSelectionRange(sel_1 + n2, sel_2 + n2, insert=insert_pt + n2)
3334 else:
3335 # Leo 5.6: insert a blank line before the selected lines.
3336 w.insert(i, '\n')
3337 w.setSelectionRange(sel_1 + 1, sel_2 + 1, insert=insert_pt + 1)
3338 # Fix bug 799695: colorizer bug after move-lines-up into a docstring
3339 c.recolor()
3340 finally:
3341 self.endCommand(changed=True, setLabel=True)
3342 #@+node:ekr.20150514063305.331: *4* ec.moveLinesUp
3343 @cmd('move-lines-up')
3344 def moveLinesUp(self, event):
3345 """
3346 Move all lines containing any selected text up one line,
3347 moving to the previous node as needed.
3348 """
3349 c = self.c
3350 w = self.editWidget(event)
3351 if not w:
3352 return # pragma: no cover (defensive)
3353 s = w.getAllText()
3354 sel_1, sel_2 = w.getSelectionRange()
3355 insert_pt = w.getInsertPoint() # 2011/04/01
3356 i, junk = g.getLine(s, sel_1)
3357 i2, j = g.getLine(s, sel_2)
3358 lines = s[i:j]
3359 self.beginCommand(w, undoType='move-lines-up')
3360 try:
3361 prev_i, prev_j = g.getLine(s, i - 1)
3362 prev_line = s[prev_i:prev_j]
3363 n2 = prev_j - prev_i
3364 if i > 0:
3365 w.delete(prev_i, j)
3366 if lines.endswith('\n'):
3367 # Simply swap positions with next line
3368 new_lines = lines + prev_line
3369 else:
3370 # Lines to be moved up don't end in a newline while the
3371 # previous line going down needs its newline taken off.
3372 new_lines = lines + '\n' + prev_line[:-1]
3373 w.insert(prev_i, new_lines)
3374 w.setSelectionRange(sel_1 - n2, sel_2 - n2, insert=insert_pt - n2)
3375 else:
3376 # Leo 5.6: Insert a blank line after the line.
3377 w.insert(j, '\n')
3378 w.setSelectionRange(sel_1, sel_2, insert=sel_1)
3379 # Fix bug 799695: colorizer bug after move-lines-up into a docstring
3380 c.recolor()
3381 finally:
3382 self.endCommand(changed=True, setLabel=True)
3383 #@+node:ekr.20150514063305.332: *4* ec.reverseRegion
3384 @cmd('reverse-region')
3385 def reverseRegion(self, event):
3386 """Reverse the order of lines in the selected text."""
3387 w = self.editWidget(event)
3388 if not w or not self._chckSel(event):
3389 return # pragma: no cover (defensive)
3390 self.beginCommand(w, undoType='reverse-region')
3391 s = w.getAllText()
3392 i1, j1 = w.getSelectionRange()
3393 i, junk = g.getLine(s, i1)
3394 junk, j = g.getLine(s, j1)
3395 txt = s[i:j]
3396 aList = txt.split('\n')
3397 aList.reverse()
3398 txt = '\n'.join(aList) + '\n'
3399 w.setAllText(s[:i1] + txt + s[j1:])
3400 ins = i1 + len(txt) - 1
3401 w.setSelectionRange(ins, ins, insert=ins)
3402 self.endCommand(changed=True, setLabel=True)
3403 #@+node:ekr.20150514063305.333: *4* ec.up/downCaseRegion & helper
3404 @cmd('downcase-region')
3405 def downCaseRegion(self, event):
3406 """Convert all characters in the selected text to lower case."""
3407 self.caseHelper(event, 'low', 'downcase-region')
3409 @cmd('toggle-case-region')
3410 def toggleCaseRegion(self, event):
3411 """Toggle the case of all characters in the selected text."""
3412 self.caseHelper(event, 'toggle', 'toggle-case-region')
3414 @cmd('upcase-region')
3415 def upCaseRegion(self, event):
3416 """Convert all characters in the selected text to UPPER CASE."""
3417 self.caseHelper(event, 'up', 'upcase-region')
3419 def caseHelper(self, event, way, undoType):
3420 w = self.editWidget(event)
3421 if not w or not w.hasSelection():
3422 return # pragma: no cover (defensive)
3423 self.beginCommand(w, undoType=undoType)
3424 s = w.getAllText()
3425 i, j = w.getSelectionRange()
3426 ins = w.getInsertPoint()
3427 s2 = s[i:j]
3428 if way == 'low':
3429 sel = s2.lower()
3430 elif way == 'up':
3431 sel = s2.upper()
3432 else:
3433 assert way == 'toggle'
3434 sel = s2.swapcase()
3435 s2 = s[:i] + sel + s[j:]
3436 changed = s2 != s
3437 if changed:
3438 w.setAllText(s2)
3439 w.setSelectionRange(i, j, insert=ins)
3440 self.endCommand(changed=changed, setLabel=True)
3441 #@+node:ekr.20150514063305.334: *3* ec: scrolling
3442 #@+node:ekr.20150514063305.335: *4* ec.scrollUp/Down & helper
3443 @cmd('scroll-down-half-page')
3444 def scrollDownHalfPage(self, event):
3445 """Scroll the presently selected pane down one line."""
3446 self.scrollHelper(event, 'down', 'half-page')
3448 @cmd('scroll-down-line')
3449 def scrollDownLine(self, event):
3450 """Scroll the presently selected pane down one line."""
3451 self.scrollHelper(event, 'down', 'line')
3453 @cmd('scroll-down-page')
3454 def scrollDownPage(self, event):
3455 """Scroll the presently selected pane down one page."""
3456 self.scrollHelper(event, 'down', 'page')
3458 @cmd('scroll-up-half-page')
3459 def scrollUpHalfPage(self, event):
3460 """Scroll the presently selected pane down one line."""
3461 self.scrollHelper(event, 'up', 'half-page')
3463 @cmd('scroll-up-line')
3464 def scrollUpLine(self, event):
3465 """Scroll the presently selected pane up one page."""
3466 self.scrollHelper(event, 'up', 'line')
3468 @cmd('scroll-up-page')
3469 def scrollUpPage(self, event):
3470 """Scroll the presently selected pane up one page."""
3471 self.scrollHelper(event, 'up', 'page')
3472 #@+node:ekr.20150514063305.336: *5* ec.scrollHelper
3473 def scrollHelper(self, event, direction, distance):
3474 """
3475 Scroll the present pane up or down one page
3476 kind is in ('up/down-half-page/line/page)
3477 """
3478 w = event and event.w
3479 if w and hasattr(w, 'scrollDelegate'):
3480 kind = direction + '-' + distance
3481 w.scrollDelegate(kind)
3482 #@+node:ekr.20150514063305.337: *4* ec.scrollOutlineUp/Down/Line/Page
3483 @cmd('scroll-outline-down-line')
3484 def scrollOutlineDownLine(self, event=None):
3485 """Scroll the outline pane down one line."""
3486 tree = self.c.frame.tree
3487 if hasattr(tree, 'scrollDelegate'):
3488 tree.scrollDelegate('down-line')
3489 elif hasattr(tree.canvas, 'leo_treeBar'):
3490 a, b = tree.canvas.leo_treeBar.get()
3491 if b < 1.0:
3492 tree.canvas.yview_scroll(1, "unit")
3494 @cmd('scroll-outline-down-page')
3495 def scrollOutlineDownPage(self, event=None):
3496 """Scroll the outline pane down one page."""
3497 tree = self.c.frame.tree
3498 if hasattr(tree, 'scrollDelegate'):
3499 tree.scrollDelegate('down-page')
3500 elif hasattr(tree.canvas, 'leo_treeBar'):
3501 a, b = tree.canvas.leo_treeBar.get()
3502 if b < 1.0:
3503 tree.canvas.yview_scroll(1, "page")
3505 @cmd('scroll-outline-up-line')
3506 def scrollOutlineUpLine(self, event=None):
3507 """Scroll the outline pane up one line."""
3508 tree = self.c.frame.tree
3509 if hasattr(tree, 'scrollDelegate'):
3510 tree.scrollDelegate('up-line')
3511 elif hasattr(tree.canvas, 'leo_treeBar'):
3512 a, b = tree.canvas.leo_treeBar.get()
3513 if a > 0.0:
3514 tree.canvas.yview_scroll(-1, "unit")
3516 @cmd('scroll-outline-up-page')
3517 def scrollOutlineUpPage(self, event=None):
3518 """Scroll the outline pane up one page."""
3519 tree = self.c.frame.tree
3520 if hasattr(tree, 'scrollDelegate'):
3521 tree.scrollDelegate('up-page')
3522 elif hasattr(tree.canvas, 'leo_treeBar'):
3523 a, b = tree.canvas.leo_treeBar.get()
3524 if a > 0.0:
3525 tree.canvas.yview_scroll(-1, "page")
3526 #@+node:ekr.20150514063305.338: *4* ec.scrollOutlineLeftRight
3527 @cmd('scroll-outline-left')
3528 def scrollOutlineLeft(self, event=None):
3529 """Scroll the outline left."""
3530 tree = self.c.frame.tree
3531 if hasattr(tree, 'scrollDelegate'):
3532 tree.scrollDelegate('left')
3533 elif hasattr(tree.canvas, 'xview_scroll'):
3534 tree.canvas.xview_scroll(1, "unit")
3536 @cmd('scroll-outline-right')
3537 def scrollOutlineRight(self, event=None):
3538 """Scroll the outline left."""
3539 tree = self.c.frame.tree
3540 if hasattr(tree, 'scrollDelegate'):
3541 tree.scrollDelegate('right')
3542 elif hasattr(tree.canvas, 'xview_scroll'):
3543 tree.canvas.xview_scroll(-1, "unit")
3544 #@+node:ekr.20150514063305.339: *3* ec: sort
3545 #@@language rest
3546 #@+at
3547 # XEmacs provides several commands for sorting text in a buffer. All
3548 # operate on the contents of the region (the text between point and the
3549 # mark). They divide the text of the region into many "sort records",
3550 # identify a "sort key" for each record, and then reorder the records
3551 # using the order determined by the sort keys. The records are ordered so
3552 # that their keys are in alphabetical order, or, for numerical sorting, in
3553 # numerical order. In alphabetical sorting, all upper-case letters `A'
3554 # through `Z' come before lower-case `a', in accordance with the ASCII
3555 # character sequence.
3556 #
3557 # The sort commands differ in how they divide the text into sort
3558 # records and in which part of each record they use as the sort key.
3559 # Most of the commands make each line a separate sort record, but some
3560 # commands use paragraphs or pages as sort records. Most of the sort
3561 # commands use each entire sort record as its own sort key, but some use
3562 # only a portion of the record as the sort key.
3563 #
3564 # `M-x sort-lines'
3565 # Divide the region into lines and sort by comparing the entire text
3566 # of a line. A prefix argument means sort in descending order.
3567 #
3568 # `M-x sort-paragraphs'
3569 # Divide the region into paragraphs and sort by comparing the entire
3570 # text of a paragraph (except for leading blank lines). A prefix
3571 # argument means sort in descending order.
3572 #
3573 # `M-x sort-pages'
3574 # Divide the region into pages and sort by comparing the entire text
3575 # of a page (except for leading blank lines). A prefix argument
3576 # means sort in descending order.
3577 #
3578 # `M-x sort-fields'
3579 # Divide the region into lines and sort by comparing the contents of
3580 # one field in each line. Fields are defined as separated by
3581 # whitespace, so the first run of consecutive non-whitespace
3582 # characters in a line constitutes field 1, the second such run
3583 # constitutes field 2, etc.
3584 #
3585 # You specify which field to sort by with a numeric argument: 1 to
3586 # sort by field 1, etc. A negative argument means sort in descending
3587 # order. Thus, minus 2 means sort by field 2 in reverse-alphabetical
3588 # order.
3589 #
3590 # `M-x sort-numeric-fields'
3591 # Like `M-x sort-fields', except the specified field is converted to
3592 # a number for each line and the numbers are compared. `10' comes
3593 # before `2' when considered as text, but after it when considered
3594 # as a number.
3595 #
3596 # `M-x sort-columns'
3597 # Like `M-x sort-fields', except that the text within each line used
3598 # for comparison comes from a fixed range of columns. An explanation
3599 # is given below.
3600 #
3601 # For example, if the buffer contains:
3602 #
3603 # On systems where clash detection (locking of files being edited) is
3604 # implemented, XEmacs also checks the first time you modify a buffer
3605 # whether the file has changed on disk since it was last visited or
3606 # saved. If it has, you are asked to confirm that you want to change
3607 # the buffer.
3608 #
3609 # then if you apply `M-x sort-lines' to the entire buffer you get:
3610 #
3611 # On systems where clash detection (locking of files being edited) is
3612 # implemented, XEmacs also checks the first time you modify a buffer
3613 # saved. If it has, you are asked to confirm that you want to change
3614 # the buffer.
3615 # whether the file has changed on disk since it was last visited or
3616 #
3617 # where the upper case `O' comes before all lower case letters. If you
3618 # apply instead `C-u 2 M-x sort-fields' you get:
3619 #
3620 # saved. If it has, you are asked to confirm that you want to change
3621 # implemented, XEmacs also checks the first time you modify a buffer
3622 # the buffer.
3623 # On systems where clash detection (locking of files being edited) is
3624 # whether the file has changed on disk since it was last visited or
3625 #
3626 # where the sort keys were `If', `XEmacs', `buffer', `systems', and `the'.
3627 #
3628 # `M-x sort-columns' requires more explanation. You specify the
3629 # columns by putting point at one of the columns and the mark at the other
3630 # column. Because this means you cannot put point or the mark at the
3631 # beginning of the first line to sort, this command uses an unusual
3632 # definition of `region': all of the line point is in is considered part
3633 # of the region, and so is all of the line the mark is in.
3634 #
3635 # For example, to sort a table by information found in columns 10 to
3636 # 15, you could put the mark on column 10 in the first line of the table,
3637 # and point on column 15 in the last line of the table, and then use this
3638 # command. Or you could put the mark on column 15 in the first line and
3639 # point on column 10 in the last line.
3640 #
3641 # This can be thought of as sorting the rectangle specified by point
3642 # and the mark, except that the text on each line to the left or right of
3643 # the rectangle moves along with the text inside the rectangle. *Note
3644 # Rectangles::.
3645 #@@language python
3646 #@+node:ekr.20150514063305.340: *4* ec.sortLines commands
3647 @cmd('reverse-sort-lines-ignoring-case')
3648 def reverseSortLinesIgnoringCase(self, event):
3649 """Sort the selected lines in reverse order, ignoring case."""
3650 return self.sortLines(event, ignoreCase=True, reverse=True)
3652 @cmd('reverse-sort-lines')
3653 def reverseSortLines(self, event):
3654 """Sort the selected lines in reverse order."""
3655 return self.sortLines(event, reverse=True)
3657 @cmd('sort-lines-ignoring-case')
3658 def sortLinesIgnoringCase(self, event):
3659 """Sort the selected lines, ignoring case."""
3660 return self.sortLines(event, ignoreCase=True)
3662 @cmd('sort-lines')
3663 def sortLines(self, event, ignoreCase=False, reverse=False):
3664 """Sort the selected lines."""
3665 w = self.editWidget(event)
3666 if not self._chckSel(event):
3667 return
3668 undoType = 'reverse-sort-lines' if reverse else 'sort-lines'
3669 self.beginCommand(w, undoType=undoType)
3670 try:
3671 s = w.getAllText()
3672 sel1, sel2 = w.getSelectionRange()
3673 ins = w.getInsertPoint()
3674 i, junk = g.getLine(s, sel1)
3675 junk, j = g.getLine(s, sel2)
3676 s2 = s[i:j]
3677 if not s2.endswith('\n'):
3678 s2 = s2 + '\n'
3679 aList = g.splitLines(s2)
3681 def lower(s):
3682 return s.lower() if ignoreCase else s
3684 aList.sort(key=lower)
3685 # key is a function that extracts args.
3686 if reverse:
3687 aList.reverse()
3688 s = ''.join(aList)
3689 w.delete(i, j)
3690 w.insert(i, s)
3691 w.setSelectionRange(sel1, sel2, insert=ins)
3692 finally:
3693 self.endCommand(changed=True, setLabel=True)
3694 #@+node:ekr.20150514063305.341: *4* ec.sortColumns
3695 @cmd('sort-columns')
3696 def sortColumns(self, event):
3697 """
3698 Sort lines of selected text using only lines in the given columns to do
3699 the comparison.
3700 """
3701 w = self.editWidget(event)
3702 if not self._chckSel(event):
3703 return # pragma: no cover (defensive)
3704 self.beginCommand(w, undoType='sort-columns')
3705 try:
3706 s = w.getAllText()
3707 sel_1, sel_2 = w.getSelectionRange()
3708 sint1, sint2 = g.convertPythonIndexToRowCol(s, sel_1)
3709 sint3, sint4 = g.convertPythonIndexToRowCol(s, sel_2)
3710 sint1 += 1
3711 sint3 += 1
3712 i, junk = g.getLine(s, sel_1)
3713 junk, j = g.getLine(s, sel_2)
3714 txt = s[i:j]
3715 columns = [w.get(f"{z}.{sint2}", f"{z}.{sint4}")
3716 for z in range(sint1, sint3 + 1)]
3717 aList = g.splitLines(txt)
3718 zlist = list(zip(columns, aList))
3719 zlist.sort()
3720 s = ''.join([z[1] for z in zlist])
3721 w.delete(i, j)
3722 w.insert(i, s)
3723 w.setSelectionRange(sel_1, sel_1 + len(s), insert=sel_1 + len(s))
3724 finally:
3725 self.endCommand(changed=True, setLabel=True)
3726 #@+node:ekr.20150514063305.342: *4* ec.sortFields
3727 @cmd('sort-fields')
3728 def sortFields(self, event, which=None):
3729 """
3730 Divide the selected text into lines and sort by comparing the contents
3731 of one field in each line. Fields are defined as separated by
3732 whitespace, so the first run of consecutive non-whitespace characters
3733 in a line constitutes field 1, the second such run constitutes field 2,
3734 etc.
3736 You specify which field to sort by with a numeric argument: 1 to sort
3737 by field 1, etc. A negative argument means sort in descending order.
3738 Thus, minus 2 means sort by field 2 in reverse-alphabetical order.
3739 """
3740 w = self.editWidget(event)
3741 if not w or not self._chckSel(event):
3742 return
3743 self.beginCommand(w, undoType='sort-fields')
3744 s = w.getAllText()
3745 ins = w.getInsertPoint()
3746 r1, r2, r3, r4 = self.getRectanglePoints(w)
3747 i, junk = g.getLine(s, r1)
3748 junk, j = g.getLine(s, r4)
3749 txt = s[i:j] # bug reported by pychecker.
3750 txt = txt.split('\n')
3751 fields = []
3752 fn = r'\w+'
3753 frx = re.compile(fn)
3754 for line in txt:
3755 f = frx.findall(line)
3756 if not which:
3757 fields.append(f[0])
3758 else:
3759 i = int(which)
3760 if len(f) < i:
3761 return
3762 i = i - 1
3763 fields.append(f[i])
3764 nz = sorted(zip(fields, txt))
3765 w.delete(i, j)
3766 int1 = i
3767 for z in nz:
3768 w.insert(f"{int1}.0", f"{z[1]}\n")
3769 int1 = int1 + 1
3770 w.setInsertPoint(ins)
3771 self.endCommand(changed=True, setLabel=True)
3772 #@+node:ekr.20150514063305.343: *3* ec: swap/transpose
3773 #@+node:ekr.20150514063305.344: *4* ec.transposeLines
3774 @cmd('transpose-lines')
3775 def transposeLines(self, event):
3776 """Transpose the line containing the cursor with the preceding line."""
3777 w = self.editWidget(event)
3778 if not w:
3779 return # pragma: no cover (defensive)
3780 ins = w.getInsertPoint()
3781 s = w.getAllText()
3782 if not s.strip():
3783 return # pragma: no cover (defensive)
3784 i, j = g.getLine(s, ins)
3785 line1 = s[i:j]
3786 self.beginCommand(w, undoType='transpose-lines')
3787 if i == 0: # Transpose the next line.
3788 i2, j2 = g.getLine(s, j + 1)
3789 line2 = s[i2:j2]
3790 w.delete(0, j2)
3791 w.insert(0, line2 + line1)
3792 w.setInsertPoint(j2 - 1)
3793 else: # Transpose the previous line.
3794 i2, j2 = g.getLine(s, i - 1)
3795 line2 = s[i2:j2]
3796 w.delete(i2, j)
3797 w.insert(i2, line1 + line2)
3798 w.setInsertPoint(j - 1)
3799 self.endCommand(changed=True, setLabel=True)
3800 #@+node:ekr.20150514063305.345: *4* ec.transposeWords
3801 @cmd('transpose-words')
3802 def transposeWords(self, event):
3803 """
3804 Transpose the word before the cursor with the word after the cursor
3805 Punctuation between words does not move. For example, ‘FOO, BAR’
3806 transposes into ‘BAR, FOO’.
3807 """
3808 w = self.editWidget(event)
3809 if not w:
3810 return
3811 self.beginCommand(w, undoType='transpose-words')
3812 s = w.getAllText()
3813 i1, j1 = self.extendToWord(event, select=False)
3814 s1 = s[i1:j1]
3815 if i1 > j1:
3816 i1, j1 = j1, i1
3817 # Search for the next word.
3818 k = j1 + 1
3819 while k < len(s) and s[k] != '\n' and not g.isWordChar1(s[k]):
3820 k += 1
3821 changed = k < len(s)
3822 if changed:
3823 ws = s[j1:k]
3824 w.setInsertPoint(k + 1)
3825 i2, j2 = self.extendToWord(event, select=False)
3826 s2 = s[i2:j2]
3827 s3 = s[:i1] + s2 + ws + s1 + s[j2:]
3828 w.setAllText(s3)
3829 w.setSelectionRange(j1, j1, insert=j1)
3830 self.endCommand(changed=changed, setLabel=True)
3831 #@+node:ekr.20150514063305.346: *4* ec.swapCharacters & transeposeCharacters
3832 @cmd('transpose-chars')
3833 def transposeCharacters(self, event):
3834 """Swap the characters at the cursor."""
3835 w = self.editWidget(event)
3836 if not w:
3837 return # pragma: no cover (defensive)
3838 self.beginCommand(w, undoType='swap-characters')
3839 s = w.getAllText()
3840 i = w.getInsertPoint()
3841 if 0 < i < len(s):
3842 w.setAllText(s[: i - 1] + s[i] + s[i - 1] + s[i + 1 :])
3843 w.setSelectionRange(i, i, insert=i)
3844 self.endCommand(changed=True, setLabel=True)
3846 swapCharacters = transposeCharacters
3847 #@+node:ekr.20150514063305.348: *3* ec: uA's
3848 #@+node:ekr.20150514063305.349: *4* ec.clearNodeUas & clearAllUas
3849 @cmd('clear-node-uas')
3850 def clearNodeUas(self, event=None):
3851 """Clear the uA's in the selected VNode."""
3852 c = self.c
3853 p = c and c.p
3854 if p and p.v.u:
3855 p.v.u = {}
3856 # #1276.
3857 p.setDirty()
3858 c.setChanged()
3859 c.redraw()
3861 @cmd('clear-all-uas')
3862 def clearAllUas(self, event=None):
3863 """Clear all uAs in the entire outline."""
3864 c = self.c
3865 # #1276.
3866 changed = False
3867 for p in self.c.all_unique_positions():
3868 if p.v.u:
3869 p.v.u = {}
3870 p.setDirty()
3871 changed = True
3872 if changed:
3873 c.setChanged()
3874 c.redraw()
3875 #@+node:ekr.20150514063305.350: *4* ec.showUas & showAllUas
3876 @cmd('show-all-uas')
3877 def showAllUas(self, event=None):
3878 """Print all uA's in the outline."""
3879 g.es_print('Dump of uAs...')
3880 for v in self.c.all_unique_nodes():
3881 if v.u:
3882 self.showNodeUas(v=v)
3884 @cmd('show-node-uas')
3885 def showNodeUas(self, event=None, v=None):
3886 """Print the uA's in the selected node."""
3887 c = self.c
3888 if v:
3889 d, h = v.u, v.h
3890 else:
3891 d, h = c.p.v.u, c.p.h
3892 g.es_print(h)
3893 g.es_print(g.objToString(d))
3894 #@+node:ekr.20150514063305.351: *4* ec.setUa
3895 @cmd('set-ua')
3896 def setUa(self, event):
3897 """Prompt for the name and value of a uA, then set the uA in the present node."""
3898 k = self.c.k
3899 self.w = self.editWidget(event)
3900 if self.w:
3901 k.setLabelBlue('Set uA: ')
3902 k.get1Arg(event, handler=self.setUa1)
3904 def setUa1(self, event):
3905 k = self.c.k
3906 self.uaName = k.arg
3907 s = f"Set uA: {self.uaName} To: "
3908 k.setLabelBlue(s)
3909 k.getNextArg(self.setUa2)
3911 def setUa2(self, event):
3912 c, k = self.c, self.c.k
3913 val = k.arg
3914 d = c.p.v.u
3915 d[self.uaName] = val
3916 self.showNodeUas()
3917 k.clearState()
3918 k.resetLabel()
3919 k.showStateAndMode()
3920 #@-others
3921#@-others
3922#@-leo