Coverage for C:\leo.repo\leo-editor\leo\commands\commanderEditCommands.py : 61%

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