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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

852 statements  

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:] # 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 

416 # Neither of these is used by any caller. 

417#@+node:ekr.20171123135625.23: ** c_ec.extract & helpers 

418@g.commander_command('extract') 

419def extract(self, event=None): 

420 #@+<< docstring for extract command >> 

421 #@+node:ekr.20201113130021.1: *3* << docstring for extract command >> 

422 r""" 

423 Create child node from the selected body text. 

424 

425 1. If the selection starts with a section reference, the section 

426 name becomes the child's headline. All following lines become 

427 the child's body text. The section reference line remains in 

428 the original body text. 

429 

430 2. If the selection looks like a definition line (for the Python, 

431 JavaScript, CoffeeScript or Clojure languages) the 

432 class/function/method name becomes the child's headline and all 

433 selected lines become the child's body text. 

434 

435 You may add additional regex patterns for definition lines using 

436 @data extract-patterns nodes. Each line of the body text should a 

437 valid regex pattern. Lines starting with # are comment lines. Use \# 

438 for patterns starting with #. 

439 

440 3. Otherwise, the first line becomes the child's headline, and all 

441 selected lines become the child's body text. 

442 """ 

443 #@-<< docstring for extract command >> 

444 c, u, w = self, self.undoer, self.frame.body.wrapper 

445 undoType = 'Extract' 

446 # Set data. 

447 head, lines, tail, oldSel, oldYview = c.getBodyLines() 

448 if not lines: 

449 return # Nothing selected. 

450 # 

451 # Remove leading whitespace. 

452 junk, ws = g.skip_leading_ws_with_indent(lines[0], 0, c.tab_width) 

453 lines = [g.removeLeadingWhitespace(s, ws, c.tab_width) for s in lines] 

454 h = lines[0].strip() 

455 ref_h = extractRef(c, h).strip() 

456 def_h = extractDef_find(c, lines) 

457 if ref_h: 

458 h, b, middle = ref_h, lines[1:], ' ' * ws + lines[0] # By vitalije. 

459 elif def_h: 

460 h, b, middle = def_h, lines, '' 

461 else: 

462 h, b, middle = lines[0].strip(), lines[1:], '' 

463 # 

464 # Start the outer undo group. 

465 u.beforeChangeGroup(c.p, undoType) 

466 undoData = u.beforeInsertNode(c.p) 

467 p = createLastChildNode(c, c.p, h, ''.join(b)) 

468 u.afterInsertNode(p, undoType, undoData) 

469 # 

470 # Start inner undo. 

471 if oldSel: 

472 i, j = oldSel 

473 w.setSelectionRange(i, j, insert=j) 

474 bunch = u.beforeChangeBody(c.p) # Not p. 

475 # 

476 # Update the text and selection 

477 c.p.v.b = head + middle + tail # Don't redraw. 

478 w.setAllText(head + middle + tail) 

479 i = len(head) 

480 j = max(i, len(head) + len(middle) - 1) 

481 w.setSelectionRange(i, j, insert=j) 

482 # 

483 # End the inner undo. 

484 u.afterChangeBody(c.p, undoType, bunch) 

485 # 

486 # Scroll as necessary. 

487 if oldYview: 

488 w.setYScrollPosition(oldYview) 

489 else: 

490 w.seeInsertPoint() 

491 # 

492 # Add the changes to the outer undo group. 

493 u.afterChangeGroup(c.p, undoType=undoType) 

494 p.parent().expand() 

495 c.redraw(p.parent()) # A bit more convenient than p. 

496 c.bodyWantsFocus() 

497 

498# Compatibility 

499 

500g.command_alias('extractSection', extract) 

501g.command_alias('extractPythonMethod', extract) 

502#@+node:ekr.20171123135625.20: *3* def createLastChildNode 

503def createLastChildNode(c, parent, headline, body): 

504 """A helper function for the three extract commands.""" 

505 # #1955: don't strip trailing lines. 

506 if not body: 

507 body = "" 

508 p = parent.insertAsLastChild() 

509 p.initHeadString(headline) 

510 p.setBodyString(body) 

511 p.setDirty() 

512 c.validateOutline() 

513 return p 

514#@+node:ekr.20171123135625.24: *3* def extractDef 

