Coverage for C:\leo.repo\leo-editor\leo\commands\commanderEditCommands.py: 61%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20171123135539.1: * @file ../commands/commanderEditCommands.py
4#@@first
5"""Edit commands that used to be defined in leoCommands.py"""
6import re
7from typing import List
8from leo.core import leoGlobals as g
9#@+others
10#@+node:ekr.20171123135625.34: ** c_ec.addComments
11@g.commander_command('add-comments')
12def addComments(self, event=None):
13 #@+<< addComments docstring >>
14 #@+node:ekr.20171123135625.35: *3* << addComments docstring >>
15 #@@pagewidth 50
16 """
17 Converts all selected lines to comment lines using
18 the comment delimiters given by the applicable @language directive.
20 Inserts single-line comments if possible; inserts
21 block comments for languages like html that lack
22 single-line comments.
24 @bool indent_added_comments
26 If True (the default), inserts opening comment
27 delimiters just before the first non-whitespace
28 character of each line. Otherwise, inserts opening
29 comment delimiters at the start of each line.
31 *See also*: delete-comments.
32 """
33 #@-<< addComments docstring >>
34 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
35 #
36 # "Before" snapshot.
37 bunch = u.beforeChangeBody(p)
38 #
39 # Make sure there is a selection.
40 head, lines, tail, oldSel, oldYview = self.getBodyLines()
41 if not lines:
42 g.warning('no text selected')
43 return
44 #
45 # The default language in effect at p.
46 language = c.frame.body.colorizer.scanLanguageDirectives(p)
47 if c.hasAmbiguousLanguage(p):
48 language = c.getLanguageAtCursor(p, language)
49 d1, d2, d3 = g.set_delims_from_language(language)
50 d2 = d2 or ''
51 d3 = d3 or ''
52 if d1:
53 openDelim, closeDelim = d1 + ' ', ''
54 else:
55 openDelim, closeDelim = d2 + ' ', ' ' + d3
56 #
57 # Calculate the result.
58 indent = c.config.getBool('indent-added-comments', default=True)
59 result = []
60 for line in lines:
61 if line.strip():
62 i = g.skip_ws(line, 0)
63 if indent:
64 s = line[i:].replace('\n', '')
65 result.append(line[0:i] + openDelim + s + closeDelim + '\n')
66 else:
67 s = line.replace('\n', '')
68 result.append(openDelim + s + closeDelim + '\n')
69 else:
70 result.append(line)
71 #
72 # Set p.b and w's text first.
73 middle = ''.join(result)
74 p.b = head + middle + tail # Sets dirty and changed bits.
75 w.setAllText(head + middle + tail)
76 #
77 # Calculate the proper selection range (i, j, ins).
78 i = len(head)
79 j = max(i, len(head) + len(middle) - 1)
80 #
81 # Set the selection range and scroll position.
82 w.setSelectionRange(i, j, insert=j)
83 w.setYScrollPosition(oldYview)
84 #
85 # "after" snapshot.
86 u.afterChangeBody(p, 'Add Comments', bunch)
87#@+node:ekr.20171123135625.3: ** c_ec.colorPanel
88@g.commander_command('set-colors')
89def colorPanel(self, event=None):
90 """Open the color dialog."""
91 c = self
92 frame = c.frame
93 if not frame.colorPanel:
94 frame.colorPanel = g.app.gui.createColorPanel(c)
95 frame.colorPanel.bringToFront()
96#@+node:ekr.20171123135625.16: ** c_ec.convertAllBlanks
97@g.commander_command('convert-all-blanks')
98def convertAllBlanks(self, event=None):
99 """Convert all blanks to tabs in the selected outline."""
100 c, u = self, self.undoer
101 undoType = 'Convert All Blanks'
102 current = c.p
103 if g.app.batchMode:
104 c.notValidInBatchMode(undoType)
105 return
106 d = c.scanAllDirectives(c.p)
107 tabWidth = d.get("tabwidth")
108 count = 0
109 u.beforeChangeGroup(current, undoType)
110 for p in current.self_and_subtree():
111 innerUndoData = u.beforeChangeNodeContents(p)
112 if p == current:
113 changed = c.convertBlanks(event)
114 if changed:
115 count += 1
116 else:
117 changed = False
118 result = []
119 text = p.v.b
120 lines = text.split('\n')
121 for line in lines:
122 i, w = g.skip_leading_ws_with_indent(line, 0, tabWidth)
123 s = g.computeLeadingWhitespace(
124 w, abs(tabWidth)) + line[i:] # use positive width.
125 if s != line:
126 changed = True
127 result.append(s)
128 if changed:
129 count += 1
130 p.setDirty()
131 p.setBodyString('\n'.join(result))
132 u.afterChangeNodeContents(p, undoType, innerUndoData)
133 u.afterChangeGroup(current, undoType)
134 if not g.unitTesting:
135 g.es("blanks converted to tabs in", count, "nodes")
136 # Must come before c.redraw().
137 if count > 0:
138 c.redraw_after_icons_changed()
139#@+node:ekr.20171123135625.17: ** c_ec.convertAllTabs
140@g.commander_command('convert-all-tabs')
141def convertAllTabs(self, event=None):
142 """Convert all tabs to blanks in the selected outline."""
143 c = self
144 u = c.undoer
145 undoType = 'Convert All Tabs'
146 current = c.p
147 if g.app.batchMode:
148 c.notValidInBatchMode(undoType)
149 return
150 theDict = c.scanAllDirectives(c.p)
151 tabWidth = theDict.get("tabwidth")
152 count = 0
153 u.beforeChangeGroup(current, undoType)
154 for p in current.self_and_subtree():
155 undoData = u.beforeChangeNodeContents(p)
156 if p == current:
157 changed = self.convertTabs(event)
158 if changed:
159 count += 1
160 else:
161 result = []
162 changed = False
163 text = p.v.b
164 lines = text.split('\n')
165 for line in lines:
166 i, w = g.skip_leading_ws_with_indent(line, 0, tabWidth)
167 s = g.computeLeadingWhitespace(
168 w, -abs(tabWidth)) + line[i:] # use negative width.
169 if s != line:
170 changed = True
171 result.append(s)
172 if changed:
173 count += 1
174 p.setDirty()
175 p.setBodyString('\n'.join(result))
176 u.afterChangeNodeContents(p, undoType, undoData)
177 u.afterChangeGroup(current, undoType)
178 if not g.unitTesting:
179 g.es("tabs converted to blanks in", count, "nodes")
180 if count > 0:
181 c.redraw_after_icons_changed()
182#@+node:ekr.20171123135625.18: ** c_ec.convertBlanks
183@g.commander_command('convert-blanks')
184def convertBlanks(self, event=None):
185 """
186 Convert *all* blanks to tabs in the selected node.
187 Return True if the the p.b was changed.
188 """
189 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
190 #
191 # "Before" snapshot.
192 bunch = u.beforeChangeBody(p)
193 oldYview = w.getYScrollPosition()
194 w.selectAllText()
195 head, lines, tail, oldSel, oldYview = c.getBodyLines()
196 #
197 # Use the relative @tabwidth, not the global one.
198 d = c.scanAllDirectives(p)
199 tabWidth = d.get("tabwidth")
200 if not tabWidth:
201 return False
202 #
203 # Calculate the result.
204 changed, result = False, []
205 for line in lines:
206 s = g.optimizeLeadingWhitespace(line, abs(tabWidth)) # Use positive width.
207 if s != line:
208 changed = True
209 result.append(s)
210 if not changed:
211 return False
212 #
213 # Set p.b and w's text first.
214 middle = ''.join(result)
215 p.b = head + middle + tail # Sets dirty and changed bits.
216 w.setAllText(head + middle + tail)
217 #
218 # Select all text and set scroll position.
219 w.selectAllText()
220 w.setYScrollPosition(oldYview)
221 #
222 # "after" snapshot.
223 u.afterChangeBody(p, 'Indent Region', bunch)
224 return True
225#@+node:ekr.20171123135625.19: ** c_ec.convertTabs
226@g.commander_command('convert-tabs')
227def convertTabs(self, event=None):
228 """Convert all tabs to blanks in the selected node."""
229 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
230 #
231 # "Before" snapshot.
232 bunch = u.beforeChangeBody(p)
233 #
234 # Data...
235 w.selectAllText()
236 head, lines, tail, oldSel, oldYview = self.getBodyLines()
237 # Use the relative @tabwidth, not the global one.
238 theDict = c.scanAllDirectives(p)
239 tabWidth = theDict.get("tabwidth")
240 if not tabWidth:
241 return False
242 #
243 # Calculate the result.
244 changed, result = False, []
245 for line in lines:
246 i, width = g.skip_leading_ws_with_indent(line, 0, tabWidth)
247 s = g.computeLeadingWhitespace(width, -abs(tabWidth)) + line[i:] # use negative width.
248 if s != line:
249 changed = True
250 result.append(s)
251 if not changed:
252 return False
253 #
254 # Set p.b and w's text first.
255 middle = ''.join(result)
256 p.b = head + middle + tail # Sets dirty and changed bits.
257 w.setAllText(head + middle + tail)
258 #
259 # Calculate the proper selection range (i, j, ins).
260 i = len(head)
261 j = max(i, len(head) + len(middle) - 1)
262 #
263 # Set the selection range and scroll position.
264 w.setSelectionRange(i, j, insert=j)
265 w.setYScrollPosition(oldYview)
266 #
267 # "after" snapshot.
268 u.afterChangeBody(p, 'Add Comments', bunch)
269 return True
270#@+node:ekr.20171123135625.21: ** c_ec.dedentBody (unindent-region)
271@g.commander_command('unindent-region')
272def dedentBody(self, event=None):
273 """Remove one tab's worth of indentation from all presently selected lines."""
274 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
275 #
276 # Initial data.
277 sel_1, sel_2 = w.getSelectionRange()
278 tab_width = c.getTabWidth(c.p)
279 head, lines, tail, oldSel, oldYview = self.getBodyLines()
280 bunch = u.beforeChangeBody(p)
281 #
282 # Calculate the result.
283 changed, result = False, []
284 for line in lines:
285 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width)
286 s = g.computeLeadingWhitespace(width - abs(tab_width), tab_width) + line[i:]
287 if s != line:
288 changed = True
289 result.append(s)
290 if not changed:
291 return
292 #
293 # Set p.b and w's text first.
294 middle = ''.join(result)
295 all = head + middle + tail
296 p.b = all # Sets dirty and changed bits.
297 w.setAllText(all)
298 #
299 # Calculate the proper selection range (i, j, ins).
300 if sel_1 == sel_2:
301 line = result[0]
302 ins, width = g.skip_leading_ws_with_indent(line, 0, tab_width)
303 i = j = len(head) + ins
304 else:
305 i = len(head)
306 j = len(head) + len(middle)
307 if middle.endswith('\n'): # #1742.
308 j -= 1
309 #
310 # Set the selection range and scroll position.
311 w.setSelectionRange(i, j, insert=j)
312 w.setYScrollPosition(oldYview)
313 u.afterChangeBody(p, 'Unindent Region', bunch)
314#@+node:ekr.20171123135625.36: ** c_ec.deleteComments
315@g.commander_command('delete-comments')
316def deleteComments(self, event=None):
317 #@+<< deleteComments docstring >>
318 #@+node:ekr.20171123135625.37: *3* << deleteComments docstring >>
319 #@@pagewidth 50
320 """
321 Removes one level of comment delimiters from all
322 selected lines. The applicable @language directive
323 determines the comment delimiters to be removed.
325 Removes single-line comments if possible; removes
326 block comments for languages like html that lack
327 single-line comments.
329 *See also*: add-comments.
330 """
331 #@-<< deleteComments docstring >>
332 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
333 #
334 # "Before" snapshot.
335 bunch = u.beforeChangeBody(p)
336 #
337 # Initial data.
338 head, lines, tail, oldSel, oldYview = self.getBodyLines()
339 if not lines:
340 g.warning('no text selected')
341 return
342 # The default language in effect at p.
343 language = c.frame.body.colorizer.scanLanguageDirectives(p)
344 if c.hasAmbiguousLanguage(p):
345 language = c.getLanguageAtCursor(p, language)
346 d1, d2, d3 = g.set_delims_from_language(language)
347 #
348 # Calculate the result.
349 changed, result = False, []
350 if d1:
351 # Remove the single-line comment delim in front of each line
352 d1b = d1 + ' '
353 n1, n1b = len(d1), len(d1b)
354 for s in lines:
355 i = g.skip_ws(s, 0)
356 if g.match(s, i, d1b):
357 result.append(s[:i] + s[i + n1b :])
358 changed = True
359 elif g.match(s, i, d1):
360 result.append(s[:i] + s[i + n1 :])
361 changed = True
362 else:
363 result.append(s)
364 else:
365 # Remove the block comment delimiters from each line.
366 n2, n3 = len(d2), len(d3)
367 for s in lines:
368 i = g.skip_ws(s, 0)
369 j = s.find(d3, i + n2)
370 if g.match(s, i, d2) and j > -1:
371 first = i + n2
372 if g.match(s, first, ' '):
373 first += 1
374 last = j
375 if g.match(s, last - 1, ' '):
376 last -= 1
377 result.append(s[:i] + s[first:last] + s[j + n3 :])
378 changed = True
379 else:
380 result.append(s)
381 if not changed:
382 return
383 #
384 # Set p.b and w's text first.
385 middle = ''.join(result)
386 p.b = head + middle + tail # Sets dirty and changed bits.
387 w.setAllText(head + middle + tail)
388 #
389 # Set the selection range and scroll position.
390 i = len(head)
391 j = ins = max(i, len(head) + len(middle) - 1)
392 w.setSelectionRange(i, j, insert=ins)
393 w.setYScrollPosition(oldYview)
394 #
395 # "after" snapshot.
396 u.afterChangeBody(p, 'Indent Region', bunch)
397#@+node:ekr.20171123135625.54: ** c_ec.editHeadline (edit-headline)
398@g.commander_command('edit-headline')
399def editHeadline(self, event=None):
400 """
401 Begin editing the headline of the selected node.
403 This is just a wrapper around tree.editLabel.
404 """
405 c = self
406 k, tree = c.k, c.frame.tree
407 if g.app.batchMode:
408 c.notValidInBatchMode("Edit Headline")
409 return None, None
410 e, wrapper = tree.editLabel(c.p)
411 if k:
412 # k.setDefaultInputState()
413 k.setEditingState()
414 k.showStateAndMode(w=wrapper)
415 return e, wrapper
416 # Neither of these is used by any caller.
417#@+node:ekr.20171123135625.23: ** c_ec.extract & helpers
418@g.commander_command('extract')
419def extract(self, event=None):
420 #@+<< docstring for extract command >>
421 #@+node:ekr.20201113130021.1: *3* << docstring for extract command >>
422 r"""
423 Create child node from the selected body text.
425 1. If the selection starts with a section reference, the section
426 name becomes the child's headline. All following lines become
427 the child's body text. The section reference line remains in
428 the original body text.
430 2. If the selection looks like a definition line (for the Python,
431 JavaScript, CoffeeScript or Clojure languages) the
432 class/function/method name becomes the child's headline and all
433 selected lines become the child's body text.
435 You may add additional regex patterns for definition lines using
436 @data extract-patterns nodes. Each line of the body text should a
437 valid regex pattern. Lines starting with # are comment lines. Use \#
438 for patterns starting with #.
440 3. Otherwise, the first line becomes the child's headline, and all
441 selected lines become the child's body text.
442 """
443 #@-<< docstring for extract command >>
444 c, u, w = self, self.undoer, self.frame.body.wrapper
445 undoType = 'Extract'
446 # Set data.
447 head, lines, tail, oldSel, oldYview = c.getBodyLines()
448 if not lines:
449 return # Nothing selected.
450 #
451 # Remove leading whitespace.
452 junk, ws = g.skip_leading_ws_with_indent(lines[0], 0, c.tab_width)
453 lines = [g.removeLeadingWhitespace(s, ws, c.tab_width) for s in lines]
454 h = lines[0].strip()
455 ref_h = extractRef(c, h).strip()
456 def_h = extractDef_find(c, lines)
457 if ref_h:
458 h, b, middle = ref_h, lines[1:], ' ' * ws + lines[0] # By vitalije.
459 elif def_h:
460 h, b, middle = def_h, lines, ''
461 else:
462 h, b, middle = lines[0].strip(), lines[1:], ''
463 #
464 # Start the outer undo group.
465 u.beforeChangeGroup(c.p, undoType)
466 undoData = u.beforeInsertNode(c.p)
467 p = createLastChildNode(c, c.p, h, ''.join(b))
468 u.afterInsertNode(p, undoType, undoData)
469 #
470 # Start inner undo.
471 if oldSel:
472 i, j = oldSel
473 w.setSelectionRange(i, j, insert=j)
474 bunch = u.beforeChangeBody(c.p) # Not p.
475 #
476 # Update the text and selection
477 c.p.v.b = head + middle + tail # Don't redraw.
478 w.setAllText(head + middle + tail)
479 i = len(head)
480 j = max(i, len(head) + len(middle) - 1)
481 w.setSelectionRange(i, j, insert=j)
482 #
483 # End the inner undo.
484 u.afterChangeBody(c.p, undoType, bunch)
485 #
486 # Scroll as necessary.
487 if oldYview:
488 w.setYScrollPosition(oldYview)
489 else:
490 w.seeInsertPoint()
491 #
492 # Add the changes to the outer undo group.
493 u.afterChangeGroup(c.p, undoType=undoType)
494 p.parent().expand()
495 c.redraw(p.parent()) # A bit more convenient than p.
496 c.bodyWantsFocus()
498# Compatibility
500g.command_alias('extractSection', extract)
501g.command_alias('extractPythonMethod', extract)
502#@+node:ekr.20171123135625.20: *3* def createLastChildNode
503def createLastChildNode(c, parent, headline, body):
504 """A helper function for the three extract commands."""
505 # #1955: don't strip trailing lines.
506 if not body:
507 body = ""
508 p = parent.insertAsLastChild()
509 p.initHeadString(headline)
510 p.setBodyString(body)
511 p.setDirty()
512 c.validateOutline()
513 return p
514#@+node:ekr.20171123135625.24: *3* def extractDef
515extractDef_patterns = (
516 re.compile(
517 r'\((?:def|defn|defui|deftype|defrecord|defonce)\s+(\S+)'), # clojure definition
518 re.compile(r'^\s*(?:def|class)\s+(\w+)'), # python definitions
519 re.compile(r'^\bvar\s+(\w+)\s*=\s*function\b'), # js function
520 re.compile(r'^(?:export\s)?\s*function\s+(\w+)\s*\('), # js function
521 re.compile(r'\b(\w+)\s*:\s*function\s'), # js function
522 re.compile(r'\.(\w+)\s*=\s*function\b'), # js function
523 re.compile(r'(?:export\s)?\b(\w+)\s*=\s(?:=>|->)'), # coffeescript function
524 re.compile(
525 r'(?:export\s)?\b(\w+)\s*=\s(?:\([^)]*\))\s*(?:=>|->)'), # coffeescript function
526 re.compile(r'\b(\w+)\s*:\s(?:=>|->)'), # coffeescript function
527 re.compile(r'\b(\w+)\s*:\s(?:\([^)]*\))\s*(?:=>|->)'), # coffeescript function
528)
530def extractDef(c, s):
531 """
532 Return the defined function/method/class name if s
533 looks like definition. Tries several different languages.
534 """
535 for pat in c.config.getData('extract-patterns') or []:
536 try:
537 pat = re.compile(pat)
538 m = pat.search(s)
539 if m:
540 return m.group(1)
541 except Exception:
542 g.es_print('bad regex in @data extract-patterns', color='blue')
543 g.es_print(pat)
544 for pat in extractDef_patterns:
545 m = pat.search(s)
546 if m:
547 return m.group(1)
548 return ''
549#@+node:ekr.20171123135625.26: *3* def extractDef_find
550def extractDef_find(c, lines):
551 for line in lines:
552 def_h = extractDef(c, line.strip())
553 if def_h:
554 return def_h
555 return None
556#@+node:ekr.20171123135625.25: *3* def extractRef
557def extractRef(c, s):
558 """Return s if it starts with a section name."""
559 i = s.find('<<')
560 j = s.find('>>')
561 if -1 < i < j:
562 return s
563 i = s.find('@<')
564 j = s.find('@>')
565 if -1 < i < j:
566 return s
567 return ''
568#@+node:ekr.20171123135625.27: ** c_ec.extractSectionNames & helper
569@g.commander_command('extract-names')
570def extractSectionNames(self, event=None):
571 """
572 Create child nodes for every section reference in the selected text.
573 - The headline of each new child node is the section reference.
574 - The body of each child node is empty.
575 """
576 c = self
577 current = c.p
578 u = c.undoer
579 undoType = 'Extract Section Names'
580 body = c.frame.body
581 head, lines, tail, oldSel, oldYview = c.getBodyLines()
582 if not lines:
583 g.warning('No lines selected')
584 return
585 u.beforeChangeGroup(current, undoType)
586 found = False
587 for s in lines:
588 name = findSectionName(c, s)
589 if name:
590 undoData = u.beforeInsertNode(current)
591 p = createLastChildNode(c, current, name, None)
592 u.afterInsertNode(p, undoType, undoData)
593 found = True
594 c.validateOutline()
595 if found:
596 u.afterChangeGroup(current, undoType)
597 c.redraw(p)
598 else:
599 g.warning("selected text should contain section names")
600 # Restore the selection.
601 i, j = oldSel
602 w = body.wrapper
603 if w:
604 w.setSelectionRange(i, j)
605 w.setFocus()
606#@+node:ekr.20171123135625.28: *3* def findSectionName
607def findSectionName(self, s):
608 head1 = s.find("<<")
609 if head1 > -1:
610 head2 = s.find(">>", head1)
611 else:
612 head1 = s.find("@<")
613 if head1 > -1:
614 head2 = s.find("@>", head1)
615 if head1 == -1 or head2 == -1 or head1 > head2:
616 name = None
617 else:
618 name = s[head1 : head2 + 2]
619 return name
620#@+node:ekr.20171123135625.15: ** c_ec.findMatchingBracket
621@g.commander_command('match-brackets')
622@g.commander_command('select-to-matching-bracket')
623def findMatchingBracket(self, event=None):
624 """Select the text between matching brackets."""
625 c, p = self, self.p
626 if g.app.batchMode:
627 c.notValidInBatchMode("Match Brackets")
628 return
629 language = g.getLanguageAtPosition(c, p)
630 if language == 'perl':
631 g.es('match-brackets not supported for', language)
632 else:
633 g.MatchBrackets(c, p, language).run()
634#@+node:ekr.20171123135625.9: ** c_ec.fontPanel
635@g.commander_command('set-font')
636def fontPanel(self, event=None):
637 """Open the font dialog."""
638 c = self
639 frame = c.frame
640 if not frame.fontPanel:
641 frame.fontPanel = g.app.gui.createFontPanel(c)
642 frame.fontPanel.bringToFront()
643#@+node:ekr.20110402084740.14490: ** c_ec.goToNext/PrevHistory
644@g.commander_command('goto-next-history-node')
645def goToNextHistory(self, event=None):
646 """Go to the next node in the history list."""
647 c = self
648 c.nodeHistory.goNext()
650@g.commander_command('goto-prev-history-node')
651def goToPrevHistory(self, event=None):
652 """Go to the previous node in the history list."""
653 c = self
654 c.nodeHistory.goPrev()
655#@+node:ekr.20171123135625.30: ** c_ec.alwaysIndentBody (always-indent-region)
656@g.commander_command('always-indent-region')
657def alwaysIndentBody(self, event=None):
658 """
659 The always-indent-region command indents each line of the selected body
660 text. The @tabwidth directive in effect determines amount of
661 indentation.
662 """
663 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
664 #
665 # #1801: Don't rely on bindings to ensure that we are editing the body.
666 event_w = event and event.w
667 if event_w != w:
668 c.insertCharFromEvent(event)
669 return
670 #
671 # "Before" snapshot.
672 bunch = u.beforeChangeBody(p)
673 #
674 # Initial data.
675 sel_1, sel_2 = w.getSelectionRange()
676 tab_width = c.getTabWidth(p)
677 head, lines, tail, oldSel, oldYview = self.getBodyLines()
678 #
679 # Calculate the result.
680 changed, result = False, []
681 for line in lines:
682 if line.strip():
683 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width)
684 s = g.computeLeadingWhitespace(width + abs(tab_width), tab_width) + line[i:]
685 result.append(s)
686 if s != line:
687 changed = True
688 else:
689 result.append('\n') # #2418
690 if not changed:
691 return
692 #
693 # Set p.b and w's text first.
694 middle = ''.join(result)
695 all = head + middle + tail
696 p.b = all # Sets dirty and changed bits.
697 w.setAllText(all)
698 #
699 # Calculate the proper selection range (i, j, ins).
700 if sel_1 == sel_2:
701 line = result[0]
702 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width)
703 i = j = len(head) + i
704 else:
705 i = len(head)
706 j = len(head) + len(middle)
707 if middle.endswith('\n'): # #1742.
708 j -= 1
709 #
710 # Set the selection range and scroll position.
711 w.setSelectionRange(i, j, insert=j)
712 w.setYScrollPosition(oldYview)
713 #
714 # "after" snapshot.
715 u.afterChangeBody(p, 'Indent Region', bunch)
716#@+node:ekr.20210104123442.1: ** c_ec.indentBody (indent-region)
717@g.commander_command('indent-region')
718def indentBody(self, event=None):
719 """
720 The indent-region command indents each line of the selected body text.
721 Unlike the always-indent-region command, this command inserts a tab
722 (soft or hard) when there is no selected text.
724 The @tabwidth directive in effect determines amount of indentation.
725 """
726 c, event_w, w = self, event and event.w, self.frame.body.wrapper
727 # #1801: Don't rely on bindings to ensure that we are editing the body.
728 if event_w != w:
729 c.insertCharFromEvent(event)
730 return
731 # # 1739. Special case for a *plain* tab bound to indent-region.
732 sel_1, sel_2 = w.getSelectionRange()
733 if sel_1 == sel_2:
734 char = getattr(event, 'char', None)
735 stroke = getattr(event, 'stroke', None)
736 if char == '\t' and stroke and stroke.isPlainKey():
737 c.editCommands.selfInsertCommand(event) # Handles undo.
738 return
739 c.alwaysIndentBody(event)
740#@+node:ekr.20171123135625.38: ** c_ec.insertBodyTime
741@g.commander_command('insert-body-time')
742def insertBodyTime(self, event=None):
743 """Insert a time/date stamp at the cursor."""
744 c, p, u = self, self.p, self.undoer
745 w = c.frame.body.wrapper
746 undoType = 'Insert Body Time'
747 if g.app.batchMode:
748 c.notValidInBatchMode(undoType)
749 return
750 bunch = u.beforeChangeBody(p)
751 w.deleteTextSelection()
752 s = self.getTime(body=True)
753 i = w.getInsertPoint()
754 w.insert(i, s)
755 p.v.b = w.getAllText()
756 u.afterChangeBody(p, undoType, bunch)
757#@+node:ekr.20171123135625.52: ** c_ec.justify-toggle-auto
758@g.commander_command("justify-toggle-auto")
759def justify_toggle_auto(self, event=None):
760 c = self
761 if c.editCommands.autojustify == 0:
762 c.editCommands.autojustify = abs(c.config.getInt("autojustify") or 0)
763 if c.editCommands.autojustify:
764 g.es(f"Autojustify on, @int autojustify == {c.editCommands.autojustify}")
765 else:
766 g.es("Set @int autojustify in @settings")
767 else:
768 c.editCommands.autojustify = 0
769 g.es("Autojustify off")
770#@+node:ekr.20190210095609.1: ** c_ec.line_to_headline
771@g.commander_command('line-to-headline')
772def line_to_headline(self, event=None):
773 """
774 Create child node from the selected line.
776 Cut the selected line and make it the new node's headline
777 """
778 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper
779 undoType = 'line-to-headline'
780 ins, s = w.getInsertPoint(), p.b
781 i = g.find_line_start(s, ins)
782 j = g.skip_line(s, i)
783 line = s[i:j].strip()
784 if not line:
785 return
786 u.beforeChangeGroup(p, undoType)
787 #
788 # Start outer undo.
789 undoData = u.beforeInsertNode(p)
790 p2 = p.insertAsLastChild()
791 p2.h = line
792 u.afterInsertNode(p2, undoType, undoData)
793 #
794 # "before" snapshot.
795 bunch = u.beforeChangeBody(p)
796 p.b = s[:i] + s[j:]
797 w.setInsertPoint(i)
798 p2.setDirty()
799 c.setChanged()
800 #
801 # "after" snapshot.
802 u.afterChangeBody(p, undoType, bunch)
803 #
804 # Finish outer undo.
805 u.afterChangeGroup(p, undoType=undoType)
806 c.redraw_after_icons_changed()
807 p.expand()
808 c.redraw(p)
809 c.bodyWantsFocus()
810#@+node:ekr.20171123135625.11: ** c_ec.preferences
811@g.commander_command('settings')
812def preferences(self, event=None):
813 """Handle the preferences command."""
814 c = self
815 c.openLeoSettings()
816#@+node:ekr.20171123135625.40: ** c_ec.reformatBody
817@g.commander_command('reformat-body')
818def reformatBody(self, event=None):
819 """Reformat all paragraphs in the body."""
820 c, p = self, self.p
821 undoType = 'reformat-body'
822 w = c.frame.body.wrapper
823 c.undoer.beforeChangeGroup(p, undoType)
824 w.setInsertPoint(0)
825 while 1:
826 progress = w.getInsertPoint()
827 c.reformatParagraph(event, undoType=undoType)
828 ins = w.getInsertPoint()
829 s = w.getAllText()
830 w.setInsertPoint(ins)
831 if ins <= progress or ins >= len(s):
832 break
833 c.undoer.afterChangeGroup(p, undoType)
834#@+node:ekr.20171123135625.41: ** c_ec.reformatParagraph & helpers
835@g.commander_command('reformat-paragraph')
836def reformatParagraph(self, event=None, undoType='Reformat Paragraph'):
837 """
838 Reformat a text paragraph
840 Wraps the concatenated text to present page width setting. Leading tabs are
841 sized to present tab width setting. First and second line of original text is
842 used to determine leading whitespace in reformatted text. Hanging indentation
843 is honored.
845 Paragraph is bound by start of body, end of body and blank lines. Paragraph is
846 selected by position of current insertion cursor.
847 """
848 c, w = self, self.frame.body.wrapper
849 if g.app.batchMode:
850 c.notValidInBatchMode("reformat-paragraph")
851 return
852 # Set the insertion point for find_bound_paragraph.
853 if w.hasSelection():
854 i, j = w.getSelectionRange()
855 w.setInsertPoint(i)
856 head, lines, tail = find_bound_paragraph(c)
857 if not lines:
858 return
859 oldSel, oldYview, original, pageWidth, tabWidth = rp_get_args(c)
860 indents, leading_ws = rp_get_leading_ws(c, lines, tabWidth)
861 result = rp_wrap_all_lines(c, indents, leading_ws, lines, pageWidth)
862 rp_reformat(c, head, oldSel, oldYview, original, result, tail, undoType)
863#@+node:ekr.20171123135625.43: *3* function: ends_paragraph & single_line_paragraph
864def ends_paragraph(s):
865 """Return True if s is a blank line."""
866 return not s.strip()
868def single_line_paragraph(s):
869 """Return True if s is a single-line paragraph."""
870 return s.startswith('@') or s.strip() in ('"""', "'''")
871#@+node:ekr.20171123135625.42: *3* function: find_bound_paragraph
872def find_bound_paragraph(c):
873 """
874 Return the lines of a paragraph to be reformatted.
875 This is a convenience method for the reformat-paragraph command.
876 """
877 head, ins, tail = c.frame.body.getInsertLines()
878 head_lines = g.splitLines(head)
879 tail_lines = g.splitLines(tail)
880 result = []
881 insert_lines = g.splitLines(ins)
882 para_lines = insert_lines + tail_lines
883 # If the present line doesn't start a paragraph,
884 # scan backward, adding trailing lines of head to ins.
885 if insert_lines and not startsParagraph(insert_lines[0]):
886 n = 0 # number of moved lines.
887 for i, s in enumerate(reversed(head_lines)):
888 if ends_paragraph(s) or single_line_paragraph(s):
889 break
890 elif startsParagraph(s):
891 n += 1
892 break
893 else: n += 1
894 if n > 0:
895 para_lines = head_lines[-n :] + para_lines
896 head_lines = head_lines[: -n]
897 ended, started = False, False
898 for i, s in enumerate(para_lines):
899 if started:
900 if ends_paragraph(s) or startsParagraph(s):
901 ended = True
902 break
903 else:
904 result.append(s)
905 elif s.strip():
906 result.append(s)
907 started = True
908 if ends_paragraph(s) or single_line_paragraph(s):
909 i += 1
910 ended = True
911 break
912 else:
913 head_lines.append(s)
914 if started:
915 head = ''.join(head_lines)
916 tail_lines = para_lines[i:] if ended else []
917 tail = ''.join(tail_lines)
918 return head, result, tail # string, list, string
919 return None, None, None
920#@+node:ekr.20171123135625.45: *3* function: rp_get_args
921def rp_get_args(c):
922 """Compute and return oldSel,oldYview,original,pageWidth,tabWidth."""
923 body = c.frame.body
924 w = body.wrapper
925 d = c.scanAllDirectives(c.p)
926 if c.editCommands.fillColumn > 0:
927 pageWidth = c.editCommands.fillColumn
928 else:
929 pageWidth = d.get("pagewidth")
930 tabWidth = d.get("tabwidth")
931 original = w.getAllText()
932 oldSel = w.getSelectionRange()
933 oldYview = w.getYScrollPosition()
934 return oldSel, oldYview, original, pageWidth, tabWidth
935#@+node:ekr.20171123135625.46: *3* function: rp_get_leading_ws
936def rp_get_leading_ws(c, lines, tabWidth):
937 """Compute and return indents and leading_ws."""
938 # c = self
939 indents = [0, 0]
940 leading_ws = ["", ""]
941 for i in (0, 1):
942 if i < len(lines):
943 # Use the original, non-optimized leading whitespace.
944 leading_ws[i] = ws = g.get_leading_ws(lines[i])
945 indents[i] = g.computeWidth(ws, tabWidth)
946 indents[1] = max(indents)
947 if len(lines) == 1:
948 leading_ws[1] = leading_ws[0]
949 return indents, leading_ws
950#@+node:ekr.20171123135625.47: *3* function: rp_reformat
951def rp_reformat(c, head, oldSel, oldYview, original, result, tail, undoType):
952 """Reformat the body and update the selection."""
953 p, u, w = c.p, c.undoer, c.frame.body.wrapper
954 s = head + result + tail
955 changed = original != s
956 bunch = u.beforeChangeBody(p)
957 if changed:
958 w.setAllText(s) # Destroys coloring.
959 #
960 # #1748: Always advance to the next paragraph.
961 i = len(head)
962 j = max(i, len(head) + len(result) - 1)
963 ins = j + 1
964 while ins < len(s):
965 i, j = g.getLine(s, ins)
966 line = s[i:j]
967 # It's annoying, imo, to treat @ lines differently.
968 if line.isspace():
969 ins = j + 1
970 else:
971 ins = i
972 break
973 ins = min(ins, len(s))
974 w.setSelectionRange(ins, ins, insert=ins)
975 #
976 # Show more lines, if they exist.
977 k = g.see_more_lines(s, ins, 4)
978 p.v.insertSpot = ins
979 w.see(k) # New in 6.4. w.see works!
980 if not changed:
981 return
982 #
983 # Finish.
984 p.v.b = s # p.b would cause a redraw.
985 u.afterChangeBody(p, undoType, bunch)
986 w.setXScrollPosition(0) # Never scroll horizontally.
987#@+node:ekr.20171123135625.48: *3* function: rp_wrap_all_lines
988def rp_wrap_all_lines(c, indents, leading_ws, lines, pageWidth):
989 """Compute the result of wrapping all lines."""
990 trailingNL = lines and lines[-1].endswith('\n')
991 lines = [z[:-1] if z.endswith('\n') else z for z in lines]
992 if lines: # Bug fix: 2013/12/22.
993 s = lines[0]
994 if startsParagraph(s):
995 # Adjust indents[1]
996 # Similar to code in startsParagraph(s)
997 i = 0
998 if s[0].isdigit():
999 while i < len(s) and s[i].isdigit():
1000 i += 1
1001 if g.match(s, i, ')') or g.match(s, i, '.'):
1002 i += 1
1003 elif s[0].isalpha():
1004 if g.match(s, 1, ')') or g.match(s, 1, '.'):
1005 i = 2
1006 elif s[0] == '-':
1007 i = 1
1008 # Never decrease indentation.
1009 i = g.skip_ws(s, i + 1)
1010 if i > indents[1]:
1011 indents[1] = i
1012 leading_ws[1] = ' ' * i
1013 # Wrap the lines, decreasing the page width by indent.
1014 result_list = g.wrap_lines(lines,
1015 pageWidth - indents[1],
1016 pageWidth - indents[0])
1017 # prefix with the leading whitespace, if any
1018 paddedResult = []
1019 paddedResult.append(leading_ws[0] + result_list[0])
1020 for line in result_list[1:]:
1021 paddedResult.append(leading_ws[1] + line)
1022 # Convert the result to a string.
1023 result = '\n'.join(paddedResult)
1024 if trailingNL:
1025 result = result + '\n'
1026 return result
1027#@+node:ekr.20171123135625.44: *3* function: startsParagraph
1028def startsParagraph(s):
1029 """Return True if line s starts a paragraph."""
1030 if not s.strip():
1031 val = False
1032 elif s.strip() in ('"""', "'''"):
1033 val = True
1034 elif s[0].isdigit():
1035 i = 0
1036 while i < len(s) and s[i].isdigit():
1037 i += 1
1038 val = g.match(s, i, ')') or g.match(s, i, '.')
1039 elif s[0].isalpha():
1040 # Careful: single characters only.
1041 # This could cause problems in some situations.
1042 val = (
1043 (g.match(s, 1, ')') or g.match(s, 1, '.')) and
1044 (len(s) < 2 or s[2] in ' \t\n'))
1045 else:
1046 val = s.startswith('@') or s.startswith('-')
1047 return val
1048#@+node:ekr.20201124191844.1: ** c_ec.reformatSelection
1049@g.commander_command('reformat-selection')
1050def reformatSelection(self, event=None, undoType='Reformat Paragraph'):
1051 """
1052 Reformat the selected text, as in reformat-paragraph, but without
1053 expanding the selection past the selected lines.
1054 """
1055 c, undoType = self, 'reformat-selection'
1056 p, u, w = c.p, c.undoer, c.frame.body.wrapper
1057 if g.app.batchMode:
1058 c.notValidInBatchMode(undoType)
1059 return
1060 bunch = u.beforeChangeBody(p)
1061 oldSel, oldYview, original, pageWidth, tabWidth = rp_get_args(c)
1062 head, middle, tail = c.frame.body.getSelectionLines()
1063 lines = g.splitLines(middle)
1064 if not lines:
1065 return
1066 indents, leading_ws = rp_get_leading_ws(c, lines, tabWidth)
1067 result = rp_wrap_all_lines(c, indents, leading_ws, lines, pageWidth)
1068 s = head + result + tail
1069 if s == original:
1070 return
1071 #
1072 # Update the text and the selection.
1073 w.setAllText(s) # Destroys coloring.
1074 i = len(head)
1075 j = max(i, len(head) + len(result) - 1)
1076 j = min(j, len(s))
1077 w.setSelectionRange(i, j, insert=j)
1078 #
1079 # Finish.
1080 p.v.b = s # p.b would cause a redraw.
1081 u.afterChangeBody(p, undoType, bunch)
1082 w.setXScrollPosition(0) # Never scroll horizontally.
1083#@+node:ekr.20171123135625.12: ** c_ec.show/hide/toggleInvisibles
1084@g.commander_command('hide-invisibles')
1085def hideInvisibles(self, event=None):
1086 """Hide invisible (whitespace) characters."""
1087 c = self
1088 showInvisiblesHelper(c, False)
1090@g.commander_command('show-invisibles')
1091def showInvisibles(self, event=None):
1092 """Show invisible (whitespace) characters."""
1093 c = self
1094 showInvisiblesHelper(c, True)
1096@g.commander_command('toggle-invisibles')
1097def toggleShowInvisibles(self, event=None):
1098 """Toggle showing of invisible (whitespace) characters."""
1099 c = self
1100 colorizer = c.frame.body.getColorizer()
1101 showInvisiblesHelper(c, not colorizer.showInvisibles)
1103def showInvisiblesHelper(c, val):
1104 frame = c.frame
1105 colorizer = frame.body.getColorizer()
1106 colorizer.showInvisibles = val
1107 colorizer.highlighter.showInvisibles = val
1108 # It is much easier to change the menu name here than in the menu updater.
1109 menu = frame.menu.getMenu("Edit")
1110 index = frame.menu.getMenuLabel(menu, 'Hide Invisibles' if val else 'Show Invisibles')
1111 if index is None:
1112 if val:
1113 frame.menu.setMenuLabel(menu, "Show Invisibles", "Hide Invisibles")
1114 else:
1115 frame.menu.setMenuLabel(menu, "Hide Invisibles", "Show Invisibles")
1116 # #240: Set the status bits here.
1117 if hasattr(frame.body, 'set_invisibles'):
1118 frame.body.set_invisibles(c)
1119 c.frame.body.recolor(c.p)
1120#@+node:ekr.20171123135625.55: ** c_ec.toggleAngleBrackets
1121@g.commander_command('toggle-angle-brackets')
1122def toggleAngleBrackets(self, event=None):
1123 """Add or remove double angle brackets from the headline of the selected node."""
1124 c, p = self, self.p
1125 if g.app.batchMode:
1126 c.notValidInBatchMode("Toggle Angle Brackets")
1127 return
1128 c.endEditing()
1129 s = p.h.strip()
1130 # 2019/09/12: Guard against black.
1131 lt = "<<"
1132 rt = ">>"
1133 if s[0:2] == lt or s[-2:] == rt:
1134 if s[0:2] == "<<":
1135 s = s[2:]
1136 if s[-2:] == ">>":
1137 s = s[:-2]
1138 s = s.strip()
1139 else:
1140 s = g.angleBrackets(' ' + s + ' ')
1141 p.setHeadString(s)
1142 p.setDirty() # #1449.
1143 c.setChanged() # #1449.
1144 c.redrawAndEdit(p, selectAll=True)
1145#@+node:ekr.20171123135625.49: ** c_ec.unformatParagraph & helper
1146@g.commander_command('unformat-paragraph')
1147def unformatParagraph(self, event=None, undoType='Unformat Paragraph'):
1148 """
1149 Unformat a text paragraph. Removes all extra whitespace in a paragraph.
1151 Paragraph is bound by start of body, end of body and blank lines. Paragraph is
1152 selected by position of current insertion cursor.
1153 """
1154 c = self
1155 body = c.frame.body
1156 w = body.wrapper
1157 if g.app.batchMode:
1158 c.notValidInBatchMode("unformat-paragraph")
1159 return
1160 if w.hasSelection():
1161 i, j = w.getSelectionRange()
1162 w.setInsertPoint(i)
1163 oldSel, oldYview, original, pageWidth, tabWidth = rp_get_args(c)
1164 head, lines, tail = find_bound_paragraph(c)
1165 if lines:
1166 result = ' '.join([z.strip() for z in lines]) + '\n'
1167 unreformat(c, head, oldSel, oldYview, original, result, tail, undoType)
1168#@+node:ekr.20171123135625.50: *3* function: unreformat
1169def unreformat(c, head, oldSel, oldYview, original, result, tail, undoType):
1170 """unformat the body and update the selection."""
1171 p, u, w = c.p, c.undoer, c.frame.body.wrapper
1172 s = head + result + tail
1173 ins = max(len(head), len(head) + len(result) - 1)
1174 bunch = u.beforeChangeBody(p)
1175 w.setAllText(s) # Destroys coloring.
1176 changed = original != s
1177 if changed:
1178 p.v.b = w.getAllText()
1179 u.afterChangeBody(p, undoType, bunch)
1180 # Advance to the next paragraph.
1181 ins += 1 # Move past the selection.
1182 while ins < len(s):
1183 i, j = g.getLine(s, ins)
1184 line = s[i:j]
1185 if line.isspace():
1186 ins = j + 1
1187 else:
1188 ins = i
1189 break
1190 c.recolor() # Required.
1191 w.setSelectionRange(ins, ins, insert=ins)
1192 # More useful than for reformat-paragraph.
1193 w.see(ins)
1194 # Make sure we never scroll horizontally.
1195 w.setXScrollPosition(0)
1196#@+node:ekr.20180410054716.1: ** c_ec: insert-jupyter-toc & insert-markdown-toc
1197@g.commander_command('insert-jupyter-toc')
1198def insertJupyterTOC(self, event=None):
1199 """
1200 Insert a Jupyter table of contents at the cursor,
1201 replacing any selected text.
1202 """
1203 insert_toc(c=self, kind='jupyter')
1205@g.commander_command('insert-markdown-toc')
1206def insertMarkdownTOC(self, event=None):
1207 """
1208 Insert a Markdown table of contents at the cursor,
1209 replacing any selected text.
1210 """
1211 insert_toc(c=self, kind='markdown')
1212#@+node:ekr.20180410074238.1: *3* insert_toc
1213def insert_toc(c, kind):
1214 """Insert a table of contents at the cursor."""
1215 p, u = c.p, c.undoer
1216 w = c.frame.body.wrapper
1217 undoType = f"Insert {kind.capitalize()} TOC"
1218 if g.app.batchMode:
1219 c.notValidInBatchMode(undoType)
1220 return
1221 bunch = u.beforeChangeBody(p)
1222 w.deleteTextSelection()
1223 s = make_toc(c, kind=kind, root=c.p)
1224 i = w.getInsertPoint()
1225 w.insert(i, s)
1226 p.v.b = w.getAllText()
1227 u.afterChangeBody(p, undoType, bunch)
1228#@+node:ekr.20180410054926.1: *3* make_toc
1229def make_toc(c, kind, root):
1230 """Return the toc for root.b as a list of lines."""
1232 def cell_type(p):
1233 language = g.getLanguageAtPosition(c, p)
1234 return 'markdown' if language in ('jupyter', 'markdown') else 'python'
1236 def clean_headline(s):
1237 # Surprisingly tricky. This could remove too much, but better to be safe.
1238 aList = [ch for ch in s if ch in '-: ' or ch.isalnum()]
1239 return ''.join(aList).rstrip('-').strip()
1241 result: List[str] = []
1242 stack: List[int] = []
1243 for p in root.subtree():
1244 if cell_type(p) == 'markdown':
1245 level = p.level() - root.level()
1246 if len(stack) < level:
1247 stack.append(1)
1248 else:
1249 stack = stack[:level]
1250 n = stack[-1]
1251 stack[-1] = n + 1
1252 # Use bullets
1253 title = clean_headline(p.h)
1254 url = clean_headline(p.h.replace(' ', '-'))
1255 if kind == 'markdown':
1256 url = url.lower()
1257 line = f"{' ' * 4 * (level - 1)}- [{title}](#{url})\n"
1258 result.append(line)
1259 if result:
1260 result.append('\n')
1261 return ''.join(result)
1262#@-others
1263#@-leo