Coverage for C:\leo.repo\leo-editor\leo\core\leoFind.py: 100%

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

1072 statements  

1#@+leo-ver=5-thin 

2#@+node:ekr.20060123151617: * @file leoFind.py 

3"""Leo's gui-independent find classes.""" 

4import keyword 

5import re 

6import sys 

7import time 

8from leo.core import leoGlobals as g 

9 

10#@+<< Theory of operation of find/change >> 

11#@+node:ekr.20031218072017.2414: ** << Theory of operation of find/change >> 

12#@@language rest 

13#@@nosearch 

14#@+at 

15# LeoFind.py contains the gui-independant part of all of Leo's 

16# find/change code. Such code is tricky, which is why it should be 

17# gui-independent code! Here are the governing principles: 

18# 

19# 1. Find and Change commands initialize themselves using only the state 

20# of the present Leo window. In particular, the Find class must not 

21# save internal state information from one invocation to the next. 

22# This means that when the user changes the nodes, or selects new 

23# text in headline or body text, those changes will affect the next 

24# invocation of any Find or Change command. Failure to follow this 

25# principle caused all kinds of problems earlier versions. 

26# 

27# This principle simplifies the code because most ivars do not 

28# persist. However, each command must ensure that the Leo window is 

29# left in a state suitable for restarting the incremental 

30# (interactive) Find and Change commands. Details of initialization 

31# are discussed below. 

32# 

33# 2. The Find and Change commands must not change the state of the 

34# outline or body pane during execution. That would cause severe 

35# flashing and slow down the commands a great deal. In particular, 

36# c.selectPosition and c.editPosition must not be called while 

37# looking for matches. 

38# 

39# 3. When incremental Find or Change commands succeed they must leave 

40# the Leo window in the proper state to execute another incremental 

41# command. We restore the Leo window as it was on entry whenever an 

42# incremental search fails and after any Find All and Replace All 

43# command. Initialization involves setting the self.c, self.v, 

44# self.in_headline, self.wrapping and self.s_text ivars. 

45# 

46# Setting self.in_headline is tricky; we must be sure to retain the 

47# state of the outline pane until initialization is complete. 

48# Initializing the Find All and Replace All commands is much easier 

49# because such initialization does not depend on the state of the Leo 

50# window. Using the same kind of text widget for both headlines and body 

51# text results in a huge simplification of the code. 

52# 

53# The searching code does not know whether it is searching headline or 

54# body text. The search code knows only that self.s_text is a text 

55# widget that contains the text to be searched or changed and the insert 

56# and sel attributes of self.search_text indicate the range of text to 

57# be searched. 

58# 

59# Searching headline and body text simultaneously is complicated. The 

60# find_next_match() method and its helpers handle the many details 

61# involved by setting self.s_text and its insert and sel attributes. 

62#@-<< Theory of operation of find/change >> 

63 

64def cmd(name): 

65 """Command decorator for the findCommands class.""" 

66 return g.new_cmd_decorator(name, ['c', 'findCommands',]) 

67 

68#@+others 

69#@+node:ekr.20061212084717: ** class LeoFind (LeoFind.py) 

70class LeoFind: 

71 """The base class for Leo's Find commands.""" 

72 #@+others 

73 #@+node:ekr.20131117164142.17021: *3* LeoFind.birth 

74 #@+node:ekr.20031218072017.3053: *4* find.__init__ 

75 def __init__(self, c): 

76 """Ctor for LeoFind class.""" 

77 self.c = c 

78 self.expert_mode = False # Set in finishCreate. 

79 self.ftm = None # Created by dw.createFindTab. 

80 self.frame = None 

81 self.k = c.k 

82 self.re_obj = None 

83 # 

84 # The work "widget". 

85 self.work_s = '' # p.b or p.c. 

86 self.work_sel = (0, 0, 0) # pos, newpos, insert. 

87 # 

88 # Options ivars: set by FindTabManager.init. 

89 self.ignore_case = None 

90 self.node_only = None 

91 self.pattern_match = None 

92 self.search_headline = None 

93 self.search_body = None 

94 self.suboutline_only = None 

95 self.mark_changes = None 

96 self.mark_finds = None 

97 self.whole_word = None 

98 # 

99 # For isearch commands... 

100 self.stack = [] # Entries are (p, sel) 

101 self.isearch_ignore_case = None 

102 self.isearch_forward_flag = None 

103 self.isearch_regexp = None 

104 self.findTextList = [] 

105 self.changeTextList = [] 

106 # 

107 # For find/change... 

108 self.find_text = "" 

109 self.change_text = "" 

110 # 

111 # State machine... 

112 self.escape_handler = None 

113 self.handler = None 

114 # "Delayed" requests for do_find_next. 

115 self.request_reverse = False 

116 self.request_pattern_match = False 

117 self.request_whole_word = False 

118 # Internal state... 

119 self.changeAllFlag = False 

120 self.findAllUniqueFlag = False 

121 self.find_def_data = None 

122 self.in_headline = False 

123 self.match_obj = None 

124 self.reverse = False 

125 self.root = None # The start of the search, especially for suboutline-only. 

126 self.unique_matches = set() 

127 # 

128 # User settings. 

129 self.minibuffer_mode = None 

130 self.reload_settings() 

131 #@+node:ekr.20210110073117.6: *4* find.default_settings 

132 def default_settings(self): 

133 """Return a dict representing all default settings.""" 

134 c = self.c 

135 return g.Bunch( 

136 # State... 

137 in_headline=False, 

138 p=c.rootPosition(), 

139 # Find/change strings... 

140 find_text='', 

141 change_text='', 

142 # Find options... 

143 ignore_case=False, 

144 mark_changes=False, 

145 mark_finds=False, 

146 node_only=False, 

147 pattern_match=False, 

148 reverse=False, 

149 search_body=True, 

150 search_headline=True, 

151 suboutline_only=False, 

152 whole_word=False, 

153 wrapping=False, 

154 ) 

155 #@+node:ekr.20131117164142.17022: *4* find.finishCreate 

156 def finishCreate(self): # pragma: no cover 

157 # New in 4.11.1. 

158 # Must be called when config settings are valid. 

159 c = self.c 

160 self.reload_settings() 

161 # now that configuration settings are valid, 

162 # we can finish creating the Find pane. 

163 dw = c.frame.top 

164 if dw: 

165 dw.finishCreateLogPane() 

166 #@+node:ekr.20210110073117.4: *4* find.init_ivars_from_settings 

167 def init_ivars_from_settings(self, settings): 

168 """ 

169 Initialize all ivars from settings, including required defaults. 

170 

171 This should be called from the do_ methods as follows: 

172 

173 self.init_ivars_from_settings(settings) 

174 if not self.check_args('find-next'): 

175 return <appropriate error indication> 

176 """ 

177 # 

178 # Init required defaults. 

179 self.reverse = False 

180 # 

181 # Init find/change strings. 

182 self.change_text = settings.change_text 

183 self.find_text = settings.find_text 

184 # 

185 # Init find options. 

186 self.ignore_case = settings.ignore_case 

187 self.mark_changes = settings.mark_changes 

188 self.mark_finds = settings.mark_finds 

189 self.node_only = settings.node_only 

190 self.pattern_match = settings.pattern_match 

191 self.search_body = settings.search_body 

192 self.search_headline = settings.search_headline 

193 self.suboutline_only = settings.suboutline_only 

194 self.whole_word = settings.whole_word 

195 # self.wrapping = settings.wrapping 

196 #@+node:ekr.20210110073117.5: *5* NEW:find.init_settings 

197 def init_settings(self, settings): 

198 """Initialize all user settings.""" 

199 

200 #@+node:ekr.20171113164709.1: *4* find.reload_settings 

201 def reload_settings(self): 

202 """LeoFind.reload_settings.""" 

203 c = self.c 

204 self.minibuffer_mode = c.config.getBool('minibuffer-find-mode', default=False) 

205 self.reverse_find_defs = c.config.getBool('reverse-find-defs', default=False) 

206 #@+node:ekr.20210108053422.1: *3* find.batch_change (script helper) & helpers 

207 def batch_change(self, root, replacements, settings=None): 

208 #@+<< docstring: find.batch_change >> 

209 #@+node:ekr.20210925161347.1: *4* << docstring: find.batch_change >> 

210 """ 

211 Support batch change scripts. 

212 

213 replacement: a list of tuples (find_string, change_string). 

214 settings: a dict or g.Bunch containing find/change settings. 

215 See find._init_from_dict for a list of valid settings. 

216 

217 Example: 

218 

219 h = '@file src/ekr/coreFind.py' 

220 root = g.findNodeAnywhere(c, h) 

221 assert root 

222 replacements = ( 

223 ('clone_find_all', 'do_clone_find_all'), 

224 ('clone_find_all_flattened', 'do_clone_find_all_flattened'), 

225 ) 

226 settings = dict(suboutline_only=True) 

227 count = c.findCommands.batch_change(root, replacements, settings) 

228 if count: 

229 c.save() 

230 """ 

231 #@-<< docstring: find.batch_change >> 

232 try: 

233 self._init_from_dict(settings or {}) 

234 count = 0 

235 for find, change in replacements: 

236 count += self._batch_change_helper(root, find, change) 

237 return count 

238 except Exception: # pragma: no cover 

239 g.es_exception() 

240 return 0 

241 #@+node:ekr.20210108070948.1: *4* find._batch_change_helper 

242 def _batch_change_helper(self, p, find_text, change_text): 

243 

244 c, p1, u = self.c, p.copy(), self.c.undoer 

245 undoType = 'Batch Change All' 

246 # Check... 

247 if not find_text: # pragma: no cover 

248 return 0 

249 if not self.search_headline and not self.search_body: 

250 return 0 # pragma: no cover 

251 if self.pattern_match: 

252 ok = self.precompile_pattern() 

253 if not ok: # pragma: no cover 

254 return 0 

255 # Init... 

256 self.find_text = find_text 

257 self.change_text = self.replace_back_slashes(change_text) 

258 if self.node_only: 

259 positions = [p1] 

260 elif self.suboutline_only: 

261 positions = p1.self_and_subtree() 

262 else: 

263 positions = c.all_unique_positions() 

264 # Init the work widget. 

265 s = p.h if self.in_headline else p.b 

266 self.work_s = s 

267 self.work_sel = (0, 0, 0) 

268 # The main loop. 

269 u.beforeChangeGroup(p1, undoType) 

270 count = 0 

271 for p in positions: 

272 count_h, count_b = 0, 0 

273 undoData = u.beforeChangeNodeContents(p) 

274 if self.search_headline: 

275 count_h, new_h = self._change_all_search_and_replace(p.h) 

276 if count_h: 

277 count += count_h 

278 p.h = new_h 

279 if self.search_body: 

280 count_b, new_b = self._change_all_search_and_replace(p.b) 

281 if count_b: 

282 count += count_b 

283 p.b = new_b 

284 if count_h or count_b: 

285 u.afterChangeNodeContents(p1, 'Replace All', undoData) 

286 u.afterChangeGroup(p1, undoType, reportFlag=True) 

287 if not g.unitTesting: # pragma: no cover 

288 print(f"{count:3}: {find_text:>30} => {change_text}") 

289 return count 

290 #@+node:ekr.20210108083003.1: *4* find._init_from_dict 

291 def _init_from_dict(self, settings): 

292 """Initialize ivars from settings (a dict or g.Bunch).""" 

293 # The valid ivars and reasonable defaults. 

294 valid = dict( 

295 ignore_case=False, 

296 node_only=False, 

297 pattern_match=False, 

298 search_body=True, 

299 search_headline=True, 

300 suboutline_only=False, # Seems safest. # Was True !!! 

301 whole_word=True, 

302 ) 

303 # Set ivars to reasonable defaults. 

304 for ivar in valid: 

305 setattr(self, ivar, valid.get(ivar)) 

306 # Override ivars from settings. 

307 errors = 0 

308 for ivar in settings.keys(): 

309 if ivar in valid: 

310 val = settings.get(ivar) 

311 if val in (True, False): 

312 setattr(self, ivar, val) 

313 else: # pragma: no cover 

314 g.trace("bad value: {ivar!r} = {val!r}") 

315 errors += 1 

316 else: # pragma: no cover 

317 g.trace(f"ignoring {ivar!r} setting") 

318 errors += 1 

319 if errors: # pragma: no cover 

320 g.printObj(sorted(valid.keys()), tag='valid keys') 

321 #@+node:ekr.20210925161148.1: *3* find.interactive_search_helper 

322 def interactive_search_helper(self, root=None, settings=None): # pragma: no cover 

323 #@+<< docstring: find.interactive_search >> 

324 #@+node:ekr.20210925161451.1: *4* << docstring: find.interactive_search >> 

325 """ 

326 Support interactive find. 

327 

328 c.findCommands.interactive_search_helper starts an interactive search with 

329 the given settings. The settings argument may be either a g.Bunch or a 

330 dict. 

331 

332 Example 1, settings is a g.Bunch: 

333 

334 c.findCommands.interactive_search_helper( 

335 root = c.p, 

336 settings = g.Bunch( 

337 find_text = '^(def )', 

338 change_text = '\1', 

339 pattern_match=True, 

340 search_headline=False, 

341 whole_word=False, 

342 ) 

343 ) 

344  

345 Example 2, settings is a python dict: 

346  

347 c.findCommands.interactive_search_helper( 

348 root = c.p, 

349 settings = { 

350 'find_text': '^(def )', 

351 'change_text': '\1', 

352 'pattern_match': True, 

353 'search_headline': False, 

354 'whole_word': False, 

355 } 

356 ) 

357 """ 

358 #@-<< docstring: find.interactive_search >> 

359 # Merge settings into default settings. 

360 c = self.c 

361 d = self.default_settings() # A g.bunch 

362 if settings: 

363 # Settings can be a dict or a g.Bunch. 

364 # g.Bunch has no update method. 

365 for key in settings.keys(): 

366 d[key] = settings[key] 

367 self.ftm.set_widgets_from_dict(d) # So the *next* find-next will work. 

368 self.show_find_options_in_status_area() 

369 if not self.check_args('find-next'): 

370 return 

371 if root: 

372 c.selectPosition(root) 

373 self.do_find_next(d) 

374 #@+node:ekr.20031218072017.3055: *3* LeoFind.Commands (immediate execution) 

375 #@+node:ekr.20031218072017.3062: *4* find.change-then-find & helper 

376 @cmd('replace-then-find') 

377 @cmd('change-then-find') 

378 def change_then_find(self, event=None): # pragma: no cover (cmd) 

379 """Handle the replace-then-find command.""" 

380 # Settings... 

381 self.init_in_headline() 

382 settings = self.ftm.get_settings() 

383 self.do_change_then_find(settings) 

384 #@+node:ekr.20210114100105.1: *5* find.do_change_then_find 

385 # A stand-alone method for unit testing. 

386 def do_change_then_find(self, settings): 

387 """ 

388 Do the change-then-find command from settings. 

389 

390 This is a stand-alone method for unit testing. 

391 """ 

392 p = self.c.p 

393 self.init_ivars_from_settings(settings) 

394 if not self.check_args('change-then-find'): 

395 return False 

396 if self.change_selection(p): 

397 self.do_find_next(settings) 

398 return True 

399 

400 #@+node:ekr.20160224175312.1: *4* find.clone-find_marked & helper 

401 @cmd('clone-find-all-marked') 

402 @cmd('cfam') 

403 def cloneFindAllMarked(self, event=None): 

404 """ 

405 clone-find-all-marked, aka cfam. 

406 

407 Create an organizer node whose descendants contain clones of all marked 

408 nodes. The list is *not* flattened: clones appear only once in the 

409 descendants of the organizer node. 

410 """ 

411 self.do_find_marked(flatten=False) 

412 

413 @cmd('clone-find-all-flattened-marked') 

414 @cmd('cffm') 

415 def cloneFindAllFlattenedMarked(self, event=None): 