515extractDef_patterns = ( 

516 re.compile( 

517 r'\((?:def|defn|defui|deftype|defrecord|defonce)\s+(\S+)'), # clojure definition 

518 re.compile(r'^\s*(?:def|class)\s+(\w+)'), # python definitions 

519 re.compile(r'^\bvar\s+(\w+)\s*=\s*function\b'), # js function 

520 re.compile(r'^(?:export\s)?\s*function\s+(\w+)\s*\('), # js function 

521 re.compile(r'\b(\w+)\s*:\s*function\s'), # js function 

522 re.compile(r'\.(\w+)\s*=\s*function\b'), # js function 

523 re.compile(r'(?:export\s)?\b(\w+)\s*=\s(?:=>|->)'), # coffeescript function 

524 re.compile( 

525 r'(?:export\s)?\b(\w+)\s*=\s(?:\([^)]*\))\s*(?:=>|->)'), # coffeescript function 

526 re.compile(r'\b(\w+)\s*:\s(?:=>|->)'), # coffeescript function 

527 re.compile(r'\b(\w+)\s*:\s(?:\([^)]*\))\s*(?:=>|->)'), # coffeescript function 

528) 

529 

530def extractDef(c, s): 

531 """ 

532 Return the defined function/method/class name if s 

533 looks like definition. Tries several different languages. 

534 """ 

535 for pat in c.config.getData('extract-patterns') or []: 

536 try: 

537 pat = re.compile(pat) 

538 m = pat.search(s) 

539 if m: 

540 return m.group(1) 

541 except Exception: 

542 g.es_print('bad regex in @data extract-patterns', color='blue') 

543 g.es_print(pat) 

544 for pat in extractDef_patterns: 

545 m = pat.search(s) 

546 if m: 

547 return m.group(1) 

548 return '' 

549#@+node:ekr.20171123135625.26: *3* def extractDef_find 

550def extractDef_find(c, lines): 

551 for line in lines: 

552 def_h = extractDef(c, line.strip()) 

553 if def_h: 

554 return def_h 

555 return None 

556#@+node:ekr.20171123135625.25: *3* def extractRef 

557def extractRef(c, s): 

558 """Return s if it starts with a section name.""" 

559 i = s.find('<<') 

560 j = s.find('>>') 

561 if -1 < i < j: 

562 return s 

563 i = s.find('@<') 

564 j = s.find('@>') 

565 if -1 < i < j: 

566 return s 

567 return '' 

568#@+node:ekr.20171123135625.27: ** c_ec.extractSectionNames & helper 

569@g.commander_command('extract-names') 

570def extractSectionNames(self, event=None): 

571 """ 

572 Create child nodes for every section reference in the selected text. 

573 - The headline of each new child node is the section reference. 

574 - The body of each child node is empty. 

575 """ 

576 c = self 

577 current = c.p 

578 u = c.undoer 

579 undoType = 'Extract Section Names' 

580 body = c.frame.body 

581 head, lines, tail, oldSel, oldYview = c.getBodyLines() 

582 if not lines: 

583 g.warning('No lines selected') 

584 return 

585 u.beforeChangeGroup(current, undoType) 

586 found = False 

587 for s in lines: 

588 name = findSectionName(c, s) 

589 if name: 

590 undoData = u.beforeInsertNode(current) 

591 p = createLastChildNode(c, current, name, None) 

592 u.afterInsertNode(p, undoType, undoData) 

593 found = True 

594 c.validateOutline() 

595 if found: 

596 u.afterChangeGroup(current, undoType) 

597 c.redraw(p) 

598 else: 

599 g.warning("selected text should contain section names") 

600 # Restore the selection. 

601 i, j = oldSel 

602 w = body.wrapper 

603 if w: 

604 w.setSelectionRange(i, j) 

605 w.setFocus() 

606#@+node:ekr.20171123135625.28: *3* def findSectionName 

607def findSectionName(self, s): 

608 head1 = s.find("<<") 

609 if head1 > -1: 

610 head2 = s.find(">>", head1) 

611 else: 

612 head1 = s.find("@<") 

613 if head1 > -1: 

614 head2 = s.find("@>", head1) 

615 if head1 == -1 or head2 == -1 or head1 > head2: 

616 name = None 

617 else: 

618 name = s[head1 : head2 + 2] 

619 return name 

620#@+node:ekr.20171123135625.15: ** c_ec.findMatchingBracket 

621@g.commander_command('match-brackets') 

622@g.commander_command('select-to-matching-bracket') 

623def findMatchingBracket(self, event=None): 

624 """Select the text between matching brackets.""" 

625 c, p = self, self.p 

626 if g.app.batchMode: 

627 c.notValidInBatchMode("Match Brackets") 

628 return 

629 language = g.getLanguageAtPosition(c, p) 

630 if language == 'perl': 

631 g.es('match-brackets not supported for', language) 

632 else: 

633 g.MatchBrackets(c, p, language).run() 

634#@+node:ekr.20171123135625.9: ** c_ec.fontPanel 

