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