416 """ 

417 clone-find-all-flattened-marked, aka cffm. 

418 

419 Create an organizer node whose direct children are clones of all marked 

420 nodes. The list is flattened: every cloned node appears as a direct 

421 child of the organizer node, even if the clone also is a descendant of 

422 another cloned node. 

423 """ 

424 self.do_find_marked(flatten=True) 

425 #@+node:ekr.20161022121036.1: *5* find.do_find_marked 

426 def do_find_marked(self, flatten): 

427 """ 

428 Helper for clone-find-marked commands. 

429 

430 This is a stand-alone method for unit testing. 

431 """ 

432 c = self.c 

433 

434 def isMarked(p): 

435 return p.isMarked() 

436 

437 root = c.cloneFindByPredicate( 

438 generator=c.all_unique_positions, 

439 predicate=isMarked, 

440 failMsg='No marked nodes', 

441 flatten=flatten, 

442 undoType='clone-find-marked', 

443 ) 

444 if root: 

445 # Unmarking all nodes is convenient. 

446 for v in c.all_unique_nodes(): 

447 if v.isMarked(): 

448 v.clearMarked() 

449 n = root.numberOfChildren() 

450 root.b = f"# Found {n} marked node{g.plural(n)}" 

451 c.selectPosition(root) 

452 c.redraw(root) 

453 return bool(root) 

454 #@+node:ekr.20140828080010.18532: *4* find.clone-find-parents 

455 @cmd('clone-find-parents') 

456 def cloneFindParents(self, event=None): 

457 """ 

458 Create an organizer node whose direct children are clones of all 

459 parents of the selected node, which must be a clone. 

460 """ 

461 c, u = self.c, self.c.undoer 

462 p = c.p 

463 if not p: # pragma: no cover 

464 return False 

465 if not p.isCloned(): # pragma: no cover 

466 g.es(f"not a clone: {p.h}") 

467 return False 

468 p0 = p.copy() 

469 undoType = 'Find Clone Parents' 

470 aList = c.vnode2allPositions(p.v) 

471 if not aList: # pragma: no cover 

472 g.trace('can not happen: no parents') 

473 return False 

474 # Create the node as the last top-level node. 

475 # All existing positions remain valid. 

476 u.beforeChangeGroup(p, undoType) 

477 b = u.beforeInsertNode(p) 

478 found = c.lastTopLevel().insertAfter() 

479 found.h = f"Found: parents of {p.h}" 

480 u.afterInsertNode(found, 'insert', b) 

481 seen = [] 

482 for p2 in aList: 

483 parent = p2.parent() 

484 if parent and parent.v not in seen: 

485 seen.append(parent.v) 

486 b = u.beforeCloneNode(parent) 

487 # Bug fix 2021/06/15: Create the clone directly as a child of found. 

488 clone = p.copy() 

489 n = found.numberOfChildren() 

490 clone._linkCopiedAsNthChild(found, n) 

491 u.afterCloneNode(clone, 'clone', b) 

492 u.afterChangeGroup(p0, undoType) 

493 c.setChanged() 

494 c.redraw(found) 

495 return True 

496 #@+node:ekr.20150629084204.1: *4* find.find-def, do_find_def & helpers 

497 @cmd('find-def') 

498 def find_def(self, event=None, strict=False): # pragma: no cover (cmd) 

499 """Find the def or class under the cursor.""" 

500 ftm, p = self.ftm, self.c.p 

501 # Check. 

502 word = self._compute_find_def_word(event) 

503 if not word: 

504 return None, None, None 

505 # Settings... 

506 prefix = 'class' if word[0].isupper() else 'def' 

507 find_pattern = prefix + ' ' + word 

508 ftm.set_find_text(find_pattern) 

509 self._save_before_find_def(p) # Save previous settings. 

510 self.init_vim_search(find_pattern) 

511 self.update_change_list(self.change_text) # Optional. An edge case. 

512 # Do the command! 

513 settings = self._compute_find_def_settings(find_pattern) 

514 return self.do_find_def(settings, word, strict) 

515 

516 def find_def_strict(self, event=None): #pragma: no cover (cmd) 

517 """Same as find_def, but don't call _switch_style.""" 

518 return self.find_def(event=event, strict=True) 

519 

520 def do_find_def(self, settings, word, strict): 

521 """A standalone helper for unit tests.""" 

522 return self._fd_helper(settings, word, def_flag=True, strict=strict) 

523 

524 #@+node:ekr.20210114202757.1: *5* find._compute_find_def_settings 

525 def _compute_find_def_settings(self, find_pattern): 

526 

527 settings = self.default_settings() 

528 table = ( 

529 ('change_text', ''), 

530 ('find_text', find_pattern), 

531 ('ignore_case', False), 

532 ('pattern_match', False), 

533 ('reverse', False), 

534 ('search_body', True), 

535 ('search_headline', False), 

536 ('whole_word', True), 

537 ) 

538 for attr, val in table: 

539 # Guard against renamings & misspellings. 

540 assert hasattr(self, attr), attr 

541 assert attr in settings.__dict__, attr 

542 # Set the values. 

543 setattr(self, attr, val) 

544 settings[attr] = val 

545 return settings 

546 #@+node:ekr.20150629084611.1: *5* find._compute_find_def_word 

547 def _compute_find_def_word(self, event): # pragma: no cover (cmd) 

548 """Init the find-def command. Return the word to find or None.""" 

549 c = self.c 

550 w = c.frame.body.wrapper 

551 # First get the word. 

552 c.bodyWantsFocusNow() 

553 if not w.hasSelection(): 

554 c.editCommands.extendToWord(event, select=True) 

555 word = w.getSelectedText().strip() 

556 if not word: 

557 return None 

558 if keyword.iskeyword(word): 

559 return None 

560 # Return word, stripped of preceding class or def. 

561 for tag in ('class ', 'def '): 

562 found = word.startswith(tag) and len(word) > len(tag) 

563 if found: 

564 return word[len(tag) :].strip() 

565 return word 

566 #@+node:ekr.20150629125733.1: *5* find._fd_helper 

567 def _fd_helper(self, settings, word, def_flag, strict): 

568 """ 

569 Find the definition of the class, def or var under the cursor. 

570 

571 return p, pos, newpos for unit tests. 

572 """ 

573 c, find, ftm = self.c, self, self.ftm 

574 # 

575 # Recompute find_text for unit tests. 

576 if def_flag: 

577 prefix = 'class' if word[0].isupper() else 'def' 

578 self.find_text = settings.find_text = prefix + ' ' + word 

579 else: 

580 self.find_text = settings.find_text = word + ' =' 

581 # g.printObj(settings, tag='_fd_helper: settings') 

582 # 

583 # Just search body text. 

584 self.search_headline = False 

585 self.search_body = True 

586 w = c.frame.body.wrapper 

587 # Check. 

588 if not w: # pragma: no cover 

589 return None, None, None 

590 save_sel = w.getSelectionRange() 

591 ins = w.getInsertPoint() 

592 old_p = c.p 

593 if self.reverse_find_defs: 

594 # #2161: start at the last position. 

595 p = c.lastPosition() 

596 else: 

597 # Start in the root position. 

598 p = c.rootPosition() 

599 # Required. 

600 c.selectPosition(p) 

601 c.redraw() 

602 c.bodyWantsFocusNow() 

603 # #1592. Ignore hits under control of @nosearch 

604 old_reverse = self.reverse 

605 try: 

606 # #2161: 

607 self.reverse = self.reverse_find_defs 

608 # # 2288: 

609 self.work_s = p.b 

610 if self.reverse_find_defs: 

611 self.work_sel = (len(p.b), len(p.b), len(p.b)) 

612 else: 

613 self.work_sel = (0, 0, 0) 

614 while True: 

615 p, pos, newpos = self.find_next_match(p) 

616 found = pos is not None 

617 if found or not g.inAtNosearch(p): # do *not* use c.p. 

618 break 

619 if not found and def_flag and not strict: 

620 # Leo 5.7.3: Look for an alternative defintion of function/methods. 

621 word2 = self._switch_style(word) 

622 if self.reverse_find_defs: 

623 # #2161: start at the last position. 

624 p = c.lastPosition() 

625 else: 

626 p = c.rootPosition() 

627 if word2: 

628 find_pattern = prefix + ' ' + word2 

629 find.find_text = find_pattern 

630 ftm.set_find_text(find_pattern) 

631 # #1592. Ignore hits under control of @nosearch 

632 while True: 

633 p, pos, newpos = self.find_next_match(p) 

634 found = pos is not None 

635 if not found or not g.inAtNosearch(p): 

636 break 

637 finally: 

638 self.reverse = old_reverse 

639 if found: 

640 c.redraw(p) 

641 w.setSelectionRange(pos, newpos, insert=newpos) 

642 c.bodyWantsFocusNow() 

643 return p, pos, newpos 

644 self._restore_after_find_def() # Avoid massive confusion! 

645 i, j = save_sel 

646 c.redraw(old_p) 

647 w.setSelectionRange(i, j, insert=ins) 

648 c.bodyWantsFocusNow() 

649 return None, None, None 

650 #@+node:ekr.20150629095511.1: *5* find._restore_after_find_def 

651 def _restore_after_find_def(self): 

652 """Restore find settings in effect before a find-def command.""" 

653 # pylint: disable=no-member 

654 b = self.find_def_data # A g.Bunch 

655 if b: 

656 self.ignore_case = b.ignore_case 

657 self.pattern_match = b.pattern_match 

658 self.search_body = b.search_body 

659 self.search_headline = b.search_headline 

660 self.whole_word = b.whole_word 

661 self.find_def_data = None 

662 #@+node:ekr.20150629095633.1: *5* find._save_before_find_def 

663 def _save_before_find_def(self, p): 

664 """Save the find settings in effect before a find-def command.""" 

665 if not self.find_def_data: 

666 self.find_def_data = g.Bunch( 

667 ignore_case=self.ignore_case, 

668 p=p.copy(), 

669 pattern_match=self.pattern_match, 

670 search_body=self.search_body, 

671 search_headline=self.search_headline, 

672 whole_word=self.whole_word, 

673 ) 

674 #@+node:ekr.20180511045458.1: *5* find._switch_style 

675 def _switch_style(self, word): 

676 """ 

677 Switch between camelCase and underscore_style function defintiions. 

678 Return None if there would be no change. 

679 """ 

680 s = word 

681 if not s: 

682 return None 

683 if s[0].isupper(): 

684 return None # Don't convert class names. 

685 if s.find('_') > -1: 

686 # Convert to CamelCase 

687 s = s.lower() 

688 while s: 

689 i = s.find('_') 

690 if i == -1: 

691 break 

692 s = s[:i] + s[i + 1 :].capitalize() 

693 return s 

694 # Convert to underscore_style. 

695 result = [] 

696 for i, ch in enumerate(s): 

697 if i > 0 and ch.isupper(): 

698 result.append('_') 

699 result.append(ch.lower()) 

700 s = ''.join(result) 

701 return None if s == word else s 

702 #@+node:ekr.20031218072017.3063: *4* find.find-next, find-prev & do_find_* 

703 @cmd('find-next') 

704 def find_next(self, event=None): # pragma: no cover (cmd) 

705 """The find-next command.""" 

706 # Settings... 

707 self.reverse = False 

708 self.init_in_headline() # Do this *before* creating the settings. 

709 settings = self.ftm.get_settings() 

710 # Do the command! 

711 self.do_find_next(settings) 

712 

713 @cmd('find-prev') 

714 def find_prev(self, event=None): # pragma: no cover (cmd) 

715 """Handle F2 (find-previous)""" 

716 # Settings... 

717 self.init_in_headline() # Do this *before* creating the settings. 

718 settings = self.ftm.get_settings() 

719 # Do the command! 

720 self.do_find_prev(settings) 

721 #@+node:ekr.20031218072017.3074: *5* find.do_find_next & do_find_prev 

722 def do_find_prev(self, settings): 

723 """Find the previous instance of self.find_text.""" 

724 self.request_reverse = True 

725 return self.do_find_next(settings) 

726 

727 def do_find_next(self, settings): 

728 """ 

729 Find the next instance of self.find_text. 

730 

731 Return True (for vim-mode) if a match was found. 

732 

733 """ 

734 c, p = self.c, self.c.p 

735 # 

736 # The gui widget may not exist for headlines. 

737 gui_w = c.edit_widget(p) if self.in_headline else c.frame.body.wrapper 

738 # 

739 # Init the work widget, so we don't get stuck. 

740 s = p.h if self.in_headline else p.b 

741 ins = gui_w.getInsertPoint() if gui_w else 0 

742 self.work_s = s 

743 self.work_sel = (ins, ins, ins) 

744 # 

745 # Set the settings *after* initing the search. 

746 self.init_ivars_from_settings(settings) 

747 # 

748 # Honor delayed requests. 

749 for ivar in ('reverse', 'pattern_match', 'whole_word'): 

750 request = 'request_' + ivar 

751 val = getattr(self, request) 

752 if val: # Only *set* the ivar! 

753 setattr(self, ivar, val) # Set the ivar. 

754 setattr(self, request, False) # Clear the request! 

755 # 

756 # Leo 6.4: set/clear self.root 

757 if self.root: # pragma: no cover 

758 if p != self.root and not self.root.isAncestorOf(p): 

759 # p is outside of self.root's tree. 

760 # Clear suboutline-only. 

761 self.root = None 

762 self.suboutline_only = False 

763 self.set_find_scope_every_where() # Update find-tab & status area. 

764 elif self.suboutline_only: 

765 # Start the range and set suboutline-only. 

766 self.root = c.p 

767 self.set_find_scope_suboutline_only() # Update find-tab & status area. 

768 # 

769 # Now check the args. 

770 tag = 'find-prev' if self.reverse else 'find-next' 

771 if not self.check_args(tag): # Issues error message. 

772 return None, None, None 

773 data = self.save() 

774 p, pos, newpos = self.find_next_match(p) 

775 found = pos is not None 

776 if found: 

777 self.show_success(p, pos, newpos) 

778 else: 

779 # Restore previous position. 

780 self.restore(data) 

781 self.show_status(found) 

782 return p, pos, newpos 

783 #@+node:ekr.20131117164142.17015: *4* find.find-tab-hide 

784 @cmd('find-tab-hide') 

785 def hide_find_tab(self, event=None): # pragma: no cover (cmd) 

786 """Hide the Find tab.""" 

787 c = self.c 

788 if self.minibuffer_mode: 

789 c.k.keyboardQuit() 

790 else: 

791 self.c.frame.log.selectTab('Log') 

792 #@+node:ekr.20131117164142.16916: *4* find.find-tab-open 

793 @cmd('find-tab-open') 

794 def open_find_tab(self, event=None, show=True): # pragma: no cover (cmd) 

795 """Open the Find tab in the log pane.""" 

796 c = self.c 

797 if c.config.getBool('use-find-dialog', default=True): 

798 g.app.gui.openFindDialog(c) 

799 else: 

800 c.frame.log.selectTab('Find') 

801 #@+node:ekr.20210118003803.1: *4* find.find-var & do_find_var 

802 @cmd('find-var') 

803 def find_var(self, event=None): # pragma: no cover (cmd) 

804 """Find the var under the cursor.""" 

805 ftm, p = self.ftm, self.c.p 

806 # Check... 

807 word = self._compute_find_def_word(event) 

808 if not word: 

809 return 

810 # Settings... 

811 self.find_pattern = find_pattern = word + ' =' 

812 ftm.set_find_text(find_pattern) 

813 self._save_before_find_def(p) # Save previous settings. 

814 self.init_vim_search(find_pattern) 

815 self.update_change_list(self.change_text) # Optional. An edge case. 

816 settings = self._compute_find_def_settings(find_pattern) 

817 # Do the command! 

818 self.do_find_var(settings, word) 

819 

820 def do_find_var(self, settings, word): 

821 """A standalone helper for unit tests.""" 

822 return self._fd_helper(settings, word, def_flag=False, strict=False) 