635@g.commander_command('set-font') 

636def fontPanel(self, event=None): 

637 """Open the font dialog.""" 

638 c = self 

639 frame = c.frame 

640 if not frame.fontPanel: 

641 frame.fontPanel = g.app.gui.createFontPanel(c) 

642 frame.fontPanel.bringToFront() 

643#@+node:ekr.20110402084740.14490: ** c_ec.goToNext/PrevHistory 

644@g.commander_command('goto-next-history-node') 

645def goToNextHistory(self, event=None): 

646 """Go to the next node in the history list.""" 

647 c = self 

648 c.nodeHistory.goNext() 

649 

650@g.commander_command('goto-prev-history-node') 

651def goToPrevHistory(self, event=None): 

652 """Go to the previous node in the history list.""" 

653 c = self 

654 c.nodeHistory.goPrev() 

655#@+node:ekr.20171123135625.30: ** c_ec.alwaysIndentBody (always-indent-region) 

656@g.commander_command('always-indent-region') 

657def alwaysIndentBody(self, event=None): 

658 """ 

659 The always-indent-region command indents each line of the selected body 

660 text. The @tabwidth directive in effect determines amount of 

661 indentation. 

662 """ 

663 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper 

664 # 

665 # #1801: Don't rely on bindings to ensure that we are editing the body. 

666 event_w = event and event.w 

667 if event_w != w: 

668 c.insertCharFromEvent(event) 

669 return 

670 # 

671 # "Before" snapshot. 

672 bunch = u.beforeChangeBody(p) 

673 # 

674 # Initial data. 

675 sel_1, sel_2 = w.getSelectionRange() 

676 tab_width = c.getTabWidth(p) 

677 head, lines, tail, oldSel, oldYview = self.getBodyLines() 

678 # 

679 # Calculate the result. 

680 changed, result = False, [] 

681 for line in lines: 

682 if line.strip(): 

683 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width) 

684 s = g.computeLeadingWhitespace(width + abs(tab_width), tab_width) + line[i:] 

685 result.append(s) 

686 if s != line: 

687 changed = True 

688 else: 

689 result.append('\n') # #2418 

690 if not changed: 

691 return 

692 # 

693 # Set p.b and w's text first. 

694 middle = ''.join(result) 

695 all = head + middle + tail 

696 p.b = all # Sets dirty and changed bits. 

697 w.setAllText(all) 

698 # 

699 # Calculate the proper selection range (i, j, ins). 

700 if sel_1 == sel_2: 

701 line = result[0] 

702 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width) 

703 i = j = len(head) + i 

704 else: 

705 i = len(head) 

706 j = len(head) + len(middle) 

707 if middle.endswith('\n'): # #1742. 

708 j -= 1 

709 # 

710 # Set the selection range and scroll position. 

711 w.setSelectionRange(i, j, insert=j) 

712 w.setYScrollPosition(oldYview) 

713 # 

714 # "after" snapshot. 

715 u.afterChangeBody(p, 'Indent Region', bunch) 

716#@+node:ekr.20210104123442.1: ** c_ec.indentBody (indent-region) 

717@g.commander_command('indent-region') 

718def indentBody(self, event=None): 

719 """ 

720 The indent-region command indents each line of the selected body text. 

721 Unlike the always-indent-region command, this command inserts a tab 

722 (soft or hard) when there is no selected text. 

723 

724 The @tabwidth directive in effect determines amount of indentation. 

725 """ 

726 c, event_w, w = self, event and event.w, self.frame.body.wrapper 

727 # #1801: Don't rely on bindings to ensure that we are editing the body. 

728 if event_w != w: 

729 c.insertCharFromEvent(event) 

730 return 

731 # # 1739. Special case for a *plain* tab bound to indent-region. 

732 sel_1, sel_2 = w.getSelectionRange() 

733 if sel_1 == sel_2: 

734 char = getattr(event, 'char', None) 

735 stroke = getattr(event, 'stroke', None) 

736 if char == '\t' and stroke and stroke.isPlainKey(): 

737 c.editCommands.selfInsertCommand(event) # Handles undo. 

738 return 

739 c.alwaysIndentBody(event) 

740#@+node:ekr.20171123135625.38: ** c_ec.insertBodyTime 

741@g.commander_command('insert-body-time') 

742def insertBodyTime(self, event=None): 

743 """Insert a time/date stamp at the cursor.""" 

744 c, p, u = self, self.p, self.undoer 

745 w = c.frame.body.wrapper 

746 undoType = 'Insert Body Time' 

747 if g.app.batchMode: 

