Coverage for C:\Repos\leo-editor\leo\core\leoFind.py: 98%

1105 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1#@+leo-ver=5-thin 

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

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

4#@+<< imports leoFind.py >> 

5#@+node:ekr.20220415005856.1: ** << imports leoFind.py >> 

6import keyword 

7import re 

8import sys 

9import time 

10from typing import Any, Callable, Dict, Generator, List, Optional, Set, Tuple, Union 

11from typing import TYPE_CHECKING 

12from leo.core import leoGlobals as g 

13#@-<< imports leoFind.py >> 

14#@+<< type aliases leoFind.py >> 

15#@+node:ekr.20220415005920.1: ** << type aliases leoFind.py >> 

16if TYPE_CHECKING: # Always False at runtime. 

17 from leo.core.leoCommands import Commands as Cmdr 

18 from leo.core.leoNodes import Position as Pos 

19else: 

20 Cmdr = Pos = Any 

21Event = Any 

22Settings = Any 

23Stroke = Any 

24Wrapper = Any 

25#@-<< type aliases leoFind.py >> 

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

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

28#@@language rest 

29#@@nosearch 

30#@+at 

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

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

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

34# 

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

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

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

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

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

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

41# principle caused all kinds of problems earlier versions. 

42# 

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

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

45# left in a state suitable for restarting the incremental 

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

47# are discussed below. 

48# 

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

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

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

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

53# looking for matches. 

54# 

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

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

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

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

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

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

61# 

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

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

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

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

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

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

68# 

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

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

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

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

73# be searched. 

74# 

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

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

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

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

79 

80def cmd(name: str) -> Callable: 

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

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

83 

84#@+others 

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

86class LeoFind: 

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

88 #@+others 

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

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

91 def __init__(self, c: Cmdr) -> None: 

92 """Ctor for LeoFind class.""" 

93 self.c = c 

94 self.expert_mode = False # Set in finishCreate. 

95 self.ftm: Any = None # Created by dw.createFindTab. 

96 self.frame: Wrapper = None 

97 self.k: Any = c.k 

98 self.re_obj: re.Pattern = None 

99 # 

100 # The work "widget". 

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

102 self.work_sel: Tuple[int, int, int] = None # pos, newpos, insert. 

103 # 

104 # Options ivars: set by FindTabManager.init. 

105 # These *must* be initially None, not False. 

106 self.ignore_case: bool = None 

107 self.node_only: bool = None 

108 self.file_only: bool = None 

109 self.pattern_match: bool = None 

110 self.search_headline: bool = None 

111 self.search_body: bool = None 

112 self.suboutline_only: bool = None 

113 self.mark_changes: bool = None 

114 self.mark_finds: bool = None 

115 self.whole_word: bool = None 

116 # 

117 # For isearch commands... 

118 self.stack: List[Tuple[Pos, int, int, bool]] = [] 

119 self.inverseBindingDict: Dict[str, List[Tuple[str, str]]] = {} 

120 self.isearch_ignore_case: bool = False 

121 self.isearch_forward_flag: bool = False 

122 self.isearch_regexp: bool = False 

123 self.iSearchStrokes: List[Stroke] = [] 

124 self.findTextList: List = [] 

125 self.changeTextList: List = [] 

126 # 

127 # For find/change... 

128 self.find_text = "" 

129 self.change_text = "" 

130 # 

131 # State machine... 

132 self.escape_handler: Callable = None 

133 self.handler: Callable = None 

134 # "Delayed" requests for do_find_next. 

135 self.request_reverse = False 

136 self.request_pattern_match = False 

137 self.request_whole_word = False 

138 # Internal state... 

139 self.changeAllFlag = False 

140 self.findAllUniqueFlag = False 

141 self.find_def_data: Any = None # A g.Bunch. 

142 self.in_headline = False 

143 self.match_obj: re.Match = None 

144 self.reverse = False 

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

146 self.unique_matches: Set = set() 

147 # 

148 # User settings. 

149 self.minibuffer_mode: bool = None 

150 self.reload_settings() 

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

152 def default_settings(self) -> Settings: 

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

154 c = self.c 

155 return g.Bunch( 

156 # State... 

157 in_headline=False, 

158 p=c.rootPosition(), 

159 # Find/change strings... 

160 find_text='', 

161 change_text='', 

162 # Find options... 

163 ignore_case=False, 

164 mark_changes=False, 

165 mark_finds=False, 

166 node_only=False, 

167 pattern_match=False, 

168 reverse=False, 

169 search_body=True, 

170 search_headline=True, 

171 suboutline_only=False, 

172 whole_word=False, 

173 wrapping=False, 

174 ) 

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

176 def finishCreate(self) -> None: # pragma: no cover 

177 # New in 4.11.1. 

178 # Must be called when config settings are valid. 

179 c = self.c 

180 self.reload_settings() 

181 # now that configuration settings are valid, 

182 # we can finish creating the Find pane. 

183 dw = c.frame.top 

184 if dw: 

185 dw.finishCreateLogPane() 

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

187 def init_ivars_from_settings(self, settings: Settings) -> None: 

188 """ 

189 Initialize all ivars from settings, including required defaults. 

190 

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

192 

193 self.init_ivars_from_settings(settings) 

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

195 return <appropriate error indication> 

196 """ 

197 # 

198 # Init required defaults. 

199 self.reverse = False 

200 # 

201 # Init find/change strings. 

202 self.change_text = settings.change_text 

203 self.find_text = settings.find_text 

204 # 

205 # Init find options. 

206 self.ignore_case = settings.ignore_case 

207 self.mark_changes = settings.mark_changes 

208 self.mark_finds = settings.mark_finds 

209 self.node_only = settings.node_only 

210 self.pattern_match = settings.pattern_match 

211 self.search_body = settings.search_body 

212 self.search_headline = settings.search_headline 

213 self.suboutline_only = settings.suboutline_only 

214 self.whole_word = settings.whole_word 

215 # self.wrapping = settings.wrapping 

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

217 def init_settings(self, settings: Settings) -> None: 

218 """Initialize all user settings.""" 

219 

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

221 def reload_settings(self) -> None: 

222 """LeoFind.reload_settings.""" 

223 c = self.c 

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

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

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

227 def batch_change(self, 

228 root: Pos, 

229 replacements: List[Tuple[str, str]], 

230 settings: Settings=None, 

231 ) -> int: 

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

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

234 """ 

235 Support batch change scripts. 

236 

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

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

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

240 

241 Example: 

242 

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

244 root = g.findNodeAnywhere(c, h) 

245 assert root 

246 replacements = ( 

247 ('clone_find_all', 'do_clone_find_all'), 

248 ('clone_find_all_flattened', 'do_clone_find_all_flattened'), 

249 ) 

250 settings = dict(suboutline_only=True) 

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

252 if count: 

253 c.save() 

254 """ 

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

256 try: 

257 self._init_from_dict(settings or {}) 

258 count = 0 

259 for find, change in replacements: 

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

261 return count 

262 except Exception: # pragma: no cover 

263 g.es_exception() 

264 return 0 

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

266 def _batch_change_helper(self, p: Pos, find_text: str, change_text: str) -> int: 

267 

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

269 undoType = 'Batch Change All' 

270 # Check... 

271 if not find_text: # pragma: no cover 

272 return 0 

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

274 return 0 # pragma: no cover 

275 if self.pattern_match: 

276 ok = self.precompile_pattern() 

277 if not ok: # pragma: no cover 

278 return 0 

279 # Init... 

280 self.find_text = find_text 

281 self.change_text = self.replace_back_slashes(change_text) 

282 positions: Union[List, Generator] 

283 if self.node_only: 

284 positions = [p1] 

285 elif self.suboutline_only: 

286 positions = p1.self_and_subtree() 

287 else: 

288 positions = c.all_unique_positions() 

289 # Init the work widget. 

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

291 self.work_s = s 

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

293 # The main loop. 

294 u.beforeChangeGroup(p1, undoType) 

295 count = 0 

296 for p in positions: 

297 count_h, count_b = 0, 0 

298 undoData = u.beforeChangeNodeContents(p) 

299 if self.search_headline: 

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

301 if count_h: 

302 count += count_h 

303 p.h = new_h 

304 if self.search_body: 

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

306 if count_b: 

307 count += count_b 

308 p.b = new_b 

309 if count_h or count_b: 

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

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

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

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

314 return count 

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

316 def _init_from_dict(self, settings: Settings) -> None: 

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

318 # The valid ivars and reasonable defaults. 

319 valid = dict( 

320 ignore_case=False, 

321 node_only=False, 

322 pattern_match=False, 

323 search_body=True, 

324 search_headline=True, 

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

326 whole_word=True, 

327 ) 

328 # Set ivars to reasonable defaults. 

329 for ivar in valid: 

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

331 # Override ivars from settings. 

332 errors = 0 

333 for ivar in settings.keys(): 

334 if ivar in valid: 

335 val = settings.get(ivar) 

336 if val in (True, False): 

337 setattr(self, ivar, val) 

338 else: # pragma: no cover 

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

340 errors += 1 

341 else: # pragma: no cover 

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

343 errors += 1 

344 if errors: # pragma: no cover 

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

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

347 def interactive_search_helper(self, 

348 root: Pos=None, 

349 settings: Settings=None, 

350 ) -> None: # pragma: no cover 

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

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

353 """ 

354 Support interactive find. 

355 

356 c.findCommands.interactive_search_helper starts an interactive search with 

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

358 dict. 

359 

360 Example 1, settings is a g.Bunch: 

361 

362 c.findCommands.interactive_search_helper( 

363 root = c.p, 

364 settings = g.Bunch( 

365 find_text = '^(def )', 

366 change_text = '\1', 

367 pattern_match=True, 

368 search_headline=False, 

369 whole_word=False, 

370 ) 

371 ) 

372 

373 Example 2, settings is a python dict: 

374 

375 c.findCommands.interactive_search_helper( 

376 root = c.p, 

377 settings = { 

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

379 'change_text': '\1', 

380 'pattern_match': True, 

381 'search_headline': False, 

382 'whole_word': False, 

383 } 

384 ) 

385 """ 

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

387 # Merge settings into default settings. 

388 c = self.c 

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

390 if settings: 

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

392 # g.Bunch has no update method. 

393 for key in settings.keys(): 

394 d[key] = settings[key] 

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

396 self.show_find_options_in_status_area() 

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

398 return 

399 if root: 

400 c.selectPosition(root) 

401 self.do_find_next(d) 

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

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

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

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

406 def change_then_find(self, event: Event=None) -> None: # pragma: no cover (cmd) 

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

408 # Settings... 

409 self.init_in_headline() 

410 settings = self.ftm.get_settings() 

411 self.do_change_then_find(settings) 

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

413 # A stand-alone method for unit testing. 

414 def do_change_then_find(self, settings: Settings) -> bool: 

415 """ 

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

417 

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

419 """ 

420 p = self.c.p 

421 self.init_ivars_from_settings(settings) 

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

423 return False 

424 if self.change_selection(p): 

425 self.do_find_next(settings) 

426 return True 

427 

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

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

430 @cmd('cfam') 

431 def cloneFindAllMarked(self, event: Event=None) -> None: 

432 """ 

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

434 

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

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

437 descendants of the organizer node. 

438 """ 

439 self.do_find_marked(flatten=False) 

440 

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

442 @cmd('cffm') 

443 def cloneFindAllFlattenedMarked(self, event: Event=None) -> None: 

444 """ 

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

446 

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

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

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

450 another cloned node. 

451 """ 

452 self.do_find_marked(flatten=True) 

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

454 def do_find_marked(self, flatten: bool) -> bool: 

455 """ 

456 Helper for clone-find-marked commands. 

457 

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

459 """ 