823 #@+node:ekr.20141113094129.6: *4* find.focus-to-find 

824 @cmd('focus-to-find') 

825 def focus_to_find(self, event=None): # pragma: no cover (cmd) 

826 c = self.c 

827 if c.config.getBool('use-find-dialog', default=True): 

828 g.app.gui.openFindDialog(c) 

829 else: 

830 c.frame.log.selectTab('Find') 

831 #@+node:ekr.20031218072017.3068: *4* find.replace 

832 @cmd('replace') 

833 @cmd('change') 

834 def change(self, event=None): # pragma: no cover (cmd) 

835 """Replace the selected text with the replacement text.""" 

836 p = self.c.p 

837 if self.check_args('replace'): 

838 self.init_in_headline() 

839 self.change_selection(p) 

840 

841 replace = change 

842 #@+node:ekr.20131117164142.17019: *4* find.set-find-* 

843 @cmd('set-find-everywhere') 

844 def set_find_scope_every_where(self, event=None): # pragma: no cover (cmd) 

845 """Set the 'Entire Outline' radio button in the Find tab.""" 

846 return self.set_find_scope('entire-outline') 

847 

848 @cmd('set-find-node-only') 

849 def set_find_scope_node_only(self, event=None): # pragma: no cover (cmd) 

850 """Set the 'Node Only' radio button in the Find tab.""" 

851 return self.set_find_scope('node-only') 

852 

853 @cmd('set-find-suboutline-only') 

854 def set_find_scope_suboutline_only(self, event=None): 

855 """Set the 'Suboutline Only' radio button in the Find tab.""" 

856 return self.set_find_scope('suboutline-only') 

857 

858 def set_find_scope(self, where): 

859 """Set the radio buttons to the given scope""" 

860 c, fc = self.c, self.c.findCommands 

861 self.ftm.set_radio_button(where) 

862 options = fc.compute_find_options_in_status_area() 

863 c.frame.statusLine.put(options) 

864 #@+node:ekr.20131117164142.16989: *4* find.show-find-options 

865 @cmd('show-find-options') 

866 def show_find_options(self, event=None): # pragma: no cover (cmd) 

867 """ 

868 Show the present find options in the status line. 

869 This is useful for commands like search-forward that do not show the Find Panel. 

870 """ 

871 frame = self.c.frame 

872 frame.clearStatusLine() 

873 part1, part2 = self.compute_find_options() 

874 frame.putStatusLine(part1, bg='blue') 

875 frame.putStatusLine(part2) 

876 #@+node:ekr.20171129205648.1: *5* LeoFind.compute_find_options 

877 def compute_find_options(self): # pragma: no cover (cmd) 

878 """Return the status line as two strings.""" 

879 z = [] 

880 # Set the scope field. 

881 head = self.search_headline 

882 body = self.search_body 

883 if self.suboutline_only: 

884 scope = 'tree' 

885 elif self.node_only: 

886 scope = 'node' 

887 else: 

888 scope = 'all' 

889 # scope = self.getOption('radio-search-scope') 

890 # d = {'entire-outline':'all','suboutline-only':'tree','node-only':'node'} 

891 # scope = d.get(scope) or '' 

892 head = 'head' if head else '' 

893 body = 'body' if body else '' 

894 sep = '+' if head and body else '' 

895 part1 = f"{head}{sep}{body} {scope} " 

896 # Set the type field. 

897 regex = self.pattern_match 

898 if regex: 

899 z.append('regex') 

900 table = ( 

901 ('reverse', 'reverse'), 

902 ('ignore_case', 'noCase'), 

903 ('whole_word', 'word'), 

904 # ('wrap', 'wrap'), 

905 ('mark_changes', 'markChg'), 

906 ('mark_finds', 'markFnd'), 

907 ) 

908 for ivar, s in table: 

909 val = getattr(self, ivar) 

910 if val: 

911 z.append(s) 

912 part2 = ' '.join(z) 

913 return part1, part2 

914 #@+node:ekr.20131117164142.16919: *4* find.toggle-find-* 

915 @cmd('toggle-find-collapses-nodes') 

916 def toggle_find_collapes_nodes(self, event): # pragma: no cover (cmd) 

917 """Toggle the 'Collapse Nodes' checkbox in the find tab.""" 

918 c = self.c 

919 c.sparse_find = not c.sparse_find 

920 if not g.unitTesting: 

921 g.es('sparse_find', c.sparse_find) 

922 

923 @cmd('toggle-find-ignore-case-option') 

924 def toggle_ignore_case_option(self, event): # pragma: no cover (cmd) 

925 """Toggle the 'Ignore Case' checkbox in the Find tab.""" 

926 return self.toggle_option('ignore_case') 

927 

928 @cmd('toggle-find-mark-changes-option') 

929 def toggle_mark_changes_option(self, event): # pragma: no cover (cmd) 

930 """Toggle the 'Mark Changes' checkbox in the Find tab.""" 

931 return self.toggle_option('mark_changes') 

932 

933 @cmd('toggle-find-mark-finds-option') 

934 def toggle_mark_finds_option(self, event): # pragma: no cover (cmd) 

935 """Toggle the 'Mark Finds' checkbox in the Find tab.""" 

936 return self.toggle_option('mark_finds') 

937 

938 @cmd('toggle-find-regex-option') 

939 def toggle_regex_option(self, event): # pragma: no cover (cmd) 

940 """Toggle the 'Regexp' checkbox in the Find tab.""" 

941 return self.toggle_option('pattern_match') 

942 

943 @cmd('toggle-find-in-body-option') 

944 def toggle_search_body_option(self, event): # pragma: no cover (cmd) 

945 """Set the 'Search Body' checkbox in the Find tab.""" 

946 return self.toggle_option('search_body') 

947 

948 @cmd('toggle-find-in-headline-option') 

949 def toggle_search_headline_option(self, event): # pragma: no cover (cmd) 

950 """Toggle the 'Search Headline' checkbox in the Find tab.""" 

951 return self.toggle_option('search_headline') 

952 

953 @cmd('toggle-find-word-option') 

954 def toggle_whole_word_option(self, event): # pragma: no cover (cmd) 

955 """Toggle the 'Whole Word' checkbox in the Find tab.""" 

956 return self.toggle_option('whole_word') 

957 

958 # @cmd('toggle-find-wrap-around-option') 

959 # def toggleWrapSearchOption(self, event): 

960 # """Toggle the 'Wrap Around' checkbox in the Find tab.""" 

961 # return self.toggle_option('wrap') 

962 

963 def toggle_option(self, checkbox_name): # pragma: no cover (cmd) 

964 c, fc = self.c, self.c.findCommands 

965 self.ftm.toggle_checkbox(checkbox_name) 

966 options = fc.compute_find_options_in_status_area() 

967 c.frame.statusLine.put(options) 

968 #@+node:ekr.20131117164142.17013: *3* LeoFind.Commands (interactive) 

969 #@+node:ekr.20131117164142.16994: *4* find.change-all & helper 

970 @cmd('change-all') 

971 @cmd('replace-all') 

972 def interactive_change_all(self, event=None): # pragma: no cover (interactive) 

973 """Replace all instances of the search string with the replacement string.""" 

974 self.ftm.clear_focus() 

975 self.ftm.set_entry_focus() 

976 prompt = 'Replace Regex: ' if self.pattern_match else 'Replace: ' 

977 self.start_state_machine(event, prompt, 

978 handler=self.interactive_replace_all1, 

979 # Allow either '\t' or '\n' to switch to the change text. 

980 escape_handler=self.interactive_replace_all1, 

981 ) 

982 

983 def interactive_replace_all1(self, event): # pragma: no cover (interactive) 

984 k = self.k 

985 find_pattern = k.arg 

986 self._sString = k.arg 

987 self.update_find_list(k.arg) 

988 regex = ' Regex' if self.pattern_match else '' 

989 prompt = f"Replace{regex}: {find_pattern} With: " 

990 k.setLabelBlue(prompt) 

991 self.add_change_string_to_label() 

992 k.getNextArg(self.interactive_replace_all2) 

993 

994 def interactive_replace_all2(self, event): # pragma: no cover (interactive) 

995 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

996 

997 # Update settings data. 

998 find_pattern = self._sString 

999 change_pattern = k.arg 

1000 self.init_vim_search(find_pattern) 

1001 self.update_change_list(change_pattern) 

1002 # Compute settings... 

1003 self.ftm.set_find_text(find_pattern) 

1004 self.ftm.set_change_text(change_pattern) 

1005 settings = self.ftm.get_settings() 

1006 # Gui... 

1007 k.clearState() 

1008 k.resetLabel() 

1009 k.showStateAndMode() 

1010 c.widgetWantsFocusNow(w) 

1011 # Do the command! 

1012 self.do_change_all(settings) 

1013 #@+node:ekr.20131117164142.17016: *5* find.do_change_all & helpers 

1014 def do_change_all(self, settings): 

1015 c = self.c 

1016 # Settings... 

1017 self.init_ivars_from_settings(settings) 

1018 if not self.check_args('change-all'): 

1019 return 0 

1020 n = self._change_all_helper(settings) 

1021 # 

1022 # Bugs #947, #880 and #722: 

1023 # Set ancestor @<file> nodes by brute force. 

1024 for p in c.all_positions(): # pragma: no cover 

1025 if ( 

1026 p.anyAtFileNodeName() 

1027 and not p.v.isDirty() 

1028 and any(p2.v.isDirty() for p2 in p.subtree()) 

1029 ): 

1030 p.setDirty() 

1031 c.redraw() 

1032 return n 

1033 #@+node:ekr.20031218072017.3069: *6* find._change_all_helper 

1034 def _change_all_helper(self, settings): 

1035 """Do the change-all command. Return the number of changes, or 0 for error.""" 

1036 # Caller has checked settings. 

1037 

1038 c, current, u = self.c, self.c.p, self.c.undoer 

1039 undoType = 'Replace All' 

1040 t1 = time.process_time() 

1041 if not self.check_args('change-all'): # pragma: no cover 

1042 return 0 

1043 self.init_in_headline() 

1044 saveData = self.save() 

1045 self.in_headline = self.search_headline # Search headlines first. 

1046 # Remember the start of the search. 

1047 p = self.root = c.p.copy() 

1048 # Set the work widget. 

1049 s = p.h if self.in_headline else p.b 

1050 ins = len(s) if self.reverse else 0 

1051 self.work_s = s 

1052 self.work_sel = (ins, ins, ins) 

1053 count = 0 

1054 u.beforeChangeGroup(current, undoType) 

1055 # Fix bug 338172: ReplaceAll will not replace newlines 

1056 # indicated as \n in target string. 

1057 if not self.find_text: # pragma: no cover 

1058 return 0 

1059 if not self.search_headline and not self.search_body: # pragma: no cover 

1060 return 0 

1061 self.change_text = self.replace_back_slashes(self.change_text) 

1062 if self.pattern_match: 

1063 ok = self.precompile_pattern() 

1064 if not ok: 

1065 return 0 

1066 # #1428: Honor limiters in replace-all. 

1067 if self.node_only: 

1068 positions = [c.p] 

1069 elif self.suboutline_only: 

1070 positions = c.p.self_and_subtree() 

1071 else: 

1072 positions = c.all_unique_positions() 

1073 count = 0 

1074 for p in positions: 

1075 count_h, count_b = 0, 0 

1076 undoData = u.beforeChangeNodeContents(p) 

1077 if self.search_headline: 

1078 count_h, new_h = self._change_all_search_and_replace(p.h) 

1079 if count_h: 

1080 count += count_h 

1081 p.h = new_h 

1082 if self.search_body: 

1083 count_b, new_b = self._change_all_search_and_replace(p.b) 

1084 if count_b: 

1085 count += count_b 

1086 p.b = new_b 

1087 if count_h or count_b: 

1088 u.afterChangeNodeContents(p, 'Replace All', undoData) 

1089 self.ftm.set_radio_button('entire-outline') 

1090 # suboutline-only is a one-shot for batch commands. 

1091 self.root = None 

1092 self.node_only = self.suboutline_only = False 

1093 p = c.p 

1094 u.afterChangeGroup(p, undoType, reportFlag=True) 

1095 t2 = time.process_time() 

1096 if not g.unitTesting: # pragma: no cover 

1097 g.es_print( 

1098 f"changed {count} instances{g.plural(count)} " 

1099 f"in {t2 - t1:4.2f} sec.") 

1100 c.recolor() 

1101 c.redraw(p) 

1102 self.restore(saveData) 

1103 return count 

1104 #@+node:ekr.20190602134414.1: *6* find._change_all_search_and_replace & helpers 

1105 def _change_all_search_and_replace(self, s): 

1106 """ 

1107 Search s for self.find_text and replace with self.change_text. 

1108 

1109 Return (found, new text) 

1110 """ 

1111 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!) 

1112 if sys.platform.lower().startswith('win'): 

1113 # Ignore '\r' characters, which may appear in @edit nodes. 

1114 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4 

1115 s = s.replace('\r', '') 

1116 if not s: 

1117 return False, None 

1118 # Order matters: regex matches ignore whole-word. 

1119 if self.pattern_match: 

1120 return self._change_all_regex(s) 

1121 if self.whole_word: 

1122 return self._change_all_word(s) 

1123 return self._change_all_plain(s) 

1124 #@+node:ekr.20190602151043.4: *7* find._change_all_plain 

1125 def _change_all_plain(self, s): 

1126 """ 

1127 Perform all plain find/replace on s. 

1128 return (count, new_s) 

1129 """ 

1130 find, change = self.find_text, self.change_text 

1131 # #1166: s0 and find0 aren't affected by ignore-case. 

1132 s0 = s 

1133 find0 = self.replace_back_slashes(find) 

1134 if self.ignore_case: 

1135 s = s0.lower() 

1136 find = find0.lower() 

1137 count, prev_i, result = 0, 0, [] 

1138 while True: 

1139 progress = prev_i 

1140 # #1166: Scan using s and find. 

1141 i = s.find(find, prev_i) 

1142 if i == -1: 

1143 break 

1144 # #1166: Replace using s0 & change. 

1145 count += 1 

1146 result.append(s0[prev_i:i]) 

1147 result.append(change) 

1148 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!) 

1149 assert prev_i > progress, prev_i 

1150 # #1166: Complete the result using s0. 

1151 result.append(s0[prev_i:]) 

1152 return count, ''.join(result) 

1153 #@+node:ekr.20190602151043.2: *7* find._change_all_regex 

1154 def _change_all_regex(self, s): 

1155 """ 

1156 Perform all regex find/replace on s. 

1157 return (count, new_s) 

1158 """ 

1159 count, prev_i, result = 0, 0, [] 

1160 

1161 flags = re.MULTILINE 

1162 if self.ignore_case: 

1163 flags |= re.IGNORECASE 

1164 for m in re.finditer(self.find_text, s, flags): 

1165 count += 1 

1166 i = m.start() 

1167 result.append(s[prev_i:i]) 

1168 # #1748. 

1169 groups = m.groups() 

1170 if groups: 

1171 change_text = self.make_regex_subs(self.change_text, groups) 

1172 else: 

1173 change_text = self.change_text 

1174 result.append(change_text) 

1175 prev_i = m.end() 

1176 # Compute the result. 

1177 result.append(s[prev_i:]) 

1178 s = ''.join(result) 

1179 return count, s 

1180 #@+node:ekr.20190602155933.1: *7* find._change_all_word 

1181 def _change_all_word(self, s): 

1182 """ 

1183 Perform all whole word find/replace on s. 

1184 return (count, new_s) 

1185 """ 

1186 find, change = self.find_text, self.change_text 

1187 # #1166: s0 and find0 aren't affected by ignore-case. 

1188 s0 = s 

1189 find0 = self.replace_back_slashes(find) 

1190 if self.ignore_case: 

1191 s = s0.lower() 

1192 find = find0.lower() 

1193 count, prev_i, result = 0, 0, [] 

