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