460 c = self.c 

461 

462 def isMarked(p: Pos) -> bool: 

463 return p.isMarked() 

464 

465 root = c.cloneFindByPredicate( 

466 generator=c.all_unique_positions, 

467 predicate=isMarked, 

468 failMsg='No marked nodes', 

469 flatten=flatten, 

470 undoType='clone-find-marked', 

471 ) 

472 if root: 

473 # Unmarking all nodes is convenient. 

474 for v in c.all_unique_nodes(): 

475 if v.isMarked(): 

476 v.clearMarked() 

477 n = root.numberOfChildren() 

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

479 c.selectPosition(root) 

480 c.redraw(root) 

481 return bool(root) 

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

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

484 def cloneFindParents(self, event: Event=None) -> bool: 

485 """ 

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

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

488 """ 

489 c, u = self.c, self.c.undoer 

490 p = c.p 

491 if not p: # pragma: no cover 

492 return False 

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

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

495 return False 

496 p0 = p.copy() 

497 undoType = 'Find Clone Parents' 

498 aList = c.vnode2allPositions(p.v) 

499 if not aList: # pragma: no cover 

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

501 return False 

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

503 # All existing positions remain valid. 

504 u.beforeChangeGroup(p, undoType) 

505 b = u.beforeInsertNode(p) 

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

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

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

509 seen = [] 

510 for p2 in aList: 

511 parent = p2.parent() 

512 if parent and parent.v not in seen: 

513 seen.append(parent.v) 

514 b = u.beforeCloneNode(parent) 

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

516 clone = p.copy() 

517 n = found.numberOfChildren() 

518 clone._linkCopiedAsNthChild(found, n) 

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

520 u.afterChangeGroup(p0, undoType) 

521 c.setChanged() 

522 c.redraw(found) 

523 return True 

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

525 @cmd('find-def') 

526 def find_def(self, event: Event=None, strict: bool=False) -> Tuple[Pos, int, int]: # pragma: no cover (cmd) 

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

528 ftm, p = self.ftm, self.c.p 

529 # Check. 

530 word = self._compute_find_def_word(event) 

531 if not word: 

532 return None, None, None 

533 # Settings... 

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

535 find_pattern = prefix + ' ' + word 

536 ftm.set_find_text(find_pattern) 

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

538 self.init_vim_search(find_pattern) 

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

540 # Do the command! 

541 settings = self._compute_find_def_settings(find_pattern) 

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

543 

544 def find_def_strict(self, event: Event=None) -> Tuple[Pos, int, int]: #pragma: no cover (cmd) 

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

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

547 

548 def do_find_def(self, settings: Settings, word: str, strict: bool) -> Tuple[Pos, int, int]: 

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

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

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

552 def _compute_find_def_settings(self, find_pattern: str) -> Settings: 

553 

554 settings = self.default_settings() 

555 table = ( 

556 ('change_text', ''), 

557 ('find_text', find_pattern), 

558 ('ignore_case', False), 

559 ('pattern_match', False), 

560 ('reverse', False), 

561 ('search_body', True), 

562 ('search_headline', False), 

563 ('whole_word', True), 

564 ) 

565 for attr, val in table: 

566 # Guard against renamings & misspellings. 

567 assert hasattr(self, attr), attr 

568 assert attr in settings.__dict__, attr 

569 # Set the values. 

570 setattr(self, attr, val) 

571 settings[attr] = val 

572 return settings 

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

574 def _compute_find_def_word(self, event: Event) -> Optional[str]: # pragma: no cover (cmd) 

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

576 c = self.c 

577 w = c.frame.body.wrapper 

578 # First get the word. 

579 c.bodyWantsFocusNow() 

580 if not w.hasSelection(): 

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

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

583 if not word: 

584 return None 

585 if keyword.iskeyword(word): 

586 return None 

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

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

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

590 if found: 

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

592 return word 

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

594 def _fd_helper(self, 

595 settings: Settings, 

596 word: str, 

597 def_flag: bool, 

598 strict: bool, 

599 ) -> Tuple[Pos, int, int]: 

600 """ 

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

602 

603 return p, pos, newpos for unit tests. 

604 """ 

605 c, find, ftm = self.c, self, self.ftm 

606 # 

607 # Recompute find_text for unit tests. 

608 if def_flag: 

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

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

611 else: 

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

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

614 # 

615 # Just search body text. 

616 self.search_headline = False 

617 self.search_body = True 

618 w = c.frame.body.wrapper 

619 # Check. 

620 if not w: # pragma: no cover 

621 return None, None, None 

622 save_sel = w.getSelectionRange() 

623 ins = w.getInsertPoint() 

624 old_p = c.p 

625 if self.reverse_find_defs: 

626 # #2161: start at the last position. 

627 p = c.lastPosition() 

628 else: 

629 # Start in the root position. 

630 p = c.rootPosition() 

631 # Required. 

632 c.selectPosition(p) 

633 c.redraw() 

634 c.bodyWantsFocusNow() 

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

636 old_reverse = self.reverse 

637 try: 

638 # #2161: 

639 self.reverse = self.reverse_find_defs 

640 # # 2288: 

641 self.work_s = p.b 

642 if self.reverse_find_defs: 

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

644 else: 

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

646 while True: 

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

648 found = pos is not None 

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

650 break 

651 if not found and def_flag and not strict: 

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

653 word2 = self._switch_style(word) 

654 if self.reverse_find_defs: 

655 # #2161: start at the last position. 

656 p = c.lastPosition() 

657 else: 

658 p = c.rootPosition() 

659 if word2: 

660 find_pattern = prefix + ' ' + word2 

661 find.find_text = find_pattern 

662 ftm.set_find_text(find_pattern) 

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

664 while True: 

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

666 found = pos is not None 

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

668 break 

669 finally: 

670 self.reverse = old_reverse 

671 if found: 

672 c.redraw(p) 

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

674 c.bodyWantsFocusNow() 

675 return p, pos, newpos 

676 self._restore_after_find_def() # Avoid massive confusion! 

677 i, j = save_sel 

678 c.redraw(old_p) 

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

680 c.bodyWantsFocusNow() 

681 return None, None, None 

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

683 def _restore_after_find_def(self) -> None: 

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

685 # pylint: disable=no-member 

686 b = self.find_def_data # A g.Bunch 

687 if b: 

688 self.ignore_case = b.ignore_case 

689 self.pattern_match = b.pattern_match 

690 self.search_body = b.search_body 

691 self.search_headline = b.search_headline 

692 self.whole_word = b.whole_word 

693 self.find_def_data = None 

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

695 def _save_before_find_def(self, p: Pos) -> None: 

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

697 if not self.find_def_data: 

698 self.find_def_data = g.Bunch( 

699 ignore_case=self.ignore_case, 

700 p=p.copy(), 

701 pattern_match=self.pattern_match, 

702 search_body=self.search_body, 

703 search_headline=self.search_headline, 

704 whole_word=self.whole_word, 

705 ) 

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

707 def _switch_style(self, word: str) -> Optional[str]: 

708 """ 

709 Switch between camelCase and underscore_style function defintiions. 

710 Return None if there would be no change. 

711 """ 

712 s = word 

713 if not s: 

714 return None 

715 if s[0].isupper(): 

716 return None # Don't convert class names. 

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

718 # Convert to CamelCase 

719 s = s.lower() 

720 while s: 

721 i = s.find('_') 

722 if i == -1: 

723 break 

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

725 return s 

726 # Convert to underscore_style. 

727 result = [] 

728 for i, ch in enumerate(s): 

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

730 result.append('_') 

731 result.append(ch.lower()) 

732 s = ''.join(result) 

733 return None if s == word else s 

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

735 @cmd('find-next') 

736 def find_next(self, event: Event=None) -> None: # pragma: no cover (cmd) 

737 """The find-next command.""" 

738 # Settings... 

739 self.reverse = False 

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

741 settings = self.ftm.get_settings() 

742 # Do the command! 

743 self.do_find_next(settings) 

744 

745 @cmd('find-prev') 

746 def find_prev(self, event: Event=None) -> None: # pragma: no cover (cmd) 

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

748 # Settings... 

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

750 settings = self.ftm.get_settings() 

751 # Do the command! 

752 self.do_find_prev(settings) 

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

754 def do_find_prev(self, settings: Settings) -> Tuple[Pos, int, int]: 

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

756 self.request_reverse = True 

757 return self.do_find_next(settings) 

758 

759 def do_find_next(self, settings: Settings) -> Tuple[Pos, int, int]: 

760 """ 

761 Find the next instance of self.find_text. 

762 

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

764 

765 """ 

766 c, p = self.c, self.c.p 

767 # 

768 # The gui widget may not exist for headlines. 

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

770 # 

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

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

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

774 self.work_s = s 

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

776 # 

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

778 self.init_ivars_from_settings(settings) 

779 # 

780 # Honor delayed requests. 

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

782 request = 'request_' + ivar 

783 val = getattr(self, request) 

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

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

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

787 # 

788 # Leo 6.4: set/clear self.root 

789 if self.root: # pragma: no cover 

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

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

792 # Clear suboutline-only. 

793 self.root = None 

794 self.suboutline_only = False 

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

796 elif self.suboutline_only: 

797 # Start the range and set suboutline-only. 

798 self.root = c.p 

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

800 

801 elif self.file_only: 

802 # Start the range and set file-only. 

803 self.root = c.p 

804 p = c.p 

805 node = self.c.p 

806 hitBase = found = False 

807 while not found and not hitBase: 

808 h = node.h 

809 if h: 

810 h = h.split()[0] 

811 if h in ("@clean", "@file", "@asis", "@thin", "@edit", 

812 "@auto", "@auto-md", "@auto-org", 

813 "@auto-otl", "@auto-rst"): 

814 found = True 

815 else: 

816 if node.level() == 0: 

817 hitBase = True 

818 else: 

819 node = node.parent() 

820 self.root = node 

821 self.set_find_scope_file_only() # Update find-tab & status area. 

822 p = node 

823 # 

824 # Now check the args. 

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

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

827 return None, None, None 

828 data = self.save() 

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

830 found = pos is not None 

831 if found: 

832 self.show_success(p, pos, newpos) 

833 else: 

834 # Restore previous position. 

835 self.restore(data) 

836 self.show_status(found) 

837 return p, pos, newpos 

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

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

840 def hide_find_tab(self, event: Event=None) -> None: # pragma: no cover (cmd) 

841 """Hide the Find tab.""" 

842 c = self.c 

843 if self.minibuffer_mode: 

844 c.k.keyboardQuit() 

845 else: 

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

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

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

849 def open_find_tab(self, event: Event=None, show: bool=True) -> None: # pragma: no cover (cmd) 

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

851 c = self.c 

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

853 g.app.gui.openFindDialog(c) 

854 else: 

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

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

857 @cmd('find-var') 

858 def find_var(self, event: Event=None) -> None: # pragma: no cover (cmd) 

859 """Find the var under the cursor.""" 

860 ftm, p = self.ftm, self.c.p 

861 # Check... 

862 word = self._compute_find_def_word(event) 

863 if not word: 

864 return 

865 # Settings... 

866 self.find_pattern = find_pattern = word + ' =' 

867 ftm.set_find_text(find_pattern) 

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

869 self.init_vim_search(find_pattern) 

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

871 settings = self._compute_find_def_settings(find_pattern) 

872 # Do the command! 

873 self.do_find_var(settings, word) 

874 

875 def do_find_var(self, settings: Settings, word: str) -> Tuple[Pos, int, int]: 

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

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

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

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

880 def focus_to_find(self, event: Event=None) -> None: # pragma: no cover (cmd) 

881 c = self.c 

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

883 g.app.gui.openFindDialog(c) 

884 else: 

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

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

887 @cmd('replace') 