1194 while True: 

1195 # #1166: Scan using s and find. 

1196 i = s.find(find, prev_i) 

1197 if i == -1: 

1198 break 

1199 # #1166: Replace using s0, change & find0. 

1200 result.append(s0[prev_i:i]) 

1201 if g.match_word(s, i, find): 

1202 count += 1 

1203 result.append(change) 

1204 else: 

1205 result.append(find0) 

1206 prev_i = i + len(find) 

1207 # #1166: Complete the result using s0. 

1208 result.append(s0[prev_i:]) 

1209 return count, ''.join(result) 

1210 #@+node:ekr.20210110073117.23: *6* new:find.replace_all_helper & helpers (merge & delete) 

1211 def replace_all_helper(self, s): 

1212 """ 

1213 Search s for self.find_text and replace with self.change_text. 

1214 

1215 Return (found, new text) 

1216 """ 

1217 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!) 

1218 if sys.platform.lower().startswith('win'): 

1219 # Ignore '\r' characters, which may appear in @edit nodes. 

1220 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4 

1221 s = s.replace('\r', '') 

1222 if not s: 

1223 return False, None 

1224 # Order matters: regex matches ignore whole-word. 

1225 if self.pattern_match: 

1226 return self.batch_regex_replace(s) 

1227 if self.whole_word: 

1228 return self.batch_word_replace(s) 

1229 return self.batch_plain_replace(s) 

1230 #@+node:ekr.20210110073117.24: *7* new:find.batch_plain_replace 

1231 def batch_plain_replace(self, s): 

1232 """ 

1233 Perform all plain find/replace on s. 

1234 return (count, new_s) 

1235 """ 

1236 find, change = self.find_text, self.change_text 

1237 # #1166: s0 and find0 aren't affected by ignore-case. 

1238 s0 = s 

1239 find0 = self.replace_back_slashes(find) 

1240 if self.ignore_case: 

1241 s = s0.lower() 

1242 find = find0.lower() 

1243 count, prev_i, result = 0, 0, [] 

1244 while True: 

1245 progress = prev_i 

1246 # #1166: Scan using s and find. 

1247 i = s.find(find, prev_i) 

1248 if i == -1: 

1249 break 

1250 # #1166: Replace using s0 & change. 

1251 count += 1 

1252 result.append(s0[prev_i:i]) 

1253 result.append(change) 

1254 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!) 

1255 assert prev_i > progress, prev_i 

1256 # #1166: Complete the result using s0. 

1257 result.append(s0[prev_i:]) 

1258 return count, ''.join(result) 

1259 #@+node:ekr.20210110073117.25: *7* new:find.batch_regex_replace 

1260 def batch_regex_replace(self, s): 

1261 """ 

1262 Perform all regex find/replace on s. 

1263 return (count, new_s) 

1264 """ 

1265 count, prev_i, result = 0, 0, [] 

1266 

1267 flags = re.MULTILINE 

1268 if self.ignore_case: 

1269 flags |= re.IGNORECASE 

1270 for m in re.finditer(self.find_text, s, flags): 

1271 count += 1 

1272 i = m.start() 

1273 result.append(s[prev_i:i]) 

1274 # #1748. 

1275 groups = m.groups() 

1276 if groups: 

1277 change_text = self.make_regex_subs(self.change_text, groups) 

1278 else: 

1279 change_text = self.change_text 

1280 result.append(change_text) 

1281 prev_i = m.end() 

1282 # Compute the result. 

1283 result.append(s[prev_i:]) 

1284 s = ''.join(result) 

1285 return count, s 

1286 #@+node:ekr.20210110073117.26: *7* new:find.batch_word_replace 

1287 def batch_word_replace(self, s): 

1288 """ 

1289 Perform all whole word find/replace on s. 

1290 return (count, new_s) 

1291 """ 

1292 find, change = self.find_text, self.change_text 

1293 # #1166: s0 and find0 aren't affected by ignore-case. 

1294 s0 = s 

1295 find0 = self.replace_back_slashes(find) 

1296 if self.ignore_case: 

1297 s = s0.lower() 

1298 find = find0.lower() 

1299 count, prev_i, result = 0, 0, [] 

1300 while True: 

1301 progress = prev_i 

1302 # #1166: Scan using s and find. 

1303 i = s.find(find, prev_i) 

1304 if i == -1: 

1305 break 

1306 # #1166: Replace using s0, change & find0. 

1307 result.append(s0[prev_i:i]) 

1308 if g.match_word(s, i, find): 

1309 count += 1 

1310 result.append(change) 

1311 else: 

1312 result.append(find0) 

1313 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!) 

1314 assert prev_i > progress, prev_i 

1315 # #1166: Complete the result using s0. 

1316 result.append(s0[prev_i:]) 

1317 return count, ''.join(result) 

1318 #@+node:ekr.20131117164142.17011: *4* find.clone-find-all & helper 

1319 @cmd('clone-find-all') 

1320 @cmd('find-clone-all') 

1321 @cmd('cfa') 

1322 def interactive_clone_find_all( 

1323 self, event=None, preloaded=None): # pragma: no cover (interactive) 

1324 """ 

1325 clone-find-all ( aka find-clone-all and cfa). 

1326 

1327 Create an organizer node whose descendants contain clones of all nodes 

1328 matching the search string, except @nosearch trees. 

1329 

1330 The list is *not* flattened: clones appear only once in the 

1331 descendants of the organizer node. 

1332 """ 

1333 w = self.c.frame.body.wrapper 

1334 if not w: 

1335 return 

1336 if not preloaded: 

1337 self.preload_find_pattern(w) 

1338 self.start_state_machine(event, 

1339 prefix='Clone Find All: ', 

1340 handler=self.interactive_clone_find_all1) 

1341 

1342 def interactive_clone_find_all1(self, event): # pragma: no cover (interactive) 

1343 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

1344 # Settings... 

1345 pattern = k.arg 

1346 self.ftm.set_find_text(pattern) 

1347 self.init_vim_search(pattern) 

1348 self.init_in_headline() 

1349 settings = self.ftm.get_settings() 

1350 # Gui... 

1351 k.clearState() 

1352 k.resetLabel() 

1353 k.showStateAndMode() 

1354 c.widgetWantsFocusNow(w) 

1355 count = self.do_clone_find_all(settings) 

1356 if count: 

1357 c.redraw() 

1358 c.treeWantsFocus() 

1359 return count 

1360 #@+node:ekr.20210114094846.1: *5* find.do_clone_find_all 

1361 # A stand-alone method for unit testing. 

1362 def do_clone_find_all(self, settings): 

1363 """ 

1364 Do the clone-all-find commands from settings. 

1365 

1366 Return the count of found nodes. 

1367 

1368 This is a stand-alone method for unit testing. 

1369 """ 

1370 self.init_ivars_from_settings(settings) 

1371 if not self.check_args('clone-find-all'): 

1372 return 0 

1373 return self._cf_helper(settings, flatten=False) 

1374 #@+node:ekr.20131117164142.16996: *4* find.clone-find-all-flattened & helper 

1375 @cmd('clone-find-all-flattened') 

1376 # @cmd('find-clone-all-flattened') 

1377 @cmd('cff') 

1378 def interactive_cff( 

1379 self, event=None, preloaded=None): # pragma: no cover (interactive) 

1380 """ 

1381 clone-find-all-flattened (aka find-clone-all-flattened and cff). 

1382 

1383 Create an organizer node whose direct children are clones of all nodes 

1384 matching the search string, except @nosearch trees. 

1385 

1386 The list is flattened: every cloned node appears as a direct child 

1387 of the organizer node, even if the clone also is a descendant of 

1388 another cloned node. 

1389 """ 

1390 w = self.c.frame.body.wrapper 

1391 if not w: 

1392 return 

1393 if not preloaded: 

1394 self.preload_find_pattern(w) 

1395 self.start_state_machine(event, 

1396 prefix='Clone Find All Flattened: ', 

1397 handler=self.interactive_cff1) 

1398 

1399 def interactive_cff1(self, event): # pragma: no cover (interactive) 

1400 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

1401 # Settings... 

1402 pattern = k.arg 

1403 self.ftm.set_find_text(pattern) 

1404 self.init_vim_search(pattern) 

1405 self.init_in_headline() 

1406 settings = self.ftm.get_settings() 

1407 # Gui... 

1408 k.clearState() 

1409 k.resetLabel() 

1410 k.showStateAndMode() 

1411 c.widgetWantsFocusNow(w) 

1412 count = self.do_clone_find_all_flattened(settings) 

1413 if count: 

1414 c.redraw() 

1415 c.treeWantsFocus() 

1416 return count 

1417 #@+node:ekr.20210114094944.1: *5* find.do_clone_find_all_flattened 

1418 # A stand-alone method for unit testing. 

1419 def do_clone_find_all_flattened(self, settings): 

1420 """ 

1421 Do the clone-find-all-flattened command from the settings. 

1422 

1423 Return the count of found nodes. 

1424 

1425 This is a stand-alone method for unit testing. 

1426 """ 

1427 self.init_ivars_from_settings(settings) 

1428 if self.check_args('clone-find-all-flattened'): 

1429 return self._cf_helper(settings, flatten=True) 

1430 return 0 

1431 #@+node:ekr.20160920110324.1: *4* find.clone-find-tag & helper 

1432 @cmd('clone-find-tag') 

1433 @cmd('find-clone-tag') 

1434 @cmd('cft') 

1435 def interactive_clone_find_tag(self, event=None): # pragma: no cover (interactive) 

1436 """ 

1437 clone-find-tag (aka find-clone-tag and cft). 

1438 

1439 Create an organizer node whose descendants contain clones of all 

1440 nodes matching the given tag, except @nosearch trees. 

1441 

1442 The list is *always* flattened: every cloned node appears as a 

1443 direct child of the organizer node, even if the clone also is a 

1444 descendant of another cloned node. 

1445 """ 

1446 w = self.c.frame.body.wrapper 

1447 if w: 

1448 self.start_state_machine(event, 

1449 prefix='Clone Find Tag: ', 

1450 handler=self.interactive_clone_find_tag1) 

1451 

1452 def interactive_clone_find_tag1(self, event): # pragma: no cover (interactive) 

1453 c, k = self.c, self.k 

1454 # Settings... 

1455 self.find_text = tag = k.arg 

1456 # Gui... 

1457 k.clearState() 

1458 k.resetLabel() 

1459 k.showStateAndMode() 

1460 self.do_clone_find_tag(tag) 

1461 c.treeWantsFocus() 

1462 #@+node:ekr.20210110073117.11: *5* find.do_clone_find_tag & helper 

1463 # A stand-alone method for unit tests. 

1464 def do_clone_find_tag(self, tag): 

1465 """ 

1466 Do the clone-all-find commands from settings. 

1467 Return (len(clones), found) for unit tests. 

1468 """ 

1469 c, u = self.c, self.c.undoer 

1470 tc = getattr(c, 'theTagController', None) 

1471 if not tc: 

1472 if not g.unitTesting: # pragma: no cover (skip) 

1473 g.es_print('nodetags not active') 

1474 return 0, c.p 

1475 clones = tc.get_tagged_nodes(tag) 

1476 if not clones: 

1477 if not g.unitTesting: # pragma: no cover (skip) 

1478 g.es_print(f"tag not found: {tag}") 

1479 tc.show_all_tags() 

1480 return 0, c.p 

1481 undoData = u.beforeInsertNode(c.p) 

1482 found = self._create_clone_tag_nodes(clones) 

1483 u.afterInsertNode(found, 'Clone Find Tag', undoData) 

1484 assert c.positionExists(found, trace=True), found 

1485 c.setChanged() 

1486 c.selectPosition(found) 

1487 c.redraw() 

1488 return len(clones), found 

1489 #@+node:ekr.20210110073117.12: *6* find._create_clone_tag_nodes 

1490 def _create_clone_tag_nodes(self, clones): 

1491 """ 

1492 Create a "Found Tag" node as the last node of the outline. 

1493 Clone all positions in the clones set as children of found. 

1494 """ 

1495 c, p = self.c, self.c.p 

1496 # Create the found node. 

1497 assert c.positionExists(c.lastTopLevel()), c.lastTopLevel() 

1498 found = c.lastTopLevel().insertAfter() 

1499 assert found 

1500 assert c.positionExists(found), found 

1501 found.h = f"Found Tag: {self.find_text}" 

1502 # Clone nodes as children of the found node. 

1503 for p in clones: 

1504 # Create the clone directly as a child of found. 

1505 p2 = p.copy() 

1506 n = found.numberOfChildren() 

1507 p2._linkCopiedAsNthChild(found, n) 

1508 return found 

1509 #@+node:ekr.20131117164142.16998: *4* find.find-all & helper 

1510 @cmd('find-all') 

1511 def interactive_find_all(self, event=None): # pragma: no cover (interactive) 

1512 """ 

1513 Create a summary node containing descriptions of all matches of the 

1514 search string. 

1515 

1516 Typing tab converts this to the change-all command. 

1517 """ 

1518 self.ftm.clear_focus() 

1519 self.ftm.set_entry_focus() 

1520 self.start_state_machine(event, 'Search: ', 

1521 handler=self.interactive_find_all1, 

1522 escape_handler=self.find_all_escape_handler, 

1523 ) 

1524 

1525 def interactive_find_all1(self, event=None): # pragma: no cover (interactive) 

1526 k = self.k 

1527 # Settings. 

1528 find_pattern = k.arg 

1529 self.ftm.set_find_text(find_pattern) 

1530 settings = self.ftm.get_settings() 

1531 self.find_text = find_pattern 

1532 self.change_text = self.ftm.get_change_text() 

1533 self.update_find_list(find_pattern) 

1534 # Gui... 

1535 k.clearState() 

1536 k.resetLabel() 

1537 k.showStateAndMode() 

1538 self.do_find_all(settings) 

1539 

1540 def find_all_escape_handler(self, event): # pragma: no cover (interactive) 

1541 k = self.k 

1542 prompt = 'Replace ' + ('Regex' if self.pattern_match else 'String') 

1543 find_pattern = k.arg 

1544 self._sString = k.arg 

1545 self.update_find_list(k.arg) 

1546 s = f"{prompt}: {find_pattern} With: " 

1547 k.setLabelBlue(s) 

1548 self.add_change_string_to_label() 

1549 k.getNextArg(self.find_all_escape_handler2) 

1550 

1551 def find_all_escape_handler2(self, event): # pragma: no cover (interactive) 

1552 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

1553 find_pattern = self._sString 

1554 change_pattern = k.arg 

1555 self.update_change_list(change_pattern) 

1556 self.ftm.set_find_text(find_pattern) 

1557 self.ftm.set_change_text(change_pattern) 

1558 self.init_vim_search(find_pattern) 

1559 self.init_in_headline() 

1560 settings = self.ftm.get_settings() 

1561 # Gui... 

1562 k.clearState() 

1563 k.resetLabel() 

1564 k.showStateAndMode() 

1565 c.widgetWantsFocusNow(w) 

1566 self.do_change_all(settings) 

1567 #@+node:ekr.20031218072017.3073: *5* find.do_find_all & helpers 

1568 def do_find_all(self, settings): 

1569 """Top-level helper for find-all command.""" 

1570 c = self.c 

1571 count = 0 

1572 self.init_ivars_from_settings(settings) 

1573 if not self.check_args('find-all'): # pragma: no cover 

1574 return count 

1575 # Init data. 

1576 self.init_in_headline() 

1577 data = self.save() 

1578 self.in_headline = self.search_headline # Search headlines first. 

1579 self.unique_matches = set() # 2021/02/20. 

1580 # Remember the start of the search. 

1581 p = self.root = c.p.copy() 

1582 # Set the work widget. 

1583 s = p.h if self.in_headline else p.b 

1584 ins = len(s) if self.reverse else 0 

1585 self.work_s = s 

1586 self.work_sel = (ins, ins, ins) 

1587 if self.pattern_match: 

