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
« 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 >>
80def cmd(name: str) -> Callable:
81 """Command decorator for the findCommands class."""
82 return g.new_cmd_decorator(name, ['c', 'findCommands',])
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.
191 This should be called from the do_ methods as follows:
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."""
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.
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.
241 Example:
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:
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.
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.
360 Example 1, settings is a g.Bunch:
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 )
373 Example 2, settings is a python dict:
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.
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
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.
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)
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.
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.
458 This is a stand-alone method for unit testing.
459 """
460 c = self.c
462 def isMarked(p: Pos) -> bool:
463 return p.isMarked()
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)
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)
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:
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.
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)
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)
759 def do_find_next(self, settings: Settings) -> Tuple[Pos, int, int]:
760 """
761 Find the next instance of self.find_text.
763 Return True (for vim-mode) if a match was found.
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.
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)
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)
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')
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')
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')
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')
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)
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')
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')
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')
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')
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')
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')
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')
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')
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 )
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)
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
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.
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.
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, []
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.
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, []
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).
1387 Create an organizer node whose descendants contain clones of all nodes
1388 matching the search string, except @nosearch trees.
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)
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.
1426 Return the count of found nodes.
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).
1442 Create an organizer node whose direct children are clones of all nodes
1443 matching the search string, except @nosearch trees.
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)
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.
1482 Return the count of found nodes.
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).
1498 Create an organizer node whose descendants contain clones of all
1499 nodes matching the given tag, except @nosearch trees.
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)
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.
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 )
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)
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)
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
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."""
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.
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 )
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)
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)
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 )
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.
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
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.
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)
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)
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")
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.
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.
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]])
2380 Return the highest index in the string where substring sub is found,
2381 such that sub is contained within s[start,end].
2383 Optional arguments start and end are interpreted as in slice notation.
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.
2493 Groups is a tuple of strings, one for every matched group.
2494 """
2496 # g.printObj(list(groups), tag=f"groups in {change_text!r}")
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)
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.
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.
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.
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.
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.
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.
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)
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)
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)
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)
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