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