1588 ok = self.precompile_pattern() 

1589 if not ok: # pragma: no cover 

1590 return count 

1591 if self.suboutline_only: 

1592 p = c.p 

1593 after = p.nodeAfterTree() 

1594 else: 

1595 # Always search the entire outline. 

1596 p = c.rootPosition() 

1597 after = None 

1598 # Fix #292: Never collapse nodes during find-all commands. 

1599 old_sparse_find = c.sparse_find 

1600 try: 

1601 c.sparse_find = False 

1602 count = self._find_all_helper(after, data, p, 'Find All') 

1603 c.contractAllHeadlines() 

1604 finally: 

1605 c.sparse_find = old_sparse_find 

1606 self.root = None 

1607 if count: 

1608 c.redraw() 

1609 g.es("found", count, "matches for", self.find_text) 

1610 return count 

1611 #@+node:ekr.20160422073500.1: *6* find._find_all_helper 

1612 def _find_all_helper(self, after, data, p, undoType): 

1613 """Handle the find-all command from p to after.""" 

1614 c, log, u = self.c, self.c.frame.log, self.c.undoer 

1615 

1616 def put_link(line, line_number, p): # pragma: no cover # #2023 

1617 """Put a link to the given line at the given line_number in p.h.""" 

1618 

1619 if g.unitTesting: 

1620 return 

1621 unl = p.get_UNL() 

1622 if self.in_headline: 

1623 line_number = 1 

1624 log.put(line.strip() + '\n', nodeLink=f"{unl}::{line_number}") # Local line. 

1625 

1626 seen = [] # List of (vnode, pos). 

1627 both = self.search_body and self.search_headline 

1628 count, found, result = 0, None, [] 

1629 while 1: 

1630 p, pos, newpos = self.find_next_match(p) 

1631 if pos is None: 

1632 break 

1633 if (p.v, pos) in seen: # 2076 

1634 continue # pragma: no cover 

1635 seen.append((p.v, pos)) 

1636 count += 1 

1637 s = self.work_s 

1638 i, j = g.getLine(s, pos) 

1639 line = s[i:j] 

1640 row, col = g.convertPythonIndexToRowCol(s, i) 

1641 line_number = row + 1 

1642 if self.findAllUniqueFlag: 

1643 m = self.match_obj 

1644 if m: 

1645 self.unique_matches.add(m.group(0).strip()) 

1646 put_link(line, line_number, p) # #2023 

1647 elif both: 

1648 result.append('%s%s\n%s%s\n' % ( 

1649 '-' * 20, p.h, 

1650 "head: " if self.in_headline else "body: ", 

1651 line.rstrip() + '\n')) 

1652 put_link(line, line_number, p) # #2023 

1653 elif p.isVisited(): 

1654 result.append(line.rstrip() + '\n') 

1655 put_link(line, line_number, p) # #2023 

1656 else: 

1657 result.append('%s%s\n%s' % ('-' * 20, p.h, line.rstrip() + '\n')) 

1658 put_link(line, line_number, p) # #2023 

1659 p.setVisited() 

1660 if result or self.unique_matches: 

1661 undoData = u.beforeInsertNode(c.p) 

1662 if self.findAllUniqueFlag: 

1663 found = self._create_find_unique_node() 

1664 count = len(list(self.unique_matches)) 

1665 else: 

1666 found = self._create_find_all_node(result) 

1667 u.afterInsertNode(found, undoType, undoData) 

1668 c.selectPosition(found) 

1669 c.setChanged() 

1670 else: 

1671 self.restore(data) 

1672 return count 

1673 #@+node:ekr.20150717105329.1: *6* find._create_find_all_node 

1674 def _create_find_all_node(self, result): 

1675 """Create a "Found All" node as the last node of the outline.""" 

1676 c = self.c 

1677 found = c.lastTopLevel().insertAfter() 

1678 assert found 

1679 found.h = f"Found All:{self.find_text}" 

1680 status = self.compute_result_status(find_all_flag=True) 

1681 status = status.strip().lstrip('(').rstrip(')').strip() 

1682 found.b = f"# {status}\n{''.join(result)}" 

1683 return found 

1684 #@+node:ekr.20171226143621.1: *6* find._create_find_unique_node 

1685 def _create_find_unique_node(self): 

1686 """Create a "Found Unique" node as the last node of the outline.""" 

1687 c = self.c 

1688 found = c.lastTopLevel().insertAfter() 

1689 assert found 

1690 found.h = f"Found Unique Regex:{self.find_text}" 

1691 result = sorted(self.unique_matches) 

1692 found.b = '\n'.join(result) 

1693 return found 

1694 #@+node:ekr.20171226140643.1: *4* find.find-all-unique-regex 

1695 @cmd('find-all-unique-regex') 

1696 def interactive_find_all_unique_regex( 

1697 self, event=None): # pragma: no cover (interactive) 

1698 """ 

1699 Create a summary node containing all unique matches of the regex search 

1700 string. This command shows only the matched string itself. 

1701 """ 

1702 self.ftm.clear_focus() 

1703 self.match_obj = None 

1704 self.changeAllFlag = False 

1705 self.findAllUniqueFlag = True 

1706 self.ftm.set_entry_focus() 

1707 self.start_state_machine(event, 

1708 prefix='Search Unique Regex: ', 

1709 handler=self.interactive_find_all_unique_regex1, 

1710 escape_handler=self.interactive_change_all_unique_regex1, 

1711 ) 

1712 

1713 def interactive_find_all_unique_regex1( 

1714 self, event=None): # pragma: no cover (interactive) 

1715 k = self.k 

1716 # Settings... 

1717 find_pattern = k.arg 

1718 self.update_find_list(find_pattern) 

1719 self.ftm.set_find_text(find_pattern) 

1720 self.init_in_headline() 

1721 settings = self.ftm.get_settings() 

1722 # Gui... 

1723 k.clearState() 

1724 k.resetLabel() 

1725 k.showStateAndMode() 

1726 return self.do_find_all(settings) 

1727 

1728 def interactive_change_all_unique_regex1( 

1729 self, event): # pragma: no cover (interactive) 

1730 k = self.k 

1731 find_pattern = self._sString = k.arg 

1732 self.update_find_list(k.arg) 

1733 s = f"'Replace All Unique Regex': {find_pattern} With: " 

1734 k.setLabelBlue(s) 

1735 self.add_change_string_to_label() 

1736 k.getNextArg(self.interactive_change_all_unique_regex2) 

1737 

1738 def interactive_change_all_unique_regex2( 

1739 self, event): # pragma: no cover (interactive) 

1740 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

1741 find_pattern = self._sString 

1742 change_pattern = k.arg 

1743 self.update_change_list(change_pattern) 

1744 self.ftm.set_find_text(find_pattern) 

1745 self.ftm.set_change_text(change_pattern) 

1746 self.init_vim_search(find_pattern) 

1747 self.init_in_headline() 

1748 settings = self.ftm.get_settings() 

1749 # Gui... 

1750 k.clearState() 

1751 k.resetLabel() 

1752 k.showStateAndMode() 

1753 c.widgetWantsFocusNow(w) 

1754 self.do_change_all(settings) 

1755 #@+node:ekr.20131117164142.17003: *4* find.re-search 

1756 @cmd('re-search') 

1757 @cmd('re-search-forward') 

1758 def interactive_re_search_forward(self, event): # pragma: no cover (interactive) 

1759 """Same as start-find, with regex.""" 

1760 # Set flag for show_find_options. 

1761 self.pattern_match = True 

1762 self.show_find_options() 

1763 # Set flag for do_find_next(). 

1764 self.request_pattern_match = True 

1765 # Go. 

1766 self.start_state_machine(event, 

1767 prefix='Regexp Search: ', 

1768 handler=self.start_search1, # See start-search 

1769 escape_handler=self.start_search_escape1, # See start-search 

1770 ) 

1771 #@+node:ekr.20210112044303.1: *4* find.re-search-backward 

1772 @cmd('re-search-backward') 

1773 def interactive_re_search_backward(self, event): # pragma: no cover (interactive) 

1774 """Same as start-find, but with regex and in reverse.""" 

1775 # Set flags for show_find_options. 

1776 self.reverse = True 

1777 self.pattern_match = True 

1778 self.show_find_options() 

1779 # Set flags for do_find_next(). 

1780 self.request_reverse = True 

1781 self.request_pattern_match = True 

1782 # Go. 

1783 self.start_state_machine(event, 

1784 prefix='Regexp Search Backward:', 

1785 handler=self.start_search1, # See start-search 

1786 escape_handler=self.start_search_escape1, # See start-search 

1787 ) 

1788 

1789 #@+node:ekr.20131117164142.17004: *4* find.search_backward 

1790 @cmd('search-backward') 

1791 def interactive_search_backward(self, event): # pragma: no cover (interactive) 

1792 """Same as start-find, but in reverse.""" 

1793 # Set flag for show_find_options. 

1794 self.reverse = True 

1795 self.show_find_options() 

1796 # Set flag for do_find_next(). 

1797 self.request_reverse = True 

1798 # Go. 

1799 self.start_state_machine(event, 

1800 prefix='Search Backward: ', 

1801 handler=self.start_search1, # See start-search 

1802 escape_handler=self.start_search_escape1, # See start-search 

1803 ) 

1804 #@+node:ekr.20131119060731.22452: *4* find.start-search (Ctrl-F) & common states 

1805 @cmd('start-search') 

1806 @cmd('search-forward') # Compatibility. 

1807 def start_search(self, event): # pragma: no cover (interactive) 

1808 """ 

1809 The default binding of Ctrl-F. 

1810 

1811 Also contains default state-machine entries for find/change commands. 

1812 """ 

1813 w = self.c.frame.body.wrapper 

1814 if not w: 

1815 return 

1816 self.preload_find_pattern(w) 

1817 # #1840: headline-only one-shot 

1818 # Do this first, so the user can override. 

1819 self.ftm.set_body_and_headline_checkbox() 

1820 if self.minibuffer_mode: 

1821 # Set up the state machine. 

1822 self.ftm.clear_focus() 

1823 self.changeAllFlag = False 

1824 self.findAllUniqueFlag = False 

1825 self.ftm.set_entry_focus() 

1826 self.start_state_machine(event, 

1827 prefix='Search: ', 

1828 handler=self.start_search1, 

1829 escape_handler=self.start_search_escape1, 

1830 ) 

1831 else: 

1832 self.open_find_tab(event) 

1833 self.ftm.init_focus() 

1834 return 

1835 

1836 startSearch = start_search # Compatibility. Do not delete. 

1837 #@+node:ekr.20210117143611.1: *5* find.start_search1 

1838 def start_search1(self, event=None): # pragma: no cover 

1839 """Common handler for use by vim commands and other find commands.""" 

1840 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

1841 # Settings... 

1842 find_pattern = k.arg 

1843 self.ftm.set_find_text(find_pattern) 

1844 self.update_find_list(find_pattern) 

1845 self.init_vim_search(find_pattern) 

1846 self.init_in_headline() # Required. 

1847 settings = self.ftm.get_settings() 

1848 # Gui... 

1849 k.clearState() 

1850 k.resetLabel() 

1851 k.showStateAndMode() 

1852 c.widgetWantsFocusNow(w) 

1853 # Do the command! 

1854 self.do_find_next(settings) # Handles reverse. 

1855 #@+node:ekr.20210117143614.1: *5* find._start_search_escape1 

1856 def start_search_escape1(self, event=None): # pragma: no cover 

1857 """ 

1858 Common escape handler for use by find commands. 

1859 

1860 Prompt for a change pattern. 

1861 """ 

1862 k = self.k 

1863 self._sString = find_pattern = k.arg 

1864 # Settings. 

1865 k.getArgEscapeFlag = False 

1866 self.ftm.set_find_text(find_pattern) 

1867 self.update_find_list(find_pattern) 

1868 self.find_text = find_pattern 

1869 self.change_text = self.ftm.get_change_text() 

1870 # Gui... 

1871 regex = ' Regex' if self.pattern_match else '' 

1872 backward = ' Backward' if self.reverse else '' 

1873 prompt = f"Replace{regex}{backward}: {find_pattern} With: " 

1874 k.setLabelBlue(prompt) 

1875 self.add_change_string_to_label() 

1876 k.getNextArg(self._start_search_escape2) 

1877 

1878 #@+node:ekr.20210117143615.1: *5* find._start_search_escape2 

1879 def _start_search_escape2(self, event): # pragma: no cover 

1880 c, k, w = self.c, self.k, self.c.frame.body.wrapper 

1881 # Compute settings... 

1882 find_pattern = self._sString 

1883 change_pattern = k.arg 

1884 self.ftm.set_find_text(find_pattern) 

1885 self.ftm.set_change_text(change_pattern) 

1886 self.update_change_list(change_pattern) 

1887 self.init_vim_search(find_pattern) 

1888 self.init_in_headline() # Required 

1889 settings = self.ftm.get_settings() 

1890 # Gui... 

1891 k.clearState() 

1892 k.resetLabel() 

1893 k.showStateAndMode() 

1894 c.widgetWantsFocusNow(w) 

1895 self.do_find_next(settings) 

1896 #@+node:ekr.20160920164418.2: *4* find.tag-children & helper 

1897 @cmd('tag-children') 

1898 def interactive_tag_children(self, event=None): # pragma: no cover (interactive) 

1899 """tag-children: prompt for a tag and add it to all children of c.p.""" 

1900 w = self.c.frame.body.wrapper 

1901 if not w: 

1902 return 

1903 self.start_state_machine(event, 

1904 prefix='Tag Children: ', 

1905 handler=self.interactive_tag_children1) 

1906 

1907 def interactive_tag_children1(self, event): # pragma: no cover (interactive) 

1908 c, k, p = self.c, self.k, self.c.p 

1909 # Settings... 

1910 tag = k.arg 

1911 # Gui... 

1912 k.clearState() 

1913 k.resetLabel() 

1914 k.showStateAndMode() 

1915 self.do_tag_children(p, tag) 

1916 c.treeWantsFocus() 

1917 #@+node:ekr.20160920164418.4: *5* find.do_tag_children 

1918 def do_tag_children(self, p, tag): 

1919 """Handle the tag-children command.""" 

1920 c = self.c 

1921 tc = getattr(c, 'theTagController', None) 

1922 if not tc: 

1923 if not g.unitTesting: # pragma: no cover (skip) 

1924 g.es_print('nodetags not active') 

1925 return 

1926 for p in p.children(): 

1927 tc.add_tag(p, tag) 

1928 if not g.unitTesting: # pragma: no cover (skip) 

1929 g.es_print(f"Added {tag} tag to {len(list(c.p.children()))} nodes") 

1930 

1931 #@+node:ekr.20210112050845.1: *4* find.word-search 

1932 @cmd('word-search') 

1933 @cmd('word-search-forward') 

1934 def word_search_forward(self, event): # pragma: no cover (interactive) 

1935 """Same as start-search, with whole_word setting.""" 

1936 # Set flag for show_find_options. 

1937 self.whole_word = True 

1938 self.show_find_options() 

1939 # Set flag for do_find_next(). 

1940 self.request_whole_world = True 

1941 # Go. 

1942 self.start_state_machine(event, 

1943 prefix='Word Search: ', 

1944 handler=self.start_search1, # See start-search 

1945 escape_handler=self.start_search_escape1, # See start-search 

1946 ) 

1947 #@+node:ekr.20131117164142.17009: *4* find.word-search-backward 

1948 @cmd('word-search-backward') 

1949 def word_search_backward(self, event): # pragma: no cover (interactive) 

1950 # Set flags for show_find_options. 

1951 self.reverse = True 

1952 self.whole_world = True 

1953 self.show_find_options() 

1954 # Set flags for do_find_next(). 

1955 self.request_reverse = True 

1956 self.request_whole_world = True 

1957 # Go 