888 @cmd('change') 

889 def change(self, event: Event=None) -> None: # pragma: no cover (cmd) 

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

891 p = self.c.p 

892 if self.check_args('replace'): 

893 self.init_in_headline() 

894 self.change_selection(p) 

895 

896 replace = change 

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

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

899 def set_find_scope_every_where(self, event: Event=None) -> None: # pragma: no cover (cmd) 

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

901 self.set_find_scope('entire-outline') 

902 

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

904 def set_find_scope_node_only(self, event: Event=None) -> None: # pragma: no cover (cmd) 

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

906 self.set_find_scope('node-only') 

907 

908 @cmd('set-find-file-only') 

909 def set_find_scope_file_only(self, event: Event=None) -> None: # pragma: no cover (cmd) 

910 """Set the 'File Only' radio button in the Find tab.""" 

911 self.set_find_scope('file-only') 

912 

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

914 def set_find_scope_suboutline_only(self, event: Event=None) -> None: 

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

916 self.set_find_scope('suboutline-only') 

917 

918 def set_find_scope(self, where: str) -> None: 

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

920 c, fc = self.c, self.c.findCommands 

921 self.ftm.set_radio_button(where) 

922 options = fc.compute_find_options_in_status_area() 

923 c.frame.statusLine.put(options) 

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

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

926 def show_find_options(self, event: Event=None) -> None: # pragma: no cover (cmd) 

927 """ 

928 Show the present find options in the status line. 

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

930 """ 

931 frame = self.c.frame 

932 frame.clearStatusLine() 

933 part1, part2 = self.compute_find_options() 

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

935 frame.putStatusLine(part2) 

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

937 def compute_find_options(self) -> Tuple[str, str]: # pragma: no cover (cmd) 

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

939 z = [] 

940 # Set the scope field. 

941 if self.suboutline_only: 

942 scope = 'tree' 

943 elif self.node_only: 

944 scope = 'node' 

945 else: 

946 scope = 'all' 

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

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

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

950 head = 'head' if self.search_headline else '' 

951 body = 'body' if self.search_body else '' 

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

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

954 # Set the type field. 

955 regex = self.pattern_match 

956 if regex: 

957 z.append('regex') 

958 table = ( 

959 ('reverse', 'reverse'), 

960 ('ignore_case', 'noCase'), 

961 ('whole_word', 'word'), 

962 # ('wrap', 'wrap'), 

963 ('mark_changes', 'markChg'), 

964 ('mark_finds', 'markFnd'), 

965 ) 

966 for ivar, s in table: 

967 val = getattr(self, ivar) 

968 if val: 

969 z.append(s) 

970 part2 = ' '.join(z) 

971 return part1, part2 

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

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

974 def toggle_find_collapes_nodes(self, event: Event) -> None: # pragma: no cover (cmd) 

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

976 c = self.c 

977 c.sparse_find = not c.sparse_find 

978 if not g.unitTesting: 

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

980 

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

982 def toggle_ignore_case_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

984 self.toggle_option('ignore_case') 

985 

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

987 def toggle_mark_changes_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

989 self.toggle_option('mark_changes') 

990 

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

992 def toggle_mark_finds_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

994 return self.toggle_option('mark_finds') 

995 

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

997 def toggle_regex_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

999 self.toggle_option('pattern_match') 

1000 

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

1002 def toggle_search_body_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

1004 return self.toggle_option('search_body') 

1005 

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

1007 def toggle_search_headline_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

1009 self.toggle_option('search_headline') 

1010 

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

1012 def toggle_whole_word_option(self, event: Event) -> None: # pragma: no cover (cmd) 

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

1014 self.toggle_option('whole_word') 

1015 

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

1017 # def toggleWrapSearchOption(self, event): 

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

1019 # return self.toggle_option('wrap') 

1020 

1021 def toggle_option(self, checkbox_name: str) -> None: # pragma: no cover (cmd) 

1022 c, fc = self.c, self.c.findCommands 

1023 self.ftm.toggle_checkbox(checkbox_name) 

1024 options = fc.compute_find_options_in_status_area() 

1025 c.frame.statusLine.put(options) 

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

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

1028 @cmd('change-all') 

1029 @cmd('replace-all') 

1030 def interactive_change_all(self, event: Event=None) -> None: # pragma: no cover (interactive) 

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

1032 self.ftm.clear_focus() 

1033 self.ftm.set_entry_focus() 

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

1035 self.start_state_machine(event, prompt, 

1036 handler=self.interactive_replace_all1, 

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

1038 escape_handler=self.interactive_replace_all1, 

1039 ) 

1040 

1041 def interactive_replace_all1(self, event: Event) -> None: # pragma: no cover (interactive) 

1042 k = self.k 

1043 find_pattern = k.arg 

1044 self._sString = k.arg 

1045 self.update_find_list(k.arg) 

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

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

1048 k.setLabelBlue(prompt) 

1049 self.add_change_string_to_label() 

1050 k.getNextArg(self.interactive_replace_all2) 

1051 