748 c.notValidInBatchMode(undoType) 

749 return 

750 bunch = u.beforeChangeBody(p) 

751 w.deleteTextSelection() 

752 s = self.getTime(body=True) 

753 i = w.getInsertPoint() 

754 w.insert(i, s) 

755 p.v.b = w.getAllText() 

756 u.afterChangeBody(p, undoType, bunch) 

757#@+node:ekr.20171123135625.52: ** c_ec.justify-toggle-auto 

758@g.commander_command("justify-toggle-auto") 

759def justify_toggle_auto(self, event=None): 

760 c = self 

761 if c.editCommands.autojustify == 0: 

762 c.editCommands.autojustify = abs(c.config.getInt("autojustify") or 0) 

763 if c.editCommands.autojustify: 

764 g.es(f"Autojustify on, @int autojustify == {c.editCommands.autojustify}") 

765 else: 

766 g.es("Set @int autojustify in @settings") 

767 else: 

768 c.editCommands.autojustify = 0 

769 g.es("Autojustify off") 

770#@+node:ekr.20190210095609.1: ** c_ec.line_to_headline 

771@g.commander_command('line-to-headline') 

772def line_to_headline(self, event=None): 

773 """ 

774 Create child node from the selected line. 

775 

776 Cut the selected line and make it the new node's headline 

777 """ 

778 c, p, u, w = self, self.p, self.undoer, self.frame.body.wrapper 

779 undoType = 'line-to-headline' 

780 ins, s = w.getInsertPoint(), p.b 

781 i = g.find_line_start(s, ins) 

782 j = g.skip_line(s, i) 

783 line = s[i:j].strip() 

784 if not line: 

785 return 

786 u.beforeChangeGroup(p, undoType) 

787 # 

788 # Start outer undo. 

789 undoData = u.beforeInsertNode(p) 

790 p2 = p.insertAsLastChild() 

791 p2.h = line 

792 u.afterInsertNode(p2, undoType, undoData) 

793 # 

794 # "before" snapshot. 

795 bunch = u.beforeChangeBody(p) 

796 p.b = s[:i] + s[j:] 

797 w.setInsertPoint(i) 

798 p2.setDirty() 

799 c.setChanged() 

800 # 

801 # "after" snapshot. 

802 u.afterChangeBody(p, undoType, bunch) 

803 # 

804 # Finish outer undo. 

805 u.afterChangeGroup(p, undoType=undoType) 

806 c.redraw_after_icons_changed() 

807 p.expand() 

808 c.redraw(p) 

809 c.bodyWantsFocus() 

810#@+node:ekr.20171123135625.11: ** c_ec.preferences 

811@g.commander_command('settings') 

812def preferences(self, event=None): 

813 """Handle the preferences command.""" 

814 c = self 

815 c.openLeoSettings() 

816#@+node:ekr.20171123135625.40: ** c_ec.reformatBody 

817@g.commander_command('reformat-body') 

818def reformatBody(self, event=None): 

819 """Reformat all paragraphs in the body.""" 

820 c, p = self, self.p 

821 undoType = 'reformat-body' 

822 w = c.frame.body.wrapper 

823 c.undoer.beforeChangeGroup(p, undoType) 

824 w.setInsertPoint(0) 

825 while 1: 

826 progress = w.getInsertPoint() 

827 c.reformatParagraph(event, undoType=undoType) 

828 ins = w.getInsertPoint() 

829 s = w.getAllText() 

830 w.setInsertPoint(ins) 

831 if ins <= progress or ins >= len(s): 

832 break 

833 c.undoer.afterChangeGroup(p, undoType) 

834#@+node:ekr.20171123135625.41: ** c_ec.reformatParagraph & helpers 

835@g.commander_command('reformat-paragraph') 

836def reformatParagraph(self, event=None, undoType='Reformat Paragraph'): 

837 """ 

838 Reformat a text paragraph 

839 

840 Wraps the concatenated text to present page width setting. Leading tabs are 

841 sized to present tab width setting. First and second line of original text is 

842 used to determine leading whitespace in reformatted text. Hanging indentation 

843 is honored. 

844 

845 Paragraph is bound by start of body, end of body and blank lines. Paragraph is 

846 selected by position of current insertion cursor. 

847 """ 

848 c, w = self, self.frame.body.wrapper 

849 if g.app.batchMode: 

850 c.notValidInBatchMode("reformat-paragraph") 

851 return 

852 # Set the insertion point for find_bound_paragraph. 

853 if w.hasSelection(): 

854 i, j = w.getSelectionRange() 

855 w.setInsertPoint(i) 