1958 self.start_state_machine(event, 

1959 prefix='Word Search Backward: ', 

1960 handler=self.start_search1, # See start-search 

1961 escape_handler=self.start_search_escape1, # See start-search 

1962 ) 

1963 #@+node:ekr.20210112192427.1: *3* LeoFind.Commands: helpers 

1964 #@+node:ekr.20210110073117.9: *4* find._cf_helper & helpers 

1965 def _cf_helper(self, settings, flatten): # Caller has checked the settings. 

1966 """ 

1967 The common part of the clone-find commands. 

1968 

1969 Return the number of found nodes. 

1970 """ 

1971 c, u = self.c, self.c.undoer 

1972 if self.pattern_match: 

1973 ok = self.compile_pattern() 

1974 if not ok: 

1975 return 0 

1976 if self.suboutline_only: 

1977 p = c.p 

1978 after = p.nodeAfterTree() 

1979 else: 

1980 p = c.rootPosition() 

1981 after = None 

1982 count, found = 0, None 

1983 clones, skip = [], set() 

1984 while p and p != after: 

1985 progress = p.copy() 

1986 if g.inAtNosearch(p): 

1987 p.moveToNodeAfterTree() 

1988 elif p.v in skip: # pragma: no cover (minor) 

1989 p.moveToThreadNext() 

1990 elif self._cfa_find_next_match(p): 

1991 count += 1 

1992 if flatten: 

1993 skip.add(p.v) 

1994 clones.append(p.copy()) 

1995 p.moveToThreadNext() 

1996 else: 

1997 if p not in clones: 

1998 clones.append(p.copy()) 

1999 # Don't look at the node or it's descendants. 

2000 for p2 in p.self_and_subtree(copy=False): 

2001 skip.add(p2.v) 

2002 p.moveToNodeAfterTree() 

2003 else: 

2004 p.moveToThreadNext() 

2005 assert p != progress 

2006 self.ftm.set_radio_button('entire-outline') 

2007 # suboutline-only is a one-shot for batch commands. 

2008 self.node_only = self.suboutline_only = False 

2009 self.root = None 

2010 if clones: 

2011 undoData = u.beforeInsertNode(c.p) 

2012 found = self._cfa_create_nodes(clones, flattened=False) 

2013 u.afterInsertNode(found, 'Clone Find All', undoData) 

2014 assert c.positionExists(found, trace=True), found 

2015 c.setChanged() 

2016 c.selectPosition(found) 

2017 # Put the count in found.h. 

2018 found.h = found.h.replace('Found:', f"Found {count}:") 

2019 g.es("found", count, "matches for", self.find_text) 

2020 return count # Might be useful for the gui update. 

2021 #@+node:ekr.20210110073117.34: *5* find._cfa_create_nodes 

2022 def _cfa_create_nodes(self, clones, flattened): 

2023 """ 

2024 Create a "Found" node as the last node of the outline. 

2025 Clone all positions in the clones set a children of found. 

2026 """ 

2027 c = self.c 

2028 # Create the found node. 

2029 assert c.positionExists(c.lastTopLevel()), c.lastTopLevel() 

2030 found = c.lastTopLevel().insertAfter() 

2031 assert found 

2032 assert c.positionExists(found), found 

2033 found.h = f"Found:{self.find_text}" 

2034 status = self.compute_result_status(find_all_flag=True) 

2035 status = status.strip().lstrip('(').rstrip(')').strip() 

2036 flat = 'flattened, ' if flattened else '' 

2037 found.b = f"@nosearch\n\n# {flat}{status}\n\n# found {len(clones)} nodes" 

2038 # Clone nodes as children of the found node. 

2039 for p in clones: 

2040 # Create the clone directly as a child of found. 

2041 p2 = p.copy() 

2042 n = found.numberOfChildren() 

2043 p2._linkCopiedAsNthChild(found, n) 

2044 # Sort the clones in place, without undo. 

2045 found.v.children.sort(key=lambda v: v.h.lower()) 

2046 return found 

2047 #@+node:ekr.20210110073117.10: *5* find._cfa_find_next_match (for unit tests) 

2048 def _cfa_find_next_match(self, p): 

2049 """ 

2050 Find the next batch match at p. 

2051 """ 

2052 # Called only from unit tests. 

2053 table = [] 

2054 if self.search_headline: 

2055 table.append(p.h) 

2056 if self.search_body: 

2057 table.append(p.b) 

2058 for s in table: 

2059 self.reverse = False 

2060 pos, newpos = self.inner_search_helper(s, 0, len(s), self.find_text) 

2061 if pos != -1: 

2062 return True 

2063 return False 

2064 #@+node:ekr.20031218072017.3070: *4* find.change_selection 

2065 def change_selection(self, p): 

2066 """Replace selection with self.change_text.""" 

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

2068 wrapper = c.frame.body and c.frame.body.wrapper 

2069 gui_w = c.edit_widget(p) if self.in_headline else wrapper 

2070 if not gui_w: # pragma: no cover 

2071 self.in_headline = False 

2072 gui_w = wrapper 

2073 if not gui_w: # pragma: no cover 

2074 return False 

2075 oldSel = sel = gui_w.getSelectionRange() 

2076 start, end = sel 

2077 if start > end: # pragma: no cover 

2078 start, end = end, start 

2079 if start == end: # pragma: no cover 

2080 g.es("no text selected") 

2081 return False 

2082 bunch = u.beforeChangeBody(p) 

2083 start, end = oldSel 

2084 change_text = self.change_text 

2085 # Perform regex substitutions of \1, \2, ...\9 in the change text. 

2086 if self.pattern_match and self.match_obj: 

2087 groups = self.match_obj.groups() 

2088 if groups: 

2089 change_text = self.make_regex_subs(change_text, groups) 

2090 change_text = self.replace_back_slashes(change_text) 

2091 # Update both the gui widget and the work "widget" 

2092 new_ins = start if self.reverse else start + len(change_text) 

2093 if start != end: 

2094 gui_w.delete(start, end) 

2095 gui_w.insert(start, change_text) 

2096 gui_w.setInsertPoint(new_ins) 

2097 self.work_s = gui_w.getAllText() # #2220. 

2098 self.work_sel = (new_ins, new_ins, new_ins) 

2099 # Update the selection for the next match. 

2100 gui_w.setSelectionRange(start, start + len(change_text)) 

2101 c.widgetWantsFocus(gui_w) 

2102 # No redraws here: they would destroy the headline selection. 

2103 if self.mark_changes: # pragma: no cover 

2104 p.setMarked() 

2105 p.setDirty() 

2106 if self.in_headline: 

2107 # #2220: Let onHeadChanged handle undo, etc. 

2108 c.frame.tree.onHeadChanged(p, undoType='Change Headline') 

2109 # gui_w will change after a redraw. 

2110 gui_w = c.edit_widget(p) 

2111 if gui_w: 

2112 # find-next and find-prev work regardless of insert point. 

2113 gui_w.setSelectionRange(start, start + len(change_text)) 

2114 else: 

2115 p.v.b = gui_w.getAllText() 

2116 u.afterChangeBody(p, 'Change Body', bunch) 

2117 c.frame.tree.updateIcon(p) # redraw only the icon. 

2118 return True 

2119 #@+node:ekr.20210110073117.31: *4* find.check_args 

2120 def check_args(self, tag): 

2121 """Check the user arguments to a command.""" 

2122 if not self.search_headline and not self.search_body: 

2123 if not g.unitTesting: 

2124 g.es_print("not searching headline or body") # pragma: no cover (skip) 

2125 return False 

2126 if not self.find_text: 

2127 if not g.unitTesting: 

2128 g.es_print(f"{tag}: empty find pattern") # pragma: no cover (skip) 

2129 return False 

2130 return True 

2131 #@+node:ekr.20210110073117.32: *4* find.compile_pattern 

2132 def compile_pattern(self): 

2133 """Precompile the regexp pattern if necessary.""" 

2134 try: # Precompile the regexp. 

2135 # pylint: disable=no-member 

2136 flags = re.MULTILINE 

2137 if self.ignore_case: 

2138 flags |= re.IGNORECASE # pragma: no cover 

2139 # Escape the search text. 

2140 # Ignore the whole_word option. 

2141 s = self.find_text 

2142 # A bad idea: insert \b automatically. 

2143 # b, s = '\\b', self.find_text 

2144 # if self.whole_word: 

2145 # if not s.startswith(b): s = b + s 

2146 # if not s.endswith(b): s = s + b 

2147 self.re_obj = re.compile(s, flags) 

2148 return True 

2149 except Exception: 

2150 if not g.unitTesting: # pragma: no cover (skip) 

2151 g.warning('invalid regular expression:', self.find_text) 

2152 return False 

2153 #@+node:ekr.20031218072017.3075: *4* find.find_next_match & helpers 

2154 def find_next_match(self, p): 

2155 """ 

2156 Resume the search where it left off. 

2157 

2158 Return (p, pos, newpos). 

2159 """ 

2160 c = self.c 

2161 if not self.search_headline and not self.search_body: # pragma: no cover 

2162 return None, None, None 

2163 if not self.find_text: # pragma: no cover 

2164 return None, None, None 

2165 attempts = 0 

2166 if self.pattern_match: 

2167 ok = self.precompile_pattern() 

2168 if not ok: 

2169 return None, None, None 

2170 while p: 

2171 pos, newpos = self._fnm_search(p) 

2172 if pos is not None: 

2173 # Success. 

2174 if self.mark_finds: # pragma: no cover 

2175 p.setMarked() 

2176 p.setDirty() 

2177 if not self.changeAllFlag: 

2178 c.frame.tree.updateIcon(p) # redraw only the icon. 

2179 return p, pos, newpos 

2180 # Searching the pane failed: switch to another pane or node. 

2181 if self._fnm_should_stay_in_node(p): 

2182 # Switching panes is possible. Do so. 

2183 self.in_headline = not self.in_headline 

2184 s = p.h if self.in_headline else p.b 

2185 ins = len(s) if self.reverse else 0 

2186 self.work_s = s 

2187 self.work_sel = (ins, ins, ins) 

2188 else: 

2189 # Switch to the next/prev node, if possible. 

2190 attempts += 1 

2191 p = self._fnm_next_after_fail(p) 

2192 if p: # Found another node: select the proper pane. 

2193 self.in_headline = self._fnm_first_search_pane() 

2194 s = p.h if self.in_headline else p.b 

2195 ins = len(s) if self.reverse else 0 

2196 self.work_s = s 

2197 self.work_sel = (ins, ins, ins) 

2198 return None, None, None 

2199 #@+node:ekr.20131123132043.16476: *5* find._fnm_next_after_fail & helper 

2200 def _fnm_next_after_fail(self, p): 

2201 """Return the next node after a failed search or None.""" 

2202 # Move to the next position. 

2203 p = p.threadBack() if self.reverse else p.threadNext() 

2204 # Check it. 

2205 if p and self._fail_outside_range(p): # pragma: no cover 

2206 return None 

2207 if not p: # pragma: no cover 

2208 return None 

2209 return p 

2210 #@+node:ekr.20131123071505.16465: *6* find._fail_outside_range 

2211 def _fail_outside_range(self, p): # pragma: no cover 

2212 """ 

2213 Return True if the search is about to go outside its range, assuming 

2214 both the headline and body text of the present node have been searched. 

2215 """ 

2216 c = self.c 

2217 if not p: 

2218 return True 

2219 if self.node_only: 

2220 return True 

2221 if self.suboutline_only: 

2222 if self.root and p != self.root and not self.root.isAncestorOf(p): 

2223 return True 

2224 if c.hoistStack: 

2225 bunch = c.hoistStack[-1] 

2226 if not bunch.p.isAncestorOf(p): 

2227 g.trace('outside hoist', p.h) 

2228 g.warning('found match outside of hoisted outline') 

2229 return True 

2230 return False # Within range. 

2231 #@+node:ekr.20131124060912.16473: *5* find._fnm_first_search_pane 

2232 def _fnm_first_search_pane(self): 

2233 """ 

2234 Set return the value of self.in_headline 

2235 indicating which pane to search first. 

2236 """ 

2237 if self.search_headline and self.search_body: 

2238 # Fix bug 1228458: Inconsistency between Find-forward and Find-backward. 

2239 if self.reverse: 

2240 return False # Search the body pane first. 

2241 return True # Search the headline pane first. 

2242 if self.search_headline or self.search_body: 

2243 # Search the only enabled pane. 

2244 return self.search_headline 

2245 g.trace('can not happen: no search enabled') # pragma: no cover 

2246 return False # pragma: no cover 

2247 #@+node:ekr.20031218072017.3077: *5* find._fnm_search 

2248 def _fnm_search(self, p): 

2249 """ 

2250 Search self.work_s for self.find_text with present options. 

2251 Returns (pos, newpos) or (None, dNone). 

2252 """ 

2253 index = self.work_sel[2] 

2254 s = self.work_s 

2255 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!) 

2256 if sys.platform.lower().startswith('win'): 

2257 # Ignore '\r' characters, which may appear in @edit nodes. 

2258 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4 

2259 s = s.replace('\r', '') 

2260 if not s: # pragma: no cover 

2261 return None, None 

2262 stopindex = 0 if self.reverse else len(s) 

2263 pos, newpos = self.inner_search_helper(s, index, stopindex, self.find_text) 

2264 if self.in_headline and not self.search_headline: # pragma: no cover 

2265 return None, None 

2266 if not self.in_headline and not self.search_body: # pragma: no cover 

2267 return None, None 

2268 if pos == -1: # pragma: no cover 

2269 return None, None 

2270 ins = min(pos, newpos) if self.reverse else max(pos, newpos) 

2271 self.work_sel = (pos, newpos, ins) 

2272 return pos, newpos 

2273 #@+node:ekr.20131124060912.16472: *5* find._fnm_should_stay_in_node 

2274 def _fnm_should_stay_in_node(self, p): 

2275 """Return True if the find should simply switch panes.""" 

2276 # Errors here cause the find command to fail badly. 

2277 # Switch only if: 

2278 # a) searching both panes and, 

2279 # b) this is the first pane of the pair. 

2280 # There is *no way* this can ever change. 

2281 # So simple in retrospect, so difficult to see. 

2282 return ( 

2283 self.search_headline and self.search_body and ( 

2284 (self.reverse and not self.in_headline) or 

2285 (not self.reverse and self.in_headline))) 

2286 #@+node:ekr.20210110073117.43: *4* find.inner_search_helper & helpers 

2287 def inner_search_helper(self, s, i, j, pattern): 

2288 """ 

2289 Dispatch the proper search method based on settings. 

2290 """ 

2291 backwards = self.reverse 

2292 nocase = self.ignore_case 

2293 regexp = self.pattern_match 

2294 word = self.whole_word 

2295 if backwards: 

2296 i, j = j, i 

2297 if not s[i:j] or not pattern: 

2298 return -1, -1 

2299 if regexp: 

2300 pos, newpos = self._inner_search_regex(s, i, j, pattern, backwards, nocase) 

2301 elif backwards: 

2302 pos, newpos = self._inner_search_backward(s, i, j, pattern, nocase, word) 

2303 else: 

2304 pos, newpos = self._inner_search_plain(s, i, j, pattern, nocase, word) 

2305 return pos, newpos 

2306 #@+node:ekr.20210110073117.44: *5* find._inner_search_backward 

2307 def _inner_search_backward(self, s, i, j, pattern, nocase, word): 

2308 """ 

2309 rfind(sub [,start [,end]]) 

2310 

2311 Return the highest index in the string where substring sub is found, 

2312 such that sub is contained within s[start,end]. 

2313 

2314 Optional arguments start and end are interpreted as in slice notation. 

2315 

2316 Return (-1, -1) on failure. 

2317 """ 

2318 if nocase: 

2319 s = s.lower() 

2320 pattern = pattern.lower() 

2321 pattern = self.replace_back_slashes(pattern) 

2322 n = len(pattern) 

2323 # Put the indices in range. Indices can get out of range 

2324 # because the search code strips '\r' characters when searching @edit nodes. 