1052 def interactive_replace_all2(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1054 

1055 # Update settings data. 

1056 find_pattern = self._sString 

1057 change_pattern = k.arg 

1058 self.init_vim_search(find_pattern) 

1059 self.update_change_list(change_pattern) 

1060 # Compute settings... 

1061 self.ftm.set_find_text(find_pattern) 

1062 self.ftm.set_change_text(change_pattern) 

1063 settings = self.ftm.get_settings() 

1064 # Gui... 

1065 k.clearState() 

1066 k.resetLabel() 

1067 k.showStateAndMode() 

1068 c.widgetWantsFocusNow(w) 

1069 # Do the command! 

1070 self.do_change_all(settings) 

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

1072 def do_change_all(self, settings: Settings) -> int: 

1073 c = self.c 

1074 # Settings... 

1075 self.init_ivars_from_settings(settings) 

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

1077 return 0 

1078 n = self._change_all_helper(settings) 

1079 # 

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

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

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

1083 if ( 

1084 p.anyAtFileNodeName() 

1085 and not p.v.isDirty() 

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

1087 ): 

1088 p.setDirty() 

1089 c.redraw() 

1090 return n 

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

1092 def _change_all_helper(self, settings: Settings) -> int: 

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

1094 # Caller has checked settings. 

1095 

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

1097 undoType = 'Replace All' 

1098 t1 = time.process_time() 

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

1100 return 0 

1101 self.init_in_headline() 

1102 saveData = self.save() 

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

1104 # Remember the start of the search. 

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

1106 # Set the work widget. 

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

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

1109 self.work_s = s 

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

1111 count = 0 

1112 u.beforeChangeGroup(current, undoType) 

1113 # Fix bug 338172: ReplaceAll will not replace newlines 

1114 # indicated as \n in target string. 

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

1116 return 0 

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

1118 return 0 

1119 self.change_text = self.replace_back_slashes(self.change_text) 

1120 if self.pattern_match: 

1121 ok = self.precompile_pattern() 

1122 if not ok: 

1123 return 0 

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

1125 if self.node_only: 

1126 positions = [c.p] 

1127 elif self.suboutline_only: 

1128 positions = c.p.self_and_subtree() 

1129 else: 

1130 positions = c.all_unique_positions() 

1131 count = 0 

1132 for p in positions: 

1133 count_h, count_b = 0, 0 

1134 undoData = u.beforeChangeNodeContents(p) 

1135 if self.search_headline: 

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

1137 if count_h: 

1138 count += count_h 

1139 p.h = new_h 

1140 if self.search_body: 

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

1142 if count_b: 

1143 count += count_b 

1144 p.b = new_b 

1145 if count_h or count_b: 

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

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

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

1149 self.root = None 

1150 self.node_only = self.suboutline_only = False 

1151 p = c.p 

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

1153 t2 = time.process_time() 

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

1155 g.es_print( 

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

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

1158 c.recolor() 

1159 c.redraw(p) 

1160 self.restore(saveData) 

1161 return count 

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

1163 def _change_all_search_and_replace(self, s: str) -> Tuple[int, str]: 

1164 """ 

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

1166 

1167 Return (found, new text) 

1168 """ 

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

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

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

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

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

1174 if not s: 

1175 return False, None 

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

1177 if self.pattern_match: 

1178 return self._change_all_regex(s) 

1179 if self.whole_word: 

1180 return self._change_all_word(s) 

1181 return self._change_all_plain(s) 

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

1183 def _change_all_plain(self, s: str) -> Tuple[int, str]: 

1184 """ 

1185 Perform all plain find/replace on s. 

1186 return (count, new_s) 

1187 """ 

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

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

1190 s0 = s 

1191 find0 = self.replace_back_slashes(find) 

1192 if self.ignore_case: 

1193 s = s0.lower() 

1194 find = find0.lower() 

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

1196 while True: 

1197 progress = prev_i 

1198 # #1166: Scan using s and find. 

1199 i = s.find(find, prev_i) 

1200 if i == -1: 

1201 break 

1202 # #1166: Replace using s0 & change. 

1203 count += 1 

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

1205 result.append(change) 

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

1207 assert prev_i > progress, prev_i 

1208 # #1166: Complete the result using s0. 

1209 result.append(s0[prev_i:]) 

1210 return count, ''.join(result) 

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

1212 def _change_all_regex(self, s: str) -> Tuple[int, str]: 

1213 """ 

1214 Perform all regex find/replace on s. 

1215 return (count, new_s) 

1216 """ 

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

1218 

1219 flags = re.MULTILINE 

1220 if self.ignore_case: 

1221 flags |= re.IGNORECASE 

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

1223 count += 1 

1224 i = m.start() 

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

1226 # #1748. 

1227 groups = m.groups() 

1228 if groups: 

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

1230 else: 

1231 change_text = self.change_text 

1232 result.append(change_text) 

1233 prev_i = m.end() 

1234 # Compute the result. 

1235 result.append(s[prev_i:]) 

1236 s = ''.join(result) 

1237 return count, s 

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

1239 def _change_all_word(self, s: str) -> Tuple[int, str]: 

1240 """ 

1241 Perform all whole word find/replace on s. 

1242 return (count, new_s) 

1243 """ 

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

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

1246 s0 = s 

1247 find0 = self.replace_back_slashes(find) 

1248 if self.ignore_case: 

1249 s = s0.lower() 

1250 find = find0.lower() 

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

1252 while True: 

1253 # #1166: Scan using s and find. 

1254 i = s.find(find, prev_i) 

1255 if i == -1: 

1256 break 

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

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

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

1260 count += 1 

1261 result.append(change) 

1262 else: 

1263 result.append(find0) 

1264 prev_i = i + len(find) 

1265 # #1166: Complete the result using s0. 

1266 result.append(s0[prev_i:]) 

1267 return count, ''.join(result) 

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

1269 def replace_all_helper(self, s: str) -> Tuple[int, str]: 

1270 """ 

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

1272 

1273 Return (found, new text) 

1274 """ 

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

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

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

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

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

1280 if not s: 

1281 return False, None 

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

1283 if self.pattern_match: 

1284 return self.batch_regex_replace(s) 

1285 if self.whole_word: 

1286 return self.batch_word_replace(s) 

1287 return self.batch_plain_replace(s) 

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

1289 def batch_plain_replace(self, s: str) -> Tuple[int, str]: 

1290 """ 

1291 Perform all plain find/replace on s. 

1292 return (count, new_s) 

1293 """ 

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

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

1296 s0 = s 

1297 find0 = self.replace_back_slashes(find) 

1298 if self.ignore_case: 

1299 s = s0.lower() 

1300 find = find0.lower() 

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

1302 while True: 

1303 progress = prev_i 

1304 # #1166: Scan using s and find. 

1305 i = s.find(find, prev_i) 

1306 if i == -1: 

1307 break 

1308 # #1166: Replace using s0 & change. 

1309 count += 1 

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

1311 result.append(change) 

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

1313 assert prev_i > progress, prev_i 

1314 # #1166: Complete the result using s0. 

1315 result.append(s0[prev_i:]) 

1316 return count, ''.join(result) 

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

1318 def batch_regex_replace(self, s: str) -> Tuple[int, str]: 

1319 """ 

1320 Perform all regex find/replace on s. 

1321 return (count, new_s) 

1322 """ 

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

1324 

1325 flags = re.MULTILINE 

1326 if self.ignore_case: 

1327 flags |= re.IGNORECASE 

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

1329 count += 1 

1330 i = m.start() 

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

1332 # #1748. 

1333 groups = m.groups() 

1334 if groups: 

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

1336 else: 

1337 change_text = self.change_text 

1338 result.append(change_text) 

1339 prev_i = m.end() 

1340 # Compute the result. 

1341 result.append(s[prev_i:]) 

1342 s = ''.join(result) 

1343 return count, s 

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

1345 def batch_word_replace(self, s: str) -> Tuple[int, str]: 

1346 """ 

1347 Perform all whole word find/replace on s. 

1348 return (count, new_s) 

1349 """ 

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

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

1352 s0 = s 

1353 find0 = self.replace_back_slashes(find) 

1354 if self.ignore_case: 

1355 s = s0.lower() 

1356 find = find0.lower() 

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

1358 while True: 

1359 progress = prev_i 

1360 # #1166: Scan using s and find. 

1361 i = s.find(find, prev_i) 

1362 if i == -1: 

1363 break 

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

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

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

1367 count += 1 

1368 result.append(change) 

1369 else: 

1370 result.append(find0) 

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

1372 assert prev_i > progress, prev_i 

1373 # #1166: Complete the result using s0. 

1374 result.append(s0[prev_i:]) 

1375 return count, ''.join(result) 

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

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

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

1379 @cmd('cfa') 

1380 def interactive_clone_find_all(self, 

1381 event: Event=None, 

1382 preloaded: bool=False, 

1383 ) -> None: # pragma: no cover (interactive) 

1384 """ 

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

1386 

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

1388 matching the search string, except @nosearch trees. 

1389 

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

1391 descendants of the organizer node. 

1392 """ 

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

1394 if not w: 

1395 return 

1396 if not preloaded: 

1397 self.preload_find_pattern(w) 

1398 self.start_state_machine(event, 

1399 prefix='Clone Find All: ', 

1400 handler=self.interactive_clone_find_all1) 

1401 

1402 def interactive_clone_find_all1(self, event: Event) -> int: # pragma: no cover (interactive) 

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

1404 # Settings... 

1405 pattern = k.arg 

1406 self.ftm.set_find_text(pattern) 

1407 self.init_vim_search(pattern) 

1408 self.init_in_headline() 

1409 settings = self.ftm.get_settings() 

1410 # Gui... 

1411 k.clearState() 

1412 k.resetLabel() 

1413 k.showStateAndMode() 

1414 c.widgetWantsFocusNow(w) 

1415 count = self.do_clone_find_all(settings) 

1416 if count: 

1417 c.redraw() 

1418 c.treeWantsFocus() 

1419 return count 

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

1421 # A stand-alone method for unit testing. 

1422 def do_clone_find_all(self, settings: Settings) -> int: 

1423 """ 

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

1425 

1426 Return the count of found nodes. 

1427 

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

1429 """ 

1430 self.init_ivars_from_settings(settings) 

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

1432 return 0 

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

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

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

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

1437 @cmd('cff') 

1438 def interactive_cff(self, event: Event=None, preloaded: bool=False) -> None: # pragma: no cover (interactive) 

1439 """ 

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

1441 

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

1443 matching the search string, except @nosearch trees. 

1444 

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

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

1447 another cloned node. 

1448 """ 

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

1450 if not w: 

1451 return 

1452 if not preloaded: 

1453 self.preload_find_pattern(w) 

1454 self.start_state_machine(event, 

1455 prefix='Clone Find All Flattened: ', 

1456 handler=self.interactive_cff1) 

1457 

1458 def interactive_cff1(self, event: Event) -> int: # pragma: no cover (interactive) 

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

1460 # Settings... 

1461 pattern = k.arg 

1462 self.ftm.set_find_text(pattern) 

1463 self.init_vim_search(pattern) 

1464 self.init_in_headline() 

1465 settings = self.ftm.get_settings() 

1466 # Gui... 

1467 k.clearState() 

1468 k.resetLabel() 

1469 k.showStateAndMode() 

1470 c.widgetWantsFocusNow(w) 

1471 count = self.do_clone_find_all_flattened(settings) 

1472 if count: 

1473 c.redraw() 

1474 c.treeWantsFocus() 

1475 return count 

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

1477 # A stand-alone method for unit testing. 

1478 def do_clone_find_all_flattened(self, settings: Settings) -> int: 

1479 """ 

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

1481 

1482 Return the count of found nodes. 

1483 

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

1485 """ 

1486 self.init_ivars_from_settings(settings) 

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

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

1489 return 0 

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

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

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

1493 @cmd('cft') 

1494 def interactive_clone_find_tag(self, event: Event=None) -> None: # pragma: no cover (interactive) 

1495 """ 

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

1497 

1498 Create an organizer node whose descendants contain clones of all 

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

1500 

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

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

1503 descendant of another cloned node. 

1504 """ 

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

1506 if w: 

1507 self.start_state_machine(event, 

1508 prefix='Clone Find Tag: ', 

1509 handler=self.interactive_clone_find_tag1) 

1510 

1511 def interactive_clone_find_tag1(self, event: Event) -> None: # pragma: no cover (interactive) 

1512 c, k = self.c, self.k 

1513 # Settings... 

1514 self.find_text = tag = k.arg 

1515 # Gui... 

1516 k.clearState() 

1517 k.resetLabel() 

1518 k.showStateAndMode() 

1519 self.do_clone_find_tag(tag) 

1520 c.treeWantsFocus() 

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

1522 # A stand-alone method for unit tests. 

1523 def do_clone_find_tag(self, tag: str) -> Tuple[int, int]: 

1524 """ 

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

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

1527 """ 

1528 c, u = self.c, self.c.undoer 

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

1530 if not tc: 

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

1532 g.es_print('nodetags not active') 

1533 return 0, c.p 

1534 clones = tc.get_tagged_nodes(tag) 

1535 if not clones: 

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

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

1538 tc.show_all_tags() 

1539 return 0, c.p 

1540 undoData = u.beforeInsertNode(c.p) 

1541 found = self._create_clone_tag_nodes(clones) 

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

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

1544 c.setChanged() 

1545 c.selectPosition(found) 

1546 c.redraw() 

1547 return len(clones), found 

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

1549 def _create_clone_tag_nodes(self, clones: List[Pos]) -> int: 

1550 """ 

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

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

1553 """ 

1554 c, p = self.c, self.c.p 

1555 # Create the found node. 

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

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

1558 assert found 

1559 assert c.positionExists(found), found 

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

1561 # Clone nodes as children of the found node. 

1562 for p in clones: 

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

1564 p2 = p.copy() 

1565 n = found.numberOfChildren() 

1566 p2._linkCopiedAsNthChild(found, n) 

1567 return found 

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

1569 @cmd('find-all') 

1570 def interactive_find_all(self, event: Event=None) -> None: # pragma: no cover (interactive) 

1571 """ 

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

1573 search string. 

1574 

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

1576 """ 

1577 self.ftm.clear_focus() 

1578 self.ftm.set_entry_focus() 

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

1580 handler=self.interactive_find_all1, 

1581 escape_handler=self.find_all_escape_handler, 

1582 ) 

1583 

1584 def interactive_find_all1(self, event: Event=None) -> None: # pragma: no cover (interactive) 

1585 k = self.k 

1586 # Settings. 

1587 find_pattern = k.arg 

1588 self.ftm.set_find_text(find_pattern) 

1589 settings = self.ftm.get_settings() 

1590 self.find_text = find_pattern 

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

1592 self.update_find_list(find_pattern) 

1593 # Gui... 

1594 k.clearState() 

1595 k.resetLabel() 

1596 k.showStateAndMode() 

1597 self.do_find_all(settings) 

1598 

1599 def find_all_escape_handler(self, event: Event) -> None: # pragma: no cover (interactive) 

1600 k = self.k 

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

1602 find_pattern = k.arg 

1603 self._sString = k.arg 

1604 self.update_find_list(k.arg) 

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

1606 k.setLabelBlue(s) 

1607 self.add_change_string_to_label() 

1608 k.getNextArg(self.find_all_escape_handler2) 

1609 

1610 def find_all_escape_handler2(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1612 find_pattern = self._sString 

1613 change_pattern = k.arg 

1614 self.update_change_list(change_pattern) 

1615 self.ftm.set_find_text(find_pattern) 

1616 self.ftm.set_change_text(change_pattern) 

1617 self.init_vim_search(find_pattern) 

1618 self.init_in_headline() 

1619 settings = self.ftm.get_settings() 

1620 # Gui... 

1621 k.clearState() 

1622 k.resetLabel() 

1623 k.showStateAndMode() 

1624 c.widgetWantsFocusNow(w) 

1625 self.do_change_all(settings) 

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

1627 def do_find_all(self, settings: Settings) -> int: 

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

1629 c = self.c 

1630 count = 0 

1631 self.init_ivars_from_settings(settings) 

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

1633 return count 

1634 # Init data. 

1635 self.init_in_headline() 

1636 data = self.save() 

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

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

1639 # Remember the start of the search. 

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

1641 # Set the work widget. 

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

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

1644 self.work_s = s 

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

1646 if self.pattern_match: 

1647 ok = self.precompile_pattern() 

1648 if not ok: # pragma: no cover 

1649 return count 

1650 if self.suboutline_only: 

1651 p = c.p 

1652 after = p.nodeAfterTree() 

1653 else: 

1654 # Always search the entire outline. 

1655 p = c.rootPosition() 

1656 after = None 

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

1658 old_sparse_find = c.sparse_find 

1659 try: 

1660 c.sparse_find = False 

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

1662 c.contractAllHeadlines() 

1663 finally: 

1664 c.sparse_find = old_sparse_find 

1665 self.root = None 

1666 if count: 

1667 c.redraw() 

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

1669 return count 

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

1671 def _find_all_helper(self, 

1672 after: Optional[Pos], 

1673 data: Any, 

1674 p: Pos, 

1675 undoType: str, 

1676 ) -> int: 

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

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

1679 

1680 def put_link(line: str, line_number: int, p: Pos) -> None: # pragma: no cover # #2023 

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

1682 

1683 if g.unitTesting: 

1684 return 

1685 unl = p.get_UNL() 

1686 if self.in_headline: 

1687 line_number = 1 

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

1689 

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

1691 both = self.search_body and self.search_headline 

1692 count = 0 

1693 found: Pos = None 

1694 result: List[str] = [] 

1695 while 1: 

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

1697 if pos is None: 

1698 break 

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

1700 continue # pragma: no cover 

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

1702 count += 1 

1703 s = self.work_s 

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

1705 line = s[i:j] 

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

1707 line_number = row + 1 

1708 if self.findAllUniqueFlag: 

1709 m = self.match_obj 

1710 if m: 

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

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

1713 elif both: 

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

1715 '-' * 20, p.h, 

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

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

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

1719 elif p.isVisited(): 

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

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

1722 else: 

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

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

1725 p.setVisited() 

1726 if result or self.unique_matches: 

1727 undoData = u.beforeInsertNode(c.p) 

1728 if self.findAllUniqueFlag: 

1729 found = self._create_find_unique_node() 

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

1731 else: 

1732 found = self._create_find_all_node(result) 

1733 u.afterInsertNode(found, undoType, undoData) 

1734 c.selectPosition(found) 

1735 c.setChanged() 

1736 else: 

1737 self.restore(data) 

1738 return count 

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

1740 def _create_find_all_node(self, result: List[str]) -> Pos: 

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

1742 c = self.c 

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

1744 assert found 

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

1746 status = self.compute_result_status(find_all_flag=True) 

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

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

1749 return found 

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

1751 def _create_find_unique_node(self) -> Pos: 

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

1753 c = self.c 

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

1755 assert found 

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

1757 result = sorted(self.unique_matches) 

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

1759 return found 

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

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

1762 def interactive_find_all_unique_regex(self, event: Event=None) -> None: # pragma: no cover (interactive) 

1763 """ 

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

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

1766 """ 

1767 self.ftm.clear_focus() 

1768 self.match_obj = None 

1769 self.changeAllFlag = False 

1770 self.findAllUniqueFlag = True 

1771 self.ftm.set_entry_focus() 

1772 self.start_state_machine(event, 

1773 prefix='Search Unique Regex: ', 

1774 handler=self.interactive_find_all_unique_regex1, 

1775 escape_handler=self.interactive_change_all_unique_regex1, 

1776 ) 

1777 

1778 def interactive_find_all_unique_regex1(self, event: Event=None) -> int: # pragma: no cover (interactive) 

1779 k = self.k 

1780 # Settings... 

1781 find_pattern = k.arg 

1782 self.update_find_list(find_pattern) 

1783 self.ftm.set_find_text(find_pattern) 

1784 self.init_in_headline() 

1785 settings = self.ftm.get_settings() 

1786 # Gui... 

1787 k.clearState() 

1788 k.resetLabel() 

1789 k.showStateAndMode() 

1790 return self.do_find_all(settings) 

1791 

1792 def interactive_change_all_unique_regex1(self, event: Event) -> None: # pragma: no cover (interactive) 

1793 k = self.k 

1794 find_pattern = self._sString = k.arg 

1795 self.update_find_list(k.arg) 

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

1797 k.setLabelBlue(s) 

1798 self.add_change_string_to_label() 

1799 k.getNextArg(self.interactive_change_all_unique_regex2) 

1800 

1801 def interactive_change_all_unique_regex2(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1803 find_pattern = self._sString 

1804 change_pattern = k.arg 

1805 self.update_change_list(change_pattern) 

1806 self.ftm.set_find_text(find_pattern) 

1807 self.ftm.set_change_text(change_pattern) 

1808 self.init_vim_search(find_pattern) 

1809 self.init_in_headline() 

1810 settings = self.ftm.get_settings() 

1811 # Gui... 

1812 k.clearState() 

1813 k.resetLabel() 

1814 k.showStateAndMode() 

1815 c.widgetWantsFocusNow(w) 

1816 self.do_change_all(settings) 

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

1818 @cmd('re-search') 

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

1820 def interactive_re_search_forward(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1822 # Set flag for show_find_options. 

1823 self.pattern_match = True 

1824 self.show_find_options() 

1825 # Set flag for do_find_next(). 

1826 self.request_pattern_match = True 

1827 # Go. 

1828 self.start_state_machine(event, 

1829 prefix='Regexp Search: ', 

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

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

1832 ) 

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

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

1835 def interactive_re_search_backward(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1837 # Set flags for show_find_options. 

1838 self.reverse = True 

1839 self.pattern_match = True 

1840 self.show_find_options() 

1841 # Set flags for do_find_next(). 

1842 self.request_reverse = True 

1843 self.request_pattern_match = True 

1844 # Go. 

1845 self.start_state_machine(event, 

1846 prefix='Regexp Search Backward:', 

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

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

1849 ) 

1850 

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

1852 @cmd('search-backward') 

1853 def interactive_search_backward(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1855 # Set flag for show_find_options. 

1856 self.reverse = True 

1857 self.show_find_options() 

1858 # Set flag for do_find_next(). 

1859 self.request_reverse = True 

1860 # Go. 

1861 self.start_state_machine(event, 

1862 prefix='Search Backward: ', 

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

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

1865 ) 

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

1867 @cmd('start-search') 

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

1869 def start_search(self, event: Event) -> None: # pragma: no cover (interactive) 

1870 """ 

1871 The default binding of Ctrl-F. 

1872 

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

1874 """ 

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

1876 if not w: 

1877 return 

1878 self.preload_find_pattern(w) 

1879 # #1840: headline-only one-shot 

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

1881 self.ftm.set_body_and_headline_checkbox() 

1882 if self.minibuffer_mode: 

1883 # Set up the state machine. 

1884 self.ftm.clear_focus() 

1885 self.changeAllFlag = False 

1886 self.findAllUniqueFlag = False 

1887 self.ftm.set_entry_focus() 

1888 self.start_state_machine(event, 

1889 prefix='Search: ', 

1890 handler=self.start_search1, 

1891 escape_handler=self.start_search_escape1, 

1892 ) 

1893 else: 

1894 self.open_find_tab(event) 

1895 self.ftm.init_focus() 

1896 return 

1897 

1898 startSearch = start_search # Compatibility. Do not delete. 

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

1900 def start_search1(self, event: Event=None) -> None: # pragma: no cover 

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

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

1903 # Settings... 

1904 find_pattern = k.arg 

1905 self.ftm.set_find_text(find_pattern) 

1906 self.update_find_list(find_pattern) 

1907 self.init_vim_search(find_pattern) 

1908 self.init_in_headline() # Required. 

1909 settings = self.ftm.get_settings() 

1910 # Gui... 

1911 k.clearState() 

1912 k.resetLabel() 

1913 k.showStateAndMode() 

1914 c.widgetWantsFocusNow(w) 

1915 # Do the command! 

1916 self.do_find_next(settings) # Handles reverse. 

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

1918 def start_search_escape1(self, event: Event=None) -> None: # pragma: no cover 

1919 """ 

1920 Common escape handler for use by find commands. 

1921 

1922 Prompt for a change pattern. 

1923 """ 

1924 k = self.k 

1925 self._sString = find_pattern = k.arg 

1926 # Settings. 

1927 k.getArgEscapeFlag = False 

1928 self.ftm.set_find_text(find_pattern) 

1929 self.update_find_list(find_pattern) 

1930 self.find_text = find_pattern 

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

1932 # Gui... 

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

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

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

1936 k.setLabelBlue(prompt) 

1937 self.add_change_string_to_label() 

1938 k.getNextArg(self._start_search_escape2) 

1939 

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

1941 def _start_search_escape2(self, event: Event) -> None: # pragma: no cover 

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

1943 # Compute settings... 

1944 find_pattern = self._sString 

1945 change_pattern = k.arg 

1946 self.ftm.set_find_text(find_pattern) 

1947 self.ftm.set_change_text(change_pattern) 

1948 self.update_change_list(change_pattern) 

1949 self.init_vim_search(find_pattern) 

1950 self.init_in_headline() # Required 

1951 settings = self.ftm.get_settings() 

1952 # Gui... 

1953 k.clearState() 

1954 k.resetLabel() 

1955 k.showStateAndMode() 

1956 c.widgetWantsFocusNow(w) 

1957 self.do_find_next(settings) 

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

1959 @cmd('tag-children') 

1960 def interactive_tag_children(self, event: Event=None) -> None: # pragma: no cover (interactive) 

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

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

1963 if not w: 

1964 return 

1965 self.start_state_machine(event, 

1966 prefix='Tag Children: ', 

1967 handler=self.interactive_tag_children1) 

1968 

1969 def interactive_tag_children1(self, event: Event) -> None: # pragma: no cover (interactive) 

1970 c, k, p = self.c, self.k, self.c.p 

1971 # Settings... 

1972 tag = k.arg 

1973 # Gui... 

1974 k.clearState() 

1975 k.resetLabel() 

1976 k.showStateAndMode() 

1977 self.do_tag_children(p, tag) 

1978 c.treeWantsFocus() 

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

1980 def do_tag_children(self, p: Pos, tag: str) -> None: 

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

1982 c = self.c 

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

1984 if not tc: 

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

1986 g.es_print('nodetags not active') 

1987 return 

1988 for p in p.children(): 

1989 tc.add_tag(p, tag) 

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

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

1992 

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

1994 @cmd('word-search') 

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

1996 def word_search_forward(self, event: Event) -> None: # pragma: no cover (interactive) 

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

1998 # Set flag for show_find_options. 

1999 self.whole_word = True 

2000 self.show_find_options() 

2001 # Set flag for do_find_next(). 

2002 self.request_whole_world = True 

2003 # Go. 

2004 self.start_state_machine(event, 

2005 prefix='Word Search: ', 

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

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

2008 ) 

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

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

2011 def word_search_backward(self, event: Event) -> None: # pragma: no cover (interactive) 

2012 # Set flags for show_find_options. 

2013 self.reverse = True 

2014 self.whole_world = True 

2015 self.show_find_options() 

2016 # Set flags for do_find_next(). 

2017 self.request_reverse = True 

2018 self.request_whole_world = True 

2019 # Go 

2020 self.start_state_machine(event, 

2021 prefix='Word Search Backward: ', 

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

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

2024 ) 

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

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

2027 def _cf_helper(self, settings: Settings, flatten: bool) -> int: # Caller has checked the settings. 

2028 """ 

2029 The common part of the clone-find commands. 

2030 

2031 Return the number of found nodes. 

2032 """ 

2033 c, u = self.c, self.c.undoer 

2034 if self.pattern_match: 

2035 ok = self.compile_pattern() 

2036 if not ok: 

2037 return 0 

2038 if self.suboutline_only: 

2039 p = c.p 

2040 after = p.nodeAfterTree() 

2041 else: 

2042 p = c.rootPosition() 

2043 after = None 

2044 count, found = 0, None 

2045 clones, skip = [], set() 

2046 while p and p != after: 

2047 progress = p.copy() 

2048 if g.inAtNosearch(p): 

2049 p.moveToNodeAfterTree() 

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

2051 p.moveToThreadNext() 

2052 elif self._cfa_find_next_match(p): 

2053 count += 1 

2054 if flatten: 

2055 skip.add(p.v) 

2056 clones.append(p.copy()) 

2057 p.moveToThreadNext() 

2058 else: 

2059 if p not in clones: 

2060 clones.append(p.copy()) 

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

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

2063 skip.add(p2.v) 

2064 p.moveToNodeAfterTree() 

2065 else: 

2066 p.moveToThreadNext() 

2067 assert p != progress 

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

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

2070 self.node_only = self.suboutline_only = False 

2071 self.root = None 

2072 if clones: 

2073 undoData = u.beforeInsertNode(c.p) 

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

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

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

2077 c.setChanged() 

2078 c.selectPosition(found) 

2079 # Put the count in found.h. 

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

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

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

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

2084 def _cfa_create_nodes(self, clones: List[Pos], flattened: bool) -> Pos: 

2085 """ 

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

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

2088 """ 

2089 c = self.c 

2090 # Create the found node. 

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

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

2093 assert found 

2094 assert c.positionExists(found), found 

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

2096 status = self.compute_result_status(find_all_flag=True) 

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

2098 flat = 'flattened, ' if flattened else '' 

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

2100 # Clone nodes as children of the found node. 

2101 for p in clones: 

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

2103 p2 = p.copy() 

2104 n = found.numberOfChildren() 

2105 p2._linkCopiedAsNthChild(found, n) 

2106 # Sort the clones in place, without undo. 

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

2108 return found 

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

2110 def _cfa_find_next_match(self, p: Pos) -> bool: 

2111 """ 

2112 Find the next batch match at p. 

2113 """ 

2114 # Called only from unit tests. 

2115 table = [] 

2116 if self.search_headline: 

2117 table.append(p.h) 

2118 if self.search_body: 

2119 table.append(p.b) 

2120 for s in table: 

2121 self.reverse = False 

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

2123 if pos != -1: 

2124 return True 

2125 return False 

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

2127 def change_selection(self, p: Pos) -> bool: 

2128 """Replace selection with self.change_text.""" 

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

2130 wrapper = c.frame.body and c.frame.body.wrapper 

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

2132 if not gui_w: # pragma: no cover 

2133 self.in_headline = False 

2134 gui_w = wrapper 

2135 if not gui_w: # pragma: no cover 

2136 return False 

2137 oldSel = sel = gui_w.getSelectionRange() 

2138 start, end = sel 

2139 if start > end: # pragma: no cover 

2140 start, end = end, start 

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

2142 g.es("no text selected") 

2143 return False 

2144 bunch = u.beforeChangeBody(p) 

2145 start, end = oldSel 

2146 change_text = self.change_text 

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

2148 if self.pattern_match and self.match_obj: 

2149 groups = self.match_obj.groups() 

2150 if groups: 

2151 change_text = self.make_regex_subs(change_text, groups) 

2152 change_text = self.replace_back_slashes(change_text) 

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

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

2155 if start != end: 

2156 gui_w.delete(start, end) 

2157 gui_w.insert(start, change_text) 

2158 gui_w.setInsertPoint(new_ins) 

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

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

2161 # Update the selection for the next match. 

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

2163 c.widgetWantsFocus(gui_w) 

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

2165 if self.mark_changes: # pragma: no cover 

2166 p.setMarked() 

2167 p.setDirty() 

2168 if self.in_headline: 

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

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

2171 # gui_w will change after a redraw. 

2172 gui_w = c.edit_widget(p) 

2173 if gui_w: 

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

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

2176 else: 

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

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

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

2180 return True 

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

2182 def check_args(self, tag: str) -> bool: 

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

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

2185 if not g.unitTesting: 

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

2187 return False 

2188 if not self.find_text: 

2189 if not g.unitTesting: 

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

2191 return False 

2192 return True 

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

2194 def compile_pattern(self) -> bool: 

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

2196 try: # Precompile the regexp. 

2197 # pylint: disable=no-member 

2198 flags = re.MULTILINE 

2199 if self.ignore_case: 

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

2201 # Escape the search text. 

2202 # Ignore the whole_word option. 

2203 s = self.find_text 

2204 # A bad idea: insert \b automatically. 

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

2206 # if self.whole_word: 

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

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

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

2210 return True 

2211 except Exception: 

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

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

2214 return False 

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

2216 def find_next_match(self, p: Pos) -> Tuple[Pos, int, int]: 

2217 """ 

2218 Resume the search where it left off. 

2219 

2220 Return (p, pos, newpos). 

2221 """ 

2222 c = self.c 

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

2224 return None, None, None 

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

2226 return None, None, None 

2227 attempts = 0 

2228 if self.pattern_match: 

2229 ok = self.precompile_pattern() 

2230 if not ok: 

2231 return None, None, None 

2232 while p: 

2233 pos, newpos = self._fnm_search(p) 

2234 if pos is not None: 

2235 # Success. 

2236 if self.mark_finds: # pragma: no cover 

2237 p.setMarked() 

2238 p.setDirty() 

2239 if not self.changeAllFlag: 

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

2241 return p, pos, newpos 

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

2243 if self._fnm_should_stay_in_node(p): 

2244 # Switching panes is possible. Do so. 

2245 self.in_headline = not self.in_headline 

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

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

2248 self.work_s = s 

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

2250 else: 

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

2252 attempts += 1 

2253 p = self._fnm_next_after_fail(p) 

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

2255 self.in_headline = self._fnm_first_search_pane() 

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

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

2258 self.work_s = s 

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

2260 return None, None, None 

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

2262 def _fnm_next_after_fail(self, p: Pos) -> Optional[Pos]: 

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

2264 # Move to the next position. 

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

2266 # Check it. 

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

2268 return None 

2269 if not p: # pragma: no cover 

2270 return None 

2271 return p 

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

2273 def _fail_outside_range(self, p: Pos) -> bool: # pragma: no cover 

2274 """ 

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

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

2277 """ 

2278 c = self.c 

2279 if not p: 

2280 return True 

2281 if self.node_only: 

2282 return True 

2283 if self.suboutline_only or self.file_only: 

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

2285 return True 

2286 if c.hoistStack: 

2287 bunch = c.hoistStack[-1] 

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

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

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

2291 return True 

2292 return False # Within range. 

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

2294 def _fnm_first_search_pane(self) -> bool: 

2295 """ 

2296 Set return the value of self.in_headline 

2297 indicating which pane to search first. 

2298 """ 

2299 if self.search_headline and self.search_body: 

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

2301 if self.reverse: 

2302 return False # Search the body pane first. 

2303 return True # Search the headline pane first. 

2304 if self.search_headline or self.search_body: 

2305 # Search the only enabled pane. 

2306 return self.search_headline 

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

2308 return False # pragma: no cover 

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

2310 def _fnm_search(self, p: Pos) -> Tuple[int, int]: 

2311 """ 

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

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

2314 """ 

2315 index = self.work_sel[2] 

2316 s = self.work_s 

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

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

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

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

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

2322 if not s: # pragma: no cover 

2323 return None, None 

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

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

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

2327 return None, None 

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

2329 return None, None 

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

2331 return None, None 

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

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

2334 return pos, newpos 

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

2336 def _fnm_should_stay_in_node(self, p: Pos) -> bool: 

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

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

2339 # Switch only if: 

2340 # a) searching both panes and, 

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

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

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

2344 return ( 

2345 self.search_headline and self.search_body and ( 

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

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

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

2349 def inner_search_helper(self, s: str, i: int, j: int, pattern: str) -> Tuple[int, int]: 

2350 """ 

2351 Dispatch the proper search method based on settings. 

2352 """ 

2353 backwards = self.reverse 

2354 nocase = self.ignore_case 

2355 regexp = self.pattern_match 

2356 word = self.whole_word 

2357 if backwards: 

2358 i, j = j, i 

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

2360 return -1, -1 

2361 if regexp: 

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

2363 elif backwards: 

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

2365 else: 

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

2367 return pos, newpos 

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

2369 def _inner_search_backward(self, 

2370 s: str, 

2371 i: int, 

2372 j: int, 

2373 pattern: str, 

2374 nocase: bool, 

2375 word: bool, 

2376 ) -> Tuple[int, int]: 

2377 """ 

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

2379 

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

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

2382 

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

2384 

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

2386 """ 

2387 if nocase: 

2388 s = s.lower() 

2389 pattern = pattern.lower() 

2390 pattern = self.replace_back_slashes(pattern) 

2391 n = len(pattern) 

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

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

2394 i = max(0, i) 

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

2396 # short circuit the search: helps debugging. 

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

2398 return -1, -1 

2399 if word: 

2400 while 1: 

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

2402 if k == -1: 

2403 break 

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

2405 return k, k + n 

2406 j = max(0, k - 1) 

2407 return -1, -1 

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

2409 if k == -1: 

2410 return -1, -1 

2411 return k, k + n 

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

2413 def _inner_search_match_word(self, s: str, i: int, pattern: str) -> bool: 

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

2415 pattern = self.replace_back_slashes(pattern) 

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

2417 return False 

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

2419 n = len(pattern) 

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

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

2422 isWordPat1 = g.isWordChar(pat1) 

2423 isWordPat2 = g.isWordChar(pat2) 

2424 isWordCh1 = g.isWordChar(ch1) 

2425 isWordCh2 = g.isWordChar(ch2) 

2426 inWord = isWordPat1 and isWordCh1 or isWordPat2 and isWordCh2 

2427 return not inWord 

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

2429 def _inner_search_plain(self, 

2430 s: str, 

2431 i: int, 

2432 j: int, 

2433 pattern: str, 

2434 nocase: bool, 

2435 word: bool, 

2436 ) -> Tuple[int, int]: 

2437 """Do a plain search.""" 

2438 if nocase: 

2439 s = s.lower() 

2440 pattern = pattern.lower() 

2441 pattern = self.replace_back_slashes(pattern) 

2442 n = len(pattern) 

2443 if word: 

2444 while 1: 

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

2446 if k == -1: 

2447 break 

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

2449 return k, k + n 

2450 i = k + n 

2451 return -1, -1 

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

2453 if k == -1: 

2454 return -1, -1 

2455 return k, k + n 

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

2457 def _inner_search_regex(self, 

2458 s: str, 

2459 i: int, 

2460 j: int, 

2461 pattern: str, 

2462 backwards: bool, 

2463 nocase: bool, 

2464 ) -> Tuple[int, int]: 

2465 """Called from inner_search_helper""" 

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

2467 if not re_obj: 

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

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

2470 return -1, -1 

2471 if backwards: 

2472 # Scan to the last match using search here. 

2473 i, last_mo = 0, None 

2474 while i < len(s): 

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

2476 if not mo: 

2477 break 

2478 i += 1 

2479 last_mo = mo 

2480 mo = last_mo 

2481 else: 

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

2483 if mo: 

2484 self.match_obj = mo 

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

2486 self.match_obj = None 

2487 return -1, -1 

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

2489 def make_regex_subs(self, change_text: str, groups: Any) -> str: 

2490 """ 

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

2492 

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

2494 """ 

2495 

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

2497 

2498 def repl(match_object: re.Match) -> str: 

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

2500 # # 1494... 

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

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

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

2504 return ( 

2505 groups[n]. 

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

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

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

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

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

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

2512 # No replacement. 

2513 return match_object.group(0) 

2514 

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

2516 return result 

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

2518 def precompile_pattern(self) -> bool: 

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

2520 try: # Precompile the regexp. 

2521 # pylint: disable=no-member 

2522 flags = re.MULTILINE 

2523 if self.ignore_case: 

2524 flags |= re.IGNORECASE 

2525 # Escape the search text. 

2526 # Ignore the whole_word option. 

2527 s = self.find_text 

2528 # A bad idea: insert \b automatically. 

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

2530 # if self.whole_word: 

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

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

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

2534 return True 

2535 except Exception: 

2536 if not g.unitTesting: 

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

2538 return False 

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

2540 def replace_back_slashes(self, s: str) -> str: 

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

2542 # This is NOT the same as: 

2543 # 

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

2545 # 

2546 # because there is no rescanning. 

2547 i = 0 

2548 while i + 1 < len(s): 

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

2550 ch = s[i + 1] 

2551 if ch == '\\': 

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

2553 elif ch == 'n': 

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

2555 elif ch == 't': 

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

2557 else: 

2558 i += 1 # Skip the escaped character. 

2559 i += 1 

2560 return s 

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

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

2563 def init_in_headline(self) -> None: 

2564 """ 

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

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

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

2568 """ 

2569 # #1228458: Inconsistency between Find-forward and Find-backward. 

2570 if self.search_headline and self.search_body: 

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

2572 self.in_headline = self.focus_in_tree() 

2573 else: 

2574 self.in_headline = self.search_headline 

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

2576 def focus_in_tree(self) -> bool: 

2577 """ 

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

2579 

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

2581 """ 

2582 c = self.c 

2583 ftm = self.ftm 

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

2585 if ftm: 

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

2587 w_name = c.widget_name(w) 

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

2589 val = False 

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

2591 val = True 

2592 else: 

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

2594 return val 

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

2596 def restore(self, data: Any) -> None: 

2597 """ 

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

2599 """ 

2600 c, p = self.c, data.p 

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

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

2603 # Better than selecting the root! 

2604 return 

2605 c.selectPosition(p) 

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

2607 if self.in_headline: 

2608 c.treeWantsFocus() 

2609 else: 

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

2611 w = c.frame.body.wrapper 

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

2613 w.seeInsertPoint() 

2614 c.widgetWantsFocus(w) 

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

2616 def save(self) -> Any: 

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

2618 c = self.c 

2619 if self.in_headline: # pragma: no cover 

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

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

2622 insert, start, end = None, None, None 

2623 else: 

2624 w = c.frame.body.wrapper 

2625 insert = w.getInsertPoint() 

2626 start, end = w.getSelectionRange() 

2627 data = g.Bunch( 

2628 end=end, 

2629 in_headline=self.in_headline, 

2630 insert=insert, 

2631 p=c.p.copy(), 

2632 start=start, 

2633 ) 

2634 return data 

2635 #@+node:ekr.20031218072017.3091: *4* find.show_success 

2636 def show_success(self, p: Pos, pos: int, newpos: int, showState: bool=True) -> Wrapper: 

2637 """Display the result of a successful find operation.""" 

2638 c = self.c 

2639 # Set state vars. 

2640 # Ensure progress in backwards searches. 

2641 insert = min(pos, newpos) if self.reverse else max(pos, newpos) 

2642 if c.sparse_find: # pragma: no cover 

2643 c.expandOnlyAncestorsOfNode(p=p) 

2644 if self.in_headline: 

2645 c.endEditing() 

2646 c.redraw(p) 

2647 c.frame.tree.editLabel(p) 

2648 w = c.edit_widget(p) # #2220 

2649 if w: 

2650 w.setSelectionRange(pos, newpos, insert) # #2220 

2651 else: 

2652 # Tricky code. Do not change without careful thought. 

2653 w = c.frame.body.wrapper 

2654 # *Always* do the full selection logic. 

2655 # This ensures that the body text is inited and recolored. 

2656 c.selectPosition(p) 

2657 c.bodyWantsFocus() 

2658 if showState: 

2659 c.k.showStateAndMode(w) 

2660 c.bodyWantsFocusNow() 

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

2662 k = g.see_more_lines(w.getAllText(), insert, 4) 

2663 w.see(k) # #78: find-next match not always scrolled into view. 

2664 c.outerUpdate() # Set the focus immediately. 

2665 if c.vim_mode and c.vimCommands: # pragma: no cover 

2666 c.vimCommands.update_selection_after_search() 

2667 # Support for the console gui. 

2668 if hasattr(g.app.gui, 'show_find_success'): # pragma: no cover 

2669 g.app.gui.show_find_success(c, self.in_headline, insert, p) 

2670 c.frame.bringToFront() 

2671 return w # Support for isearch. 

2672 #@+node:ekr.20131117164142.16939: *3* LeoFind.ISearch 

2673 #@+node:ekr.20210112192011.1: *4* LeoFind.Isearch commands 

2674 #@+node:ekr.20131117164142.16941: *5* find.isearch_forward 

2675 @cmd('isearch-forward') 

2676 def isearch_forward(self, event: Event) -> None: # pragma: no cover (cmd) 

2677 """ 

2678 Begin a forward incremental search. 

2679 

2680 - Plain characters extend the search. 

2681 - !<isearch-forward>! repeats the search. 

2682 - Esc or any non-plain key ends the search. 

2683 - Backspace reverses the search. 

2684 - Backspacing to an empty search pattern 

2685 completely undoes the effect of the search. 

2686 """ 

2687 self.start_incremental(event, 'isearch-forward', 

2688 forward=True, ignoreCase=False, regexp=False) 

2689 #@+node:ekr.20131117164142.16942: *5* find.isearch_backward 

2690 @cmd('isearch-backward') 

2691 def isearch_backward(self, event: Event) -> None: # pragma: no cover (cmd) 

2692 """ 

2693 Begin a backward incremental search. 

2694 

2695 - Plain characters extend the search backward. 

2696 - !<isearch-forward>! repeats the search. 

2697 - Esc or any non-plain key ends the search. 

2698 - Backspace reverses the search. 

2699 - Backspacing to an empty search pattern 

2700 completely undoes the effect of the search. 

2701 """ 

2702 self.start_incremental(event, 'isearch-backward', 

2703 forward=False, ignoreCase=False, regexp=False) 

2704 #@+node:ekr.20131117164142.16943: *5* find.isearch_forward_regexp 

2705 @cmd('isearch-forward-regexp') 

2706 def isearch_forward_regexp(self, event: Event) -> None: # pragma: no cover (cmd) 

2707 """ 

2708 Begin a forward incremental regexp search. 

2709 

2710 - Plain characters extend the search. 

2711 - !<isearch-forward-regexp>! repeats the search. 

2712 - Esc or any non-plain key ends the search. 

2713 - Backspace reverses the search. 

2714 - Backspacing to an empty search pattern 

2715 completely undoes the effect of the search. 

2716 """ 

2717 self.start_incremental(event, 'isearch-forward-regexp', 

2718 forward=True, ignoreCase=False, regexp=True) 

2719 #@+node:ekr.20131117164142.16944: *5* find.isearch_backward_regexp 

2720 @cmd('isearch-backward-regexp') 

2721 def isearch_backward_regexp(self, event: Event) -> None: # pragma: no cover (cmd) 

2722 """ 

2723 Begin a backward incremental regexp search. 

2724 

2725 - Plain characters extend the search. 

2726 - !<isearch-forward-regexp>! repeats the search. 

2727 - Esc or any non-plain key ends the search. 

2728 - Backspace reverses the search. 

2729 - Backspacing to an empty search pattern 

2730 completely undoes the effect of the search. 

2731 """ 

2732 self.start_incremental(event, 'isearch-backward-regexp', 

2733 forward=False, ignoreCase=False, regexp=True) 

2734 #@+node:ekr.20131117164142.16945: *5* find.isearch_with_present_options 

2735 @cmd('isearch-with-present-options') 

2736 def isearch_with_present_options(self, event: Event) -> None: # pragma: no cover (cmd) 

2737 """ 

2738 Begin an incremental search using find panel options. 

2739 

2740 - Plain characters extend the search. 

2741 - !<isearch-forward-regexp>! repeats the search. 

2742 - Esc or any non-plain key ends the search. 

2743 - Backspace reverses the search. 

2744 - Backspacing to an empty search pattern 

2745 completely undoes the effect of the search. 

2746 """ 

2747 self.start_incremental(event, 'isearch-with-present-options', 

2748 forward=None, ignoreCase=None, regexp=None) 

2749 #@+node:ekr.20131117164142.16946: *4* LeoFind.Isearch utils 

2750 #@+node:ekr.20131117164142.16947: *5* find.abort_search (incremental) 

2751 def abort_search(self) -> None: # pragma: no cover (cmd) 

2752 """Restore the original position and selection.""" 

2753 c, k = self.c, self.k 

2754 w = c.frame.body.wrapper 

2755 k.clearState() 

2756 k.resetLabel() 

2757 p, i, j, in_headline = self.stack[0] 

2758 self.in_headline = in_headline 

2759 c.selectPosition(p) 

2760 c.redraw_after_select(p) 

2761 c.bodyWantsFocus() 

2762 w.setSelectionRange(i, j) 

2763 #@+node:ekr.20131117164142.16948: *5* find.end_search 

2764 def end_search(self) -> None: # pragma: no cover (cmd) 

2765 c, k = self.c, self.k 

2766 k.clearState() 

2767 k.resetLabel() 

2768 c.bodyWantsFocus() 

2769 #@+node:ekr.20131117164142.16949: *5* find.iSearch_helper 

2770 def iSearch_helper(self, again: bool=False) -> None: # pragma: no cover (cmd) 

2771 """Handle the actual incremental search.""" 

2772 c, k, p = self.c, self.k, self.c.p 

2773 reverse = not self.isearch_forward_flag 

2774 pattern = k.getLabel(ignorePrompt=True) 

2775 if not pattern: 

2776 self.abort_search() 

2777 return 

2778 # Settings... 

2779 self.find_text = self.ftm.get_find_text() 

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

2781 # Save 

2782 oldPattern = self.find_text 

2783 oldRegexp = self.pattern_match 

2784 oldWord = self.whole_word 

2785 # Override 

2786 self.pattern_match = self.isearch_regexp 

2787 self.reverse = reverse 

2788 self.find_text = pattern 

2789 self.whole_word = False # Word option can't be used! 

2790 # Prepare the search. 

2791 if len(self.stack) <= 1: 

2792 self.in_headline = False 

2793 # Init the work widget from the gui widget. 

2794 gui_w = self.set_widget() 

2795 s = gui_w.getAllText() 

2796 i, j = gui_w.getSelectionRange() 

2797 if again: 

2798 ins = i if reverse else j + len(pattern) 

2799 else: 

2800 ins = j + len(pattern) if reverse else i 

2801 self.work_s = s 

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

2803 # Do the search! 

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

2805 # Restore. 

2806 self.find_text = oldPattern 

2807 self.pattern_match = oldRegexp 

2808 self.reverse = False 

2809 self.whole_word = oldWord 

2810 # Handle the results of the search. 

2811 if pos is not None: # success. 

2812 w = self.show_success(p, pos, newpos, showState=False) 

2813 if w: 

2814 i, j = w.getSelectionRange(sort=False) 

2815 if not again: 

2816 self.push(c.p, i, j, self.in_headline) 

2817 else: 

2818 g.es(f"not found: {pattern}") 

2819 if not again: 

2820 event = g.app.gui.create_key_event( 

2821 c, binding='BackSpace', char='\b', w=w) 

2822 k.updateLabel(event) 

2823 #@+node:ekr.20131117164142.16950: *5* find.isearch_state_handler 

2824 def isearch_state_handler(self, event: Event) -> None: # pragma: no cover (cmd) 

2825 """The state manager when the state is 'isearch""" 

2826 # c = self.c 

2827 k = self.k 

2828 stroke = event.stroke if event else None 

2829 s = stroke.s if stroke else '' 

2830 # No need to recognize ctrl-z. 

2831 if s in ('Escape', '\n', 'Return'): 

2832 self.end_search() 

2833 elif stroke in self.iSearchStrokes: 

2834 self.iSearch_helper(again=True) 

2835 elif s in ('\b', 'BackSpace'): 

2836 k.updateLabel(event) 

2837 self.isearch_backspace() 

2838 elif ( 

2839 s.startswith('Ctrl+') or 

2840 s.startswith('Alt+') or 

2841 k.isFKey(s) # 2011/06/13. 

2842 ): 

2843 # End the search. 

2844 self.end_search() 

2845 k.masterKeyHandler(event) 

2846 # Fix bug 1267921: isearch-forward accepts non-alphanumeric keys as input. 

2847 elif k.isPlainKey(stroke): 

2848 k.updateLabel(event) 

2849 self.iSearch_helper() 

2850 #@+node:ekr.20131117164142.16951: *5* find.isearch_backspace 

2851 def isearch_backspace(self) -> None: # pragma: no cover (cmd) 

2852 

2853 c = self.c 

2854 if len(self.stack) <= 1: 

2855 self.abort_search() 

2856 return 

2857 # Reduce the stack by net 1. 

2858 self.pop() 

2859 p, i, j, in_headline = self.pop() 

2860 self.push(p, i, j, in_headline) 

2861 if in_headline: 

2862 # Like self.show_success. 

2863 selection = i, j, i 

2864 c.redrawAndEdit(p, selectAll=False, 

2865 selection=selection, 

2866 keepMinibuffer=True) 

2867 else: 

2868 c.selectPosition(p) 

2869 w = c.frame.body.wrapper 

2870 c.bodyWantsFocus() 

2871 if i > j: 

2872 i, j = j, i 

2873 w.setSelectionRange(i, j) 

2874 if len(self.stack) <= 1: 

2875 self.abort_search() 

2876 #@+node:ekr.20131117164142.16952: *5* find.get_strokes 

2877 def get_strokes(self, commandName: str) -> List[str]: # pragma: no cover (cmd) 

2878 aList = self.inverseBindingDict.get(commandName, []) 

2879 return [key for pane, key in aList] 

2880 #@+node:ekr.20131117164142.16953: *5* find.push & pop 

2881 def push(self, p: Pos, i: int, j: int, in_headline: bool) -> None: # pragma: no cover (cmd) 

2882 data = p.copy(), i, j, in_headline 

2883 self.stack.append(data) 

2884 

2885 def pop(self) -> Tuple[Pos, int, int, bool]: # pragma: no cover (cmd) 

2886 data = self.stack.pop() 

2887 p, i, j, in_headline = data 

2888 return p, i, j, in_headline 

2889 #@+node:ekr.20131117164142.16954: *5* find.set_widget 

2890 def set_widget(self) -> Wrapper: # pragma: no cover (cmd) 

2891 c, p = self.c, self.c.p 

2892 wrapper = c.frame.body.wrapper 

2893 if self.in_headline: 

2894 w = c.edit_widget(p) 

2895 if not w: 

2896 # Selecting the minibuffer can kill the edit widget. 

2897 selection = 0, 0, 0 

2898 c.redrawAndEdit(p, selectAll=False, 

2899 selection=selection, keepMinibuffer=True) 

2900 w = c.edit_widget(p) 

2901 if not w: # Should never happen. 

2902 g.trace('**** no edit widget!') 

2903 self.in_headline = False 

2904 w = wrapper 

2905 else: 

2906 w = wrapper 

2907 if w == wrapper: 

2908 c.bodyWantsFocus() 

2909 return w 

2910 #@+node:ekr.20131117164142.16955: *5* find.start_incremental 

2911 def start_incremental(self, 

2912 event: Event, 

2913 commandName: str, 

2914 forward: bool, 

2915 ignoreCase: bool, 

2916 regexp: bool, 

2917 ) -> None: # pragma: no cover (cmd) 

2918 c, k = self.c, self.k 

2919 # None is a signal to get the option from the find tab. 

2920 self.event = event 

2921 self.isearch_forward_flag = not self.reverse if forward is None else forward 

2922 self.isearch_ignore_case = self.ignore_case if ignoreCase is None else ignoreCase 

2923 self.isearch_regexp = self.pattern_match if regexp is None else regexp 

2924 # Note: the word option can't be used with isearches! 

2925 w = c.frame.body.wrapper 

2926 self.p1 = c.p 

2927 self.sel1 = w.getSelectionRange(sort=False) 

2928 i, j = self.sel1 

2929 self.push(c.p, i, j, self.in_headline) 

2930 self.inverseBindingDict = k.computeInverseBindingDict() 

2931 self.iSearchStrokes = self.get_strokes(commandName) 

2932 k.setLabelBlue( 

2933 "Isearch" 

2934 f"{' Backward' if not self.isearch_forward_flag else ''}" 

2935 f"{' Regexp' if self.isearch_regexp else ''}" 

2936 f"{' NoCase' if self.isearch_ignore_case else ''}" 

2937 ": " 

2938 ) 

2939 k.setState('isearch', 1, handler=self.isearch_state_handler) 

2940 c.minibufferWantsFocus() 

2941 #@+node:ekr.20031218072017.3067: *3* LeoFind.Utils 

2942 #@+node:ekr.20131117164142.16992: *4* find.add_change_string_to_label 

2943 def add_change_string_to_label(self) -> None: # pragma: no cover (cmd) 

2944 """Add an unprotected change string to the minibuffer label.""" 

2945 c = self.c 

2946 s = self.ftm.get_change_text() 

2947 c.minibufferWantsFocus() 

2948 while s.endswith('\n') or s.endswith('\r'): 

2949 s = s[:-1] 

2950 c.k.extendLabel(s, select=True, protect=False) 

2951 #@+node:ekr.20131117164142.16993: *4* find.add_find_string_to_label 

2952 def add_find_string_to_label(self, protect: bool=True) -> None: # pragma: no cover (cmd) 

2953 c, k = self.c, self.c.k 

2954 ftm = c.findCommands.ftm 

2955 s = ftm.get_find_text() 

2956 c.minibufferWantsFocus() 

2957 while s.endswith('\n') or s.endswith('\r'): 

2958 s = s[:-1] 

2959 k.extendLabel(s, select=True, protect=protect) 

2960 #@+node:ekr.20210110073117.33: *4* find.compute_result_status 

2961 def compute_result_status(self, find_all_flag: bool=False) -> str: # pragma: no cover (cmd) 

2962 """Return the status to be shown in the status line after a find command completes.""" 

2963 # Too similar to another method... 

2964 status = [] 

2965 table = ( 

2966 ('whole_word', 'Word'), 

2967 ('ignore_case', 'Ignore Case'), 

2968 ('pattern_match', 'Regex'), 

2969 ('suboutline_only', '[Outline Only]'), 

2970 ('node_only', '[Node Only]'), 

2971 ('search_headline', 'Head'), 

2972 ('search_body', 'Body'), 

2973 ) 

2974 for ivar, val in table: 

2975 if getattr(self, ivar): 

2976 status.append(val) 

2977 return f" ({', '.join(status)})" if status else '' 

2978 #@+node:ekr.20131119204029.16479: *4* find.help_for_find_commands 

2979 def help_for_find_commands(self, event: Event=None) -> None: # pragma: no cover (cmd) 

2980 """Called from Find panel. Redirect.""" 

2981 self.c.helpCommands.help_for_find_commands(event) 

2982 #@+node:ekr.20210111082524.1: *4* find.init_vim_search 

2983 def init_vim_search(self, pattern: str) -> None: # pragma: no cover (cmd) 

2984 """Initialize searches in vim mode.""" 

2985 c = self.c 

2986 if c.vim_mode and c.vimCommands: 

2987 c.vimCommands.update_dot_before_search( 

2988 find_pattern=pattern, 

2989 change_pattern=None) # A flag. 

2990 #@+node:ekr.20150629072547.1: *4* find.preload_find_pattern 

2991 def preload_find_pattern(self, w: Wrapper) -> None: # pragma: no cover (cmd) 

2992 """Preload the find pattern from the selected text of widget w.""" 

2993 c, ftm = self.c, self.ftm 

2994 if not c.config.getBool('preload-find-pattern', default=False): 

2995 # Make *sure* we don't preload the find pattern if it is not wanted. 

2996 return 

2997 if not w: 

2998 return 

2999 # 

3000 # #1436: Don't create a selection if there isn't one. 

3001 # Leave the search pattern alone! 

3002 # 

3003 # if not w.hasSelection(): 

3004 # c.editCommands.extendToWord(event=None, select=True, w=w) 

3005 # 

3006 # #177: Use selected text as the find string. 

3007 # #1436: Make make sure there is a significant search pattern. 

3008 s = w.getSelectedText() 

3009 if s.strip(): 

3010 ftm.set_find_text(s) 

3011 ftm.init_focus() 

3012 #@+node:ekr.20150619070602.1: *4* find.show_status 

3013 def show_status(self, found: bool) -> None: 

3014 """Show the find status the Find dialog, if present, and the status line.""" 

3015 c = self.c 

3016 status = 'found' if found else 'not found' 

3017 options = self.compute_result_status() 

3018 s = f"{status}:{options} {self.find_text}" 

3019 # Set colors. 

3020 found_bg = c.config.getColor('find-found-bg') or 'blue' 

3021 not_found_bg = c.config.getColor('find-not-found-bg') or 'red' 

3022 found_fg = c.config.getColor('find-found-fg') or 'white' 

3023 not_found_fg = c.config.getColor('find-not-found-fg') or 'white' 

3024 bg = found_bg if found else not_found_bg 

3025 fg = found_fg if found else not_found_fg 

3026 if c.config.getBool("show-find-result-in-status") is not False: 

3027 c.frame.putStatusLine(s, bg=bg, fg=fg) 

3028 #@+node:ekr.20150615174549.1: *4* find.show_find_options_in_status_area & helper 

3029 def show_find_options_in_status_area(self) -> None: # pragma: no cover (cmd) 

3030 """Show find options in the status area.""" 

3031 c = self.c 

3032 s = self.compute_find_options_in_status_area() 

3033 c.frame.putStatusLine(s) 

3034 #@+node:ekr.20171129211238.1: *5* find.compute_find_options_in_status_area 

3035 def compute_find_options_in_status_area(self) -> str: 

3036 c = self.c 

3037 ftm = c.findCommands.ftm 

3038 table = ( 

3039 ('Word', ftm.check_box_whole_word), 

3040 ('Ig-case', ftm.check_box_ignore_case), 

3041 ('regeXp', ftm.check_box_regexp), 

3042 ('Body', ftm.check_box_search_body), 

3043 ('Head', ftm.check_box_search_headline), 

3044 # ('wrap-Around', ftm.check_box_wrap_around), 

3045 ('mark-Changes', ftm.check_box_mark_changes), 

3046 ('mark-Finds', ftm.check_box_mark_finds), 

3047 ) 

3048 result = [option for option, ivar in table if ivar.isChecked()] 

3049 table2 = ( 

3050 ('Suboutline', ftm.radio_button_suboutline_only), 

3051 ('Node', ftm.radio_button_node_only), 

3052 ('File', ftm.radio_button_file_only), 

3053 ) 

3054 for option, ivar in table2: 

3055 if ivar.isChecked(): 

3056 result.append(f"[{option}]") 

3057 break 

3058 return f"Find: {' '.join(result)}" 

3059 #@+node:ekr.20131117164142.17007: *4* find.start_state_machine 

3060 def start_state_machine(self, 

3061 event: Event, 

3062 prefix: str, 

3063 handler: Callable, 

3064 escape_handler: Callable=None, 

3065 ) -> None: # pragma: no cover (cmd) 

3066 """ 

3067 Initialize and start the state machine used to get user arguments. 

3068 """ 

3069 c, k = self.c, self.k 

3070 w = c.frame.body.wrapper 

3071 if not w: 

3072 return 

3073 # Gui... 

3074 k.setLabelBlue(prefix) 

3075 # New in Leo 5.2: minibuffer modes shows options in status area. 

3076 if self.minibuffer_mode: 

3077 self.show_find_options_in_status_area() 

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

3079 g.app.gui.openFindDialog(c) 

3080 else: 

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

3082 self.add_find_string_to_label(protect=False) 

3083 k.getArgEscapes = ['\t'] if escape_handler else [] 

3084 self.handler = handler 

3085 self.escape_handler = escape_handler 

3086 # Start the state maching! 

3087 k.get1Arg(event, handler=self.state0, tabList=self.findTextList, completion=True) 

3088 

3089 def state0(self, event: Event) -> None: # pragma: no cover (cmd) 

3090 """Dispatch the next handler.""" 

3091 k = self.k 

3092 if k.getArgEscapeFlag: 

3093 k.getArgEscapeFlag = False 

3094 self.escape_handler(event) 

3095 else: 

3096 self.handler(event) 

3097 #@+node:ekr.20131117164142.17008: *4* find.updateChange/FindList 

3098 def update_change_list(self, s: str) -> None: # pragma: no cover (cmd) 

3099 if s not in self.changeTextList: 

3100 self.changeTextList.append(s) 

3101 

3102 def update_find_list(self, s: str) -> None: # pragma: no cover (cmd) 

3103 if s not in self.findTextList: 

3104 self.findTextList.append(s) 

3105 #@-others 

3106#@-others 

3107#@@language python 

3108#@@tabwidth -4 

3109#@@pagewidth 70 

3110#@-leo