856 head, lines, tail = find_bound_paragraph(c) 

857 if not lines: 

858 return 

859 oldSel, oldYview, original, pageWidth, tabWidth = rp_get_args(c) 

860 indents, leading_ws = rp_get_leading_ws(c, lines, tabWidth) 

861 result = rp_wrap_all_lines(c, indents, leading_ws, lines, pageWidth) 

862 rp_reformat(c, head, oldSel, oldYview, original, result, tail, undoType) 

863#@+node:ekr.20171123135625.43: *3* function: ends_paragraph & single_line_paragraph 

864def ends_paragraph(s): 

865 """Return True if s is a blank line.""" 

866 return not s.strip() 

867 

868def single_line_paragraph(s): 

869 """Return True if s is a single-line paragraph.""" 

870 return s.startswith('@') or s.strip() in ('"""', "'''") 

871#@+node:ekr.20171123135625.42: *3* function: find_bound_paragraph 

872def find_bound_paragraph(c): 

873 """ 

874 Return the lines of a paragraph to be reformatted. 

875 This is a convenience method for the reformat-paragraph command. 

876 """ 

877 head, ins, tail = c.frame.body.getInsertLines() 

878 head_lines = g.splitLines(head) 

879 tail_lines = g.splitLines(tail) 

880 result = [] 

881 insert_lines = g.splitLines(ins) 

882 para_lines = insert_lines + tail_lines 

883 # If the present line doesn't start a paragraph, 

884 # scan backward, adding trailing lines of head to ins. 

885 if insert_lines and not startsParagraph(insert_lines[0]): 

886 n = 0 # number of moved lines. 

887 for i, s in enumerate(reversed(head_lines)): 

888 if ends_paragraph(s) or single_line_paragraph(s): 

889 break 

890 elif startsParagraph(s): 

891 n += 1 

892 break 

893 else: n += 1 

894 if n > 0: 

895 para_lines = head_lines[-n :] + para_lines 

896 head_lines = head_lines[: -n] 

897 ended, started = False, False 

898 for i, s in enumerate(para_lines): 

899 if started: 

900 if ends_paragraph(s) or startsParagraph(s): 

901 ended = True 

902 break 

903 else: 

904 result.append(s) 

905 elif s.strip(): 

906 result.append(s) 

907 started = True 

908 if ends_paragraph(s) or single_line_paragraph(s): 

909 i += 1 

910 ended = True 

911 break 

912 else: 

913 head_lines.append(s) 

914 if started: 

915 head = ''.join(head_lines) 

916 tail_lines = para_lines[i:] if ended else [] 

917 tail = ''.join(tail_lines) 

918 return head, result, tail # string, list, string 

919 return None, None, None 

920#@+node:ekr.20171123135625.45: *3* function: rp_get_args 

921def rp_get_args(c): 

922 """Compute and return oldSel,oldYview,original,pageWidth,tabWidth.""" 

923 body = c.frame.body 

924 w = body.wrapper 

925 d = c.scanAllDirectives(c.p) 

926 if c.editCommands.fillColumn > 0: 

927 pageWidth = c.editCommands.fillColumn 

928 else: 

929 pageWidth = d.get("pagewidth") 

930 tabWidth = d.get("tabwidth") 

931 original = w.getAllText() 

932 oldSel = w.getSelectionRange() 

933 oldYview = w.getYScrollPosition() 

934 return oldSel, oldYview, original, pageWidth, tabWidth 

935#@+node:ekr.20171123135625.46: *3* function: rp_get_leading_ws 

936def rp_get_leading_ws(c, lines, tabWidth): 

937 """Compute and return indents and leading_ws.""" 

938 # c = self 

939 indents = [0, 0] 

940 leading_ws = ["", ""] 

941 for i in (0, 1): 

942 if i < len(lines): 

943 # Use the original, non-optimized leading whitespace. 

944 leading_ws[i] = ws = g.get_leading_ws(lines[i]) 

945 indents[i] = g.computeWidth(ws, tabWidth) 

946 indents[1] = max(indents) 

947 if len(lines) == 1: 

948 leading_ws[1] = leading_ws[0] 

949 return indents, leading_ws 

950#@+node:ekr.20171123135625.47: *3* function: rp_reformat 

951def rp_reformat(c, head, oldSel, oldYview, original, result, tail, undoType): 

952 """Reformat the body and update the selection.""" 

953 p, u, w = c.p, c.undoer, c.frame.body.wrapper 

954 s = head + result + tail 

955 changed = original != s 

956 bunch = u.beforeChangeBody(p) 

957 if changed: 

958 w.setAllText(s) # Destroys coloring. 