2325 i = max(0, i) 

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

2327 # short circuit the search: helps debugging. 

2328 if s.find(pattern) == -1: 

2329 return -1, -1 

2330 if word: 

2331 while 1: 

2332 k = s.rfind(pattern, i, j) 

2333 if k == -1: 

2334 break 

2335 if self._inner_search_match_word(s, k, pattern): 

2336 return k, k + n 

2337 j = max(0, k - 1) 

2338 return -1, -1 

2339 k = s.rfind(pattern, i, j) 

2340 if k == -1: 

2341 return -1, -1 

2342 return k, k + n 

2343 #@+node:ekr.20210110073117.45: *5* find._inner_search_match_word 

2344 def _inner_search_match_word(self, s, i, pattern): 

2345 """Do a whole-word search.""" 

2346 pattern = self.replace_back_slashes(pattern) 

2347 if not s or not pattern or not g.match(s, i, pattern): 

2348 return False 

2349 pat1, pat2 = pattern[0], pattern[-1] 

2350 n = len(pattern) 

2351 ch1 = s[i - 1] if 0 <= i - 1 < len(s) else '.' 

2352 ch2 = s[i + n] if 0 <= i + n < len(s) else '.' 

2353 isWordPat1 = g.isWordChar(pat1) 

2354 isWordPat2 = g.isWordChar(pat2) 

2355 isWordCh1 = g.isWordChar(ch1) 

2356 isWordCh2 = g.isWordChar(ch2) 

2357 inWord = isWordPat1 and isWordCh1 or isWordPat2 and isWordCh2 

2358 return not inWord 

2359 #@+node:ekr.20210110073117.46: *5* find._inner_search_plain 

2360 def _inner_search_plain(self, s, i, j, pattern, nocase, word): 

2361 """Do a plain search.""" 

2362 if nocase: 

2363 s = s.lower() 

2364 pattern = pattern.lower() 

2365 pattern = self.replace_back_slashes(pattern) 

2366 n = len(pattern) 

2367 if word: 

2368 while 1: 

2369 k = s.find(pattern, i, j) 

2370 if k == -1: 

2371 break 

2372 if self._inner_search_match_word(s, k, pattern): 

2373 return k, k + n 

2374 i = k + n 

2375 return -1, -1 

2376 k = s.find(pattern, i, j) 

2377 if k == -1: 

2378 return -1, -1 

2379 return k, k + n 

2380 #@+node:ekr.20210110073117.47: *5* find._inner_search_regex 

2381 def _inner_search_regex(self, s, i, j, pattern, backwards, nocase): 

2382 """Called from inner_search_helper""" 

2383 re_obj = self.re_obj # Use the pre-compiled object 

2384 if not re_obj: 

2385 if not g.unitTesting: # pragma: no cover (skip) 

2386 g.trace('can not happen: no re_obj') 

2387 return -1, -1 

2388 if backwards: 

2389 # Scan to the last match using search here. 

2390 i, last_mo = 0, None 

2391 while i < len(s): 

2392 mo = re_obj.search(s, i, j) 

2393 if not mo: 

2394 break 

2395 i += 1 

2396 last_mo = mo 

2397 mo = last_mo 

2398 else: 

2399 mo = re_obj.search(s, i, j) 

2400 if mo: 

2401 self.match_obj = mo 

2402 return mo.start(), mo.end() 

2403 self.match_obj = None 

2404 return -1, -1 

2405 #@+node:ekr.20210110073117.48: *4* find.make_regex_subs 

2406 def make_regex_subs(self, change_text, groups): 

2407 """ 

2408 Substitute group[i-1] for \\i strings in change_text. 

2409 

2410 Groups is a tuple of strings, one for every matched group. 

2411 """ 

2412 

2413 # g.printObj(list(groups), tag=f"groups in {change_text!r}") 

2414 

2415 def repl(match_object): 

2416 """re.sub calls this function once per group.""" 

2417 # # 1494... 

2418 n = int(match_object.group(1)) - 1 

2419 if 0 <= n < len(groups): 

2420 # Executed only if the change text contains groups that match. 

2421 return ( 

2422 groups[n]. 

2423 replace(r'\b', r'\\b'). 

2424 replace(r'\f', r'\\f'). 

2425 replace(r'\n', r'\\n'). 

2426 replace(r'\r', r'\\r'). 

2427 replace(r'\t', r'\\t'). 

2428 replace(r'\v', r'\\v')) 

2429 # No replacement. 

2430 return match_object.group(0) 

2431 

2432 result = re.sub(r'\\([0-9])', repl, change_text) 

2433 return result 

2434 #@+node:ekr.20131123071505.16467: *4* find.precompile_pattern 

2435 def precompile_pattern(self): 

2436 """Precompile the regexp pattern if necessary.""" 

2437 try: # Precompile the regexp. 

2438 # pylint: disable=no-member 

2439 flags = re.MULTILINE 

2440 if self.ignore_case: 

2441 flags |= re.IGNORECASE 

2442 # Escape the search text. 

2443 # Ignore the whole_word option. 

2444 s = self.find_text 

2445 # A bad idea: insert \b automatically. 

2446 # b, s = '\\b', self.find_text 

2447 # if self.whole_word: 

2448 # if not s.startswith(b): s = b + s 

2449 # if not s.endswith(b): s = s + b 

2450 self.re_obj = re.compile(s, flags) 

2451 return True 

2452 except Exception: 

2453 if not g.unitTesting: 

2454 g.warning('invalid regular expression:', self.find_text) # pragma: no cover 

2455 return False 

2456 #@+node:ekr.20210110073117.49: *4* find.replace_back_slashes 

2457 def replace_back_slashes(self, s): 

2458 """Carefully replace backslashes in a search pattern.""" 

2459 # This is NOT the same as: 

2460 # 

2461 # s.replace('\\n','\n').replace('\\t','\t').replace('\\\\','\\') 

2462 # 

2463 # because there is no rescanning. 

2464 i = 0 

2465 while i + 1 < len(s): 

2466 if s[i] == '\\': 

2467 ch = s[i + 1] 

2468 if ch == '\\': 

2469 s = s[:i] + s[i + 1 :] # replace \\ by \ 

2470 elif ch == 'n': 

2471 s = s[:i] + '\n' + s[i + 2 :] # replace the \n by a newline 

2472 elif ch == 't': 

2473 s = s[:i] + '\t' + s[i + 2 :] # replace \t by a tab 

2474 else: 

2475 i += 1 # Skip the escaped character. 

2476 i += 1 

2477 return s 

2478 #@+node:ekr.20031218072017.3082: *3* LeoFind.Initing & finalizing 

2479 #@+node:ekr.20031218072017.3086: *4* find.init_in_headline & helper 

2480 def init_in_headline(self): 

2481 """ 

2482 Select the first pane to search for incremental searches and changes. 

2483 This is called only at the start of each search. 

2484 This must not alter the current insertion point or selection range. 

2485 """ 

2486 # 

2487 # Fix bug 1228458: Inconsistency between Find-forward and Find-backward. 

2488 if self.search_headline and self.search_body: 

2489 # We have no choice: we *must* search the present widget! 

2490 self.in_headline = self.focus_in_tree() 

2491 else: 

2492 self.in_headline = self.search_headline 

2493 #@+node:ekr.20131126085250.16651: *5* find.focus_in_tree 

2494 def focus_in_tree(self): 

2495 """ 

2496 Return True is the focus widget w is anywhere in the tree pane. 

2497 

2498 Note: the focus may be in the find pane. 

2499 """ 

2500 c = self.c 

2501 ftm = self.ftm 

2502 w = ftm and ftm.entry_focus or g.app.gui.get_focus(raw=True) 

2503 if ftm: 

2504 ftm.entry_focus = None # Only use this focus widget once! 

2505 w_name = c.widget_name(w) 

2506 if w == c.frame.body.wrapper: 

2507 val = False 

2508 elif w == c.frame.tree.treeWidget: # pragma: no cover 

2509 val = True 

2510 else: 

2511 val = w_name.startswith('head') # pragma: no cover 

2512 return val 

2513 #@+node:ekr.20031218072017.3089: *4* find.restore 

2514 def restore(self, data): 

2515 """ 

2516 Restore Leo's gui and settings from data, a g.Bunch. 

2517 """ 

2518 c, p = self.c, data.p 

2519 c.frame.bringToFront() # Needed on the Mac 

2520 if not p or not c.positionExists(p): # pragma: no cover 

2521 # Better than selecting the root! 

2522 return 

2523 c.selectPosition(p) 

2524 # Fix bug 1258373: https://bugs.launchpad.net/leo-editor/+bug/1258373 

2525 if self.in_headline: 

2526 c.treeWantsFocus() 

2527 else: 

2528 # Looks good and provides clear indication of failure or termination. 

2529 w = c.frame.body.wrapper 

2530 w.setSelectionRange(data.start, data.end, insert=data.insert) 

2531 w.seeInsertPoint() 

2532 c.widgetWantsFocus(w) 

2533 #@+node:ekr.20031218072017.3090: *4* find.save 

2534 def save(self): 

2535 """Save everything needed to restore after a search fails.""" 

2536 c = self.c 

2537 if self.in_headline: # pragma: no cover 

2538 # Fix bug 1258373: https://bugs.launchpad.net/leo-editor/+bug/1258373 

2539 # Don't try to re-edit the headline. 

2540 insert, start, end = None, None, None 

2541 else: 

2542 w = c.frame.body.wrapper 

2543 insert = w.getInsertPoint() 

2544 start, end = w.getSelectionRange() 

2545 data = g.Bunch( 

2546 end=end, 

2547 in_headline=self.in_headline, 

2548 insert=insert, 

2549 p=c.p.copy(), 

2550 start=start, 

2551 ) 

2552 return data 

2553 #@+node:ekr.20031218072017.3091: *4* find.show_success 

2554 def show_success(self, p, pos, newpos, showState=True): 

2555 """Display the result of a successful find operation.""" 

2556 c = self.c 

2557 # Set state vars. 

2558 # Ensure progress in backwards searches. 

2559 insert = min(pos, newpos) if self.reverse else max(pos, newpos) 

2560 if c.sparse_find: # pragma: no cover 

2561 c.expandOnlyAncestorsOfNode(p=p) 

2562 if self.in_headline: 

2563 c.endEditing() 

2564 c.redraw(p) 

2565 c.frame.tree.editLabel(p) 

2566 w = c.edit_widget(p) # #2220 

2567 if w: 

2568 w.setSelectionRange(pos, newpos, insert) # #2220 

2569 else: 

2570 # Tricky code. Do not change without careful thought. 

2571 w = c.frame.body.wrapper 

2572 # *Always* do the full selection logic. 

2573 # This ensures that the body text is inited and recolored. 

2574 c.selectPosition(p) 

2575 c.bodyWantsFocus() 

2576 if showState: 

2577 c.k.showStateAndMode(w) 

2578 c.bodyWantsFocusNow() 

2579 w.setSelectionRange(pos, newpos, insert=insert) 

2580 k = g.see_more_lines(w.getAllText(), insert, 4) 

2581 w.see(k) 

2582 # #78: find-next match not always scrolled into view. 

2583 c.outerUpdate() 

2584 # Set the focus immediately. 

2585 if c.vim_mode and c.vimCommands: # pragma: no cover 

2586 c.vimCommands.update_selection_after_search() 

2587 # Support for the console gui. 

2588 if hasattr(g.app.gui, 'show_find_success'): # pragma: no cover 

2589 g.app.gui.show_find_success(c, self.in_headline, insert, p) 

2590 c.frame.bringToFront() 

2591 return w # Support for isearch. 

2592 #@+node:ekr.20131117164142.16939: *3* LeoFind.ISearch 

2593 #@+node:ekr.20210112192011.1: *4* LeoFind.Isearch commands 

2594 #@+node:ekr.20131117164142.16941: *5* find.isearch_forward 

2595 @cmd('isearch-forward') 

2596 def isearch_forward(self, event): # pragma: no cover (cmd) 

2597 """ 

2598 Begin a forward incremental search. 

2599 

2600 - Plain characters extend the search. 

2601 - !<isearch-forward>! repeats the search. 

2602 - Esc or any non-plain key ends the search. 

2603 - Backspace reverses the search. 

2604 - Backspacing to an empty search pattern 

2605 completely undoes the effect of the search. 

2606 """ 

2607 self.start_incremental(event, 'isearch-forward', 

2608 forward=True, ignoreCase=False, regexp=False) 

2609 #@+node:ekr.20131117164142.16942: *5* find.isearch_backward 

2610 @cmd('isearch-backward') 

2611 def isearch_backward(self, event): # pragma: no cover (cmd) 

2612 """ 

2613 Begin a backward incremental search. 

2614 

2615 - Plain characters extend the search backward. 

2616 - !<isearch-forward>! repeats the search. 

2617 - Esc or any non-plain key ends the search. 

2618 - Backspace reverses the search. 

2619 - Backspacing to an empty search pattern 

2620 completely undoes the effect of the search. 

2621 """ 

2622 self.start_incremental(event, 'isearch-backward', 

2623 forward=False, ignoreCase=False, regexp=False) 

2624 #@+node:ekr.20131117164142.16943: *5* find.isearch_forward_regexp 

2625 @cmd('isearch-forward-regexp') 

2626 def isearch_forward_regexp(self, event): # pragma: no cover (cmd) 

2627 """ 

2628 Begin a forward incremental regexp search. 

2629 

2630 - Plain characters extend the search. 

2631 - !<isearch-forward-regexp>! repeats the search. 

2632 - Esc or any non-plain key ends the search. 

2633 - Backspace reverses the search. 

2634 - Backspacing to an empty search pattern 

2635 completely undoes the effect of the search. 

2636 """ 

2637 self.start_incremental(event, 'isearch-forward-regexp', 

2638 forward=True, ignoreCase=False, regexp=True) 

2639 #@+node:ekr.20131117164142.16944: *5* find.isearch_backward_regexp 

2640 @cmd('isearch-backward-regexp') 

2641 def isearch_backward_regexp(self, event): # pragma: no cover (cmd) 

2642 """ 

2643 Begin a backward incremental regexp search. 

2644 

2645 - Plain characters extend the search. 

2646 - !<isearch-forward-regexp>! repeats the search. 

2647 - Esc or any non-plain key ends the search. 

2648 - Backspace reverses the search. 

2649 - Backspacing to an empty search pattern 

2650 completely undoes the effect of the search. 

2651 """ 

2652 self.start_incremental(event, 'isearch-backward-regexp', 

2653 forward=False, ignoreCase=False, regexp=True) 

2654 #@+node:ekr.20131117164142.16945: *5* find.isearch_with_present_options 

2655 @cmd('isearch-with-present-options') 

2656 def isearch_with_present_options(self, event): # pragma: no cover (cmd) 

2657 """ 

2658 Begin an incremental search using find panel options. 

2659 

2660 - Plain characters extend the search. 

2661 - !<isearch-forward-regexp>! repeats the search. 

2662 - Esc or any non-plain key ends the search. 

2663 - Backspace reverses the search. 

2664 - Backspacing to an empty search pattern 

2665 completely undoes the effect of the search. 

2666 """ 

2667 self.start_incremental(event, 'isearch-with-present-options', 

2668 forward=None, ignoreCase=None, regexp=None) 

2669 #@+node:ekr.20131117164142.16946: *4* LeoFind.Isearch utils 

2670 #@+node:ekr.20131117164142.16947: *5* find.abort_search (incremental) 

2671 def abort_search(self): # pragma: no cover (cmd) 

2672 """Restore the original position and selection.""" 

2673 c, k = self.c, self.k 

2674 w = c.frame.body.wrapper 

2675 k.clearState() 

2676 k.resetLabel() 

2677 p, i, j, in_headline = self.stack[0] 

2678 self.in_headline = in_headline 

2679 c.selectPosition(p) 

2680 c.redraw_after_select(p) 

2681 c.bodyWantsFocus() 

