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

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

324 

325 Removes single-line comments if possible; removes 

326 block comments for languages like html that lack 

327 single-line comments. 

328 

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. 

402 

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. 

423 

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. 

428 

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. 

433 

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

438 

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

496 

497# Compatibility 

498 

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) 

528 

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

648 

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. 

722 

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. 

774 

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 

838 

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. 

843 

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

866 

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) 

1088 

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

1090def showInvisibles(self, event=None): 

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

1092 c = self 

1093 showInvisiblesHelper(c, True) 

1094 

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) 

1101 

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. 

1149 

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

1203 

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

1230 

1231 def cell_type(p): 

1232 language = g.getLanguageAtPosition(c, p) 

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

1234 

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

1239 

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