Coverage for C:\leo.repo\leo-editor\leo\core\leoFind.py: 100%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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): # pragma: no cover
157 # New in 4.11.1.
158 # Must be called when config settings are valid.
159 c = self.c
160 self.reload_settings()
161 # now that configuration settings are valid,
162 # we can finish creating the Find pane.
163 dw = c.frame.top
164 if dw:
165 dw.finishCreateLogPane()
166 #@+node:ekr.20210110073117.4: *4* find.init_ivars_from_settings
167 def init_ivars_from_settings(self, settings):
168 """
169 Initialize all ivars from settings, including required defaults.
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: # pragma: no cover
239 g.es_exception()
240 return 0
241 #@+node:ekr.20210108070948.1: *4* find._batch_change_helper
242 def _batch_change_helper(self, p, find_text, change_text):
244 c, p1, u = self.c, p.copy(), self.c.undoer
245 undoType = 'Batch Change All'
246 # Check...
247 if not find_text: # pragma: no cover
248 return 0
249 if not self.search_headline and not self.search_body:
250 return 0 # pragma: no cover
251 if self.pattern_match:
252 ok = self.precompile_pattern()
253 if not ok: # pragma: no cover
254 return 0
255 # Init...
256 self.find_text = find_text
257 self.change_text = self.replace_back_slashes(change_text)
258 if self.node_only:
259 positions = [p1]
260 elif self.suboutline_only:
261 positions = p1.self_and_subtree()
262 else:
263 positions = c.all_unique_positions()
264 # Init the work widget.
265 s = p.h if self.in_headline else p.b
266 self.work_s = s
267 self.work_sel = (0, 0, 0)
268 # The main loop.
269 u.beforeChangeGroup(p1, undoType)
270 count = 0
271 for p in positions:
272 count_h, count_b = 0, 0
273 undoData = u.beforeChangeNodeContents(p)
274 if self.search_headline:
275 count_h, new_h = self._change_all_search_and_replace(p.h)
276 if count_h:
277 count += count_h
278 p.h = new_h
279 if self.search_body:
280 count_b, new_b = self._change_all_search_and_replace(p.b)
281 if count_b:
282 count += count_b
283 p.b = new_b
284 if count_h or count_b:
285 u.afterChangeNodeContents(p1, 'Replace All', undoData)
286 u.afterChangeGroup(p1, undoType, reportFlag=True)
287 if not g.unitTesting: # pragma: no cover
288 print(f"{count:3}: {find_text:>30} => {change_text}")
289 return count
290 #@+node:ekr.20210108083003.1: *4* find._init_from_dict
291 def _init_from_dict(self, settings):
292 """Initialize ivars from settings (a dict or g.Bunch)."""
293 # The valid ivars and reasonable defaults.
294 valid = dict(
295 ignore_case=False,
296 node_only=False,
297 pattern_match=False,
298 search_body=True,
299 search_headline=True,
300 suboutline_only=False, # Seems safest. # Was True !!!
301 whole_word=True,
302 )
303 # Set ivars to reasonable defaults.
304 for ivar in valid:
305 setattr(self, ivar, valid.get(ivar))
306 # Override ivars from settings.
307 errors = 0
308 for ivar in settings.keys():
309 if ivar in valid:
310 val = settings.get(ivar)
311 if val in (True, False):
312 setattr(self, ivar, val)
313 else: # pragma: no cover
314 g.trace("bad value: {ivar!r} = {val!r}")
315 errors += 1
316 else: # pragma: no cover
317 g.trace(f"ignoring {ivar!r} setting")
318 errors += 1
319 if errors: # pragma: no cover
320 g.printObj(sorted(valid.keys()), tag='valid keys')
321 #@+node:ekr.20210925161148.1: *3* find.interactive_search_helper
322 def interactive_search_helper(self, root=None, settings=None): # pragma: no cover
323 #@+<< docstring: find.interactive_search >>
324 #@+node:ekr.20210925161451.1: *4* << docstring: find.interactive_search >>
325 """
326 Support interactive find.
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 None, None, None
505 # Settings...
506 prefix = 'class' if word[0].isupper() else 'def'
507 find_pattern = prefix + ' ' + word
508 ftm.set_find_text(find_pattern)
509 self._save_before_find_def(p) # Save previous settings.
510 self.init_vim_search(find_pattern)
511 self.update_change_list(self.change_text) # Optional. An edge case.
512 # Do the command!
513 settings = self._compute_find_def_settings(find_pattern)
514 return self.do_find_def(settings, word, strict)
516 def find_def_strict(self, event=None): #pragma: no cover (cmd)
517 """Same as find_def, but don't call _switch_style."""
518 return self.find_def(event=event, strict=True)
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 old_reverse = self.reverse
605 try:
606 # #2161:
607 self.reverse = self.reverse_find_defs
608 # # 2288:
609 self.work_s = p.b
610 if self.reverse_find_defs:
611 self.work_sel = (len(p.b), len(p.b), len(p.b))
612 else:
613 self.work_sel = (0, 0, 0)
614 while True:
615 p, pos, newpos = self.find_next_match(p)
616 found = pos is not None
617 if found or not g.inAtNosearch(p): # do *not* use c.p.
618 break
619 if not found and def_flag and not strict:
620 # Leo 5.7.3: Look for an alternative defintion of function/methods.
621 word2 = self._switch_style(word)
622 if self.reverse_find_defs:
623 # #2161: start at the last position.
624 p = c.lastPosition()
625 else:
626 p = c.rootPosition()
627 if word2:
628 find_pattern = prefix + ' ' + word2
629 find.find_text = find_pattern
630 ftm.set_find_text(find_pattern)
631 # #1592. Ignore hits under control of @nosearch
632 while True:
633 p, pos, newpos = self.find_next_match(p)
634 found = pos is not None
635 if not found or not g.inAtNosearch(p):
636 break
637 finally:
638 self.reverse = old_reverse
639 if found:
640 c.redraw(p)
641 w.setSelectionRange(pos, newpos, insert=newpos)
642 c.bodyWantsFocusNow()
643 return p, pos, newpos
644 self._restore_after_find_def() # Avoid massive confusion!
645 i, j = save_sel
646 c.redraw(old_p)
647 w.setSelectionRange(i, j, insert=ins)
648 c.bodyWantsFocusNow()
649 return None, None, None
650 #@+node:ekr.20150629095511.1: *5* find._restore_after_find_def
651 def _restore_after_find_def(self):
652 """Restore find settings in effect before a find-def command."""
653 # pylint: disable=no-member
654 b = self.find_def_data # A g.Bunch
655 if b:
656 self.ignore_case = b.ignore_case
657 self.pattern_match = b.pattern_match
658 self.search_body = b.search_body
659 self.search_headline = b.search_headline
660 self.whole_word = b.whole_word
661 self.find_def_data = None
662 #@+node:ekr.20150629095633.1: *5* find._save_before_find_def
663 def _save_before_find_def(self, p):
664 """Save the find settings in effect before a find-def command."""
665 if not self.find_def_data:
666 self.find_def_data = g.Bunch(
667 ignore_case=self.ignore_case,
668 p=p.copy(),
669 pattern_match=self.pattern_match,
670 search_body=self.search_body,
671 search_headline=self.search_headline,
672 whole_word=self.whole_word,
673 )
674 #@+node:ekr.20180511045458.1: *5* find._switch_style
675 def _switch_style(self, word):
676 """
677 Switch between camelCase and underscore_style function defintiions.
678 Return None if there would be no change.
679 """
680 s = word
681 if not s:
682 return None
683 if s[0].isupper():
684 return None # Don't convert class names.
685 if s.find('_') > -1:
686 # Convert to CamelCase
687 s = s.lower()
688 while s:
689 i = s.find('_')
690 if i == -1:
691 break
692 s = s[:i] + s[i + 1 :].capitalize()
693 return s
694 # Convert to underscore_style.
695 result = []
696 for i, ch in enumerate(s):
697 if i > 0 and ch.isupper():
698 result.append('_')
699 result.append(ch.lower())
700 s = ''.join(result)
701 return None if s == word else s
702 #@+node:ekr.20031218072017.3063: *4* find.find-next, find-prev & do_find_*
703 @cmd('find-next')
704 def find_next(self, event=None): # pragma: no cover (cmd)
705 """The find-next command."""
706 # Settings...
707 self.reverse = False
708 self.init_in_headline() # Do this *before* creating the settings.
709 settings = self.ftm.get_settings()
710 # Do the command!
711 self.do_find_next(settings)
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 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!)
1112 if sys.platform.lower().startswith('win'):
1113 # Ignore '\r' characters, which may appear in @edit nodes.
1114 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4
1115 s = s.replace('\r', '')
1116 if not s:
1117 return False, None
1118 # Order matters: regex matches ignore whole-word.
1119 if self.pattern_match:
1120 return self._change_all_regex(s)
1121 if self.whole_word:
1122 return self._change_all_word(s)
1123 return self._change_all_plain(s)
1124 #@+node:ekr.20190602151043.4: *7* find._change_all_plain
1125 def _change_all_plain(self, s):
1126 """
1127 Perform all plain find/replace on s.
1128 return (count, new_s)
1129 """
1130 find, change = self.find_text, self.change_text
1131 # #1166: s0 and find0 aren't affected by ignore-case.
1132 s0 = s
1133 find0 = self.replace_back_slashes(find)
1134 if self.ignore_case:
1135 s = s0.lower()
1136 find = find0.lower()
1137 count, prev_i, result = 0, 0, []
1138 while True:
1139 progress = prev_i
1140 # #1166: Scan using s and find.
1141 i = s.find(find, prev_i)
1142 if i == -1:
1143 break
1144 # #1166: Replace using s0 & change.
1145 count += 1
1146 result.append(s0[prev_i:i])
1147 result.append(change)
1148 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!)
1149 assert prev_i > progress, prev_i
1150 # #1166: Complete the result using s0.
1151 result.append(s0[prev_i:])
1152 return count, ''.join(result)
1153 #@+node:ekr.20190602151043.2: *7* find._change_all_regex
1154 def _change_all_regex(self, s):
1155 """
1156 Perform all regex find/replace on s.
1157 return (count, new_s)
1158 """
1159 count, prev_i, result = 0, 0, []
1161 flags = re.MULTILINE
1162 if self.ignore_case:
1163 flags |= re.IGNORECASE
1164 for m in re.finditer(self.find_text, s, flags):
1165 count += 1
1166 i = m.start()
1167 result.append(s[prev_i:i])
1168 # #1748.
1169 groups = m.groups()
1170 if groups:
1171 change_text = self.make_regex_subs(self.change_text, groups)
1172 else:
1173 change_text = self.change_text
1174 result.append(change_text)
1175 prev_i = m.end()
1176 # Compute the result.
1177 result.append(s[prev_i:])
1178 s = ''.join(result)
1179 return count, s
1180 #@+node:ekr.20190602155933.1: *7* find._change_all_word
1181 def _change_all_word(self, s):
1182 """
1183 Perform all whole word find/replace on s.
1184 return (count, new_s)
1185 """
1186 find, change = self.find_text, self.change_text
1187 # #1166: s0 and find0 aren't affected by ignore-case.
1188 s0 = s
1189 find0 = self.replace_back_slashes(find)
1190 if self.ignore_case:
1191 s = s0.lower()
1192 find = find0.lower()
1193 count, prev_i, result = 0, 0, []
1194 while True:
1195 # #1166: Scan using s and find.
1196 i = s.find(find, prev_i)
1197 if i == -1:
1198 break
1199 # #1166: Replace using s0, change & find0.
1200 result.append(s0[prev_i:i])
1201 if g.match_word(s, i, find):
1202 count += 1
1203 result.append(change)
1204 else:
1205 result.append(find0)
1206 prev_i = i + len(find)
1207 # #1166: Complete the result using s0.
1208 result.append(s0[prev_i:])
1209 return count, ''.join(result)
1210 #@+node:ekr.20210110073117.23: *6* new:find.replace_all_helper & helpers (merge & delete)
1211 def replace_all_helper(self, s):
1212 """
1213 Search s for self.find_text and replace with self.change_text.
1215 Return (found, new text)
1216 """
1217 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!)
1218 if sys.platform.lower().startswith('win'):
1219 # Ignore '\r' characters, which may appear in @edit nodes.
1220 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4
1221 s = s.replace('\r', '')
1222 if not s:
1223 return False, None
1224 # Order matters: regex matches ignore whole-word.
1225 if self.pattern_match:
1226 return self.batch_regex_replace(s)
1227 if self.whole_word:
1228 return self.batch_word_replace(s)
1229 return self.batch_plain_replace(s)
1230 #@+node:ekr.20210110073117.24: *7* new:find.batch_plain_replace
1231 def batch_plain_replace(self, s):
1232 """
1233 Perform all plain find/replace on s.
1234 return (count, new_s)
1235 """
1236 find, change = self.find_text, self.change_text
1237 # #1166: s0 and find0 aren't affected by ignore-case.
1238 s0 = s
1239 find0 = self.replace_back_slashes(find)
1240 if self.ignore_case:
1241 s = s0.lower()
1242 find = find0.lower()
1243 count, prev_i, result = 0, 0, []
1244 while True:
1245 progress = prev_i
1246 # #1166: Scan using s and find.
1247 i = s.find(find, prev_i)
1248 if i == -1:
1249 break
1250 # #1166: Replace using s0 & change.
1251 count += 1
1252 result.append(s0[prev_i:i])
1253 result.append(change)
1254 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!)
1255 assert prev_i > progress, prev_i
1256 # #1166: Complete the result using s0.
1257 result.append(s0[prev_i:])
1258 return count, ''.join(result)
1259 #@+node:ekr.20210110073117.25: *7* new:find.batch_regex_replace
1260 def batch_regex_replace(self, s):
1261 """
1262 Perform all regex find/replace on s.
1263 return (count, new_s)
1264 """
1265 count, prev_i, result = 0, 0, []
1267 flags = re.MULTILINE
1268 if self.ignore_case:
1269 flags |= re.IGNORECASE
1270 for m in re.finditer(self.find_text, s, flags):
1271 count += 1
1272 i = m.start()
1273 result.append(s[prev_i:i])
1274 # #1748.
1275 groups = m.groups()
1276 if groups:
1277 change_text = self.make_regex_subs(self.change_text, groups)
1278 else:
1279 change_text = self.change_text
1280 result.append(change_text)
1281 prev_i = m.end()
1282 # Compute the result.
1283 result.append(s[prev_i:])
1284 s = ''.join(result)
1285 return count, s
1286 #@+node:ekr.20210110073117.26: *7* new:find.batch_word_replace
1287 def batch_word_replace(self, s):
1288 """
1289 Perform all whole word find/replace on s.
1290 return (count, new_s)
1291 """
1292 find, change = self.find_text, self.change_text
1293 # #1166: s0 and find0 aren't affected by ignore-case.
1294 s0 = s
1295 find0 = self.replace_back_slashes(find)
1296 if self.ignore_case:
1297 s = s0.lower()
1298 find = find0.lower()
1299 count, prev_i, result = 0, 0, []
1300 while True:
1301 progress = prev_i
1302 # #1166: Scan using s and find.
1303 i = s.find(find, prev_i)
1304 if i == -1:
1305 break
1306 # #1166: Replace using s0, change & find0.
1307 result.append(s0[prev_i:i])
1308 if g.match_word(s, i, find):
1309 count += 1
1310 result.append(change)
1311 else:
1312 result.append(find0)
1313 prev_i = max(prev_i + 1, i + len(find)) # 2021/01/08 (!)
1314 assert prev_i > progress, prev_i
1315 # #1166: Complete the result using s0.
1316 result.append(s0[prev_i:])
1317 return count, ''.join(result)
1318 #@+node:ekr.20131117164142.17011: *4* find.clone-find-all & helper
1319 @cmd('clone-find-all')
1320 @cmd('find-clone-all')
1321 @cmd('cfa')
1322 def interactive_clone_find_all(
1323 self, event=None, preloaded=None): # pragma: no cover (interactive)
1324 """
1325 clone-find-all ( aka find-clone-all and cfa).
1327 Create an organizer node whose descendants contain clones of all nodes
1328 matching the search string, except @nosearch trees.
1330 The list is *not* flattened: clones appear only once in the
1331 descendants of the organizer node.
1332 """
1333 w = self.c.frame.body.wrapper
1334 if not w:
1335 return
1336 if not preloaded:
1337 self.preload_find_pattern(w)
1338 self.start_state_machine(event,
1339 prefix='Clone Find All: ',
1340 handler=self.interactive_clone_find_all1)
1342 def interactive_clone_find_all1(self, event): # pragma: no cover (interactive)
1343 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1344 # Settings...
1345 pattern = k.arg
1346 self.ftm.set_find_text(pattern)
1347 self.init_vim_search(pattern)
1348 self.init_in_headline()
1349 settings = self.ftm.get_settings()
1350 # Gui...
1351 k.clearState()
1352 k.resetLabel()
1353 k.showStateAndMode()
1354 c.widgetWantsFocusNow(w)
1355 count = self.do_clone_find_all(settings)
1356 if count:
1357 c.redraw()
1358 c.treeWantsFocus()
1359 return count
1360 #@+node:ekr.20210114094846.1: *5* find.do_clone_find_all
1361 # A stand-alone method for unit testing.
1362 def do_clone_find_all(self, settings):
1363 """
1364 Do the clone-all-find commands from settings.
1366 Return the count of found nodes.
1368 This is a stand-alone method for unit testing.
1369 """
1370 self.init_ivars_from_settings(settings)
1371 if not self.check_args('clone-find-all'):
1372 return 0
1373 return self._cf_helper(settings, flatten=False)
1374 #@+node:ekr.20131117164142.16996: *4* find.clone-find-all-flattened & helper
1375 @cmd('clone-find-all-flattened')
1376 # @cmd('find-clone-all-flattened')
1377 @cmd('cff')
1378 def interactive_cff(
1379 self, event=None, preloaded=None): # pragma: no cover (interactive)
1380 """
1381 clone-find-all-flattened (aka find-clone-all-flattened and cff).
1383 Create an organizer node whose direct children are clones of all nodes
1384 matching the search string, except @nosearch trees.
1386 The list is flattened: every cloned node appears as a direct child
1387 of the organizer node, even if the clone also is a descendant of
1388 another cloned node.
1389 """
1390 w = self.c.frame.body.wrapper
1391 if not w:
1392 return
1393 if not preloaded:
1394 self.preload_find_pattern(w)
1395 self.start_state_machine(event,
1396 prefix='Clone Find All Flattened: ',
1397 handler=self.interactive_cff1)
1399 def interactive_cff1(self, event): # pragma: no cover (interactive)
1400 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1401 # Settings...
1402 pattern = k.arg
1403 self.ftm.set_find_text(pattern)
1404 self.init_vim_search(pattern)
1405 self.init_in_headline()
1406 settings = self.ftm.get_settings()
1407 # Gui...
1408 k.clearState()
1409 k.resetLabel()
1410 k.showStateAndMode()
1411 c.widgetWantsFocusNow(w)
1412 count = self.do_clone_find_all_flattened(settings)
1413 if count:
1414 c.redraw()
1415 c.treeWantsFocus()
1416 return count
1417 #@+node:ekr.20210114094944.1: *5* find.do_clone_find_all_flattened
1418 # A stand-alone method for unit testing.
1419 def do_clone_find_all_flattened(self, settings):
1420 """
1421 Do the clone-find-all-flattened command from the settings.
1423 Return the count of found nodes.
1425 This is a stand-alone method for unit testing.
1426 """
1427 self.init_ivars_from_settings(settings)
1428 if self.check_args('clone-find-all-flattened'):
1429 return self._cf_helper(settings, flatten=True)
1430 return 0
1431 #@+node:ekr.20160920110324.1: *4* find.clone-find-tag & helper
1432 @cmd('clone-find-tag')
1433 @cmd('find-clone-tag')
1434 @cmd('cft')
1435 def interactive_clone_find_tag(self, event=None): # pragma: no cover (interactive)
1436 """
1437 clone-find-tag (aka find-clone-tag and cft).
1439 Create an organizer node whose descendants contain clones of all
1440 nodes matching the given tag, except @nosearch trees.
1442 The list is *always* flattened: every cloned node appears as a
1443 direct child of the organizer node, even if the clone also is a
1444 descendant of another cloned node.
1445 """
1446 w = self.c.frame.body.wrapper
1447 if w:
1448 self.start_state_machine(event,
1449 prefix='Clone Find Tag: ',
1450 handler=self.interactive_clone_find_tag1)
1452 def interactive_clone_find_tag1(self, event): # pragma: no cover (interactive)
1453 c, k = self.c, self.k
1454 # Settings...
1455 self.find_text = tag = k.arg
1456 # Gui...
1457 k.clearState()
1458 k.resetLabel()
1459 k.showStateAndMode()
1460 self.do_clone_find_tag(tag)
1461 c.treeWantsFocus()
1462 #@+node:ekr.20210110073117.11: *5* find.do_clone_find_tag & helper
1463 # A stand-alone method for unit tests.
1464 def do_clone_find_tag(self, tag):
1465 """
1466 Do the clone-all-find commands from settings.
1467 Return (len(clones), found) for unit tests.
1468 """
1469 c, u = self.c, self.c.undoer
1470 tc = getattr(c, 'theTagController', None)
1471 if not tc:
1472 if not g.unitTesting: # pragma: no cover (skip)
1473 g.es_print('nodetags not active')
1474 return 0, c.p
1475 clones = tc.get_tagged_nodes(tag)
1476 if not clones:
1477 if not g.unitTesting: # pragma: no cover (skip)
1478 g.es_print(f"tag not found: {tag}")
1479 tc.show_all_tags()
1480 return 0, c.p
1481 undoData = u.beforeInsertNode(c.p)
1482 found = self._create_clone_tag_nodes(clones)
1483 u.afterInsertNode(found, 'Clone Find Tag', undoData)
1484 assert c.positionExists(found, trace=True), found
1485 c.setChanged()
1486 c.selectPosition(found)
1487 c.redraw()
1488 return len(clones), found
1489 #@+node:ekr.20210110073117.12: *6* find._create_clone_tag_nodes
1490 def _create_clone_tag_nodes(self, clones):
1491 """
1492 Create a "Found Tag" node as the last node of the outline.
1493 Clone all positions in the clones set as children of found.
1494 """
1495 c, p = self.c, self.c.p
1496 # Create the found node.
1497 assert c.positionExists(c.lastTopLevel()), c.lastTopLevel()
1498 found = c.lastTopLevel().insertAfter()
1499 assert found
1500 assert c.positionExists(found), found
1501 found.h = f"Found Tag: {self.find_text}"
1502 # Clone nodes as children of the found node.
1503 for p in clones:
1504 # Create the clone directly as a child of found.
1505 p2 = p.copy()
1506 n = found.numberOfChildren()
1507 p2._linkCopiedAsNthChild(found, n)
1508 return found
1509 #@+node:ekr.20131117164142.16998: *4* find.find-all & helper
1510 @cmd('find-all')
1511 def interactive_find_all(self, event=None): # pragma: no cover (interactive)
1512 """
1513 Create a summary node containing descriptions of all matches of the
1514 search string.
1516 Typing tab converts this to the change-all command.
1517 """
1518 self.ftm.clear_focus()
1519 self.ftm.set_entry_focus()
1520 self.start_state_machine(event, 'Search: ',
1521 handler=self.interactive_find_all1,
1522 escape_handler=self.find_all_escape_handler,
1523 )
1525 def interactive_find_all1(self, event=None): # pragma: no cover (interactive)
1526 k = self.k
1527 # Settings.
1528 find_pattern = k.arg
1529 self.ftm.set_find_text(find_pattern)
1530 settings = self.ftm.get_settings()
1531 self.find_text = find_pattern
1532 self.change_text = self.ftm.get_change_text()
1533 self.update_find_list(find_pattern)
1534 # Gui...
1535 k.clearState()
1536 k.resetLabel()
1537 k.showStateAndMode()
1538 self.do_find_all(settings)
1540 def find_all_escape_handler(self, event): # pragma: no cover (interactive)
1541 k = self.k
1542 prompt = 'Replace ' + ('Regex' if self.pattern_match else 'String')
1543 find_pattern = k.arg
1544 self._sString = k.arg
1545 self.update_find_list(k.arg)
1546 s = f"{prompt}: {find_pattern} With: "
1547 k.setLabelBlue(s)
1548 self.add_change_string_to_label()
1549 k.getNextArg(self.find_all_escape_handler2)
1551 def find_all_escape_handler2(self, event): # pragma: no cover (interactive)
1552 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1553 find_pattern = self._sString
1554 change_pattern = k.arg
1555 self.update_change_list(change_pattern)
1556 self.ftm.set_find_text(find_pattern)
1557 self.ftm.set_change_text(change_pattern)
1558 self.init_vim_search(find_pattern)
1559 self.init_in_headline()
1560 settings = self.ftm.get_settings()
1561 # Gui...
1562 k.clearState()
1563 k.resetLabel()
1564 k.showStateAndMode()
1565 c.widgetWantsFocusNow(w)
1566 self.do_change_all(settings)
1567 #@+node:ekr.20031218072017.3073: *5* find.do_find_all & helpers
1568 def do_find_all(self, settings):
1569 """Top-level helper for find-all command."""
1570 c = self.c
1571 count = 0
1572 self.init_ivars_from_settings(settings)
1573 if not self.check_args('find-all'): # pragma: no cover
1574 return count
1575 # Init data.
1576 self.init_in_headline()
1577 data = self.save()
1578 self.in_headline = self.search_headline # Search headlines first.
1579 self.unique_matches = set() # 2021/02/20.
1580 # Remember the start of the search.
1581 p = self.root = c.p.copy()
1582 # Set the work widget.
1583 s = p.h if self.in_headline else p.b
1584 ins = len(s) if self.reverse else 0
1585 self.work_s = s
1586 self.work_sel = (ins, ins, ins)
1587 if self.pattern_match:
1588 ok = self.precompile_pattern()
1589 if not ok: # pragma: no cover
1590 return count
1591 if self.suboutline_only:
1592 p = c.p
1593 after = p.nodeAfterTree()
1594 else:
1595 # Always search the entire outline.
1596 p = c.rootPosition()
1597 after = None
1598 # Fix #292: Never collapse nodes during find-all commands.
1599 old_sparse_find = c.sparse_find
1600 try:
1601 c.sparse_find = False
1602 count = self._find_all_helper(after, data, p, 'Find All')
1603 c.contractAllHeadlines()
1604 finally:
1605 c.sparse_find = old_sparse_find
1606 self.root = None
1607 if count:
1608 c.redraw()
1609 g.es("found", count, "matches for", self.find_text)
1610 return count
1611 #@+node:ekr.20160422073500.1: *6* find._find_all_helper
1612 def _find_all_helper(self, after, data, p, undoType):
1613 """Handle the find-all command from p to after."""
1614 c, log, u = self.c, self.c.frame.log, self.c.undoer
1616 def put_link(line, line_number, p): # pragma: no cover # #2023
1617 """Put a link to the given line at the given line_number in p.h."""
1619 if g.unitTesting:
1620 return
1621 unl = p.get_UNL()
1622 if self.in_headline:
1623 line_number = 1
1624 log.put(line.strip() + '\n', nodeLink=f"{unl}::{line_number}") # Local line.
1626 seen = [] # List of (vnode, pos).
1627 both = self.search_body and self.search_headline
1628 count, found, result = 0, None, []
1629 while 1:
1630 p, pos, newpos = self.find_next_match(p)
1631 if pos is None:
1632 break
1633 if (p.v, pos) in seen: # 2076
1634 continue # pragma: no cover
1635 seen.append((p.v, pos))
1636 count += 1
1637 s = self.work_s
1638 i, j = g.getLine(s, pos)
1639 line = s[i:j]
1640 row, col = g.convertPythonIndexToRowCol(s, i)
1641 line_number = row + 1
1642 if self.findAllUniqueFlag:
1643 m = self.match_obj
1644 if m:
1645 self.unique_matches.add(m.group(0).strip())
1646 put_link(line, line_number, p) # #2023
1647 elif both:
1648 result.append('%s%s\n%s%s\n' % (
1649 '-' * 20, p.h,
1650 "head: " if self.in_headline else "body: ",
1651 line.rstrip() + '\n'))
1652 put_link(line, line_number, p) # #2023
1653 elif p.isVisited():
1654 result.append(line.rstrip() + '\n')
1655 put_link(line, line_number, p) # #2023
1656 else:
1657 result.append('%s%s\n%s' % ('-' * 20, p.h, line.rstrip() + '\n'))
1658 put_link(line, line_number, p) # #2023
1659 p.setVisited()
1660 if result or self.unique_matches:
1661 undoData = u.beforeInsertNode(c.p)
1662 if self.findAllUniqueFlag:
1663 found = self._create_find_unique_node()
1664 count = len(list(self.unique_matches))
1665 else:
1666 found = self._create_find_all_node(result)
1667 u.afterInsertNode(found, undoType, undoData)
1668 c.selectPosition(found)
1669 c.setChanged()
1670 else:
1671 self.restore(data)
1672 return count
1673 #@+node:ekr.20150717105329.1: *6* find._create_find_all_node
1674 def _create_find_all_node(self, result):
1675 """Create a "Found All" node as the last node of the outline."""
1676 c = self.c
1677 found = c.lastTopLevel().insertAfter()
1678 assert found
1679 found.h = f"Found All:{self.find_text}"
1680 status = self.compute_result_status(find_all_flag=True)
1681 status = status.strip().lstrip('(').rstrip(')').strip()
1682 found.b = f"# {status}\n{''.join(result)}"
1683 return found
1684 #@+node:ekr.20171226143621.1: *6* find._create_find_unique_node
1685 def _create_find_unique_node(self):
1686 """Create a "Found Unique" node as the last node of the outline."""
1687 c = self.c
1688 found = c.lastTopLevel().insertAfter()
1689 assert found
1690 found.h = f"Found Unique Regex:{self.find_text}"
1691 result = sorted(self.unique_matches)
1692 found.b = '\n'.join(result)
1693 return found
1694 #@+node:ekr.20171226140643.1: *4* find.find-all-unique-regex
1695 @cmd('find-all-unique-regex')
1696 def interactive_find_all_unique_regex(
1697 self, event=None): # pragma: no cover (interactive)
1698 """
1699 Create a summary node containing all unique matches of the regex search
1700 string. This command shows only the matched string itself.
1701 """
1702 self.ftm.clear_focus()
1703 self.match_obj = None
1704 self.changeAllFlag = False
1705 self.findAllUniqueFlag = True
1706 self.ftm.set_entry_focus()
1707 self.start_state_machine(event,
1708 prefix='Search Unique Regex: ',
1709 handler=self.interactive_find_all_unique_regex1,
1710 escape_handler=self.interactive_change_all_unique_regex1,
1711 )
1713 def interactive_find_all_unique_regex1(
1714 self, event=None): # pragma: no cover (interactive)
1715 k = self.k
1716 # Settings...
1717 find_pattern = k.arg
1718 self.update_find_list(find_pattern)
1719 self.ftm.set_find_text(find_pattern)
1720 self.init_in_headline()
1721 settings = self.ftm.get_settings()
1722 # Gui...
1723 k.clearState()
1724 k.resetLabel()
1725 k.showStateAndMode()
1726 return self.do_find_all(settings)
1728 def interactive_change_all_unique_regex1(
1729 self, event): # pragma: no cover (interactive)
1730 k = self.k
1731 find_pattern = self._sString = k.arg
1732 self.update_find_list(k.arg)
1733 s = f"'Replace All Unique Regex': {find_pattern} With: "
1734 k.setLabelBlue(s)
1735 self.add_change_string_to_label()
1736 k.getNextArg(self.interactive_change_all_unique_regex2)
1738 def interactive_change_all_unique_regex2(
1739 self, event): # pragma: no cover (interactive)
1740 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1741 find_pattern = self._sString
1742 change_pattern = k.arg
1743 self.update_change_list(change_pattern)
1744 self.ftm.set_find_text(find_pattern)
1745 self.ftm.set_change_text(change_pattern)
1746 self.init_vim_search(find_pattern)
1747 self.init_in_headline()
1748 settings = self.ftm.get_settings()
1749 # Gui...
1750 k.clearState()
1751 k.resetLabel()
1752 k.showStateAndMode()
1753 c.widgetWantsFocusNow(w)
1754 self.do_change_all(settings)
1755 #@+node:ekr.20131117164142.17003: *4* find.re-search
1756 @cmd('re-search')
1757 @cmd('re-search-forward')
1758 def interactive_re_search_forward(self, event): # pragma: no cover (interactive)
1759 """Same as start-find, with regex."""
1760 # Set flag for show_find_options.
1761 self.pattern_match = True
1762 self.show_find_options()
1763 # Set flag for do_find_next().
1764 self.request_pattern_match = True
1765 # Go.
1766 self.start_state_machine(event,
1767 prefix='Regexp Search: ',
1768 handler=self.start_search1, # See start-search
1769 escape_handler=self.start_search_escape1, # See start-search
1770 )
1771 #@+node:ekr.20210112044303.1: *4* find.re-search-backward
1772 @cmd('re-search-backward')
1773 def interactive_re_search_backward(self, event): # pragma: no cover (interactive)
1774 """Same as start-find, but with regex and in reverse."""
1775 # Set flags for show_find_options.
1776 self.reverse = True
1777 self.pattern_match = True
1778 self.show_find_options()
1779 # Set flags for do_find_next().
1780 self.request_reverse = True
1781 self.request_pattern_match = True
1782 # Go.
1783 self.start_state_machine(event,
1784 prefix='Regexp Search Backward:',
1785 handler=self.start_search1, # See start-search
1786 escape_handler=self.start_search_escape1, # See start-search
1787 )
1789 #@+node:ekr.20131117164142.17004: *4* find.search_backward
1790 @cmd('search-backward')
1791 def interactive_search_backward(self, event): # pragma: no cover (interactive)
1792 """Same as start-find, but in reverse."""
1793 # Set flag for show_find_options.
1794 self.reverse = True
1795 self.show_find_options()
1796 # Set flag for do_find_next().
1797 self.request_reverse = True
1798 # Go.
1799 self.start_state_machine(event,
1800 prefix='Search Backward: ',
1801 handler=self.start_search1, # See start-search
1802 escape_handler=self.start_search_escape1, # See start-search
1803 )
1804 #@+node:ekr.20131119060731.22452: *4* find.start-search (Ctrl-F) & common states
1805 @cmd('start-search')
1806 @cmd('search-forward') # Compatibility.
1807 def start_search(self, event): # pragma: no cover (interactive)
1808 """
1809 The default binding of Ctrl-F.
1811 Also contains default state-machine entries for find/change commands.
1812 """
1813 w = self.c.frame.body.wrapper
1814 if not w:
1815 return
1816 self.preload_find_pattern(w)
1817 # #1840: headline-only one-shot
1818 # Do this first, so the user can override.
1819 self.ftm.set_body_and_headline_checkbox()
1820 if self.minibuffer_mode:
1821 # Set up the state machine.
1822 self.ftm.clear_focus()
1823 self.changeAllFlag = False
1824 self.findAllUniqueFlag = False
1825 self.ftm.set_entry_focus()
1826 self.start_state_machine(event,
1827 prefix='Search: ',
1828 handler=self.start_search1,
1829 escape_handler=self.start_search_escape1,
1830 )
1831 else:
1832 self.open_find_tab(event)
1833 self.ftm.init_focus()
1834 return
1836 startSearch = start_search # Compatibility. Do not delete.
1837 #@+node:ekr.20210117143611.1: *5* find.start_search1
1838 def start_search1(self, event=None): # pragma: no cover
1839 """Common handler for use by vim commands and other find commands."""
1840 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1841 # Settings...
1842 find_pattern = k.arg
1843 self.ftm.set_find_text(find_pattern)
1844 self.update_find_list(find_pattern)
1845 self.init_vim_search(find_pattern)
1846 self.init_in_headline() # Required.
1847 settings = self.ftm.get_settings()
1848 # Gui...
1849 k.clearState()
1850 k.resetLabel()
1851 k.showStateAndMode()
1852 c.widgetWantsFocusNow(w)
1853 # Do the command!
1854 self.do_find_next(settings) # Handles reverse.
1855 #@+node:ekr.20210117143614.1: *5* find._start_search_escape1
1856 def start_search_escape1(self, event=None): # pragma: no cover
1857 """
1858 Common escape handler for use by find commands.
1860 Prompt for a change pattern.
1861 """
1862 k = self.k
1863 self._sString = find_pattern = k.arg
1864 # Settings.
1865 k.getArgEscapeFlag = False
1866 self.ftm.set_find_text(find_pattern)
1867 self.update_find_list(find_pattern)
1868 self.find_text = find_pattern
1869 self.change_text = self.ftm.get_change_text()
1870 # Gui...
1871 regex = ' Regex' if self.pattern_match else ''
1872 backward = ' Backward' if self.reverse else ''
1873 prompt = f"Replace{regex}{backward}: {find_pattern} With: "
1874 k.setLabelBlue(prompt)
1875 self.add_change_string_to_label()
1876 k.getNextArg(self._start_search_escape2)
1878 #@+node:ekr.20210117143615.1: *5* find._start_search_escape2
1879 def _start_search_escape2(self, event): # pragma: no cover
1880 c, k, w = self.c, self.k, self.c.frame.body.wrapper
1881 # Compute settings...
1882 find_pattern = self._sString
1883 change_pattern = k.arg
1884 self.ftm.set_find_text(find_pattern)
1885 self.ftm.set_change_text(change_pattern)
1886 self.update_change_list(change_pattern)
1887 self.init_vim_search(find_pattern)
1888 self.init_in_headline() # Required
1889 settings = self.ftm.get_settings()
1890 # Gui...
1891 k.clearState()
1892 k.resetLabel()
1893 k.showStateAndMode()
1894 c.widgetWantsFocusNow(w)
1895 self.do_find_next(settings)
1896 #@+node:ekr.20160920164418.2: *4* find.tag-children & helper
1897 @cmd('tag-children')
1898 def interactive_tag_children(self, event=None): # pragma: no cover (interactive)
1899 """tag-children: prompt for a tag and add it to all children of c.p."""
1900 w = self.c.frame.body.wrapper
1901 if not w:
1902 return
1903 self.start_state_machine(event,
1904 prefix='Tag Children: ',
1905 handler=self.interactive_tag_children1)
1907 def interactive_tag_children1(self, event): # pragma: no cover (interactive)
1908 c, k, p = self.c, self.k, self.c.p
1909 # Settings...
1910 tag = k.arg
1911 # Gui...
1912 k.clearState()
1913 k.resetLabel()
1914 k.showStateAndMode()
1915 self.do_tag_children(p, tag)
1916 c.treeWantsFocus()
1917 #@+node:ekr.20160920164418.4: *5* find.do_tag_children
1918 def do_tag_children(self, p, tag):
1919 """Handle the tag-children command."""
1920 c = self.c
1921 tc = getattr(c, 'theTagController', None)
1922 if not tc:
1923 if not g.unitTesting: # pragma: no cover (skip)
1924 g.es_print('nodetags not active')
1925 return
1926 for p in p.children():
1927 tc.add_tag(p, tag)
1928 if not g.unitTesting: # pragma: no cover (skip)
1929 g.es_print(f"Added {tag} tag to {len(list(c.p.children()))} nodes")
1931 #@+node:ekr.20210112050845.1: *4* find.word-search
1932 @cmd('word-search')
1933 @cmd('word-search-forward')
1934 def word_search_forward(self, event): # pragma: no cover (interactive)
1935 """Same as start-search, with whole_word setting."""
1936 # Set flag for show_find_options.
1937 self.whole_word = True
1938 self.show_find_options()
1939 # Set flag for do_find_next().
1940 self.request_whole_world = True
1941 # Go.
1942 self.start_state_machine(event,
1943 prefix='Word Search: ',
1944 handler=self.start_search1, # See start-search
1945 escape_handler=self.start_search_escape1, # See start-search
1946 )
1947 #@+node:ekr.20131117164142.17009: *4* find.word-search-backward
1948 @cmd('word-search-backward')
1949 def word_search_backward(self, event): # pragma: no cover (interactive)
1950 # Set flags for show_find_options.
1951 self.reverse = True
1952 self.whole_world = True
1953 self.show_find_options()
1954 # Set flags for do_find_next().
1955 self.request_reverse = True
1956 self.request_whole_world = True
1957 # Go
1958 self.start_state_machine(event,
1959 prefix='Word Search Backward: ',
1960 handler=self.start_search1, # See start-search
1961 escape_handler=self.start_search_escape1, # See start-search
1962 )
1963 #@+node:ekr.20210112192427.1: *3* LeoFind.Commands: helpers
1964 #@+node:ekr.20210110073117.9: *4* find._cf_helper & helpers
1965 def _cf_helper(self, settings, flatten): # Caller has checked the settings.
1966 """
1967 The common part of the clone-find commands.
1969 Return the number of found nodes.
1970 """
1971 c, u = self.c, self.c.undoer
1972 if self.pattern_match:
1973 ok = self.compile_pattern()
1974 if not ok:
1975 return 0
1976 if self.suboutline_only:
1977 p = c.p
1978 after = p.nodeAfterTree()
1979 else:
1980 p = c.rootPosition()
1981 after = None
1982 count, found = 0, None
1983 clones, skip = [], set()
1984 while p and p != after:
1985 progress = p.copy()
1986 if g.inAtNosearch(p):
1987 p.moveToNodeAfterTree()
1988 elif p.v in skip: # pragma: no cover (minor)
1989 p.moveToThreadNext()
1990 elif self._cfa_find_next_match(p):
1991 count += 1
1992 if flatten:
1993 skip.add(p.v)
1994 clones.append(p.copy())
1995 p.moveToThreadNext()
1996 else:
1997 if p not in clones:
1998 clones.append(p.copy())
1999 # Don't look at the node or it's descendants.
2000 for p2 in p.self_and_subtree(copy=False):
2001 skip.add(p2.v)
2002 p.moveToNodeAfterTree()
2003 else:
2004 p.moveToThreadNext()
2005 assert p != progress
2006 self.ftm.set_radio_button('entire-outline')
2007 # suboutline-only is a one-shot for batch commands.
2008 self.node_only = self.suboutline_only = False
2009 self.root = None
2010 if clones:
2011 undoData = u.beforeInsertNode(c.p)
2012 found = self._cfa_create_nodes(clones, flattened=False)
2013 u.afterInsertNode(found, 'Clone Find All', undoData)
2014 assert c.positionExists(found, trace=True), found
2015 c.setChanged()
2016 c.selectPosition(found)
2017 # Put the count in found.h.
2018 found.h = found.h.replace('Found:', f"Found {count}:")
2019 g.es("found", count, "matches for", self.find_text)
2020 return count # Might be useful for the gui update.
2021 #@+node:ekr.20210110073117.34: *5* find._cfa_create_nodes
2022 def _cfa_create_nodes(self, clones, flattened):
2023 """
2024 Create a "Found" node as the last node of the outline.
2025 Clone all positions in the clones set a children of found.
2026 """
2027 c = self.c
2028 # Create the found node.
2029 assert c.positionExists(c.lastTopLevel()), c.lastTopLevel()
2030 found = c.lastTopLevel().insertAfter()
2031 assert found
2032 assert c.positionExists(found), found
2033 found.h = f"Found:{self.find_text}"
2034 status = self.compute_result_status(find_all_flag=True)
2035 status = status.strip().lstrip('(').rstrip(')').strip()
2036 flat = 'flattened, ' if flattened else ''
2037 found.b = f"@nosearch\n\n# {flat}{status}\n\n# found {len(clones)} nodes"
2038 # Clone nodes as children of the found node.
2039 for p in clones:
2040 # Create the clone directly as a child of found.
2041 p2 = p.copy()
2042 n = found.numberOfChildren()
2043 p2._linkCopiedAsNthChild(found, n)
2044 # Sort the clones in place, without undo.
2045 found.v.children.sort(key=lambda v: v.h.lower())
2046 return found
2047 #@+node:ekr.20210110073117.10: *5* find._cfa_find_next_match (for unit tests)
2048 def _cfa_find_next_match(self, p):
2049 """
2050 Find the next batch match at p.
2051 """
2052 # Called only from unit tests.
2053 table = []
2054 if self.search_headline:
2055 table.append(p.h)
2056 if self.search_body:
2057 table.append(p.b)
2058 for s in table:
2059 self.reverse = False
2060 pos, newpos = self.inner_search_helper(s, 0, len(s), self.find_text)
2061 if pos != -1:
2062 return True
2063 return False
2064 #@+node:ekr.20031218072017.3070: *4* find.change_selection
2065 def change_selection(self, p):
2066 """Replace selection with self.change_text."""
2067 c, p, u = self.c, self.c.p, self.c.undoer
2068 wrapper = c.frame.body and c.frame.body.wrapper
2069 gui_w = c.edit_widget(p) if self.in_headline else wrapper
2070 if not gui_w: # pragma: no cover
2071 self.in_headline = False
2072 gui_w = wrapper
2073 if not gui_w: # pragma: no cover
2074 return False
2075 oldSel = sel = gui_w.getSelectionRange()
2076 start, end = sel
2077 if start > end: # pragma: no cover
2078 start, end = end, start
2079 if start == end: # pragma: no cover
2080 g.es("no text selected")
2081 return False
2082 bunch = u.beforeChangeBody(p)
2083 start, end = oldSel
2084 change_text = self.change_text
2085 # Perform regex substitutions of \1, \2, ...\9 in the change text.
2086 if self.pattern_match and self.match_obj:
2087 groups = self.match_obj.groups()
2088 if groups:
2089 change_text = self.make_regex_subs(change_text, groups)
2090 change_text = self.replace_back_slashes(change_text)
2091 # Update both the gui widget and the work "widget"
2092 new_ins = start if self.reverse else start + len(change_text)
2093 if start != end:
2094 gui_w.delete(start, end)
2095 gui_w.insert(start, change_text)
2096 gui_w.setInsertPoint(new_ins)
2097 self.work_s = gui_w.getAllText() # #2220.
2098 self.work_sel = (new_ins, new_ins, new_ins)
2099 # Update the selection for the next match.
2100 gui_w.setSelectionRange(start, start + len(change_text))
2101 c.widgetWantsFocus(gui_w)
2102 # No redraws here: they would destroy the headline selection.
2103 if self.mark_changes: # pragma: no cover
2104 p.setMarked()
2105 p.setDirty()
2106 if self.in_headline:
2107 # #2220: Let onHeadChanged handle undo, etc.
2108 c.frame.tree.onHeadChanged(p, undoType='Change Headline')
2109 # gui_w will change after a redraw.
2110 gui_w = c.edit_widget(p)
2111 if gui_w:
2112 # find-next and find-prev work regardless of insert point.
2113 gui_w.setSelectionRange(start, start + len(change_text))
2114 else:
2115 p.v.b = gui_w.getAllText()
2116 u.afterChangeBody(p, 'Change Body', bunch)
2117 c.frame.tree.updateIcon(p) # redraw only the icon.
2118 return True
2119 #@+node:ekr.20210110073117.31: *4* find.check_args
2120 def check_args(self, tag):
2121 """Check the user arguments to a command."""
2122 if not self.search_headline and not self.search_body:
2123 if not g.unitTesting:
2124 g.es_print("not searching headline or body") # pragma: no cover (skip)
2125 return False
2126 if not self.find_text:
2127 if not g.unitTesting:
2128 g.es_print(f"{tag}: empty find pattern") # pragma: no cover (skip)
2129 return False
2130 return True
2131 #@+node:ekr.20210110073117.32: *4* find.compile_pattern
2132 def compile_pattern(self):
2133 """Precompile the regexp pattern if necessary."""
2134 try: # Precompile the regexp.
2135 # pylint: disable=no-member
2136 flags = re.MULTILINE
2137 if self.ignore_case:
2138 flags |= re.IGNORECASE # pragma: no cover
2139 # Escape the search text.
2140 # Ignore the whole_word option.
2141 s = self.find_text
2142 # A bad idea: insert \b automatically.
2143 # b, s = '\\b', self.find_text
2144 # if self.whole_word:
2145 # if not s.startswith(b): s = b + s
2146 # if not s.endswith(b): s = s + b
2147 self.re_obj = re.compile(s, flags)
2148 return True
2149 except Exception:
2150 if not g.unitTesting: # pragma: no cover (skip)
2151 g.warning('invalid regular expression:', self.find_text)
2152 return False
2153 #@+node:ekr.20031218072017.3075: *4* find.find_next_match & helpers
2154 def find_next_match(self, p):
2155 """
2156 Resume the search where it left off.
2158 Return (p, pos, newpos).
2159 """
2160 c = self.c
2161 if not self.search_headline and not self.search_body: # pragma: no cover
2162 return None, None, None
2163 if not self.find_text: # pragma: no cover
2164 return None, None, None
2165 attempts = 0
2166 if self.pattern_match:
2167 ok = self.precompile_pattern()
2168 if not ok:
2169 return None, None, None
2170 while p:
2171 pos, newpos = self._fnm_search(p)
2172 if pos is not None:
2173 # Success.
2174 if self.mark_finds: # pragma: no cover
2175 p.setMarked()
2176 p.setDirty()
2177 if not self.changeAllFlag:
2178 c.frame.tree.updateIcon(p) # redraw only the icon.
2179 return p, pos, newpos
2180 # Searching the pane failed: switch to another pane or node.
2181 if self._fnm_should_stay_in_node(p):
2182 # Switching panes is possible. Do so.
2183 self.in_headline = not self.in_headline
2184 s = p.h if self.in_headline else p.b
2185 ins = len(s) if self.reverse else 0
2186 self.work_s = s
2187 self.work_sel = (ins, ins, ins)
2188 else:
2189 # Switch to the next/prev node, if possible.
2190 attempts += 1
2191 p = self._fnm_next_after_fail(p)
2192 if p: # Found another node: select the proper pane.
2193 self.in_headline = self._fnm_first_search_pane()
2194 s = p.h if self.in_headline else p.b
2195 ins = len(s) if self.reverse else 0
2196 self.work_s = s
2197 self.work_sel = (ins, ins, ins)
2198 return None, None, None
2199 #@+node:ekr.20131123132043.16476: *5* find._fnm_next_after_fail & helper
2200 def _fnm_next_after_fail(self, p):
2201 """Return the next node after a failed search or None."""
2202 # Move to the next position.
2203 p = p.threadBack() if self.reverse else p.threadNext()
2204 # Check it.
2205 if p and self._fail_outside_range(p): # pragma: no cover
2206 return None
2207 if not p: # pragma: no cover
2208 return None
2209 return p
2210 #@+node:ekr.20131123071505.16465: *6* find._fail_outside_range
2211 def _fail_outside_range(self, p): # pragma: no cover
2212 """
2213 Return True if the search is about to go outside its range, assuming
2214 both the headline and body text of the present node have been searched.
2215 """
2216 c = self.c
2217 if not p:
2218 return True
2219 if self.node_only:
2220 return True
2221 if self.suboutline_only:
2222 if self.root and p != self.root and not self.root.isAncestorOf(p):
2223 return True
2224 if c.hoistStack:
2225 bunch = c.hoistStack[-1]
2226 if not bunch.p.isAncestorOf(p):
2227 g.trace('outside hoist', p.h)
2228 g.warning('found match outside of hoisted outline')
2229 return True
2230 return False # Within range.
2231 #@+node:ekr.20131124060912.16473: *5* find._fnm_first_search_pane
2232 def _fnm_first_search_pane(self):
2233 """
2234 Set return the value of self.in_headline
2235 indicating which pane to search first.
2236 """
2237 if self.search_headline and self.search_body:
2238 # Fix bug 1228458: Inconsistency between Find-forward and Find-backward.
2239 if self.reverse:
2240 return False # Search the body pane first.
2241 return True # Search the headline pane first.
2242 if self.search_headline or self.search_body:
2243 # Search the only enabled pane.
2244 return self.search_headline
2245 g.trace('can not happen: no search enabled') # pragma: no cover
2246 return False # pragma: no cover
2247 #@+node:ekr.20031218072017.3077: *5* find._fnm_search
2248 def _fnm_search(self, p):
2249 """
2250 Search self.work_s for self.find_text with present options.
2251 Returns (pos, newpos) or (None, dNone).
2252 """
2253 index = self.work_sel[2]
2254 s = self.work_s
2255 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!)
2256 if sys.platform.lower().startswith('win'):
2257 # Ignore '\r' characters, which may appear in @edit nodes.
2258 # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4
2259 s = s.replace('\r', '')
2260 if not s: # pragma: no cover
2261 return None, None
2262 stopindex = 0 if self.reverse else len(s)
2263 pos, newpos = self.inner_search_helper(s, index, stopindex, self.find_text)
2264 if self.in_headline and not self.search_headline: # pragma: no cover
2265 return None, None
2266 if not self.in_headline and not self.search_body: # pragma: no cover
2267 return None, None
2268 if pos == -1: # pragma: no cover
2269 return None, None
2270 ins = min(pos, newpos) if self.reverse else max(pos, newpos)
2271 self.work_sel = (pos, newpos, ins)
2272 return pos, newpos
2273 #@+node:ekr.20131124060912.16472: *5* find._fnm_should_stay_in_node
2274 def _fnm_should_stay_in_node(self, p):
2275 """Return True if the find should simply switch panes."""
2276 # Errors here cause the find command to fail badly.
2277 # Switch only if:
2278 # a) searching both panes and,
2279 # b) this is the first pane of the pair.
2280 # There is *no way* this can ever change.
2281 # So simple in retrospect, so difficult to see.
2282 return (
2283 self.search_headline and self.search_body and (
2284 (self.reverse and not self.in_headline) or
2285 (not self.reverse and self.in_headline)))
2286 #@+node:ekr.20210110073117.43: *4* find.inner_search_helper & helpers
2287 def inner_search_helper(self, s, i, j, pattern):
2288 """
2289 Dispatch the proper search method based on settings.
2290 """
2291 backwards = self.reverse
2292 nocase = self.ignore_case
2293 regexp = self.pattern_match
2294 word = self.whole_word
2295 if backwards:
2296 i, j = j, i
2297 if not s[i:j] or not pattern:
2298 return -1, -1
2299 if regexp:
2300 pos, newpos = self._inner_search_regex(s, i, j, pattern, backwards, nocase)
2301 elif backwards:
2302 pos, newpos = self._inner_search_backward(s, i, j, pattern, nocase, word)
2303 else:
2304 pos, newpos = self._inner_search_plain(s, i, j, pattern, nocase, word)
2305 return pos, newpos
2306 #@+node:ekr.20210110073117.44: *5* find._inner_search_backward
2307 def _inner_search_backward(self, s, i, j, pattern, nocase, word):
2308 """
2309 rfind(sub [,start [,end]])
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) # pragma: no cover
2455 return False
2456 #@+node:ekr.20210110073117.49: *4* find.replace_back_slashes
2457 def replace_back_slashes(self, s):
2458 """Carefully replace backslashes in a search pattern."""
2459 # This is NOT the same as:
2460 #
2461 # s.replace('\\n','\n').replace('\\t','\t').replace('\\\\','\\')
2462 #
2463 # because there is no rescanning.
2464 i = 0
2465 while i + 1 < len(s):
2466 if s[i] == '\\':
2467 ch = s[i + 1]
2468 if ch == '\\':
2469 s = s[:i] + s[i + 1 :] # replace \\ by \
2470 elif ch == 'n':
2471 s = s[:i] + '\n' + s[i + 2 :] # replace the \n by a newline
2472 elif ch == 't':
2473 s = s[:i] + '\t' + s[i + 2 :] # replace \t by a tab
2474 else:
2475 i += 1 # Skip the escaped character.
2476 i += 1
2477 return s
2478 #@+node:ekr.20031218072017.3082: *3* LeoFind.Initing & finalizing
2479 #@+node:ekr.20031218072017.3086: *4* find.init_in_headline & helper
2480 def init_in_headline(self):
2481 """
2482 Select the first pane to search for incremental searches and changes.
2483 This is called only at the start of each search.
2484 This must not alter the current insertion point or selection range.
2485 """
2486 #
2487 # Fix bug 1228458: Inconsistency between Find-forward and Find-backward.
2488 if self.search_headline and self.search_body:
2489 # We have no choice: we *must* search the present widget!
2490 self.in_headline = self.focus_in_tree()
2491 else:
2492 self.in_headline = self.search_headline
2493 #@+node:ekr.20131126085250.16651: *5* find.focus_in_tree
2494 def focus_in_tree(self):
2495 """
2496 Return True is the focus widget w is anywhere in the tree pane.
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