2682 w.setSelectionRange(i, j) 

2683 #@+node:ekr.20131117164142.16948: *5* find.end_search 

2684 def end_search(self): # pragma: no cover (cmd) 

2685 c, k = self.c, self.k 

2686 k.clearState() 

2687 k.resetLabel() 

2688 c.bodyWantsFocus() 

2689 #@+node:ekr.20131117164142.16949: *5* find.iSearch_helper 

2690 def iSearch_helper(self, again=False): # pragma: no cover (cmd) 

2691 """Handle the actual incremental search.""" 

2692 c, k, p = self.c, self.k, self.c.p 

2693 reverse = not self.isearch_forward_flag 

2694 pattern = k.getLabel(ignorePrompt=True) 

2695 if not pattern: 

2696 self.abort_search() 

2697 return 

2698 # Settings... 

2699 self.find_text = self.ftm.get_find_text() 

2700 self.change_text = self.ftm.get_change_text() 

2701 # Save 

2702 oldPattern = self.find_text 

2703 oldRegexp = self.pattern_match 

2704 oldWord = self.whole_word 

2705 # Override 

2706 self.pattern_match = self.isearch_regexp 

2707 self.reverse = reverse 

2708 self.find_text = pattern 

2709 self.whole_word = False # Word option can't be used! 

2710 # Prepare the search. 

2711 if len(self.stack) <= 1: 

2712 self.in_headline = False 

2713 # Init the work widget from the gui widget. 

2714 gui_w = self.set_widget() 

2715 s = gui_w.getAllText() 

2716 i, j = gui_w.getSelectionRange() 

2717 if again: 

2718 ins = i if reverse else j + len(pattern) 

2719 else: 

2720 ins = j + len(pattern) if reverse else i 

2721 self.work_s = s 

2722 self.work_sel = (ins, ins, ins) 

2723 # Do the search! 

2724 p, pos, newpos = self.find_next_match(p) 

2725 # Restore. 

2726 self.find_text = oldPattern 

2727 self.pattern_match = oldRegexp 

2728 self.reverse = False 

2729 self.whole_word = oldWord 

2730 # Handle the results of the search. 

2731 if pos is not None: # success. 

2732 w = self.show_success(p, pos, newpos, showState=False) 

2733 if w: 

2734 i, j = w.getSelectionRange(sort=False) 

2735 if not again: 

2736 self.push(c.p, i, j, self.in_headline) 

2737 else: 

2738 g.es(f"not found: {pattern}") 

2739 if not again: 

2740 event = g.app.gui.create_key_event( 

2741 c, binding='BackSpace', char='\b', w=w) 

2742 k.updateLabel(event) 

2743 #@+node:ekr.20131117164142.16950: *5* find.isearch_state_handler 

2744 def isearch_state_handler(self, event): # pragma: no cover (cmd) 

2745 """The state manager when the state is 'isearch""" 

2746 # c = self.c 

2747 k = self.k 

2748 stroke = event.stroke if event else None 

2749 s = stroke.s if stroke else '' 

2750 # No need to recognize ctrl-z. 

2751 if s in ('Escape', '\n', 'Return'): 

2752 self.end_search() 

2753 elif stroke in self.iSearchStrokes: 

2754 self.iSearch_helper(again=True) 

2755 elif s in ('\b', 'BackSpace'): 

2756 k.updateLabel(event) 

2757 self.isearch_backspace() 

2758 elif ( 

2759 s.startswith('Ctrl+') or 

2760 s.startswith('Alt+') or 

2761 k.isFKey(s) # 2011/06/13. 

2762 ): 

2763 # End the search. 

2764 self.end_search() 

2765 k.masterKeyHandler(event) 

2766 # Fix bug 1267921: isearch-forward accepts non-alphanumeric keys as input. 

2767 elif k.isPlainKey(stroke): 

2768 k.updateLabel(event) 

2769 self.iSearch_helper() 

2770 #@+node:ekr.20131117164142.16951: *5* find.isearch_backspace 

2771 def isearch_backspace(self): # pragma: no cover (cmd) 

2772 

2773 c = self.c 

2774 if len(self.stack) <= 1: 

2775 self.abort_search() 

2776 return 

2777 # Reduce the stack by net 1. 

2778 self.pop() 

2779 p, i, j, in_headline = self.pop() 

2780 self.push(p, i, j, in_headline) 

2781 if in_headline: 

2782 # Like self.show_success. 

2783 selection = i, j, i 

2784 c.redrawAndEdit(p, selectAll=False, 

2785 selection=selection, 

2786 keepMinibuffer=True) 

2787 else: 

2788 c.selectPosition(p) 

2789 w = c.frame.body.wrapper 

2790 c.bodyWantsFocus() 

2791 if i > j: 

2792 i, j = j, i 

2793 w.setSelectionRange(i, j) 

2794 if len(self.stack) <= 1: 

2795 self.abort_search() 

2796 #@+node:ekr.20131117164142.16952: *5* find.get_strokes 

2797 def get_strokes(self, commandName): # pragma: no cover (cmd) 

2798 aList = self.inverseBindingDict.get(commandName, []) 

2799 return [key for pane, key in aList] 

2800 #@+node:ekr.20131117164142.16953: *5* find.push & pop 

2801 def push(self, p, i, j, in_headline): # pragma: no cover (cmd) 

2802 data = p.copy(), i, j, in_headline 

2803 self.stack.append(data) 

2804 

2805 def pop(self): # pragma: no cover (cmd) 

2806 data = self.stack.pop() 

2807 p, i, j, in_headline = data 

2808 return p, i, j, in_headline 

2809 #@+node:ekr.20131117164142.16954: *5* find.set_widget 

2810 def set_widget(self): # pragma: no cover (cmd) 

2811 c, p = self.c, self.c.p 

2812 wrapper = c.frame.body.wrapper 

2813 if self.in_headline: 

2814 w = c.edit_widget(p) 

2815 if not w: 

2816 # Selecting the minibuffer can kill the edit widget. 

2817 selection = 0, 0, 0 

2818 c.redrawAndEdit(p, selectAll=False, 

2819 selection=selection, keepMinibuffer=True) 

2820 w = c.edit_widget(p) 

2821 if not w: # Should never happen. 

2822 g.trace('**** no edit widget!') 

2823 self.in_headline = False 

2824 w = wrapper 

2825 else: 

2826 w = wrapper 

2827 if w == wrapper: 

2828 c.bodyWantsFocus() 

2829 return w 

2830 #@+node:ekr.20131117164142.16955: *5* find.start_incremental 

2831 def start_incremental(self, event, commandName, forward, ignoreCase, regexp): # pragma: no cover (cmd) 

2832 c, k = self.c, self.k 

2833 # None is a signal to get the option from the find tab. 

2834 self.event = event 

2835 self.isearch_forward_flag = not self.reverse if forward is None else forward 

2836 self.isearch_ignore_case = self.ignore_case if ignoreCase is None else ignoreCase 

2837 self.isearch_regexp = self.pattern_match if regexp is None else regexp 

2838 # Note: the word option can't be used with isearches! 

2839 w = c.frame.body.wrapper 

2840 self.p1 = c.p 

2841 self.sel1 = w.getSelectionRange(sort=False) 

2842 i, j = self.sel1 

2843 self.push(c.p, i, j, self.in_headline) 

2844 self.inverseBindingDict = k.computeInverseBindingDict() 

2845 self.iSearchStrokes = self.get_strokes(commandName) 

2846 k.setLabelBlue( 

2847 "Isearch" 

2848 f"{' Backward' if not self.isearch_forward_flag else ''}" 

2849 f"{' Regexp' if self.isearch_regexp else ''}" 

2850 f"{' NoCase' if self.isearch_ignore_case else ''}" 

2851 ": " 

2852 ) 

2853 k.setState('isearch', 1, handler=self.isearch_state_handler) 

2854 c.minibufferWantsFocus() 

2855 #@+node:ekr.20031218072017.3067: *3* LeoFind.Utils 

2856 #@+node:ekr.20131117164142.16992: *4* find.add_change_string_to_label 

2857 def add_change_string_to_label(self): # pragma: no cover (cmd) 

2858 """Add an unprotected change string to the minibuffer label.""" 

2859 c = self.c 

2860 s = self.ftm.get_change_text() 

2861 c.minibufferWantsFocus() 

2862 while s.endswith('\n') or s.endswith('\r'): 

2863 s = s[:-1] 

2864 c.k.extendLabel(s, select=True, protect=False) 

2865 #@+node:ekr.20131117164142.16993: *4* find.add_find_string_to_label 

2866 def add_find_string_to_label(self, protect=True): # pragma: no cover (cmd) 

2867 c, k = self.c, self.c.k 

2868 ftm = c.findCommands.ftm 

2869 s = ftm.get_find_text() 

2870 c.minibufferWantsFocus() 

2871 while s.endswith('\n') or s.endswith('\r'): 

2872 s = s[:-1] 

2873 k.extendLabel(s, select=True, protect=protect) 

2874 #@+node:ekr.20210110073117.33: *4* find.compute_result_status 

2875 def compute_result_status(self, find_all_flag=False): # pragma: no cover (cmd) 

2876 """Return the status to be shown in the status line after a find command completes.""" 

2877 # Too similar to another method... 

2878 status = [] 

2879 table = ( 

2880 ('whole_word', 'Word'), 

2881 ('ignore_case', 'Ignore Case'), 

2882 ('pattern_match', 'Regex'), 

2883 ('suboutline_only', '[Outline Only]'), 

2884 ('node_only', '[Node Only]'), 

2885 ('search_headline', 'Head'), 

2886 ('search_body', 'Body'), 

2887 ) 

2888 for ivar, val in table: 

2889 if getattr(self, ivar): 

2890 status.append(val) 

2891 return f" ({', '.join(status)})" if status else '' 

2892 #@+node:ekr.20131119204029.16479: *4* find.help_for_find_commands 

2893 def help_for_find_commands(self, event=None): # pragma: no cover (cmd) 

2894 """Called from Find panel. Redirect.""" 

2895 self.c.helpCommands.help_for_find_commands(event) 

2896 #@+node:ekr.20210111082524.1: *4* find.init_vim_search 

2897 def init_vim_search(self, pattern): # pragma: no cover (cmd) 

2898 """Initialize searches in vim mode.""" 

2899 c = self.c 

2900 if c.vim_mode and c.vimCommands: 

2901 c.vimCommands.update_dot_before_search( 

2902 find_pattern=pattern, 

2903 change_pattern=None) # A flag. 

2904 #@+node:ekr.20150629072547.1: *4* find.preload_find_pattern 

2905 def preload_find_pattern(self, w): # pragma: no cover (cmd) 

2906 """Preload the find pattern from the selected text of widget w.""" 

2907 c, ftm = self.c, self.ftm 

2908 if not c.config.getBool('preload-find-pattern', default=False): 

2909 # Make *sure* we don't preload the find pattern if it is not wanted. 

2910 return 

2911 if not w: 

2912 return 

2913 # 

2914 # #1436: Don't create a selection if there isn't one. 

2915 # Leave the search pattern alone! 

2916 # 

2917 # if not w.hasSelection(): 

2918 # c.editCommands.extendToWord(event=None, select=True, w=w) 

2919 # 

2920 # #177: Use selected text as the find string. 

2921 # #1436: Make make sure there is a significant search pattern. 

2922 s = w.getSelectedText() 

2923 if s.strip(): 

2924 ftm.set_find_text(s) 

2925 ftm.init_focus() 

2926 #@+node:ekr.20150619070602.1: *4* find.show_status 

2927 def show_status(self, found): 

2928 """Show the find status the Find dialog, if present, and the status line.""" 

2929 c = self.c 

2930 status = 'found' if found else 'not found' 

2931 options = self.compute_result_status() 

2932 s = f"{status}:{options} {self.find_text}" 

2933 # Set colors. 

2934 found_bg = c.config.getColor('find-found-bg') or 'blue' 

2935 not_found_bg = c.config.getColor('find-not-found-bg') or 'red' 

2936 found_fg = c.config.getColor('find-found-fg') or 'white' 

2937 not_found_fg = c.config.getColor('find-not-found-fg') or 'white' 

2938 bg = found_bg if found else not_found_bg 

2939 fg = found_fg if found else not_found_fg 

2940 if c.config.getBool("show-find-result-in-status") is not False: 

2941 c.frame.putStatusLine(s, bg=bg, fg=fg) 

2942 #@+node:ekr.20150615174549.1: *4* find.show_find_options_in_status_area & helper 

2943 def show_find_options_in_status_area(self): # pragma: no cover (cmd) 

2944 """Show find options in the status area.""" 

2945 c = self.c 

2946 s = self.compute_find_options_in_status_area() 

2947 c.frame.putStatusLine(s) 

2948 #@+node:ekr.20171129211238.1: *5* find.compute_find_options_in_status_area 

2949 def compute_find_options_in_status_area(self): 

2950 c = self.c 

2951 ftm = c.findCommands.ftm 

2952 table = ( 

2953 ('Word', ftm.check_box_whole_word), 

2954 ('Ig-case', ftm.check_box_ignore_case), 

2955 ('regeXp', ftm.check_box_regexp), 

2956 ('Body', ftm.check_box_search_body), 

2957 ('Head', ftm.check_box_search_headline), 

2958 # ('wrap-Around', ftm.check_box_wrap_around), 

2959 ('mark-Changes', ftm.check_box_mark_changes), 

2960 ('mark-Finds', ftm.check_box_mark_finds), 

2961 ) 

2962 result = [option for option, ivar in table if ivar.isChecked()] 

2963 table2 = ( 

2964 ('Suboutline', ftm.radio_button_suboutline_only), 

2965 ('Node', ftm.radio_button_node_only), 

2966 ) 

2967 for option, ivar in table2: 

2968 if ivar.isChecked(): 

2969 result.append(f"[{option}]") 

2970 break 

2971 return f"Find: {' '.join(result)}" 

2972 #@+node:ekr.20131117164142.17007: *4* find.start_state_machine 

2973 def start_state_machine(self, event, prefix, handler, escape_handler=None): # pragma: no cover (cmd) 

2974 """ 

2975 Initialize and start the state machine used to get user arguments. 

2976 """ 

2977 c, k = self.c, self.k 

2978 w = c.frame.body.wrapper 

2979 if not w: 

2980 return 

2981 # Gui... 

2982 k.setLabelBlue(prefix) 

2983 # New in Leo 5.2: minibuffer modes shows options in status area. 

2984 if self.minibuffer_mode: 

2985 self.show_find_options_in_status_area() 

2986 elif c.config.getBool('use-find-dialog', default=True): 

2987 g.app.gui.openFindDialog(c) 

2988 else: 

2989 c.frame.log.selectTab('Find') 

2990 self.add_find_string_to_label(protect=False) 

2991 k.getArgEscapes = ['\t'] if escape_handler else [] 

2992 self.handler = handler 

2993 self.escape_handler = escape_handler 

2994 # Start the state maching! 

2995 k.get1Arg(event, handler=self.state0, tabList=self.findTextList, completion=True) 

2996 

2997 def state0(self, event): # pragma: no cover (cmd) 

2998 """Dispatch the next handler.""" 

2999 k = self.k 

3000 if k.getArgEscapeFlag: 

3001 k.getArgEscapeFlag = False 

3002 self.escape_handler(event) 

3003 else: 

3004 self.handler(event) 

3005 #@+node:ekr.20131117164142.17008: *4* find.updateChange/FindList 

3006 def update_change_list(self, s): # pragma: no cover (cmd) 

3007 if s not in self.changeTextList: 

3008 self.changeTextList.append(s) 

3009 

3010 def update_find_list(self, s): # pragma: no cover (cmd) 

3011 if s not in self.findTextList: 

3012 self.findTextList.append(s) 

3013 #@-others 

3014#@-others 

3015#@@language python 

3016#@@tabwidth -4 

3017#@@pagewidth 70 

3018#@-leo