959 # 

960 # #1748: Always advance to the next paragraph. 

961 i = len(head) 

962 j = max(i, len(head) + len(result) - 1) 

963 ins = j + 1 

964 while ins < len(s): 

965 i, j = g.getLine(s, ins) 

966 line = s[i:j] 

967 # It's annoying, imo, to treat @ lines differently. 

968 if line.isspace(): 

969 ins = j + 1 

970 else: 

971 ins = i 

972 break 

973 ins = min(ins, len(s)) 

974 w.setSelectionRange(ins, ins, insert=ins) 

975 # 

976 # Show more lines, if they exist. 

977 k = g.see_more_lines(s, ins, 4) 

978 p.v.insertSpot = ins 

979 w.see(k) # New in 6.4. w.see works! 

980 if not changed: 

981 return 

982 # 

983 # Finish. 

984 p.v.b = s # p.b would cause a redraw. 

985 u.afterChangeBody(p, undoType, bunch) 

986 w.setXScrollPosition(0) # Never scroll horizontally. 

987#@+node:ekr.20171123135625.48: *3* function: rp_wrap_all_lines 

988def rp_wrap_all_lines(c, indents, leading_ws, lines, pageWidth): 

989 """Compute the result of wrapping all lines.""" 

990 trailingNL = lines and lines[-1].endswith('\n') 

991 lines = [z[:-1] if z.endswith('\n') else z for z in lines] 

992 if lines: # Bug fix: 2013/12/22. 

993 s = lines[0] 

994 if startsParagraph(s): 

995 # Adjust indents[1] 

996 # Similar to code in startsParagraph(s) 

997 i = 0 

998 if s[0].isdigit(): 

999 while i < len(s) and s[i].isdigit(): 

1000 i += 1 

1001 if g.match(s, i, ')') or g.match(s, i, '.'): 

1002 i += 1 

1003 elif s[0].isalpha(): 

1004 if g.match(s, 1, ')') or g.match(s, 1, '.'): 

1005 i = 2 

1006 elif s[0] == '-': 

1007 i = 1 

1008 # Never decrease indentation. 

1009 i = g.skip_ws(s, i + 1) 

1010 if i > indents[1]: 

1011 indents[1] = i 

1012 leading_ws[1] = ' ' * i 

1013 # Wrap the lines, decreasing the page width by indent. 

1014 result_list = g.wrap_lines(lines, 

1015 pageWidth - indents[1], 

1016 pageWidth - indents[0]) 

1017 # prefix with the leading whitespace, if any 

1018 paddedResult = [] 

1019 paddedResult.append(leading_ws[0] + result_list[0]) 

1020 for line in result_list[1:]: 

1021 paddedResult.append(leading_ws[1] + line) 

1022 # Convert the result to a string. 

1023 result = '\n'.join(paddedResult) 

1024 if trailingNL: 

1025 result = result + '\n' 

1026 return result 

1027#@+node:ekr.20171123135625.44: *3* function: startsParagraph 

1028def startsParagraph(s): 

1029 """Return True if line s starts a paragraph.""" 

1030 if not s.strip(): 

1031 val = False 

1032 elif s.strip() in ('"""', "'''"): 

1033 val = True 

1034 elif s[0].isdigit(): 

1035 i = 0 

1036 while i < len(s) and s[i].isdigit(): 

1037 i += 1 

1038 val = g.match(s, i, ')') or g.match(s, i, '.') 

1039 elif s[0].isalpha(): 

1040 # Careful: single characters only. 

1041 # This could cause problems in some situations. 

1042 val = ( 

1043 (g.match(s, 1, ')') or g.match(s, 1, '.')) and 

1044 (len(s) < 2 or s[2] in ' \t\n')) 

1045 else: 

1046 val = s.startswith('@') or s.startswith('-') 

1047 return val 

1048#@+node:ekr.20201124191844.1: ** c_ec.reformatSelection 

1049@g.commander_command('reformat-selection') 

1050def reformatSelection(self, event=None, undoType='Reformat Paragraph'): 

1051 """ 

1052 Reformat the selected text, as in reformat-paragraph, but without 

1053 expanding the selection past the selected lines. 

1054 """ 

1055 c, undoType = self, 'reformat-selection' 

1056 p, u, w = c.p, c.undoer, c.frame.body.wrapper 

1057 if g.app.batchMode: 

1058 c.notValidInBatchMode(undoType) 

1059 return 

1060 bunch = u.beforeChangeBody(p) 

1061 oldSel, oldYview, original, pageWidth, tabWidth = rp_get_args(c) 

