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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#@+leo-ver=5-thin
2#@+node:ekr.20060123151617: * @file leoFind.py
3"""Leo's gui-independent find classes."""
4import keyword
5import re
6import sys
7import time
8from leo.core import leoGlobals as g
10#@+<< Theory of operation of find/change >>
11#@+node:ekr.20031218072017.2414: ** << Theory of operation of find/change >>
12#@@language rest
13#@@nosearch
14#@+at
15# LeoFind.py contains the gui-independant part of all of Leo's
16# find/change code. Such code is tricky, which is why it should be
17# gui-independent code! Here are the governing principles:
18#
19# 1. Find and Change commands initialize themselves using only the state
20# of the present Leo window. In particular, the Find class must not
21# save internal state information from one invocation to the next.
22# This means that when the user changes the nodes, or selects new
23# text in headline or body text, those changes will affect the next
24# invocation of any Find or Change command. Failure to follow this
25# principle caused all kinds of problems earlier versions.
26#
27# This principle simplifies the code because most ivars do not
28# persist. However, each command must ensure that the Leo window is
29# left in a state suitable for restarting the incremental
30# (interactive) Find and Change commands. Details of initialization
31# are discussed below.
32#
33# 2. The Find and Change commands must not change the state of the
34# outline or body pane during execution. That would cause severe
35# flashing and slow down the commands a great deal. In particular,
36# c.selectPosition and c.editPosition must not be called while
37# looking for matches.
38#
39# 3. When incremental Find or Change commands succeed they must leave
40# the Leo window in the proper state to execute another incremental
41# command. We restore the Leo window as it was on entry whenever an
42# incremental search fails and after any Find All and Replace All
43# command. Initialization involves setting the self.c, self.v,
44# self.in_headline, self.wrapping and self.s_text ivars.
45#
46# Setting self.in_headline is tricky; we must be sure to retain the
47# state of the outline pane until initialization is complete.
48# Initializing the Find All and Replace All commands is much easier
49# because such initialization does not depend on the state of the Leo
50# window. Using the same kind of text widget for both headlines and body
51# text results in a huge simplification of the code.
52#
53# The searching code does not know whether it is searching headline or
54# body text. The search code knows only that self.s_text is a text
55# widget that contains the text to be searched or changed and the insert
56# and sel attributes of self.search_text indicate the range of text to
57# be searched.
58#
59# Searching headline and body text simultaneously is complicated. The
60# find_next_match() method and its helpers handle the many details
61# involved by setting self.s_text and its insert and sel attributes.
62#@-<< Theory of operation of find/change >>
64def cmd(name):
65 """Command decorator for the findCommands class."""
66 return g.new_cmd_decorator(name, ['c', 'findCommands',])
68#@+others
69#@+node:ekr.20061212084717: ** class LeoFind (LeoFind.py)
70class LeoFind:
71 """The base class for Leo's Find commands."""
72 #@+others
73 #@+node:ekr.20131117164142.17021: *3* LeoFind.birth
74 #@+node:ekr.20031218072017.3053: *4* find.__init__
75 def __init__(self, c):
76 """Ctor for LeoFind class."""
77 self.c = c
78 self.expert_mode = False # Set in finishCreate.
79 self.ftm = None # Created by dw.createFindTab.
80 self.frame = None
81 self.k = c.k
82 self.re_obj = None
83 #
84 # The work "widget".
85 self.work_s = '' # p.b or p.c.
86 self.work_sel = (0, 0, 0) # pos, newpos, insert.
87 #
88 # Options ivars: set by FindTabManager.init.
89 self.ignore_case = None
90 self.node_only = None
91 self.pattern_match = None
92 self.search_headline = None
93 self.search_body = None
94 self.suboutline_only = None
95 self.mark_changes = None
96 self.mark_finds = None
97 self.whole_word = None
98 #
99 # For isearch commands...
100 self.stack = [] # Entries are (p, sel)
101 self.isearch_ignore_case = None
102 self.isearch_forward_flag = None
103 self.isearch_regexp = None
104 self.findTextList = []
105 self.changeTextList = []
106 #
107 # For find/change...
108 self.find_text = ""
109 self.change_text = ""
110 #
111 # State machine...
112 self.escape_handler = None
113 self.handler = None
114 # "Delayed" requests for do_find_next.
115 self.request_reverse = False
116 self.request_pattern_match = False
117 self.request_whole_word = False
118 # Internal state...
119 self.changeAllFlag = False
120 self.findAllUniqueFlag = False
121 self.find_def_data = None
122 self.in_headline = False
123 self.match_obj = None
124 self.reverse = False
125 self.root = None # The start of the search, especially for suboutline-only.
126 self.unique_matches = set()
127 #
128 # User settings.
129 self.minibuffer_mode = None
130 self.reload_settings()
131 #@+node:ekr.20210110073117.6: *4* find.default_settings
132 def default_settings(self):
133 """Return a dict representing all default settings."""
134 c = self.c
135 return g.Bunch(
136 # State...
137 in_headline=False,
138 p=c.rootPosition(),
139 # Find/change strings...
140 find_text='',
141 change_text='',
142 # Find options...
143 ignore_case=False,
144 mark_changes=False,
145 mark_finds=False,
146 node_only=False,
147 pattern_match=False,
148 reverse=False,
149 search_body=True,
150 search_headline=True,
151 suboutline_only=False,
152 whole_word=False,
153 wrapping=False,
154 )
155 #@+node:ekr.20131117164142.17022: *4* find.finishCreate
156 def finishCreate(self):
157 # New in 4.11.1.
158 # Must be called when config settings are valid.
159 c = self.c
160 self.reload_settings()
161 # now that configuration settings are valid,
162 # we can finish creating the Find pane.
163 dw = c.frame.top
164 if dw:
165 dw.finishCreateLogPane()
166 #@+node:ekr.20210110073117.4: *4* find.init_ivars_from_settings
167 def init_ivars_from_settings(self, settings):
168 """
169 Initialize all ivars from settings, including required defaults.
171 This should be called from the do_ methods as follows:
173 self.init_ivars_from_settings(settings)
174 if not self.check_args('find-next'):
175 return <appropriate error indication>
176 """
177 #
178 # Init required defaults.
179 self.reverse = False
180 #
181 # Init find/change strings.
182 self.change_text = settings.change_text
183 self.find_text = settings.find_text
184 #
185 # Init find options.
186 self.ignore_case = settings.ignore_case
187 self.mark_changes = settings.mark_changes
188 self.mark_finds = settings.mark_finds
189 self.node_only = settings.node_only
190 self.pattern_match = settings.pattern_match
191 self.search_body = settings.search_body
192 self.search_headline = settings.search_headline
193 self.suboutline_only = settings.suboutline_only
194 self.whole_word = settings.whole_word
195 # self.wrapping = settings.wrapping
196 #@+node:ekr.20210110073117.5: *5* NEW:find.init_settings
197 def init_settings(self, settings):
198 """Initialize all user settings."""
200 #@+node:ekr.20171113164709.1: *4* find.reload_settings
201 def reload_settings(self):
202 """LeoFind.reload_settings."""
203 c = self.c
204 self.minibuffer_mode = c.config.getBool('minibuffer-find-mode', default=False)
205 self.reverse_find_defs = c.config.getBool('reverse-find-defs', default=False)
206 #@+node:ekr.20210108053422.1: *3* find.batch_change (script helper) & helpers
207 def batch_change(self, root, replacements, settings=None):
208 #@+<< docstring: find.batch_change >>
209 #@+node:ekr.20210925161347.1: *4* << docstring: find.batch_change >>
210 """
211 Support batch change scripts.
213 replacement: a list of tuples (find_string, change_string).
214 settings: a dict or g.Bunch containing find/change settings.
215 See find._init_from_dict for a list of valid settings.
217 Example:
219 h = '@file src/ekr/coreFind.py'
220 root = g.findNodeAnywhere(c, h)
221 assert root
222 replacements = (
223 ('clone_find_all', 'do_clone_find_all'),
224 ('clone_find_all_flattened', 'do_clone_find_all_flattened'),
225 )
226 settings = dict(suboutline_only=True)
227 count = c.findCommands.batch_change(root, replacements, settings)
228 if count:
229 c.save()
230 """
231 #@-<< docstring: find.batch_change >>
232 try:
233 self._init_from_dict(settings or {})
234 count = 0
235 for find, change in replacements:
236 count += self._batch_change_helper(root, find, change)
237 return count
238 except Exception:
239 g.es_exception()
240 return 0
241 #@+node:ekr.20210108070948.1: *4* find._batch_change_helper
242 def _batch_change_helper(self, p, find_text, change_text):
244 c, p1, u = self.c, p.copy(), self.c.undoer
245 undoType = 'Batch Change All'
246 # Check...
247 if not find_text: # pragma: no cover
248 return 0
249 if not self.search_headline and not self.search_body:
250 return 0 # pragma: no cover
251 if self.pattern_match:
252 ok = self.precompile_pattern()
253 if not ok: # pragma: no cover
254 return 0
255 # Init...
256 self.find_text = find_text
257 self.change_text = self.replace_back_slashes(change_text)
258 if self.node_only:
259 positions = [p1]
260 elif self.suboutline_only:
261 positions = p1.self_and_subtree()
262 else:
263 positions = c.all_unique_positions()
264 # Init the work widget.
265 s = p.h if self.in_headline else p.b
266 self.work_s = s
267 self.work_sel = (0, 0, 0)
268 # The main loop.
269 u.beforeChangeGroup(p1, undoType)
270 count = 0
271 for p in positions:
272 count_h, count_b = 0, 0
273 undoData = u.beforeChangeNodeContents(p)
274 if self.search_headline:
275 count_h, new_h = self._change_all_search_and_replace(p.h)
276 if count_h:
277 count += count_h
278 p.h = new_h
279 if self.search_body:
280 count_b, new_b = self._change_all_search_and_replace(p.b)
281 if count_b:
282 count += count_b
283 p.b = new_b
284 if count_h or count_b:
285 u.afterChangeNodeContents(p1, 'Replace All', undoData)
286 u.afterChangeGroup(p1, undoType, reportFlag=True)
287 if not g.unitTesting: # pragma: no cover
288 print(f"{count:3}: {find_text:>30} => {change_text}")
289 return count
290 #@+node:ekr.20210108083003.1: *4* find._init_from_dict
291 def _init_from_dict(self, settings):
292 """Initialize ivars from settings (a dict or g.Bunch)."""
293 # The valid ivars and reasonable defaults.
294 valid = dict(
295 ignore_case=False,
296 node_only=False,
297 pattern_match=False,
298 search_body=True,
299 search_headline=True,
300 suboutline_only=False, # Seems safest. # Was True !!!
301 whole_word=True,
302 )
303 # Set ivars to reasonable defaults.
304 for ivar in valid:
305 setattr(self, ivar, valid.get(ivar))
306 # Override ivars from settings.
307 errors = 0
308 for ivar in settings.keys():
309 if ivar in valid:
310 val = settings.get(ivar)
311 if val in (True, False):
312 setattr(self, ivar, val)
313 else: # pragma: no cover
314 g.trace("bad value: {ivar!r} = {val!r}")
315 errors += 1
316 else: # pragma: no cover
317 g.trace(f"ignoring {ivar!r} setting")
318 errors += 1
319 if errors: # pragma: no cover
320 g.printObj(sorted(valid.keys()), tag='valid keys')
321 #@+node:ekr.20210925161148.1: *3* find.interactive_search_helper
322 def interactive_search_helper(self, root=None, settings=None):
323 #@+<< docstring: find.interactive_search >>
324 #@+node:ekr.20210925161451.1: *4* << docstring: find.interactive_search >>
325 """
326 Support interactive find.
328 c.findCommands.interactive_search_helper starts an interactive search with
329 the given settings. The settings argument may be either a g.Bunch or a
330 dict.
332 Example 1, settings is a g.Bunch:
334 c.findCommands.interactive_search_helper(
335 root = c.p,
336 settings = g.Bunch(
337 find_text = '^(def )',
338 change_text = '\1',
339 pattern_match=True,
340 search_headline=False,
341 whole_word=False,
342 )
343 )
345 Example 2, settings is a python dict:
347 c.findCommands.interactive_search_helper(
348 root = c.p,
349 settings = {
350 'find_text': '^(def )',
351 'change_text': '\1',
352 'pattern_match': True,
353 'search_headline': False,
354 'whole_word': False,
355 }
356 )
357 """
358 #@-<< docstring: find.interactive_search >>
359 # Merge settings into default settings.
360 c = self.c
361 d = self.default_settings() # A g.bunch
362 if settings:
363 # Settings can be a dict or a g.Bunch.
364 # g.Bunch has no update method.
365 for key in settings.keys():
366 d[key] = settings[key]
367 self.ftm.set_widgets_from_dict(d) # So the *next* find-next will work.
368 self.show_find_options_in_status_area()
369 if not self.check_args('find-next'):
370 return
371 if root:
372 c.selectPosition(root)
373 self.do_find_next(d)
374 #@+node:ekr.20031218072017.3055: *3* LeoFind.Commands (immediate execution)
375 #@+node:ekr.20031218072017.3062: *4* find.change-then-find & helper
376 @cmd('replace-then-find')
377 @cmd('change-then-find')
378 def change_then_find(self, event=None): # pragma: no cover (cmd)
379 """Handle the replace-then-find command."""
380 # Settings...
381 self.init_in_headline()
382 settings = self.ftm.get_settings()
383 self.do_change_then_find(settings)
384 #@+node:ekr.20210114100105.1: *5* find.do_change_then_find
385 # A stand-alone method for unit testing.
386 def do_change_then_find(self, settings):
387 """
388 Do the change-then-find command from settings.
390 This is a stand-alone method for unit testing.
391 """
392 p = self.c.p
393 self.init_ivars_from_settings(settings)
394 if not self.check_args('change-then-find'):
395 return False
396 if self.change_selection(p):
397 self.do_find_next(settings)
398 return True
400 #@+node:ekr.20160224175312.1: *4* find.clone-find_marked & helper
401 @cmd('clone-find-all-marked')
402 @cmd('cfam')
403 def cloneFindAllMarked(self, event=None):
404 """
405 clone-find-all-marked, aka cfam.
407 Create an organizer node whose descendants contain clones of all marked
408 nodes. The list is *not* flattened: clones appear only once in the
409 descendants of the organizer node.
410 """
411 self.do_find_marked(flatten=False)
413 @cmd('clone-find-all-flattened-marked')
414 @cmd('cffm')
415 def cloneFindAllFlattenedMarked(self, event=None):
416 """
417 clone-find-all-flattened-marked, aka cffm.
419 Create an organizer node whose direct children are clones of all marked
420 nodes. The list is flattened: every cloned node appears as a direct
421 child of the organizer node, even if the clone also is a descendant of
422 another cloned node.
423 """
424 self.do_find_marked(flatten=True)
425 #@+node:ekr.20161022121036.1: *5* find.do_find_marked
426 def do_find_marked(self, flatten):
427 """
428 Helper for clone-find-marked commands.
430 This is a stand-alone method for unit testing.
431 """
432 c = self.c
434 def isMarked(p):
435 return p.isMarked()
437 root = c.cloneFindByPredicate(
438 generator=c.all_unique_positions,
439 predicate=isMarked,
440 failMsg='No marked nodes',
441 flatten=flatten,
442 undoType='clone-find-marked',
443 )
444 if root:
445 # Unmarking all nodes is convenient.
446 for v in c.all_unique_nodes():
447 if v.isMarked():
448 v.clearMarked()
449 n = root.numberOfChildren()
450 root.b = f"# Found {n} marked node{g.plural(n)}"
451 c.selectPosition(root)
452 c.redraw(root)
453 return bool(root)
454 #@+node:ekr.20140828080010.18532: *4* find.clone-find-parents
455 @cmd('clone-find-parents')
456 def cloneFindParents(self, event=None):
457 """
458 Create an organizer node whose direct children are clones of all
459 parents of the selected node, which must be a clone.
460 """
461 c, u = self.c, self.c.undoer
462 p = c.p
463 if not p: # pragma: no cover
464 return False
465 if not p.isCloned(): # pragma: no cover
466 g.es(f"not a clone: {p.h}")
467 return False
468 p0 = p.copy()
469 undoType = 'Find Clone Parents'
470 aList = c.vnode2allPositions(p.v)
471 if not aList: # pragma: no cover
472 g.trace('can not happen: no parents')
473 return False
474 # Create the node as the last top-level node.
475 # All existing positions remain valid.
476 u.beforeChangeGroup(p, undoType)
477 b = u.beforeInsertNode(p)
478 found = c.lastTopLevel().insertAfter()
479 found.h = f"Found: parents of {p.h}"
480 u.afterInsertNode(found, 'insert', b)
481 seen = []
482 for p2 in aList:
483 parent = p2.parent()
484 if parent and parent.v not in seen:
485 seen.append(parent.v)
486 b = u.beforeCloneNode(parent)
487 # Bug fix 2021/06/15: Create the clone directly as a child of found.
488 clone = p.copy()
489 n = found.numberOfChildren()
490 clone._linkCopiedAsNthChild(found, n)
491 u.afterCloneNode(clone, 'clone', b)
492 u.afterChangeGroup(p0, undoType)
493 c.setChanged()
494 c.redraw(found)
495 return True
496 #@+node:ekr.20150629084204.1: *4* find.find-def, do_find_def & helpers
497 @cmd('find-def')
498 def find_def(self, event=None, strict=False): # pragma: no cover (cmd)
499 """Find the def or class under the cursor."""
500 ftm, p = self.ftm, self.c.p
501 # Check.
502 word = self._compute_find_def_word(event)
503 if not word:
504 return
505 # Settings...
506 prefix = 'class' if word[0].isupper() else 'def'
507 find_pattern = prefix + ' ' + word
508 ftm.set_find_text(find_pattern)
509 self._save_before_find_def(p) # Save previous settings.
510 self.init_vim_search(find_pattern)
511 self.update_change_list(self.change_text) # Optional. An edge case.
512 # Do the command!
513 settings = self._compute_find_def_settings(find_pattern)
514 self.do_find_def(settings, word, strict)
516 def find_def_strict(self, event=None): #pragma: no cover (cmd)
517 """Same as find_def, but don't call _switch_style."""
518 self.find_def(event=event, strict=True)
520 def do_find_def(self, settings, word, strict):
521 """A standalone helper for unit tests."""
522 return self._fd_helper(settings, word, def_flag=True, strict=strict)
524 #@+node:ekr.20210114202757.1: *5* find._compute_find_def_settings
525 def _compute_find_def_settings(self, find_pattern):
527 settings = self.default_settings()
528 table = (
529 ('change_text', ''),
530 ('find_text', find_pattern),
531 ('ignore_case', False),
532 ('pattern_match', False),
533 ('reverse', False),
534 ('search_body', True),
535 ('search_headline', False),
536 ('whole_word', True),
537 )
538 for attr, val in table:
539 # Guard against renamings & misspellings.
540 assert hasattr(self, attr), attr
541 assert attr in settings.__dict__, attr
542 # Set the values.
543 setattr(self, attr, val)
544 settings[attr] = val
545 return settings
546 #@+node:ekr.20150629084611.1: *5* find._compute_find_def_word
547 def _compute_find_def_word(self, event): # pragma: no cover (cmd)
548 """Init the find-def command. Return the word to find or None."""
549 c = self.c
550 w = c.frame.body.wrapper
551 # First get the word.
552 c.bodyWantsFocusNow()
553 if not w.hasSelection():
554 c.editCommands.extendToWord(event, select=True)
555 word = w.getSelectedText().strip()
556 if not word:
557 return None
558 if keyword.iskeyword(word):
559 return None
560 # Return word, stripped of preceding class or def.
561 for tag in ('class ', 'def '):
562 found = word.startswith(tag) and len(word) > len(tag)
563 if found:
564 return word[len(tag) :].strip()
565 return word
566 #@+node:ekr.20150629125733.1: *5* find._fd_helper
567 def _fd_helper(self, settings, word, def_flag, strict):
568 """
569 Find the definition of the class, def or var under the cursor.
571 return p, pos, newpos for unit tests.
572 """
573 c, find, ftm = self.c, self, self.ftm
574 #
575 # Recompute find_text for unit tests.
576 if def_flag:
577 prefix = 'class' if word[0].isupper() else 'def'
578 self.find_text = settings.find_text = prefix + ' ' + word
579 else:
580 self.find_text = settings.find_text = word + ' ='
581 # g.printObj(settings, tag='_fd_helper: settings')
582 #
583 # Just search body text.
584 self.search_headline = False
585 self.search_body = True
586 w = c.frame.body.wrapper
587 # Check.
588 if not w: # pragma: no cover
589 return None, None, None
590 save_sel = w.getSelectionRange()
591 ins = w.getInsertPoint()
592 old_p = c.p
593 if self.reverse_find_defs:
594 # #2161: start at the last position.
595 p = c.lastPosition()
596 else:
597 # Start in the root position.
598 p = c.rootPosition()
599 # Required.
600 c.selectPosition(p)
601 c.redraw()
602 c.bodyWantsFocusNow()
603 # #1592. Ignore hits under control of @nosearch
604 try:
605 # #2161:
606 old_reverse = self.reverse
607 self.reverse = self.reverse_find_defs
608 # # 2288:
609 self.work_s = p.b
610 if self.reverse_find_defs:
611 self.work_sel = (len(p.b), len(p.b), len(p.b))
612 else:
613 self.work_sel = (0, 0, 0)
614 while True:
615 p, pos, newpos = self.find_next_match(p)
616 found = pos is not None
617 if found or not g.inAtNosearch(p): # do *not* use c.p.
618 break
619 if not found and def_flag and not strict:
620 # Leo 5.7.3: Look for an alternative defintion of function/methods.
621 word2 = self._switch_style(word)
622 if self.reverse_find_defs:
623 # #2161: start at the last position.
624 p = c.lastPosition()
625 else:
626 p = c.rootPosition()
627 if word2:
628 find_pattern = prefix + ' ' + word2
629 find.find_text = find_pattern
630 ftm.set_find_text(find_pattern)
631 # #1592. Ignore hits under control of @nosearch
632 while True:
633 p, pos, newpos = self.find_next_match(p)
634 found = pos is not None
635 if not found or not g.inAtNosearch(p):
636 break
637 finally:
638 self.reverse = old_reverse
639 if found:
640 c.redraw(p)
641 w.setSelectionRange(pos, newpos, insert=newpos)
642 c.bodyWantsFocusNow()
643 return p, pos, newpos
644 self._restore_after_find_def() # Avoid massive confusion!
645 i, j = save_sel
646 c.redraw(old_p)
647 w.setSelectionRange(i, j, insert=ins)
648 c.bodyWantsFocusNow()
649 return None, None, None
650 #@+node:ekr.20150629095511.1: *5* find._restore_after_find_def
651 def _restore_after_find_def(self):
652 """Restore find settings in effect before a find-def command."""
653 # pylint: disable=no-member
654 b = self.find_def_data # A g.Bunch
655 if b:
656 self.ignore_case = b.ignore_case
657 self.pattern_match = b.pattern_match
658 self.search_body = b.search_body
659 self.search_headline = b.search_headline
660 self.whole_word = b.whole_word
661 self.find_def_data = None
662 #@+node:ekr.20150629095633.1: *5* find._save_before_find_def
663 def _save_before_find_def(self, p):
664 """Save the find settings in effect before a find-def command."""
665 if not self.find_def_data:
666 self.find_def_data = g.Bunch(
667 ignore_case=self.ignore_case,
668 p=p.copy(),
669 pattern_match=self.pattern_match,
670 search_body=self.search_body,
671 search_headline=self.search_headline,
672 whole_word=self.whole_word,
673 )
674 #@+node:ekr.20180511045458.1: *5* find._switch_style
675 def _switch_style(self, word):
676 """
677 Switch between camelCase and underscore_style function defintiions.
678 Return None if there would be no change.
679 """
680 s = word
681 if not s:
682 return None
683 if s[0].isupper():
684 return None # Don't convert class names.
685 if s.find('_') > -1:
686 # Convert to CamelCase
687 s = s.lower()
688 while s:
689 i = s.find('_')
690 if i == -1:
691 break
692 s = s[:i] + s[i + 1 :].capitalize()
693 return s
694 # Convert to underscore_style.
695 result = []
696 for i, ch in enumerate(s):
697 if i > 0 and ch.isupper():
698 result.append('_')
699 result.append(ch.lower())
700 s = ''.join(result)
701 return None if s == word else s
702 #@+node:ekr.20031218072017.3063: *4* find.find-next, find-prev & do_find_*
703 @cmd('find-next')
704 def find_next(self, event=None): # pragma: no cover (cmd)
705 """The find-next command."""
706 # Settings...
707 self.reverse = False
708 self.init_in_headline() # Do this *before* creating the settings.
709 settings = self.ftm.get_settings()
710 # Do the command!
711 self.do_find_next(settings)
713 @cmd('find-prev')
714 def find_prev(self, event=None): # pragma: no cover (cmd)
715 """Handle F2 (find-previous)"""
716 # Settings...
717 self.init_in_headline() # Do this *before* creating the settings.
718 settings = self.ftm.get_settings()
719 # Do the command!
720 self.do_find_prev(settings)
721 #@+node:ekr.20031218072017.3074: *5* find.do_find_next & do_find_prev
722 def do_find_prev(self, settings):
723 """Find the previous instance of self.find_text."""
724 self.request_reverse = True
725 return self.do_find_next(settings)
727 def do_find_next(self, settings):
728 """
729 Find the next instance of self.find_text.
731 Return True (for vim-mode) if a match was found.
733 """
734 c, p = self.c, self.c.p
735 #
736 # The gui widget may not exist for headlines.
737 gui_w = c.edit_widget(p) if self.in_headline else c.frame.body.wrapper
738 #
739 # Init the work widget, so we don't get stuck.
740 s = p.h if self.in_headline else p.b
741 ins = gui_w.getInsertPoint() if gui_w else 0
742 self.work_s = s
743 self.work_sel = (ins, ins, ins)
744 #
745 # Set the settings *after* initing the search.
746 self.init_ivars_from_settings(settings)
747 #
748 # Honor delayed requests.
749 for ivar in ('reverse', 'pattern_match', 'whole_word'):
750 request = 'request_' + ivar
751 val = getattr(self, request)
752 if val: # Only *set* the ivar!
753 setattr(self, ivar, val) # Set the ivar.
754 setattr(self, request, False) # Clear the request!
755 #
756 # Leo 6.4: set/clear self.root
757 if self.root: # pragma: no cover
758 if p != self.root and not self.root.isAncestorOf(p):
759 # p is outside of self.root's tree.
760 # Clear suboutline-only.
761 self.root = None
762 self.suboutline_only = False
763 self.set_find_scope_every_where() # Update find-tab & status area.
764 elif self.suboutline_only:
765 # Start the range and set suboutline-only.
766 self.root = c.p
767 self.set_find_scope_suboutline_only() # Update find-tab & status area.
768 #
769 # Now check the args.
770 tag = 'find-prev' if self.reverse else 'find-next'
771 if not self.check_args(tag): # Issues error message.
772 return None, None, None
773 data = self.save()
774 p, pos, newpos = self.find_next_match(p)
775 found = pos is not None
776 if found:
777 self.show_success(p, pos, newpos)
778 else:
779 # Restore previous position.
780 self.restore(data)
781 self.show_status(found)
782 return p, pos, newpos
783 #@+node:ekr.20131117164142.17015: *4* find.find-tab-hide
784 @cmd('find-tab-hide')
785 def hide_find_tab(self, event=None): # pragma: no cover (cmd)
786 """Hide the Find tab."""
787 c = self.c
788 if self.minibuffer_mode:
789 c.k.keyboardQuit()
790 else:
791 self.c.frame.log.selectTab('Log')
792 #@+node:ekr.20131117164142.16916: *4* find.find-tab-open
793 @cmd('find-tab-open')
794 def open_find_tab(self, event=None, show=True): # pragma: no cover (cmd)
795 """Open the Find tab in the log pane."""
796 c = self.c
797 if c.config.getBool('use-find-dialog', default=True):
798 g.app.gui.openFindDialog(c)
799 else:
800 c.frame.log.selectTab('Find')
801 #@+node:ekr.20210118003803.1: *4* find.find-var & do_find_var
802 @cmd('find-var')
803 def find_var(self, event=None): # pragma: no cover (cmd)
804 """Find the var under the cursor."""
805 ftm, p = self.ftm, self.c.p
806 # Check...
807 word = self._compute_find_def_word(event)
808 if not word:
809 return
810 # Settings...
811 self.find_pattern = find_pattern = word + ' ='
812 ftm.set_find_text(find_pattern)
813 self._save_before_find_def(p) # Save previous settings.
814 self.init_vim_search(find_pattern)
815 self.update_change_list(self.change_text) # Optional. An edge case.
816 settings = self._compute_find_def_settings(find_pattern)
817 # Do the command!
818 self.do_find_var(settings, word)
820 def do_find_var(self, settings, word):
821 """A standalone helper for unit tests."""
822 return self._fd_helper(settings, word, def_flag=False, strict=False)
823 #@+node:ekr.20141113094129.6: *4* find.focus-to-find
824 @cmd('focus-to-find')
825 def focus_to_find(self, event=None): # pragma: no cover (cmd)
826 c = self.c
827 if c.config.getBool('use-find-dialog', default=True):
828 g.app.gui.openFindDialog(c)
829 else:
830 c.frame.log.selectTab('Find')
831 #@+node:ekr.20031218072017.3068: *4* find.replace
832 @cmd('replace')
833 @cmd('change')
834 def change(self, event=None): # pragma: no cover (cmd)
835 """Replace the selected text with the replacement text."""
836 p = self.c.p
837 if self.check_args('replace'):
838 self.init_in_headline()
839 self.change_selection(p)
841 replace = change
842 #@+node:ekr.20131117164142.17019: *4* find.set-find-*
843 @cmd('set-find-everywhere')
844 def set_find_scope_every_where(self, event=None): # pragma: no cover (cmd)
845 """Set the 'Entire Outline' radio button in the Find tab."""
846 return self.set_find_scope('entire-outline')
848 @cmd('set-find-node-only')
849 def set_find_scope_node_only(self, event=None): # pragma: no cover (cmd)
850 """Set the 'Node Only' radio button in the Find tab."""
851 return self.set_find_scope('node-only')
853 @cmd('set-find-suboutline-only')
854 def set_find_scope_suboutline_only(self, event=None):
855 """Set the 'Suboutline Only' radio button in the Find tab."""
856 return self.set_find_scope('suboutline-only')
858 def set_find_scope(self, where):
859 """Set the radio buttons to the given scope"""
860 c, fc = self.c, self.c.findCommands
861 self.ftm.set_radio_button(where)
862 options = fc.compute_find_options_in_status_area()
863 c.frame.statusLine.put(options)
864 #@+node:ekr.20131117164142.16989: *4* find.show-find-options
865 @cmd('show-find-options')
866 def show_find_options(self, event=None): # pragma: no cover (cmd)
867 """
868 Show the present find options in the status line.
869 This is useful for commands like search-forward that do not show the Find Panel.
870 """
871 frame = self.c.frame
872 frame.clearStatusLine()
873 part1, part2 = self.compute_find_options()
874 frame.putStatusLine(part1, bg='blue')
875 frame.putStatusLine(part2)
876 #@+node:ekr.20171129205648.1: *5* LeoFind.compute_find_options
877 def compute_find_options(self): # pragma: no cover (cmd)
878 """Return the status line as two strings."""
879 z = []
880 # Set the scope field.
881 head = self.search_headline
882 body = self.search_body
883 if self.suboutline_only:
884 scope = 'tree'
885 elif self.node_only:
886 scope = 'node'
887 else:
888 scope = 'all'
889 # scope = self.getOption('radio-search-scope')
890 # d = {'entire-outline':'all','suboutline-only':'tree','node-only':'node'}
891 # scope = d.get(scope) or ''
892 head = 'head' if head else ''
893 body = 'body' if body else ''
894 sep = '+' if head and body else ''
895 part1 = f"{head}{sep}{body} {scope} "
896 # Set the type field.
897 regex = self.pattern_match
898 if regex:
899 z.append('regex')
900 table = (
901 ('reverse', 'reverse'),
902 ('ignore_case', 'noCase'),
903 ('whole_word', 'word'),
904 # ('wrap', 'wrap'),
905 ('mark_changes', 'markChg'),
906 ('mark_finds', 'markFnd'),
907 )
908 for ivar, s in table:
909 val = getattr(self, ivar)
910 if val:
911 z.append(s)
912 part2 = ' '.join(z)
913 return part1, part2
914 #@+node:ekr.20131117164142.16919: *4* find.toggle-find-*
915 @cmd('toggle-find-collapses-nodes')
916 def toggle_find_collapes_nodes(self, event): # pragma: no cover (cmd)
917 """Toggle the 'Collapse Nodes' checkbox in the find tab."""
918 c = self.c
919 c.sparse_find = not c.sparse_find
920 if not g.unitTesting:
921 g.es('sparse_find', c.sparse_find)
923 @cmd('toggle-find-ignore-case-option')
924 def toggle_ignore_case_option(self, event): # pragma: no cover (cmd)
925 """Toggle the 'Ignore Case' checkbox in the Find tab."""
926 return self.toggle_option('ignore_case')
928 @cmd('toggle-find-mark-changes-option')
929 def toggle_mark_changes_option(self, event): # pragma: no cover (cmd)
930 """Toggle the 'Mark Changes' checkbox in the Find tab."""
931 return self.toggle_option('mark_changes')
933 @cmd('toggle-find-mark-finds-option')
934 def toggle_mark_finds_option(self, event): # pragma: no cover (cmd)
935 """Toggle the 'Mark Finds' checkbox in the Find tab."""
936 return self.toggle_option('mark_finds')
938 @cmd('toggle-find-regex-option')
939 def toggle_regex_option(self, event): # pragma: no cover (cmd)
940 """Toggle the 'Regexp' checkbox in the Find tab."""
941 return self.toggle_option('pattern_match')
943 @cmd('toggle-find-in-body-option')
944 def toggle_search_body_option(self, event): # pragma: no cover (cmd)
945 """Set the 'Search Body' checkbox in the Find tab."""
946 return self.toggle_option('search_body')
948 @cmd('toggle-find-in-headline-option')
949 def toggle_search_headline_option(self, event): # pragma: no cover (cmd)
950 """Toggle the 'Search Headline' checkbox in the Find tab."""
951 return self.toggle_option('search_headline')
953 @cmd('toggle-find-word-option')
954 def toggle_whole_word_option(self, event): # pragma: no cover (cmd)
955 """Toggle the 'Whole Word' checkbox in the Find tab."""
956 return self.toggle_option('whole_word')
958 # @cmd('toggle-find-wrap-around-option')
959 # def toggleWrapSearchOption(self, event):
960 # """Toggle the 'Wrap Around' checkbox in the Find tab."""
961 # return self.toggle_option('wrap')
963 def toggle_option(self, checkbox_name): # pragma: no cover (cmd)
964 c, fc = self.c, self.c.findCommands
965 self.ftm.toggle_checkbox(checkbox_name)
966 options = fc.compute_find_options_in_status_area()
967 c.frame.statusLine.put(options)
968 #@+node:ekr.20131117164142.17013: *3* LeoFind.Commands (interactive)
969 #@+node:ekr.20131117164142.16994: *4* find.change-all & helper
970 @cmd('change-all')
971 @cmd('replace-all')
972 def interactive_change_all(self, event=None): # pragma: no cover (interactive)
973 """Replace all instances of the search string with the replacement string."""
974 self.ftm.clear_focus()
975 self.ftm.set_entry_focus()
976 prompt = 'Replace Regex: ' if self.pattern_match else 'Replace: '
977 self.start_state_machine(event, prompt,
978 handler=self.interactive_replace_all1,
979 # Allow either '\t' or '\n' to switch to the change text.
980 escape_handler=self.interactive_replace_all1,
981 )
983 def interactive_replace_all1(self, event): # pragma: no cover (interactive)
984 k = self.k
985 find_pattern = k.arg
986 self._sString = k.arg
987 self.update_find_list(k.arg)
988 regex = ' Regex' if self.pattern_match else ''
989 prompt = f"Replace{regex}: {find_pattern} With: "
990 k.setLabelBlue(prompt)
991 self.add_change_string_to_label()
992 k.getNextArg(self.interactive_replace_all2)
994 def interactive_replace_all2(self, event): # pragma: no cover (interactive)
995 c, k, w = self.c, self.k, self.c.frame.body.wrapper
997 # Update settings data.
998 find_pattern = self._sString
999 change_pattern = k.arg
1000 self.init_vim_search(find_pattern)
1001 self.update_change_list(change_pattern)
1002 # Compute settings...
1003 self.ftm.set_find_text(find_pattern)
1004 self.ftm.set_change_text(change_pattern)
1005 settings = self.ftm.get_settings()
1006 # Gui...
1007 k.clearState()
1008 k.resetLabel()
1009 k.showStateAndMode()
1010 c.widgetWantsFocusNow(w)
1011 # Do the command!
1012 self.do_change_all(settings)
1013 #@+node:ekr.20131117164142.17016: *5* find.do_change_all & helpers
1014 def do_change_all(self, settings):
1015 c = self.c
1016 # Settings...
1017 self.init_ivars_from_settings(settings)
1018 if not self.check_args('change-all'):
1019 return 0
1020 n = self._change_all_helper(settings)
1021 #
1022 # Bugs #947, #880 and #722:
1023 # Set ancestor @<file> nodes by brute force.
1024 for p in c.all_positions(): # pragma: no cover
1025 if (
1026 p.anyAtFileNodeName()
1027 and not p.v.isDirty()
1028 and any(p2.v.isDirty() for p2 in p.subtree())
1029 ):
1030 p.setDirty()
1031 c.redraw()
1032 return n
1033 #@+node:ekr.20031218072017.3069: *6* find._change_all_helper
1034 def _change_all_helper(self, settings):
1035 """Do the change-all command. Return the number of changes, or 0 for error."""
1036 # Caller has checked settings.
1038 c, current, u = self.c, self.c.p, self.c.undoer
1039 undoType = 'Replace All'
1040 t1 = time.process_time()
1041 if not self.check_args('change-all'): # pragma: no cover
1042 return 0
1043 self.init_in_headline()
1044 saveData = self.save()
1045 self.in_headline = self.search_headline # Search headlines first.
1046 # Remember the start of the search.
1047 p = self.root = c.p.copy()
1048 # Set the work widget.
1049 s = p.h if self.in_headline else p.b
1050 ins = len(s) if self.reverse else 0
1051 self.work_s = s
1052 self.work_sel = (ins, ins, ins)
1053 count = 0
1054 u.beforeChangeGroup(current, undoType)
1055 # Fix bug 338172: ReplaceAll will not replace newlines
1056 # indicated as \n in target string.
1057 if not self.find_text: # pragma: no cover
1058 return 0
1059 if not self.search_headline and not self.search_body: # pragma: no cover
1060 return 0
1061 self.change_text = self.replace_back_slashes(self.change_text)
1062 if self.pattern_match:
1063 ok = self.precompile_pattern()
1064 if not ok:
1065 return 0
1066 # #1428: Honor limiters in replace-all.
1067 if self.node_only:
1068 positions = [c.p]
1069 elif self.suboutline_only:
1070 positions = c.p.self_and_subtree()
1071 else:
1072 positions = c.all_unique_positions()
1073 count = 0
1074 for p in positions:
1075 count_h, count_b = 0, 0
1076 undoData = u.beforeChangeNodeContents(p)
1077 if self.search_headline:
1078 count_h, new_h = self._change_all_search_and_replace(p.h)
1079 if count_h:
1080 count += count_h
1081 p.h = new_h
1082 if self.search_body:
1083 count_b, new_b = self._change_all_search_and_replace(p.b)
1084 if count_b:
1085 count += count_b
1086 p.b = new_b
1087 if count_h or count_b:
1088 u.afterChangeNodeContents(p, 'Replace All', undoData)
1089 self.ftm.set_radio_button('entire-outline')
1090 # suboutline-only is a one-shot for batch commands.
1091 self.root = None
1092 self.node_only = self.suboutline_only = False
1093 p = c.p
1094 u.afterChangeGroup(p, undoType, reportFlag=True)
1095 t2 = time.process_time()
1096 if not g.unitTesting: # pragma: no cover
1097 g.es_print(
1098 f"changed {count} instances{g.plural(count)} "
1099 f"in {t2 - t1:4.2f} sec.")
1100 c.recolor()
1101 c.redraw(p)
1102 self.restore(saveData)
1103 return count
1104 #@+node:ekr.20190602134414.1: *6* find._change_all_search_and_replace & helpers
1105 def _change_all_search_and_replace(self, s):
1106 """
1107 Search s for self.find_text and replace with self.change_text.
1109 Return (found, new text)
1110 """
1111 if sys.platform.lower().startswith('win'):
1112 s = s.replace('\r', '')
1113 # Ignore '\r' characters, which may appear in @edit nodes.
1114 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4
1115 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!)
1116 if not s:
1117 return False, None
1118 #
1119 # Order matters: regex matches ignore whole-word.
1120 if self.pattern_match:
1121 return self._change_all_regex(s)
1122 if self.whole_word:
1123 return self._change_all_word(s)
1124 return self._change_all_plain(s)
1125 #@+node:ekr.20190602151043.4: *7* find._change_all_plain
1126 def _change_all_plain(self, s):
1127 """
1128 Perform all plain find/replace on s.
1129 return (count, new_s)
1130 """
1131 find, change = self.find_text, self.change_text
1132 # #1166: s0 and find0 aren't affected by ignore-case.
1133 s0 = s
1134 find0 = self.replace_back_slashes(find)
1135 if self.ignore_case:
1136 s = s0.lower()
1137 find = find0.lower()
1138 count, prev_i, result = 0, 0, []
1139 while True:
1140 progress = prev_i
1141 # #1166: Scan using s and find.
1142 i = s.find(find, prev_i)
1143 if i == -1:
1144 break
1145 # #1166: Replace using s0 & change.
1146 count += 1
1147 result.append(s0[prev_i:i])
1148 result.append(change)
1149 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!)
1150 assert prev_i > progress, prev_i
1151 # #1166: Complete the result using s0.
1152 result.append(s0[prev_i:])
1153 return count, ''.join(result)
1154 #@+node:ekr.20190602151043.2: *7* find._change_all_regex
1155 def _change_all_regex(self, s):
1156 """
1157 Perform all regex find/replace on s.
1158 return (count, new_s)
1159 """
1160 count, prev_i, result = 0, 0, []
1162 flags = re.MULTILINE
1163 if self.ignore_case:
1164 flags |= re.IGNORECASE
1165 for m in re.finditer(self.find_text, s, flags):
1166 count += 1
1167 i = m.start()
1168 result.append(s[prev_i:i])
1169 # #1748.
1170 groups = m.groups()
1171 if groups:
1172 change_text = self.make_regex_subs(self.change_text, groups)
1173 else:
1174 change_text = self.change_text
1175 result.append(change_text)
1176 prev_i = m.end()
1177 # Compute the result.
1178 result.append(s[prev_i:])
1179 s = ''.join(result)
1180 return count, s
1181 #@+node:ekr.20190602155933.1: *7* find._change_all_word
1182 def _change_all_word(self, s):
1183 """
1184 Perform all whole word find/replace on s.
1185 return (count, new_s)
1186 """
1187 find, change = self.find_text, self.change_text
1188 # #1166: s0 and find0 aren't affected by ignore-case.
1189 s0 = s
1190 find0 = self.replace_back_slashes(find)
1191 if self.ignore_case:
1192 s = s0.lower()
1193 find = find0.lower()
1194 count, prev_i, result = 0, 0, []
1195 while True:
1196 # #1166: Scan using s and find.
1197 i = s.find(find, prev_i)
1198 if i == -1:
1199 break
1200 # #1166: Replace using s0, change & find0.
1201 result.append(s0[prev_i:i])
1202 if g.match_word(s, i, find):
1203 count += 1
1204 result.append(change)
1205 else:
1206 result.append(find0)
1207 prev_i = i + len(find)
1208 # #1166: Complete the result using s0.
1209 result.append(s0[prev_i:])
1210 return count, ''.join(result)
1211 #@+node:ekr.20210110073117.23: *6* new:find.replace_all_helper & helpers (merge & delete)
1212 def replace_all_helper(self, s):
1213 """
1214 Search s for self.find_text and replace with self.change_text.
1216 Return (found, new text)
1217 """
1218 if sys.platform.lower().startswith('win'):
1219 s = s.replace('\r', '')
1220 # Ignore '\r' characters, which may appear in @edit nodes.
1221 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4
1222 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!)
1223 if not s:
1224 return False, None
1225 #
1226 # Order matters: regex matches ignore whole-word.
1227 if self.pattern_match:
1228 return self.batch_regex_replace(s)
1229 if self.whole_word:
1230 return self.batch_word_replace(s)
1231 return self.batch_plain_replace(s)
1232 #@+node:ekr.20210110073117.24: *7* new:find.batch_plain_replace
1233 def batch_plain_replace(self, s):
1234 """
1235 Perform all plain find/replace on s.
1236 return (count, new_s)
1237 """
1238 find, change = self.find_text, self.change_text
1239 # #1166: s0 and find0 aren't affected by ignore-case.
1240 s0 = s
1241 find0 = self.replace_back_slashes(find)
1242 if self.ignore_case:
1243 s = s0.lower()
1244 find = find0.lower()
1245 count, prev_i, result = 0, 0, []
1246 while True:
1247 progress = prev_i
1248 # #1166: Scan using s and find.
1249 i = s.find(find, prev_i)
1250 if i == -1:
1251 break
1252 # #1166: Replace using s0 & change.
1253 count += 1
1254 result.append(s0[prev_i:i])
1255 result.append(change)
1256 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!)
1257 assert prev_i > progress, prev_i
1258 # #1166: Complete the result using s0.
1259 result.append(s0[prev_i:])
1260 return count, ''.join(result)
1261 #@+node:ekr.20210110073117.25: *7* new:find.batch_regex_replace
1262 def batch_regex_replace(self, s):
1263 """
1264 Perform all regex find/replace on s.
1265 return (count, new_s)
1266 """
1267 count, prev_i, result = 0, 0, []
1269 flags = re.MULTILINE
1270 if self.ignore_case:
1271 flags |= re.IGNORECASE
1272 for m in re.finditer(self.find_text, s, flags):
1273 count += 1
1274 i = m.start()
1275 result.append(s[prev_i:i])
1276 # #1748.
1277 groups = m.groups()
1278 if groups:
1279 change_text = self.make_regex_subs(self.change_text, groups)
1280 else:
1281 change_text = self.change_text
1282 result.append(change_text)
1283 prev_i = m.end()
1284 # Compute the result.
1285 result.append(s[prev_i:])
1286 s = ''.join(result)
1287 return count, s
1288 #@+node:ekr.20210110073117.26: *7* new:find.batch_word_replace
1289 def batch_word_replace(self, s):
1290 """
1291 Perform all whole word 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 & find0.
1309 result.append(s0[prev_i:i])
1310 if g.match_word(s, i, find):
1311 count += 1
1312 result.append(change)
1313 else:
1314 result.append(find0)
1315 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!)
1316 assert prev_i > progress, prev_i
1317 # #1166: Complete the result using s0.
1318 result.append(s0[prev_i:])
1319 return count, ''.join(result)
1320 #@+node:ekr.20131117164142.17011: *4* find.clone-find-all & helper
1321 @cmd('clone-find-all')
1322 @cmd('find-clone-all')
1323 @cmd('cfa')
1324 def interactive_clone_find_all(
1325 self, event=None, preloaded=None): # pragma: no cover (interactive)
1326 """
1327 clone-find-all ( aka find-clone-all and cfa).
1329 Create an organizer node whose descendants contain clones of all nodes
1330 matching the search string, except @nosearch trees.
1332 The list is *not* flattened: clones appear only once in the
1333 descendants of the organizer node.
1334 """
1335 w = self.c.frame.body.wrapper
1336 if not w:
1337 return
1338 if not preloaded:
1339 self.preload_find_pattern(w)
1340 self.start_state_machine(event,
1341 prefix='Clone Find All: ',
1342 handler=self.interactive_clone_find_all1)
1344 def interactive_clone_find_all1(self, event): # pragma: no cover (interactive)
1345 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1346 # Settings...
1347 pattern = k.arg
1348 self.ftm.set_find_text(pattern)
1349 self.init_vim_search(pattern)
1350 self.init_in_headline()
1351 settings = self.ftm.get_settings()
1352 # Gui...
1353 k.clearState()
1354 k.resetLabel()
1355 k.showStateAndMode()
1356 c.widgetWantsFocusNow(w)
1357 count = self.do_clone_find_all(settings)
1358 if count:
1359 c.redraw()
1360 c.treeWantsFocus()
1361 return count
1362 #@+node:ekr.20210114094846.1: *5* find.do_clone_find_all
1363 # A stand-alone method for unit testing.
1364 def do_clone_find_all(self, settings):
1365 """
1366 Do the clone-all-find commands from settings.
1368 Return the count of found nodes.
1370 This is a stand-alone method for unit testing.
1371 """
1372 self.init_ivars_from_settings(settings)
1373 if not self.check_args('clone-find-all'):
1374 return 0
1375 return self._cf_helper(settings, flatten=False)
1376 #@+node:ekr.20131117164142.16996: *4* find.clone-find-all-flattened & helper
1377 @cmd('clone-find-all-flattened')
1378 # @cmd('find-clone-all-flattened')
1379 @cmd('cff')
1380 def interactive_cff(
1381 self, event=None, preloaded=None): # pragma: no cover (interactive)
1382 """
1383 clone-find-all-flattened (aka find-clone-all-flattened and cff).
1385 Create an organizer node whose direct children are clones of all nodes
1386 matching the search string, except @nosearch trees.
1388 The list is flattened: every cloned node appears as a direct child
1389 of the organizer node, even if the clone also is a descendant of
1390 another cloned node.
1391 """
1392 w = self.c.frame.body.wrapper
1393 if not w:
1394 return
1395 if not preloaded:
1396 self.preload_find_pattern(w)
1397 self.start_state_machine(event,
1398 prefix='Clone Find All Flattened: ',
1399 handler=self.interactive_cff1)
1401 def interactive_cff1(self, event): # pragma: no cover (interactive)
1402 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1403 # Settings...
1404 pattern = k.arg
1405 self.ftm.set_find_text(pattern)
1406 self.init_vim_search(pattern)
1407 self.init_in_headline()
1408 settings = self.ftm.get_settings()
1409 # Gui...
1410 k.clearState()
1411 k.resetLabel()
1412 k.showStateAndMode()
1413 c.widgetWantsFocusNow(w)
1414 count = self.do_clone_find_all(settings)
1415 if count:
1416 c.redraw()
1417 c.treeWantsFocus()
1418 return count
1419 #@+node:ekr.20210114094944.1: *5* find.do_clone_find_all_flattened
1420 # A stand-alone method for unit testing.
1421 def do_clone_find_all_flattened(self, settings):
1422 """
1423 Do the clone-find-all-flattened command from the settings.
1425 Return the count of found nodes.
1427 This is a stand-alone method for unit testing.
1428 """
1429 self.init_ivars_from_settings(settings)
1430 if self.check_args('clone-find-all-flattened'):
1431 return self._cf_helper(settings, flatten=True)
1432 return 0
1433 #@+node:ekr.20160920110324.1: *4* find.clone-find-tag & helper
1434 @cmd('clone-find-tag')
1435 @cmd('find-clone-tag')
1436 @cmd('cft')
1437 def interactive_clone_find_tag(self, event=None): # pragma: no cover (interactive)
1438 """
1439 clone-find-tag (aka find-clone-tag and cft).
1441 Create an organizer node whose descendants contain clones of all
1442 nodes matching the given tag, except @nosearch trees.
1444 The list is *always* flattened: every cloned node appears as a
1445 direct child of the organizer node, even if the clone also is a
1446 descendant of another cloned node.
1447 """
1448 w = self.c.frame.body.wrapper
1449 if w:
1450 self.start_state_machine(event,
1451 prefix='Clone Find Tag: ',
1452 handler=self.interactive_clone_find_tag1)
1454 def interactive_clone_find_tag1(self, event): # pragma: no cover (interactive)
1455 c, k = self.c, self.k
1456 # Settings...
1457 self.find_text = tag = k.arg
1458 # Gui...
1459 k.clearState()
1460 k.resetLabel()
1461 k.showStateAndMode()
1462 self.do_clone_find_tag(tag)
1463 c.treeWantsFocus()
1464 #@+node:ekr.20210110073117.11: *5* find.do_clone_find_tag & helper
1465 # A stand-alone method for unit tests.
1466 def do_clone_find_tag(self, tag):
1467 """
1468 Do the clone-all-find commands from settings.
1469 Return (len(clones), found) for unit tests.
1470 """
1471 c, u = self.c, self.c.undoer
1472 tc = getattr(c, 'theTagController', None)
1473 if not tc:
1474 if not g.unitTesting: # pragma: no cover (skip)
1475 g.es_print('nodetags not active')
1476 return 0, c.p
1477 clones = tc.get_tagged_nodes(tag)
1478 if not clones:
1479 if not g.unitTesting: # pragma: no cover (skip)
1480 g.es_print(f"tag not found: {tag}")
1481 tc.show_all_tags()
1482 return 0, c.p
1483 undoData = u.beforeInsertNode(c.p)
1484 found = self._create_clone_tag_nodes(clones)
1485 u.afterInsertNode(found, 'Clone Find Tag', undoData)
1486 assert c.positionExists(found, trace=True), found
1487 c.setChanged()
1488 c.selectPosition(found)
1489 c.redraw()
1490 return len(clones), found
1491 #@+node:ekr.20210110073117.12: *6* find._create_clone_tag_nodes
1492 def _create_clone_tag_nodes(self, clones):
1493 """
1494 Create a "Found Tag" node as the last node of the outline.
1495 Clone all positions in the clones set as children of found.
1496 """
1497 c, p = self.c, self.c.p
1498 # Create the found node.
1499 assert c.positionExists(c.lastTopLevel()), c.lastTopLevel()
1500 found = c.lastTopLevel().insertAfter()
1501 assert found
1502 assert c.positionExists(found), found
1503 found.h = f"Found Tag: {self.find_text}"
1504 # Clone nodes as children of the found node.
1505 for p in clones:
1506 # Create the clone directly as a child of found.
1507 p2 = p.copy()
1508 n = found.numberOfChildren()
1509 p2._linkCopiedAsNthChild(found, n)
1510 return found
1511 #@+node:ekr.20131117164142.16998: *4* find.find-all & helper
1512 @cmd('find-all')
1513 def interactive_find_all(self, event=None): # pragma: no cover (interactive)
1514 """
1515 Create a summary node containing descriptions of all matches of the
1516 search string.
1518 Typing tab converts this to the change-all command.
1519 """
1520 self.ftm.clear_focus()
1521 self.ftm.set_entry_focus()
1522 self.start_state_machine(event, 'Search: ',
1523 handler=self.interactive_find_all1,
1524 escape_handler=self.find_all_escape_handler,
1525 )
1527 def interactive_find_all1(self, event=None): # pragma: no cover (interactive)
1528 k = self.k
1529 # Settings.
1530 find_pattern = k.arg
1531 self.ftm.set_find_text(find_pattern)
1532 settings = self.ftm.get_settings()
1533 self.find_text = find_pattern
1534 self.change_text = self.ftm.get_change_text()
1535 self.update_find_list(find_pattern)
1536 # Gui...
1537 k.clearState()
1538 k.resetLabel()
1539 k.showStateAndMode()
1540 self.do_find_all(settings)
1542 def find_all_escape_handler(self, event): # pragma: no cover (interactive)
1543 k = self.k
1544 prompt = 'Replace ' + ('Regex' if self.pattern_match else 'String')
1545 find_pattern = k.arg
1546 self._sString = k.arg
1547 self.update_find_list(k.arg)
1548 s = f"{prompt}: {find_pattern} With: "
1549 k.setLabelBlue(s)
1550 self.add_change_string_to_label()
1551 k.getNextArg(self.find_all_escape_handler2)
1553 def find_all_escape_handler2(self, event): # pragma: no cover (interactive)
1554 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1555 find_pattern = self._sString
1556 change_pattern = k.arg
1557 self.update_change_list(change_pattern)
1558 self.ftm.set_find_text(find_pattern)
1559 self.ftm.set_change_text(change_pattern)
1560 self.init_vim_search(find_pattern)
1561 self.init_in_headline()
1562 settings = self.ftm.get_settings()
1563 # Gui...
1564 k.clearState()
1565 k.resetLabel()
1566 k.showStateAndMode()
1567 c.widgetWantsFocusNow(w)
1568 self.do_change_all(settings)
1569 #@+node:ekr.20031218072017.3073: *5* find.do_find_all & helpers
1570 def do_find_all(self, settings):
1571 """Top-level helper for find-all command."""
1572 c = self.c
1573 count = 0
1574 self.init_ivars_from_settings(settings)
1575 if not self.check_args('find-all'): # pragma: no cover
1576 return count
1577 # Init data.
1578 self.init_in_headline()
1579 data = self.save()
1580 self.in_headline = self.search_headline # Search headlines first.
1581 self.unique_matches = set() # 2021/02/20.
1582 # Remember the start of the search.
1583 p = self.root = c.p.copy()
1584 # Set the work widget.
1585 s = p.h if self.in_headline else p.b
1586 ins = len(s) if self.reverse else 0
1587 self.work_s = s
1588 self.work_sel = (ins, ins, ins)
1589 if self.pattern_match:
1590 ok = self.precompile_pattern()
1591 if not ok: # pragma: no cover
1592 return count
1593 if self.suboutline_only:
1594 p = c.p
1595 after = p.nodeAfterTree()
1596 else:
1597 # Always search the entire outline.
1598 p = c.rootPosition()
1599 after = None
1600 # Fix #292: Never collapse nodes during find-all commands.
1601 old_sparse_find = c.sparse_find
1602 try:
1603 c.sparse_find = False
1604 count = self._find_all_helper(after, data, p, 'Find All')
1605 c.contractAllHeadlines()
1606 finally:
1607 c.sparse_find = old_sparse_find
1608 self.root = None
1609 if count:
1610 c.redraw()
1611 g.es("found", count, "matches for", self.find_text)
1612 return count
1613 #@+node:ekr.20160422073500.1: *6* find._find_all_helper
1614 def _find_all_helper(self, after, data, p, undoType):
1615 """Handle the find-all command from p to after."""
1616 c, log, u = self.c, self.c.frame.log, self.c.undoer
1618 def put_link(line, line_number, p): # pragma: no cover # #2023
1619 """Put a link to the given line at the given line_number in p.h."""
1621 if g.unitTesting:
1622 return
1623 unl = p.get_UNL()
1624 if self.in_headline:
1625 line_number = 1
1626 log.put(line.strip() + '\n', nodeLink=f"{unl}::{line_number}") # Local line.
1628 seen = [] # List of (vnode, pos).
1629 both = self.search_body and self.search_headline
1630 count, found, result = 0, None, []
1631 while 1:
1632 p, pos, newpos = self.find_next_match(p)
1633 if pos is None:
1634 break
1635 if (p.v, pos) in seen: # 2076
1636 continue # pragma: no cover
1637 seen.append((p.v, pos))
1638 count += 1
1639 s = self.work_s
1640 i, j = g.getLine(s, pos)
1641 line = s[i:j]
1642 row, col = g.convertPythonIndexToRowCol(s, i)
1643 line_number = row + 1
1644 if self.findAllUniqueFlag:
1645 m = self.match_obj
1646 if m:
1647 self.unique_matches.add(m.group(0).strip())
1648 put_link(line, line_number, p) # #2023
1649 elif both:
1650 result.append('%s%s\n%s%s\n' % (
1651 '-' * 20, p.h,
1652 "head: " if self.in_headline else "body: ",
1653 line.rstrip() + '\n'))
1654 put_link(line, line_number, p) # #2023
1655 elif p.isVisited():
1656 result.append(line.rstrip() + '\n')
1657 put_link(line, line_number, p) # #2023
1658 else:
1659 result.append('%s%s\n%s' % ('-' * 20, p.h, line.rstrip() + '\n'))
1660 put_link(line, line_number, p) # #2023
1661 p.setVisited()
1662 if result or self.unique_matches:
1663 undoData = u.beforeInsertNode(c.p)
1664 if self.findAllUniqueFlag:
1665 found = self._create_find_unique_node()
1666 count = len(list(self.unique_matches))
1667 else:
1668 found = self._create_find_all_node(result)
1669 u.afterInsertNode(found, undoType, undoData)
1670 c.selectPosition(found)
1671 c.setChanged()
1672 else:
1673 self.restore(data)
1674 return count
1675 #@+node:ekr.20150717105329.1: *6* find._create_find_all_node
1676 def _create_find_all_node(self, result):
1677 """Create a "Found All" node as the last node of the outline."""
1678 c = self.c
1679 found = c.lastTopLevel().insertAfter()
1680 assert found
1681 found.h = f"Found All:{self.find_text}"
1682 status = self.compute_result_status(find_all_flag=True)
1683 status = status.strip().lstrip('(').rstrip(')').strip()
1684 found.b = f"# {status}\n{''.join(result)}"
1685 return found
1686 #@+node:ekr.20171226143621.1: *6* find._create_find_unique_node
1687 def _create_find_unique_node(self):
1688 """Create a "Found Unique" node as the last node of the outline."""
1689 c = self.c
1690 found = c.lastTopLevel().insertAfter()
1691 assert found
1692 found.h = f"Found Unique Regex:{self.find_text}"
1693 result = sorted(self.unique_matches)
1694 found.b = '\n'.join(result)
1695 return found
1696 #@+node:ekr.20171226140643.1: *4* find.find-all-unique-regex
1697 @cmd('find-all-unique-regex')
1698 def interactive_find_all_unique_regex(
1699 self, event=None): # pragma: no cover (interactive)
1700 """
1701 Create a summary node containing all unique matches of the regex search
1702 string. This command shows only the matched string itself.
1703 """
1704 self.ftm.clear_focus()
1705 self.match_obj = None
1706 self.changeAllFlag = False
1707 self.findAllUniqueFlag = True
1708 self.ftm.set_entry_focus()
1709 self.start_state_machine(event,
1710 prefix='Search Unique Regex: ',
1711 handler=self.interactive_find_all_unique_regex1,
1712 escape_handler=self.interactive_change_all_unique_regex1,
1713 )
1715 def interactive_find_all_unique_regex1(
1716 self, event=None): # pragma: no cover (interactive)
1717 k = self.k
1718 # Settings...
1719 find_pattern = k.arg
1720 self.update_find_list(find_pattern)
1721 self.ftm.set_find_text(find_pattern)
1722 self.init_in_headline()
1723 settings = self.ftm.get_settings()
1724 # Gui...
1725 k.clearState()
1726 k.resetLabel()
1727 k.showStateAndMode()
1728 return self.do_find_all(settings)
1730 def interactive_change_all_unique_regex1(
1731 self, event): # pragma: no cover (interactive)
1732 k = self.k
1733 find_pattern = self._sString = k.arg
1734 self.update_find_list(k.arg)
1735 s = f"'Replace All Unique Regex': {find_pattern} With: "
1736 k.setLabelBlue(s)
1737 self.add_change_string_to_label()
1738 k.getNextArg(self.interactive_change_all_unique_regex2)
1740 def interactive_change_all_unique_regex2(
1741 self, event): # pragma: no cover (interactive)
1742 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1743 find_pattern = self._sString
1744 change_pattern = k.arg
1745 self.update_change_list(change_pattern)
1746 self.ftm.set_find_text(find_pattern)
1747 self.ftm.set_change_text(change_pattern)
1748 self.init_vim_search(find_pattern)
1749 self.init_in_headline()
1750 settings = self.ftm.get_settings()
1751 # Gui...
1752 k.clearState()
1753 k.resetLabel()
1754 k.showStateAndMode()
1755 c.widgetWantsFocusNow(w)
1756 self.do_change_all(settings)
1757 #@+node:ekr.20131117164142.17003: *4* find.re-search
1758 @cmd('re-search')
1759 @cmd('re-search-forward')
1760 def interactive_re_search_forward(self, event): # pragma: no cover (interactive)
1761 """Same as start-find, with regex."""
1762 # Set flag for show_find_options.
1763 self.pattern_match = True
1764 self.show_find_options()
1765 # Set flag for do_find_next().
1766 self.request_pattern_match = True
1767 # Go.
1768 self.start_state_machine(event,
1769 prefix='Regexp Search: ',
1770 handler=self.start_search1, # See start-search
1771 escape_handler=self.start_search_escape1, # See start-search
1772 )
1773 #@+node:ekr.20210112044303.1: *4* find.re-search-backward
1774 @cmd('re-search-backward')
1775 def interactive_re_search_backward(self, event): # pragma: no cover (interactive)
1776 """Same as start-find, but with regex and in reverse."""
1777 # Set flags for show_find_options.
1778 self.reverse = True
1779 self.pattern_match = True
1780 self.show_find_options()
1781 # Set flags for do_find_next().
1782 self.request_reverse = True
1783 self.request_pattern_match = True
1784 # Go.
1785 self.start_state_machine(event,
1786 prefix='Regexp Search Backward:',
1787 handler=self.start_search1, # See start-search
1788 escape_handler=self.start_search_escape1, # See start-search
1789 )
1791 #@+node:ekr.20131117164142.17004: *4* find.search_backward
1792 @cmd('search-backward')
1793 def interactive_search_backward(self, event): # pragma: no cover (interactive)
1794 """Same as start-find, but in reverse."""
1795 # Set flag for show_find_options.
1796 self.reverse = True
1797 self.show_find_options()
1798 # Set flag for do_find_next().
1799 self.request_reverse = True
1800 # Go.
1801 self.start_state_machine(event,
1802 prefix='Search Backward: ',
1803 handler=self.start_search1, # See start-search
1804 escape_handler=self.start_search_escape1, # See start-search
1805 )
1806 #@+node:ekr.20131119060731.22452: *4* find.start-search (Ctrl-F) & common states
1807 @cmd('start-search')
1808 @cmd('search-forward') # Compatibility.
1809 def start_search(self, event): # pragma: no cover (interactive)
1810 """
1811 The default binding of Ctrl-F.
1813 Also contains default state-machine entries for find/change commands.
1814 """
1815 w = self.c.frame.body.wrapper
1816 if not w:
1817 return
1818 self.preload_find_pattern(w)
1819 # #1840: headline-only one-shot
1820 # Do this first, so the user can override.
1821 self.ftm.set_body_and_headline_checkbox()
1822 if self.minibuffer_mode:
1823 # Set up the state machine.
1824 self.ftm.clear_focus()
1825 self.changeAllFlag = False
1826 self.findAllUniqueFlag = False
1827 self.ftm.set_entry_focus()
1828 self.start_state_machine(event,
1829 prefix='Search: ',
1830 handler=self.start_search1,
1831 escape_handler=self.start_search_escape1,
1832 )
1833 else:
1834 self.open_find_tab(event)
1835 self.ftm.init_focus()
1836 return
1838 startSearch = start_search # Compatibility. Do not delete.
1839 #@+node:ekr.20210117143611.1: *5* find.start_search1
1840 def start_search1(self, event=None): # pragma: no cover
1841 """Common handler for use by vim commands and other find commands."""
1842 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1843 # Settings...
1844 find_pattern = k.arg
1845 self.ftm.set_find_text(find_pattern)
1846 self.update_find_list(find_pattern)
1847 self.init_vim_search(find_pattern)
1848 self.init_in_headline() # Required.
1849 settings = self.ftm.get_settings()
1850 # Gui...
1851 k.clearState()
1852 k.resetLabel()
1853 k.showStateAndMode()
1854 c.widgetWantsFocusNow(w)
1855 # Do the command!
1856 self.do_find_next(settings) # Handles reverse.
1857 #@+node:ekr.20210117143614.1: *5* find._start_search_escape1
1858 def start_search_escape1(self, event=None): # pragma: no cover
1859 """
1860 Common escape handler for use by find commands.
1862 Prompt for a change pattern.
1863 """
1864 k = self.k
1865 self._sString = find_pattern = k.arg
1866 # Settings.
1867 k.getArgEscapeFlag = False
1868 self.ftm.set_find_text(find_pattern)
1869 self.update_find_list(find_pattern)
1870 self.find_text = find_pattern
1871 self.change_text = self.ftm.get_change_text()
1872 # Gui...
1873 regex = ' Regex' if self.pattern_match else ''
1874 backward = ' Backward' if self.reverse else ''
1875 prompt = f"Replace{regex}{backward}: {find_pattern} With: "
1876 k.setLabelBlue(prompt)
1877 self.add_change_string_to_label()
1878 k.getNextArg(self._start_search_escape2)
1880 #@+node:ekr.20210117143615.1: *5* find._start_search_escape2
1881 def _start_search_escape2(self, event): # pragma: no cover
1882 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1883 # Compute settings...
1884 find_pattern = self._sString
1885 change_pattern = k.arg
1886 self.ftm.set_find_text(find_pattern)
1887 self.ftm.set_change_text(change_pattern)
1888 self.update_change_list(change_pattern)
1889 self.init_vim_search(find_pattern)
1890 self.init_in_headline() # Required
1891 settings = self.ftm.get_settings()
1892 # Gui...
1893 k.clearState()
1894 k.resetLabel()
1895 k.showStateAndMode()
1896 c.widgetWantsFocusNow(w)
1897 self.do_find_next(settings)
1898 #@+node:ekr.20160920164418.2: *4* find.tag-children & helper
1899 @cmd('tag-children')
1900 def interactive_tag_children(self, event=None): # pragma: no cover (interactive)
1901 """tag-children: prompt for a tag and add it to all children of c.p."""
1902 w = self.c.frame.body.wrapper
1903 if not w:
1904 return
1905 self.start_state_machine(event,
1906 prefix='Tag Children: ',
1907 handler=self.interactive_tag_children1)
1909 def interactive_tag_children1(self, event): # pragma: no cover (interactive)
1910 c, k, p = self.c, self.k, self.c.p
1911 # Settings...
1912 tag = k.arg
1913 # Gui...
1914 k.clearState()
1915 k.resetLabel()
1916 k.showStateAndMode()
1917 self.do_tag_children(p, tag)
1918 c.treeWantsFocus()
1919 #@+node:ekr.20160920164418.4: *5* find.do_tag_children
1920 def do_tag_children(self, p, tag):
1921 """Handle the tag-children command."""
1922 c = self.c
1923 tc = getattr(c, 'theTagController', None)
1924 if not tc:
1925 if not g.unitTesting: # pragma: no cover (skip)
1926 g.es_print('nodetags not active')
1927 return
1928 for p in p.children():
1929 tc.add_tag(p, tag)
1930 if not g.unitTesting: # pragma: no cover (skip)
1931 g.es_print(f"Added {tag} tag to {len(list(c.p.children()))} nodes")
1933 #@+node:ekr.20210112050845.1: *4* find.word-search
1934 @cmd('word-search')
1935 @cmd('word-search-forward')
1936 def word_search_forward(self, event): # pragma: no cover (interactive)
1937 """Same as start-search, with whole_word setting."""
1938 # Set flag for show_find_options.
1939 self.whole_word = True
1940 self.show_find_options()
1941 # Set flag for do_find_next().
1942 self.request_whole_world = True
1943 # Go.
1944 self.start_state_machine(event,
1945 prefix='Word Search: ',
1946 handler=self.start_search1, # See start-search
1947 escape_handler=self.start_search_escape1, # See start-search
1948 )
1949 #@+node:ekr.20131117164142.17009: *4* find.word-search-backward
1950 @cmd('word-search-backward')
1951 def word_search_backward(self, event): # pragma: no cover (interactive)
1952 # Set flags for show_find_options.
1953 self.reverse = True
1954 self.whole_world = True
1955 self.show_find_options()
1956 # Set flags for do_find_next().
1957 self.request_reverse = True
1958 self.request_whole_world = True
1959 # Go
1960 self.start_state_machine(event,
1961 prefix='Word Search Backward: ',
1962 handler=self.start_search1, # See start-search
1963 escape_handler=self.start_search_escape1, # See start-search
1964 )
1965 #@+node:ekr.20210112192427.1: *3* LeoFind.Commands: helpers
1966 #@+node:ekr.20210110073117.9: *4* find._cf_helper & helpers
1967 def _cf_helper(self, settings, flatten): # Caller has checked the settings.
1968 """
1969 The common part of the clone-find commands.
1971 Return the number of found nodes.
1972 """
1973 c, u = self.c, self.c.undoer
1974 if self.pattern_match:
1975 ok = self.compile_pattern()
1976 if not ok:
1977 return 0
1978 if self.suboutline_only:
1979 p = c.p
1980 after = p.nodeAfterTree()
1981 else:
1982 p = c.rootPosition()
1983 after = None
1984 count, found = 0, None
1985 clones, skip = [], set()
1986 while p and p != after:
1987 progress = p.copy()
1988 if p.v in skip: # pragma: no cover (minor)
1989 p.moveToThreadNext()
1990 elif g.inAtNosearch(p):
1991 p.moveToNodeAfterTree()
1992 elif self._cfa_find_next_match(p):
1993 count += 1
1994 if p not in clones:
1995 clones.append(p.copy())
1996 if flatten:
1997 p.moveToThreadNext()
1998 else:
1999 # Don't look at the node or it's descendants.
2000 for p2 in p.self_and_subtree(copy=False):
2001 skip.add(p2.v)
2002 p.moveToNodeAfterTree()
2003 else: # pragma: no cover (minor)
2004 p.moveToThreadNext()
2005 assert p != progress
2006 self.ftm.set_radio_button('entire-outline')
2007 # suboutline-only is a one-shot for batch commands.
2008 self.node_only = self.suboutline_only = False
2009 self.root = None
2010 if clones:
2011 undoData = u.beforeInsertNode(c.p)
2012 found = self._cfa_create_nodes(clones, flattened=False)
2013 u.afterInsertNode(found, 'Clone Find All', undoData)
2014 assert c.positionExists(found, trace=True), found
2015 c.setChanged()
2016 c.selectPosition(found)
2017 # Put the count in found.h.
2018 found.h = found.h.replace('Found:', f"Found {count}:")
2019 g.es("found", count, "matches for", self.find_text)
2020 return count # Might be useful for the gui update.
2021 #@+node:ekr.20210110073117.34: *5* find._cfa_create_nodes
2022 def _cfa_create_nodes(self, clones, flattened):
2023 """
2024 Create a "Found" node as the last node of the outline.
2025 Clone all positions in the clones set a children of found.
2026 """
2027 c = self.c
2028 # Create the found node.
2029 assert c.positionExists(c.lastTopLevel()), c.lastTopLevel()
2030 found = c.lastTopLevel().insertAfter()
2031 assert found
2032 assert c.positionExists(found), found
2033 found.h = f"Found:{self.find_text}"
2034 status = self.compute_result_status(find_all_flag=True)
2035 status = status.strip().lstrip('(').rstrip(')').strip()
2036 flat = 'flattened, ' if flattened else ''
2037 found.b = f"@nosearch\n\n# {flat}{status}\n\n# found {len(clones)} nodes"
2038 # Clone nodes as children of the found node.
2039 for p in clones:
2040 # Create the clone directly as a child of found.
2041 p2 = p.copy()
2042 n = found.numberOfChildren()
2043 p2._linkCopiedAsNthChild(found, n)
2044 # Sort the clones in place, without undo.
2045 found.v.children.sort(key=lambda v: v.h.lower())
2046 return found
2047 #@+node:ekr.20210110073117.10: *5* find._cfa_find_next_match (for unit tests)
2048 def _cfa_find_next_match(self, p):
2049 """
2050 Find the next batch match at p.
2051 """
2052 # Called only from unit tests.
2053 table = []
2054 if self.search_headline:
2055 table.append(p.h)
2056 if self.search_body:
2057 table.append(p.b)
2058 for s in table:
2059 self.reverse = False
2060 pos, newpos = self.inner_search_helper(s, 0, len(s), self.find_text)
2061 if pos != -1:
2062 return True
2063 return False
2064 #@+node:ekr.20031218072017.3070: *4* find.change_selection
2065 def change_selection(self, p):
2066 """Replace selection with self.change_text."""
2067 c, p, u = self.c, self.c.p, self.c.undoer
2068 wrapper = c.frame.body and c.frame.body.wrapper
2069 gui_w = c.edit_widget(p) if self.in_headline else wrapper
2070 if not gui_w: # pragma: no cover
2071 self.in_headline = False
2072 gui_w = wrapper
2073 if not gui_w: # pragma: no cover
2074 return False
2075 oldSel = sel = gui_w.getSelectionRange()
2076 start, end = sel
2077 if start > end: # pragma: no cover
2078 start, end = end, start
2079 if start == end: # pragma: no cover
2080 g.es("no text selected")
2081 return False
2082 bunch = u.beforeChangeBody(p)
2083 start, end = oldSel
2084 change_text = self.change_text
2085 # Perform regex substitutions of \1, \2, ...\9 in the change text.
2086 if self.pattern_match and self.match_obj:
2087 groups = self.match_obj.groups()
2088 if groups:
2089 change_text = self.make_regex_subs(change_text, groups)
2090 change_text = self.replace_back_slashes(change_text)
2091 # Update both the gui widget and the work "widget"
2092 new_ins = start if self.reverse else start + len(change_text)
2093 if start != end:
2094 gui_w.delete(start, end)
2095 gui_w.insert(start, change_text)
2096 gui_w.setInsertPoint(new_ins)
2097 self.work_s = gui_w.getAllText() # #2220.
2098 self.work_sel = (new_ins, new_ins, new_ins)
2099 # Update the selection for the next match.
2100 gui_w.setSelectionRange(start, start + len(change_text))
2101 c.widgetWantsFocus(gui_w)
2102 # No redraws here: they would destroy the headline selection.
2103 if self.mark_changes: # pragma: no cover
2104 p.setMarked()
2105 p.setDirty()
2106 if self.in_headline:
2107 # #2220: Let onHeadChanged handle undo, etc.
2108 c.frame.tree.onHeadChanged(p, undoType='Change Headline')
2109 # gui_w will change after a redraw.
2110 gui_w = c.edit_widget(p)
2111 if gui_w:
2112 # find-next and find-prev work regardless of insert point.
2113 gui_w.setSelectionRange(start, start + len(change_text))
2114 else:
2115 p.v.b = gui_w.getAllText()
2116 u.afterChangeBody(p, 'Change Body', bunch)
2117 c.frame.tree.updateIcon(p) # redraw only the icon.
2118 return True
2119 #@+node:ekr.20210110073117.31: *4* find.check_args
2120 def check_args(self, tag):
2121 """Check the user arguments to a command."""
2122 if not self.search_headline and not self.search_body:
2123 if not g.unitTesting:
2124 g.es_print("not searching headline or body") # pragma: no cover (skip)
2125 return False
2126 if not self.find_text:
2127 if not g.unitTesting:
2128 g.es_print(f"{tag}: empty find pattern") # pragma: no cover (skip)
2129 return False
2130 return True
2131 #@+node:ekr.20210110073117.32: *4* find.compile_pattern
2132 def compile_pattern(self):
2133 """Precompile the regexp pattern if necessary."""
2134 try: # Precompile the regexp.
2135 # pylint: disable=no-member
2136 flags = re.MULTILINE
2137 if self.ignore_case:
2138 flags |= re.IGNORECASE
2139 # Escape the search text.
2140 # Ignore the whole_word option.
2141 s = self.find_text
2142 # A bad idea: insert \b automatically.
2143 # b, s = '\\b', self.find_text
2144 # if self.whole_word:
2145 # if not s.startswith(b): s = b + s
2146 # if not s.endswith(b): s = s + b
2147 self.re_obj = re.compile(s, flags)
2148 return True
2149 except Exception:
2150 if not g.unitTesting: # pragma: no cover (skip)
2151 g.warning('invalid regular expression:', self.find_text)
2152 return False
2153 #@+node:ekr.20031218072017.3075: *4* find.find_next_match & helpers
2154 def find_next_match(self, p):
2155 """
2156 Resume the search where it left off.
2158 Return (p, pos, newpos).
2159 """
2160 c = self.c
2161 if not self.search_headline and not self.search_body: # pragma: no cover
2162 return None, None, None
2163 if not self.find_text: # pragma: no cover
2164 return None, None, None
2165 attempts = 0
2166 if self.pattern_match:
2167 ok = self.precompile_pattern()
2168 if not ok:
2169 return None, None, None
2170 while p:
2171 pos, newpos = self._fnm_search(p)
2172 if pos is not None:
2173 # Success.
2174 if self.mark_finds: # pragma: no cover
2175 p.setMarked()
2176 p.setDirty()
2177 if not self.changeAllFlag:
2178 c.frame.tree.updateIcon(p) # redraw only the icon.
2179 return p, pos, newpos
2180 # Searching the pane failed: switch to another pane or node.
2181 if self._fnm_should_stay_in_node(p):
2182 # Switching panes is possible. Do so.
2183 self.in_headline = not self.in_headline
2184 s = p.h if self.in_headline else p.b
2185 ins = len(s) if self.reverse else 0
2186 self.work_s = s
2187 self.work_sel = (ins, ins, ins)
2188 else:
2189 # Switch to the next/prev node, if possible.
2190 attempts += 1
2191 p = self._fnm_next_after_fail(p)
2192 if p: # Found another node: select the proper pane.
2193 self.in_headline = self._fnm_first_search_pane()
2194 s = p.h if self.in_headline else p.b
2195 ins = len(s) if self.reverse else 0
2196 self.work_s = s
2197 self.work_sel = (ins, ins, ins)
2198 return None, None, None
2199 #@+node:ekr.20131123132043.16476: *5* find._fnm_next_after_fail & helper
2200 def _fnm_next_after_fail(self, p):
2201 """Return the next node after a failed search or None."""
2202 # Move to the next position.
2203 p = p.threadBack() if self.reverse else p.threadNext()
2204 # Check it.
2205 if p and self._fail_outside_range(p): # pragma: no cover
2206 return None
2207 if not p: # pragma: no cover
2208 return None
2209 return p
2210 #@+node:ekr.20131123071505.16465: *6* find._fail_outside_range
2211 def _fail_outside_range(self, p): # pragma: no cover
2212 """
2213 Return True if the search is about to go outside its range, assuming
2214 both the headline and body text of the present node have been searched.
2215 """
2216 c = self.c
2217 if not p:
2218 return True
2219 if self.node_only:
2220 return True
2221 if self.suboutline_only:
2222 if self.root and p != self.root and not self.root.isAncestorOf(p):
2223 return True
2224 if c.hoistStack:
2225 bunch = c.hoistStack[-1]
2226 if not bunch.p.isAncestorOf(p):
2227 g.trace('outside hoist', p.h)
2228 g.warning('found match outside of hoisted outline')
2229 return True
2230 return False # Within range.
2231 #@+node:ekr.20131124060912.16473: *5* find._fnm_first_search_pane
2232 def _fnm_first_search_pane(self):
2233 """
2234 Set return the value of self.in_headline
2235 indicating which pane to search first.
2236 """
2237 if self.search_headline and self.search_body:
2238 # Fix bug 1228458: Inconsistency between Find-forward and Find-backward.
2239 if self.reverse:
2240 return False # Search the body pane first.
2241 return True # Search the headline pane first.
2242 if self.search_headline or self.search_body:
2243 # Search the only enabled pane.
2244 return self.search_headline
2245 g.trace('can not happen: no search enabled')
2246 return False # pragma: no cover (now search the body)
2247 #@+node:ekr.20031218072017.3077: *5* find._fnm_search
2248 def _fnm_search(self, p):
2249 """
2250 Search self.work_s for self.find_text with present options.
2251 Returns (pos, newpos) or (None, dNone).
2252 """
2253 index = self.work_sel[2]
2254 s = self.work_s
2255 if sys.platform.lower().startswith('win'):
2256 s = s.replace('\r', '')
2257 # Ignore '\r' characters, which may appear in @edit nodes.
2258 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4
2259 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!)
2260 if not s: # pragma: no cover
2261 return None, None
2262 stopindex = 0 if self.reverse else len(s)
2263 pos, newpos = self.inner_search_helper(s, index, stopindex, self.find_text)
2264 if self.in_headline and not self.search_headline: # pragma: no cover
2265 return None, None
2266 if not self.in_headline and not self.search_body: # pragma: no cover
2267 return None, None
2268 if pos == -1: # pragma: no cover
2269 return None, None
2270 ins = min(pos, newpos) if self.reverse else max(pos, newpos)
2271 self.work_sel = (pos, newpos, ins)
2272 return pos, newpos
2273 #@+node:ekr.20131124060912.16472: *5* find._fnm_should_stay_in_node
2274 def _fnm_should_stay_in_node(self, p):
2275 """Return True if the find should simply switch panes."""
2276 # Errors here cause the find command to fail badly.
2277 # Switch only if:
2278 # a) searching both panes and,
2279 # b) this is the first pane of the pair.
2280 # There is *no way* this can ever change.
2281 # So simple in retrospect, so difficult to see.
2282 return (
2283 self.search_headline and self.search_body and (
2284 (self.reverse and not self.in_headline) or
2285 (not self.reverse and self.in_headline)))
2286 #@+node:ekr.20210110073117.43: *4* find.inner_search_helper & helpers
2287 def inner_search_helper(self, s, i, j, pattern):
2288 """
2289 Dispatch the proper search method based on settings.
2290 """
2291 backwards = self.reverse
2292 nocase = self.ignore_case
2293 regexp = self.pattern_match
2294 word = self.whole_word
2295 if backwards:
2296 i, j = j, i
2297 if not s[i:j] or not pattern:
2298 return -1, -1
2299 if regexp:
2300 pos, newpos = self._inner_search_regex(s, i, j, pattern, backwards, nocase)
2301 elif backwards:
2302 pos, newpos = self._inner_search_backward(s, i, j, pattern, nocase, word)
2303 else:
2304 pos, newpos = self._inner_search_plain(s, i, j, pattern, nocase, word)
2305 return pos, newpos
2306 #@+node:ekr.20210110073117.44: *5* find._inner_search_backward
2307 def _inner_search_backward(self, s, i, j, pattern, nocase, word):
2308 """
2309 rfind(sub [,start [,end]])
2311 Return the highest index in the string where substring sub is found,
2312 such that sub is contained within s[start,end].
2314 Optional arguments start and end are interpreted as in slice notation.
2316 Return (-1, -1) on failure.
2317 """
2318 if nocase:
2319 s = s.lower()
2320 pattern = pattern.lower()
2321 pattern = self.replace_back_slashes(pattern)
2322 n = len(pattern)
2323 # Put the indices in range. Indices can get out of range
2324 # because the search code strips '\r' characters when searching @edit nodes.
2325 i = max(0, i)
2326 j = min(len(s), j)
2327 # short circuit the search: helps debugging.
2328 if s.find(pattern) == -1:
2329 return -1, -1
2330 if word:
2331 while 1:
2332 k = s.rfind(pattern, i, j)
2333 if k == -1:
2334 break
2335 if self._inner_search_match_word(s, k, pattern):
2336 return k, k + n
2337 j = max(0, k - 1)
2338 return -1, -1
2339 k = s.rfind(pattern, i, j)
2340 if k == -1:
2341 return -1, -1
2342 return k, k + n
2343 #@+node:ekr.20210110073117.45: *5* find._inner_search_match_word
2344 def _inner_search_match_word(self, s, i, pattern):
2345 """Do a whole-word search."""
2346 pattern = self.replace_back_slashes(pattern)
2347 if not s or not pattern or not g.match(s, i, pattern):
2348 return False
2349 pat1, pat2 = pattern[0], pattern[-1]
2350 n = len(pattern)
2351 ch1 = s[i - 1] if 0 <= i - 1 < len(s) else '.'
2352 ch2 = s[i + n] if 0 <= i + n < len(s) else '.'
2353 isWordPat1 = g.isWordChar(pat1)
2354 isWordPat2 = g.isWordChar(pat2)
2355 isWordCh1 = g.isWordChar(ch1)
2356 isWordCh2 = g.isWordChar(ch2)
2357 inWord = isWordPat1 and isWordCh1 or isWordPat2 and isWordCh2
2358 return not inWord
2359 #@+node:ekr.20210110073117.46: *5* find._inner_search_plain
2360 def _inner_search_plain(self, s, i, j, pattern, nocase, word):
2361 """Do a plain search."""
2362 if nocase:
2363 s = s.lower()
2364 pattern = pattern.lower()
2365 pattern = self.replace_back_slashes(pattern)
2366 n = len(pattern)
2367 if word:
2368 while 1:
2369 k = s.find(pattern, i, j)
2370 if k == -1:
2371 break
2372 if self._inner_search_match_word(s, k, pattern):
2373 return k, k + n
2374 i = k + n
2375 return -1, -1
2376 k = s.find(pattern, i, j)
2377 if k == -1:
2378 return -1, -1
2379 return k, k + n
2380 #@+node:ekr.20210110073117.47: *5* find._inner_search_regex
2381 def _inner_search_regex(self, s, i, j, pattern, backwards, nocase):
2382 """Called from inner_search_helper"""
2383 re_obj = self.re_obj # Use the pre-compiled object
2384 if not re_obj:
2385 if not g.unitTesting: # pragma: no cover (skip)
2386 g.trace('can not happen: no re_obj')
2387 return -1, -1
2388 if backwards:
2389 # Scan to the last match using search here.
2390 i, last_mo = 0, None
2391 while i < len(s):
2392 mo = re_obj.search(s, i, j)
2393 if not mo:
2394 break
2395 i += 1
2396 last_mo = mo
2397 mo = last_mo
2398 else:
2399 mo = re_obj.search(s, i, j)
2400 if mo:
2401 self.match_obj = mo
2402 return mo.start(), mo.end()
2403 self.match_obj = None
2404 return -1, -1
2405 #@+node:ekr.20210110073117.48: *4* find.make_regex_subs
2406 def make_regex_subs(self, change_text, groups):
2407 """
2408 Substitute group[i-1] for \\i strings in change_text.
2410 Groups is a tuple of strings, one for every matched group.
2411 """
2413 # g.printObj(list(groups), tag=f"groups in {change_text!r}")
2415 def repl(match_object):
2416 """re.sub calls this function once per group."""
2417 # # 1494...
2418 n = int(match_object.group(1)) - 1
2419 if 0 <= n < len(groups):
2420 # Executed only if the change text contains groups that match.
2421 return (
2422 groups[n].
2423 replace(r'\b', r'\\b').
2424 replace(r'\f', r'\\f').
2425 replace(r'\n', r'\\n').
2426 replace(r'\r', r'\\r').
2427 replace(r'\t', r'\\t').
2428 replace(r'\v', r'\\v'))
2429 # No replacement.
2430 return match_object.group(0)
2432 result = re.sub(r'\\([0-9])', repl, change_text)
2433 return result
2434 #@+node:ekr.20131123071505.16467: *4* find.precompile_pattern
2435 def precompile_pattern(self):
2436 """Precompile the regexp pattern if necessary."""
2437 try: # Precompile the regexp.
2438 # pylint: disable=no-member
2439 flags = re.MULTILINE
2440 if self.ignore_case:
2441 flags |= re.IGNORECASE
2442 # Escape the search text.
2443 # Ignore the whole_word option.
2444 s = self.find_text
2445 # A bad idea: insert \b automatically.
2446 # b, s = '\\b', self.find_text
2447 # if self.whole_word:
2448 # if not s.startswith(b): s = b + s
2449 # if not s.endswith(b): s = s + b
2450 self.re_obj = re.compile(s, flags)
2451 return True
2452 except Exception:
2453 if not g.unitTesting:
2454 g.warning('invalid regular expression:', self.find_text)
2455 return False
2456 #@+node:ekr.20210110073117.49: *4* find.replace_back_slashes
2457 def replace_back_slashes(self, s):
2458 """Carefully replace backslashes in a search pattern."""
2459 # This is NOT the same as:
2460 #
2461 # s.replace('\\n','\n').replace('\\t','\t').replace('\\\\','\\')
2462 #
2463 # because there is no rescanning.
2464 i = 0
2465 while i + 1 < len(s):
2466 if s[i] == '\\':
2467 ch = s[i + 1]
2468 if ch == '\\':
2469 s = s[:i] + s[i + 1 :] # replace \\ by \
2470 elif ch == 'n':
2471 s = s[:i] + '\n' + s[i + 2 :] # replace the \n by a newline
2472 elif ch == 't':
2473 s = s[:i] + '\t' + s[i + 2 :] # replace \t by a tab
2474 else:
2475 i += 1 # Skip the escaped character.
2476 i += 1
2477 return s
2478 #@+node:ekr.20031218072017.3082: *3* LeoFind.Initing & finalizing
2479 #@+node:ekr.20031218072017.3086: *4* find.init_in_headline & helper
2480 def init_in_headline(self):
2481 """
2482 Select the first pane to search for incremental searches and changes.
2483 This is called only at the start of each search.
2484 This must not alter the current insertion point or selection range.
2485 """
2486 #
2487 # Fix bug 1228458: Inconsistency between Find-forward and Find-backward.
2488 if self.search_headline and self.search_body:
2489 # We have no choice: we *must* search the present widget!
2490 self.in_headline = self.focus_in_tree()
2491 else:
2492 self.in_headline = self.search_headline
2493 #@+node:ekr.20131126085250.16651: *5* find.focus_in_tree
2494 def focus_in_tree(self):
2495 """
2496 Return True is the focus widget w is anywhere in the tree pane.
2498 Note: the focus may be in the find pane.
2499 """
2500 c = self.c
2501 ftm = self.ftm
2502 w = ftm and ftm.entry_focus or g.app.gui.get_focus(raw=True)
2503 if ftm:
2504 ftm.entry_focus = None # Only use this focus widget once!
2505 w_name = c.widget_name(w)
2506 if w == c.frame.body.wrapper:
2507 val = False
2508 elif w == c.frame.tree.treeWidget: # pragma: no cover
2509 val = True
2510 else:
2511 val = w_name.startswith('head') # pragma: no cover
2512 return val
2513 #@+node:ekr.20031218072017.3089: *4* find.restore
2514 def restore(self, data):
2515 """
2516 Restore Leo's gui and settings from data, a g.Bunch.
2517 """
2518 c, p = self.c, data.p
2519 c.frame.bringToFront() # Needed on the Mac
2520 if not p or not c.positionExists(p): # pragma: no cover
2521 # Better than selecting the root!
2522 return
2523 c.selectPosition(p)
2524 # Fix bug 1258373: https://bugs.launchpad.net/leo-editor/+bug/1258373
2525 if self.in_headline:
2526 c.treeWantsFocus()
2527 else:
2528 # Looks good and provides clear indication of failure or termination.
2529 w = c.frame.body.wrapper
2530 w.setSelectionRange(data.start, data.end, insert=data.insert)
2531 w.seeInsertPoint()
2532 c.widgetWantsFocus(w)
2533 #@+node:ekr.20031218072017.3090: *4* find.save
2534 def save(self):
2535 """Save everything needed to restore after a search fails."""
2536 c = self.c
2537 if self.in_headline: # pragma: no cover
2538 # Fix bug 1258373: https://bugs.launchpad.net/leo-editor/+bug/1258373
2539 # Don't try to re-edit the headline.
2540 insert, start, end = None, None, None
2541 else:
2542 w = c.frame.body.wrapper
2543 insert = w.getInsertPoint()
2544 start, end = w.getSelectionRange()
2545 data = g.Bunch(
2546 end=end,
2547 in_headline=self.in_headline,
2548 insert=insert,
2549 p=c.p.copy(),
2550 start=start,
2551 )
2552 return data
2553 #@+node:ekr.20031218072017.3091: *4* find.show_success
2554 def show_success(self, p, pos, newpos, showState=True):
2555 """Display the result of a successful find operation."""
2556 c = self.c
2557 # Set state vars.
2558 # Ensure progress in backwards searches.
2559 insert = min(pos, newpos) if self.reverse else max(pos, newpos)
2560 if c.sparse_find: # pragma: no cover
2561 c.expandOnlyAncestorsOfNode(p=p)
2562 if self.in_headline:
2563 c.endEditing()
2564 c.redraw(p)
2565 c.frame.tree.editLabel(p)
2566 w = c.edit_widget(p) # #2220
2567 if w:
2568 w.setSelectionRange(pos, newpos, insert) # #2220
2569 else:
2570 # Tricky code. Do not change without careful thought.
2571 w = c.frame.body.wrapper
2572 # *Always* do the full selection logic.
2573 # This ensures that the body text is inited and recolored.
2574 c.selectPosition(p)
2575 c.bodyWantsFocus()
2576 if showState:
2577 c.k.showStateAndMode(w)
2578 c.bodyWantsFocusNow()
2579 w.setSelectionRange(pos, newpos, insert=insert)
2580 k = g.see_more_lines(w.getAllText(), insert, 4)
2581 w.see(k)
2582 # #78: find-next match not always scrolled into view.
2583 c.outerUpdate()
2584 # Set the focus immediately.
2585 if c.vim_mode and c.vimCommands: # pragma: no cover
2586 c.vimCommands.update_selection_after_search()
2587 # Support for the console gui.
2588 if hasattr(g.app.gui, 'show_find_success'): # pragma: no cover
2589 g.app.gui.show_find_success(c, self.in_headline, insert, p)
2590 c.frame.bringToFront()
2591 return w # Support for isearch.
2592 #@+node:ekr.20131117164142.16939: *3* LeoFind.ISearch
2593 #@+node:ekr.20210112192011.1: *4* LeoFind.Isearch commands
2594 #@+node:ekr.20131117164142.16941: *5* find.isearch_forward
2595 @cmd('isearch-forward')
2596 def isearch_forward(self, event): # pragma: no cover (cmd)
2597 """
2598 Begin a forward incremental search.
2600 - Plain characters extend the search.
2601 - !<isearch-forward>! repeats the search.
2602 - Esc or any non-plain key ends the search.
2603 - Backspace reverses the search.
2604 - Backspacing to an empty search pattern
2605 completely undoes the effect of the search.
2606 """
2607 self.start_incremental(event, 'isearch-forward',
2608 forward=True, ignoreCase=False, regexp=False)
2609 #@+node:ekr.20131117164142.16942: *5* find.isearch_backward
2610 @cmd('isearch-backward')
2611 def isearch_backward(self, event): # pragma: no cover (cmd)
2612 """
2613 Begin a backward incremental search.
2615 - Plain characters extend the search backward.
2616 - !<isearch-forward>! repeats the search.
2617 - Esc or any non-plain key ends the search.
2618 - Backspace reverses the search.
2619 - Backspacing to an empty search pattern
2620 completely undoes the effect of the search.
2621 """
2622 self.start_incremental(event, 'isearch-backward',
2623 forward=False, ignoreCase=False, regexp=False)
2624 #@+node:ekr.20131117164142.16943: *5* find.isearch_forward_regexp
2625 @cmd('isearch-forward-regexp')
2626 def isearch_forward_regexp(self, event): # pragma: no cover (cmd)
2627 """
2628 Begin a forward incremental regexp search.
2630 - Plain characters extend the search.
2631 - !<isearch-forward-regexp>! repeats the search.
2632 - Esc or any non-plain key ends the search.
2633 - Backspace reverses the search.
2634 - Backspacing to an empty search pattern
2635 completely undoes the effect of the search.
2636 """
2637 self.start_incremental(event, 'isearch-forward-regexp',
2638 forward=True, ignoreCase=False, regexp=True)
2639 #@+node:ekr.20131117164142.16944: *5* find.isearch_backward_regexp
2640 @cmd('isearch-backward-regexp')
2641 def isearch_backward_regexp(self, event): # pragma: no cover (cmd)
2642 """
2643 Begin a backward incremental regexp search.
2645 - Plain characters extend the search.
2646 - !<isearch-forward-regexp>! repeats the search.
2647 - Esc or any non-plain key ends the search.
2648 - Backspace reverses the search.
2649 - Backspacing to an empty search pattern
2650 completely undoes the effect of the search.
2651 """
2652 self.start_incremental(event, 'isearch-backward-regexp',
2653 forward=False, ignoreCase=False, regexp=True)
2654 #@+node:ekr.20131117164142.16945: *5* find.isearch_with_present_options
2655 @cmd('isearch-with-present-options')
2656 def isearch_with_present_options(self, event): # pragma: no cover (cmd)
2657 """
2658 Begin an incremental search using find panel options.
2660 - Plain characters extend the search.
2661 - !<isearch-forward-regexp>! repeats the search.
2662 - Esc or any non-plain key ends the search.
2663 - Backspace reverses the search.
2664 - Backspacing to an empty search pattern
2665 completely undoes the effect of the search.
2666 """
2667 self.start_incremental(event, 'isearch-with-present-options',
2668 forward=None, ignoreCase=None, regexp=None)
2669 #@+node:ekr.20131117164142.16946: *4* LeoFind.Isearch utils
2670 #@+node:ekr.20131117164142.16947: *5* find.abort_search (incremental)
2671 def abort_search(self): # pragma: no cover (cmd)
2672 """Restore the original position and selection."""
2673 c, k = self.c, self.k
2674 w = c.frame.body.wrapper
2675 k.clearState()
2676 k.resetLabel()
2677 p, i, j, in_headline = self.stack[0]
2678 self.in_headline = in_headline
2679 c.selectPosition(p)
2680 c.redraw_after_select(p)
2681 c.bodyWantsFocus()
2682 w.setSelectionRange(i, j)
2683 #@+node:ekr.20131117164142.16948: *5* find.end_search
2684 def end_search(self): # pragma: no cover (cmd)
2685 c, k = self.c, self.k
2686 k.clearState()
2687 k.resetLabel()
2688 c.bodyWantsFocus()
2689 #@+node:ekr.20131117164142.16949: *5* find.iSearch_helper
2690 def iSearch_helper(self, again=False): # pragma: no cover (cmd)
2691 """Handle the actual incremental search."""
2692 c, k, p = self.c, self.k, self.c.p
2693 reverse = not self.isearch_forward_flag
2694 pattern = k.getLabel(ignorePrompt=True)
2695 if not pattern:
2696 self.abort_search()
2697 return
2698 # Settings...
2699 self.find_text = self.ftm.get_find_text()
2700 self.change_text = self.ftm.get_change_text()
2701 # Save
2702 oldPattern = self.find_text
2703 oldRegexp = self.pattern_match
2704 oldWord = self.whole_word
2705 # Override
2706 self.pattern_match = self.isearch_regexp
2707 self.reverse = reverse
2708 self.find_text = pattern
2709 self.whole_word = False # Word option can't be used!
2710 # Prepare the search.
2711 if len(self.stack) <= 1:
2712 self.in_headline = False
2713 # Init the work widget from the gui widget.
2714 gui_w = self.set_widget()
2715 s = gui_w.getAllText()
2716 i, j = gui_w.getSelectionRange()
2717 if again:
2718 ins = i if reverse else j + len(pattern)
2719 else:
2720 ins = j + len(pattern) if reverse else i
2721 self.work_s = s
2722 self.work_sel = (ins, ins, ins)
2723 # Do the search!
2724 p, pos, newpos = self.find_next_match(p)
2725 # Restore.
2726 self.find_text = oldPattern
2727 self.pattern_match = oldRegexp
2728 self.reverse = False
2729 self.whole_word = oldWord
2730 # Handle the results of the search.
2731 if pos is not None: # success.
2732 w = self.show_success(p, pos, newpos, showState=False)
2733 if w:
2734 i, j = w.getSelectionRange(sort=False)
2735 if not again:
2736 self.push(c.p, i, j, self.in_headline)
2737 else:
2738 g.es(f"not found: {pattern}")
2739 if not again:
2740 event = g.app.gui.create_key_event(
2741 c, binding='BackSpace', char='\b', w=w)
2742 k.updateLabel(event)
2743 #@+node:ekr.20131117164142.16950: *5* find.isearch_state_handler
2744 def isearch_state_handler(self, event): # pragma: no cover (cmd)
2745 """The state manager when the state is 'isearch"""
2746 # c = self.c
2747 k = self.k
2748 stroke = event.stroke if event else None
2749 s = stroke.s if stroke else ''
2750 # No need to recognize ctrl-z.
2751 if s in ('Escape', '\n', 'Return'):
2752 self.end_search()
2753 elif stroke in self.iSearchStrokes:
2754 self.iSearch_helper(again=True)
2755 elif s in ('\b', 'BackSpace'):
2756 k.updateLabel(event)
2757 self.isearch_backspace()
2758 elif (
2759 s.startswith('Ctrl+') or
2760 s.startswith('Alt+') or
2761 k.isFKey(s) # 2011/06/13.
2762 ):
2763 # End the search.
2764 self.end_search()
2765 k.masterKeyHandler(event)
2766 # Fix bug 1267921: isearch-forward accepts non-alphanumeric keys as input.
2767 elif k.isPlainKey(stroke):
2768 k.updateLabel(event)
2769 self.iSearch_helper()
2770 #@+node:ekr.20131117164142.16951: *5* find.isearch_backspace
2771 def isearch_backspace(self): # pragma: no cover (cmd)
2773 c = self.c
2774 if len(self.stack) <= 1:
2775 self.abort_search()
2776 return
2777 # Reduce the stack by net 1.
2778 self.pop()
2779 p, i, j, in_headline = self.pop()
2780 self.push(p, i, j, in_headline)
2781 if in_headline:
2782 # Like self.show_success.
2783 selection = i, j, i
2784 c.redrawAndEdit(p, selectAll=False,
2785 selection=selection,
2786 keepMinibuffer=True)
2787 else:
2788 c.selectPosition(p)
2789 w = c.frame.body.wrapper
2790 c.bodyWantsFocus()
2791 if i > j:
2792 i, j = j, i
2793 w.setSelectionRange(i, j)
2794 if len(self.stack) <= 1:
2795 self.abort_search()
2796 #@+node:ekr.20131117164142.16952: *5* find.get_strokes
2797 def get_strokes(self, commandName): # pragma: no cover (cmd)
2798 aList = self.inverseBindingDict.get(commandName, [])
2799 return [key for pane, key in aList]
2800 #@+node:ekr.20131117164142.16953: *5* find.push & pop
2801 def push(self, p, i, j, in_headline): # pragma: no cover (cmd)
2802 data = p.copy(), i, j, in_headline
2803 self.stack.append(data)
2805 def pop(self): # pragma: no cover (cmd)
2806 data = self.stack.pop()
2807 p, i, j, in_headline = data
2808 return p, i, j, in_headline
2809 #@+node:ekr.20131117164142.16954: *5* find.set_widget
2810 def set_widget(self): # pragma: no cover (cmd)
2811 c, p = self.c, self.c.p
2812 wrapper = c.frame.body.wrapper
2813 if self.in_headline:
2814 w = c.edit_widget(p)
2815 if not w:
2816 # Selecting the minibuffer can kill the edit widget.
2817 selection = 0, 0, 0
2818 c.redrawAndEdit(p, selectAll=False,
2819 selection=selection, keepMinibuffer=True)
2820 w = c.edit_widget(p)
2821 if not w: # Should never happen.
2822 g.trace('**** no edit widget!')
2823 self.in_headline = False
2824 w = wrapper
2825 else:
2826 w = wrapper
2827 if w == wrapper:
2828 c.bodyWantsFocus()
2829 return w
2830 #@+node:ekr.20131117164142.16955: *5* find.start_incremental
2831 def start_incremental(self, event, commandName, forward, ignoreCase, regexp): # pragma: no cover (cmd)
2832 c, k = self.c, self.k
2833 # None is a signal to get the option from the find tab.
2834 self.event = event
2835 self.isearch_forward_flag = not self.reverse if forward is None else forward
2836 self.isearch_ignore_case = self.ignore_case if ignoreCase is None else ignoreCase
2837 self.isearch_regexp = self.pattern_match if regexp is None else regexp
2838 # Note: the word option can't be used with isearches!
2839 w = c.frame.body.wrapper
2840 self.p1 = c.p
2841 self.sel1 = w.getSelectionRange(sort=False)
2842 i, j = self.sel1
2843 self.push(c.p, i, j, self.in_headline)
2844 self.inverseBindingDict = k.computeInverseBindingDict()
2845 self.iSearchStrokes = self.get_strokes(commandName)
2846 k.setLabelBlue(
2847 "Isearch"
2848 f"{' Backward' if not self.isearch_forward_flag else ''}"
2849 f"{' Regexp' if self.isearch_regexp else ''}"
2850 f"{' NoCase' if self.isearch_ignore_case else ''}"
2851 ": "
2852 )
2853 k.setState('isearch', 1, handler=self.isearch_state_handler)
2854 c.minibufferWantsFocus()
2855 #@+node:ekr.20031218072017.3067: *3* LeoFind.Utils
2856 #@+node:ekr.20131117164142.16992: *4* find.add_change_string_to_label
2857 def add_change_string_to_label(self): # pragma: no cover (cmd)
2858 """Add an unprotected change string to the minibuffer label."""
2859 c = self.c
2860 s = self.ftm.get_change_text()
2861 c.minibufferWantsFocus()
2862 while s.endswith('\n') or s.endswith('\r'):
2863 s = s[:-1]
2864 c.k.extendLabel(s, select=True, protect=False)
2865 #@+node:ekr.20131117164142.16993: *4* find.add_find_string_to_label
2866 def add_find_string_to_label(self, protect=True): # pragma: no cover (cmd)
2867 c, k = self.c, self.c.k
2868 ftm = c.findCommands.ftm
2869 s = ftm.get_find_text()
2870 c.minibufferWantsFocus()
2871 while s.endswith('\n') or s.endswith('\r'):
2872 s = s[:-1]
2873 k.extendLabel(s, select=True, protect=protect)
2874 #@+node:ekr.20210110073117.33: *4* find.compute_result_status
2875 def compute_result_status(self, find_all_flag=False): # pragma: no cover (cmd)
2876 """Return the status to be shown in the status line after a find command completes."""
2877 # Too similar to another method...
2878 status = []
2879 table = (
2880 ('whole_word', 'Word'),
2881 ('ignore_case', 'Ignore Case'),
2882 ('pattern_match', 'Regex'),
2883 ('suboutline_only', '[Outline Only]'),
2884 ('node_only', '[Node Only]'),
2885 ('search_headline', 'Head'),
2886 ('search_body', 'Body'),
2887 )
2888 for ivar, val in table:
2889 if getattr(self, ivar):
2890 status.append(val)
2891 return f" ({', '.join(status)})" if status else ''
2892 #@+node:ekr.20131119204029.16479: *4* find.help_for_find_commands
2893 def help_for_find_commands(self, event=None): # pragma: no cover (cmd)
2894 """Called from Find panel. Redirect."""
2895 self.c.helpCommands.help_for_find_commands(event)
2896 #@+node:ekr.20210111082524.1: *4* find.init_vim_search
2897 def init_vim_search(self, pattern): # pragma: no cover (cmd)
2898 """Initialize searches in vim mode."""
2899 c = self.c
2900 if c.vim_mode and c.vimCommands:
2901 c.vimCommands.update_dot_before_search(
2902 find_pattern=pattern,
2903 change_pattern=None) # A flag.
2904 #@+node:ekr.20150629072547.1: *4* find.preload_find_pattern
2905 def preload_find_pattern(self, w): # pragma: no cover (cmd)
2906 """Preload the find pattern from the selected text of widget w."""
2907 c, ftm = self.c, self.ftm
2908 if not c.config.getBool('preload-find-pattern', default=False):
2909 # Make *sure* we don't preload the find pattern if it is not wanted.
2910 return
2911 if not w:
2912 return
2913 #
2914 # #1436: Don't create a selection if there isn't one.
2915 # Leave the search pattern alone!
2916 #
2917 # if not w.hasSelection():
2918 # c.editCommands.extendToWord(event=None, select=True, w=w)
2919 #
2920 # #177: Use selected text as the find string.
2921 # #1436: Make make sure there is a significant search pattern.
2922 s = w.getSelectedText()
2923 if s.strip():
2924 ftm.set_find_text(s)
2925 ftm.init_focus()
2926 #@+node:ekr.20150619070602.1: *4* find.show_status
2927 def show_status(self, found):
2928 """Show the find status the Find dialog, if present, and the status line."""
2929 c = self.c
2930 status = 'found' if found else 'not found'
2931 options = self.compute_result_status()
2932 s = f"{status}:{options} {self.find_text}"
2933 # Set colors.
2934 found_bg = c.config.getColor('find-found-bg') or 'blue'
2935 not_found_bg = c.config.getColor('find-not-found-bg') or 'red'
2936 found_fg = c.config.getColor('find-found-fg') or 'white'
2937 not_found_fg = c.config.getColor('find-not-found-fg') or 'white'
2938 bg = found_bg if found else not_found_bg
2939 fg = found_fg if found else not_found_fg
2940 if c.config.getBool("show-find-result-in-status") is not False:
2941 c.frame.putStatusLine(s, bg=bg, fg=fg)
2942 #@+node:ekr.20150615174549.1: *4* find.show_find_options_in_status_area & helper
2943 def show_find_options_in_status_area(self): # pragma: no cover (cmd)
2944 """Show find options in the status area."""
2945 c = self.c
2946 s = self.compute_find_options_in_status_area()
2947 c.frame.putStatusLine(s)
2948 #@+node:ekr.20171129211238.1: *5* find.compute_find_options_in_status_area
2949 def compute_find_options_in_status_area(self):
2950 c = self.c
2951 ftm = c.findCommands.ftm
2952 table = (
2953 ('Word', ftm.check_box_whole_word),
2954 ('Ig-case', ftm.check_box_ignore_case),
2955 ('regeXp', ftm.check_box_regexp),
2956 ('Body', ftm.check_box_search_body),
2957 ('Head', ftm.check_box_search_headline),
2958 # ('wrap-Around', ftm.check_box_wrap_around),
2959 ('mark-Changes', ftm.check_box_mark_changes),
2960 ('mark-Finds', ftm.check_box_mark_finds),
2961 )
2962 result = [option for option, ivar in table if ivar.isChecked()]
2963 table2 = (
2964 ('Suboutline', ftm.radio_button_suboutline_only),
2965 ('Node', ftm.radio_button_node_only),
2966 )
2967 for option, ivar in table2:
2968 if ivar.isChecked():
2969 result.append(f"[{option}]")
2970 break
2971 return f"Find: {' '.join(result)}"
2972 #@+node:ekr.20131117164142.17007: *4* find.start_state_machine
2973 def start_state_machine(self, event, prefix, handler, escape_handler=None): # pragma: no cover (cmd)
2974 """
2975 Initialize and start the state machine used to get user arguments.
2976 """
2977 c, k = self.c, self.k
2978 w = c.frame.body.wrapper
2979 if not w:
2980 return
2981 # Gui...
2982 k.setLabelBlue(prefix)
2983 # New in Leo 5.2: minibuffer modes shows options in status area.
2984 if self.minibuffer_mode:
2985 self.show_find_options_in_status_area()
2986 elif c.config.getBool('use-find-dialog', default=True):
2987 g.app.gui.openFindDialog(c)
2988 else:
2989 c.frame.log.selectTab('Find')
2990 self.add_find_string_to_label(protect=False)
2991 k.getArgEscapes = ['\t'] if escape_handler else []
2992 self.handler = handler
2993 self.escape_handler = escape_handler
2994 # Start the state maching!
2995 k.get1Arg(event, handler=self.state0, tabList=self.findTextList, completion=True)
2997 def state0(self, event): # pragma: no cover (cmd)
2998 """Dispatch the next handler."""
2999 k = self.k
3000 if k.getArgEscapeFlag:
3001 k.getArgEscapeFlag = False
3002 self.escape_handler(event)
3003 else:
3004 self.handler(event)
3005 #@+node:ekr.20131117164142.17008: *4* find.updateChange/FindList
3006 def update_change_list(self, s): # pragma: no cover (cmd)
3007 if s not in self.changeTextList:
3008 self.changeTextList.append(s)
3010 def update_find_list(self, s): # pragma: no cover (cmd)
3011 if s not in self.findTextList:
3012 self.findTextList.append(s)
3013 #@-others
3014#@-others
3015#@@language python
3016#@@tabwidth -4
3017#@@pagewidth 70
3018#@-leo