Hide keyboard shortcuts

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. 

19 

20 Inserts single-line comments if possible; inserts 

21 block comments for languages like html that lack 

22 single-line comments. 

23 

24 @bool indent_added_comments 

25 

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. 

30 

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. 

325 

326 Removes single-line comments if possible; removes 

327 block comments for languages like html that lack 

328 single-line comments. 

329 

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. 

403 

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. 

425 

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. 

430 

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. 

435 

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 #. 

440 

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() 

498 

499# Compatibility 

500 

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) 

530 

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() 

650 

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. 

724 

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. 

776 

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 

840 

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. 

845 

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() 

868 

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) 

1090 

1091@g.commander_command('show-invisibles') 

1092def showInvisibles(self, event=None): 

1093 """Show invisible (whitespace) characters.""" 

1094 c = self 

1095 showInvisiblesHelper(c, True) 

1096 

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) 

1103 

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. 

1151 

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') 

1205 

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.""" 

1232 

1233 def cell_type(p): 

1234 language = g.getLanguageAtPosition(c, p) 

1235 return 'markdown' if language in ('jupyter', 'markdown') else 'python' 

1236 

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() 

1241 

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