1062 head, middle, tail = c.frame.body.getSelectionLines() 

1063 lines = g.splitLines(middle) 

1064 if not lines: 

1065 return 

1066 indents, leading_ws = rp_get_leading_ws(c, lines, tabWidth) 

1067 result = rp_wrap_all_lines(c, indents, leading_ws, lines, pageWidth) 

1068 s = head + result + tail 

1069 if s == original: 

1070 return 

1071 # 

1072 # Update the text and the selection. 

1073 w.setAllText(s) # Destroys coloring. 

1074 i = len(head) 

1075 j = max(i, len(head) + len(result) - 1) 

1076 j = min(j, len(s)) 

1077 w.setSelectionRange(i, j, insert=j) 

1078 # 

1079 # Finish. 

1080 p.v.b = s # p.b would cause a redraw. 

1081 u.afterChangeBody(p, undoType, bunch) 

1082 w.setXScrollPosition(0) # Never scroll horizontally. 

1083#@+node:ekr.20171123135625.12: ** c_ec.show/hide/toggleInvisibles 

1084@g.commander_command('hide-invisibles') 

1085def hideInvisibles(self, event=None): 

1086 """Hide invisible (whitespace) characters.""" 

1087 c = self 

1088 showInvisiblesHelper(c, False) 

1089 

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

1091def showInvisibles(self, event=None): 

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

1093 c = self 

1094 showInvisiblesHelper(c, True) 

1095 

1096@g.commander_command('toggle-invisibles') 

1097def toggleShowInvisibles(self, event=None): 

1098 """Toggle showing of invisible (whitespace) characters.""" 

1099 c = self 

1100 colorizer = c.frame.body.getColorizer() 

1101 showInvisiblesHelper(c, not colorizer.showInvisibles) 

1102 

1103def showInvisiblesHelper(c, val): 

1104 frame = c.frame 

1105 colorizer = frame.body.getColorizer() 

1106 colorizer.showInvisibles = val 

1107 colorizer.highlighter.showInvisibles = val 

1108 # It is much easier to change the menu name here than in the menu updater. 

1109 menu = frame.menu.getMenu("Edit") 

1110 index = frame.menu.getMenuLabel(menu, 'Hide Invisibles' if val else 'Show Invisibles') 

1111 if index is None: 

1112 if val: 

1113 frame.menu.setMenuLabel(menu, "Show Invisibles", "Hide Invisibles") 

1114 else: 

1115 frame.menu.setMenuLabel(menu, "Hide Invisibles", "Show Invisibles") 

1116 # #240: Set the status bits here. 

1117 if hasattr(frame.body, 'set_invisibles'): 

1118 frame.body.set_invisibles(c) 

1119 c.frame.body.recolor(c.p) 

1120#@+node:ekr.20171123135625.55: ** c_ec.toggleAngleBrackets 

1121@g.commander_command('toggle-angle-brackets') 

1122def toggleAngleBrackets(self, event=None): 

1123 """Add or remove double angle brackets from the headline of the selected node.""" 

1124 c, p = self, self.p 

1125 if g.app.batchMode: 

1126 c.notValidInBatchMode("Toggle Angle Brackets") 

1127 return 

1128 c.endEditing() 

1129 s = p.h.strip() 

1130 # 2019/09/12: Guard against black. 

1131 lt = "<<" 

1132 rt = ">>" 

1133 if s[0:2] == lt or s[-2:] == rt: 

1134 if s[0:2] == "<<": 

1135 s = s[2:] 

1136 if s[-2:] == ">>": 

1137 s = s[:-2] 

1138 s = s.strip() 

1139 else: 

1140 s = g.angleBrackets(' ' + s + ' ') 

1141 p.setHeadString(s) 

1142 p.setDirty() # #1449. 

1143 c.setChanged() # #1449. 

1144 c.redrawAndEdit(p, selectAll=True) 

1145#@+node:ekr.20171123135625.49: ** c_ec.unformatParagraph & helper 

1146@g.commander_command('unformat-paragraph') 

1147def unformatParagraph(self, event=None, undoType='Unformat Paragraph'): 

1148 """ 

1149 Unformat a text paragraph. Removes all extra whitespace in a paragraph. 

1150 

1151 Paragraph is bound by start of body, end of body and blank lines. Paragraph is 

1152 selected by position of current insertion cursor. 

1153 """ 

1154 c = self 

1155 body = c.frame.body 

1156 w = body.wrapper 

1157 if g.app.batchMode: 

1158 c.notValidInBatchMode("unformat-paragraph") 

1159 return 

1160 if w.hasSelection(): 

1161 i, j = w.getSelectionRange() 

1162 w.setInsertPoint(i) 

1163 oldSel, oldYview, original, pageWidth, tabWidth = rp_get_args(c) 

1164 head, lines, tail = find_bound_paragraph(c) 

1165 if lines: 

1166 result = ' '.join([z.strip() for z in lines]) + '\n' 

1167 unreformat(c, head, oldSel, oldYview, original, result, tail, undoType) 

1168#@+node:ekr.20171123135625.50: *3* function: unreformat 

1169def unreformat(c, head, oldSel, oldYview, original, result, tail, undoType): 

1170 """unformat the body and update the selection.""" 

1171 p, u, w = c.p, c.undoer, c.frame.body.wrapper 

1172 s = head + result + tail 

1173 ins = max(len(head), len(head) + len(result) - 1) 

1174 bunch = u.beforeChangeBody(p) 

1175 w.setAllText(s) # Destroys coloring. 

1176 changed = original != s 

1177 if changed: 

1178 p.v.b = w.getAllText() 

1179 u.afterChangeBody(p, undoType, bunch) 

1180 # Advance to the next paragraph. 

1181 ins += 1 # Move past the selection. 

1182 while ins < len(s): 

1183 i, j = g.getLine(s, ins) 

1184 line = s[i:j] 

1185 if line.isspace(): 

1186 ins = j + 1 

1187 else: 

1188 ins = i 

1189 break 

1190 c.recolor() # Required. 

1191 w.setSelectionRange(ins, ins, insert=ins) 

1192 # More useful than for reformat-paragraph. 

1193 w.see(ins) 

1194 # Make sure we never scroll horizontally. 

1195 w.setXScrollPosition(0) 

1196#@+node:ekr.20180410054716.1: ** c_ec: insert-jupyter-toc & insert-markdown-toc 

1197@g.commander_command('insert-jupyter-toc') 

1198def insertJupyterTOC(self, event=None): 

1199 """ 

1200 Insert a Jupyter table of contents at the cursor, 

1201 replacing any selected text. 

1202 """ 

1203 insert_toc(c=self, kind='jupyter') 

1204 

1205@g.commander_command('insert-markdown-toc') 

1206def insertMarkdownTOC(self, event=None): 

1207 """ 

1208 Insert a Markdown table of contents at the cursor, 

1209 replacing any selected text. 

1210 """ 

1211 insert_toc(c=self, kind='markdown') 

1212#@+node:ekr.20180410074238.1: *3* insert_toc 

1213def insert_toc(c, kind): 

1214 """Insert a table of contents at the cursor.""" 

1215 p, u = c.p, c.undoer 

1216 w = c.frame.body.wrapper 

1217 undoType = f"Insert {kind.capitalize()} TOC" 

1218 if g.app.batchMode: 

1219 c.notValidInBatchMode(undoType) 

1220 return 

1221 bunch = u.beforeChangeBody(p) 

1222 w.deleteTextSelection() 

1223 s = make_toc(c, kind=kind, root=c.p) 

1224 i = w.getInsertPoint() 

1225 w.insert(i, s) 

1226 p.v.b = w.getAllText() 

1227 u.afterChangeBody(p, undoType, bunch) 

1228#@+node:ekr.20180410054926.1: *3* make_toc 

1229def make_toc(c, kind, root): 

1230 """Return the toc for root.b as a list of lines.""" 

1231 

1232 def cell_type(p): 

1233 language = g.getLanguageAtPosition(c, p) 

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

1235 

1236 def clean_headline(s): 

1237 # Surprisingly tricky. This could remove too much, but better to be safe. 

1238 aList = [ch for ch in s if ch in '-: ' or ch.isalnum()] 

1239 return ''.join(aList).rstrip('-').strip() 

1240 

1241 result: List[str] = [] 

1242 stack: List[int] = [] 

1243 for p in root.subtree(): 

1244 if cell_type(p) == 'markdown': 

1245 level = p.level() - root.level() 

1246 if len(stack) < level: 

1247 stack.append(1) 

1248 else: 

1249 stack = stack[:level] 

1250 n = stack[-1] 

1251 stack[-1] = n + 1 

1252 # Use bullets 

1253 title = clean_headline(p.h) 

1254 url = clean_headline(p.h.replace(' ', '-')) 

1255 if kind == 'markdown': 

1256 url = url.lower() 

1257 line = f"{' ' * 4 * (level - 1)}- [{title}](#{url})\n" 

1258 result.append(line) 

1259 if result: 

1260 result.append('\n') 

1261 return ''.join(result) 

1262#@-others 

1263#@-leo